Files
2026-07-13 12:58:18 +08:00

199 lines
6.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import path from "node:path";
import { globSync } from "glob";
import { loadFixtureFile, matchFixture } from "@copilotkit/aimock";
import type {
ChatCompletionRequest,
Fixture,
TextResponse,
ToolCallResponse,
} from "@copilotkit/aimock";
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..");
// Load fixtures for a single integration (langgraph-python, the reference
// integration) plus shared. At runtime each integration only sees its own
// scoped fixtures via X-AIMock-Context, so loading a single integration's
// fixture set is the correct simulation — loading all 18 integrations'
// fixtures would produce first-match collisions across identical prompts.
function loadBundledFixtures(): Fixture[] {
const fixtureFiles = [
...globSync("showcase/aimock/shared/*.json", {
cwd: REPO_ROOT,
absolute: true,
}),
...globSync("showcase/aimock/d4/langgraph-python/*.json", {
cwd: REPO_ROOT,
absolute: true,
}),
...globSync("showcase/aimock/d6/langgraph-python/*.json", {
cwd: REPO_ROOT,
absolute: true,
}),
];
return fixtureFiles.flatMap((f) => loadFixtureFile(f));
}
// D6 subagent fixtures use turnIndex-based chaining instead of toolCallId.
// Each turn in the conversation increments the turn index, and the fixture
// matches on the combination of userMessage + turnIndex + toolName.
//
// Turn 0: initial request → emits research_agent tool call
// Turn 1: after research result → emits writing_agent tool call
// Turn 2: after writing result → emits critique_agent tool call
// Turn 3: after critique result → emits final content
function buildRequest(opts: {
userMessage: string;
turnCount?: number;
toolName?: string;
toolResultCallId?: string;
}): ChatCompletionRequest {
const messages: ChatCompletionRequest["messages"] = [
{ role: "user", content: opts.userMessage },
];
// Add assistant+tool turn pairs to reach the desired turnIndex.
// Each pair simulates the agent calling a sub-agent tool and getting a result.
const turns = opts.turnCount ?? 0;
for (let i = 0; i < turns; i++) {
messages.push({
role: "assistant",
content: "",
tool_calls: [
{
id: `call_turn_${i}`,
type: "function",
function: { name: "sub_agent", arguments: "{}" },
},
],
});
messages.push({
role: "tool",
content: "ok",
tool_call_id: `call_turn_${i}`,
});
}
return {
model: "gpt-5.4",
messages,
// D6 fixtures use match.context for per-integration scoping; aimock's
// matchFixture checks req._context against it.
_context: "langgraph-python",
tools: [
{
type: "function",
function: {
name: "research_agent",
description: "research",
parameters: { type: "object" },
},
},
{
type: "function",
function: {
name: "writing_agent",
description: "writing",
parameters: { type: "object" },
},
},
{
type: "function",
function: {
name: "critique_agent",
description: "critique",
parameters: { type: "object" },
},
},
],
} as ChatCompletionRequest;
}
const CHAINS = [
{
title: "blog",
prompt:
"Produce a short blog post about the benefits of cold exposure training",
research: "call_d5_subagents_p1_research_001",
writing: "call_d5_subagents_p1_writing_001",
critique: "call_d5_subagents_p1_critique_001",
},
{
title: "explain",
prompt: "Explain how large language models handle tool calling",
research: "call_d5_subagents_p2_research_001",
writing: "call_d5_subagents_p2_writing_001",
critique: "call_d5_subagents_p2_critique_001",
},
{
title: "summarize",
prompt: "Summarize the current state of reusable rockets",
research: "call_d5_subagents_p3_research_001",
writing: "call_d5_subagents_p3_writing_001",
critique: "call_d5_subagents_p3_critique_001",
},
] as const;
describe("subagents bundled fixture routing", () => {
it("each pill chains research -> writing -> critique -> final via turnIndex", () => {
const fixtures = loadBundledFixtures();
for (const chain of CHAINS) {
// Turn 0: initial request → research_agent
const first = matchFixture(
fixtures,
buildRequest({ userMessage: chain.prompt, turnCount: 0 }),
);
expect(first, `${chain.title}: first leg should match`).not.toBeNull();
expect(
(first!.response as ToolCallResponse).toolCalls?.[0],
).toMatchObject({
id: chain.research,
name: "research_agent",
});
// Turn 1: after research → writing_agent
const second = matchFixture(
fixtures,
buildRequest({ userMessage: chain.prompt, turnCount: 1 }),
);
expect(
second,
`${chain.title}: second leg (turnIndex=1) should match`,
).not.toBeNull();
expect(
(second!.response as ToolCallResponse).toolCalls?.[0],
).toMatchObject({
id: chain.writing,
name: "writing_agent",
});
// Turn 2: after writing → critique_agent
const third = matchFixture(
fixtures,
buildRequest({ userMessage: chain.prompt, turnCount: 2 }),
);
expect(
third,
`${chain.title}: third leg (turnIndex=2) should match`,
).not.toBeNull();
expect(
(third!.response as ToolCallResponse).toolCalls?.[0],
).toMatchObject({
id: chain.critique,
name: "critique_agent",
});
// Turn 3: after critique → final content
const final = matchFixture(
fixtures,
buildRequest({ userMessage: chain.prompt, turnCount: 3 }),
);
expect(
final,
`${chain.title}: final leg (turnIndex=3) should match`,
).not.toBeNull();
expect((final!.response as TextResponse).content).toContain(
"after research",
);
}
});
});