Files
copilotkit__copilotkit/showcase/scripts/__tests__/subagents-fixture-routing.test.ts
Jordan Ritter ef3e59643b fix(showcase): update fixture routing tests for per-integration D6 layout
The four fixture routing regression tests still referenced the old
monolithic d5-all.json / smoke.json / feature-parity.json files that
were reorganized into per-integration d4/ d6/ shared/ directories.

Changes:
- Load fixtures via glob from d6/langgraph-python/ (reference
  integration) instead of deleted monolithic files
- Add _context to test requests (aimock 1.26.1 checks req._context
  against match.context for per-integration scoping)
- Adapt subagents test from toolCallId-based chaining to
  turnIndex-based chaining (matches new D6 fixture structure)
- Adapt state-context test to D6 context-scoping model (no
  systemMessage matching; routing happens via X-AIMock-Context)
- Remove _migrated-from-*.json shared files (all fixtures already
  exist in per-integration D6 dirs; the migration files caused
  620 shared-vs-scoped collisions)
- Add KNOWN_DUPLICATE_CEILING=11 ratchet for pre-existing D6
  intra-feature duplicate match keys
2026-05-26 11:54:06 -07: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",
);
}
});
});