Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"source": {
"source": "npm",
"package": "@copilotkit/aimock",
"version": "^1.27.1"
"version": "^1.27.2"
},
"description": "Fixture authoring skill for @copilotkit/aimock — LLM, multimedia (image/TTS/transcription/video), MCP, A2A, AG-UI, vector, embeddings, structured output, sequential responses, streaming physics, record/replay, agent loop patterns, and debugging"
}
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aimock",
"version": "1.27.1",
"version": "1.27.2",
"description": "Fixture authoring guidance for @copilotkit/aimock — LLM, multimedia, MCP, A2A, AG-UI, vector, and service mocking",
"author": {
"name": "CopilotKit"
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## [Unreleased]

## [1.27.2] - 2026-05-26

### Fixed

- **Fixture loader** — full recursive directory traversal replaces 2-level cap; supports per-integration showcase layouts like `d6/<integration>/<feature>.json`.

### Changed

- **Docker image** — added `git` binary to production stage for sparse-checkout fixture fetching at boot.

## [1.27.1] - 2026-05-22

### Fixed
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ LABEL org.opencontainers.image.source="https://github.com/CopilotKit/llmock"

WORKDIR /app

# No runtime dependencies — all imports are node:* built-ins
# git is needed for sparse-checkout fixture fetching in production
RUN apk add --no-cache git

COPY --from=build /app/dist/ dist/
COPY fixtures/ fixtures/

Expand Down
2 changes: 1 addition & 1 deletion charts/aimock/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ name: aimock
description: Mock infrastructure for AI application testing (OpenAI, Anthropic, Gemini, MCP, A2A, vector)
type: application
version: 0.1.0
appVersion: "1.27.1"
appVersion: "1.27.2"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@copilotkit/aimock",
"version": "1.27.1",
"version": "1.27.2",
"description": "Mock infrastructure for AI application testing — LLM APIs, image generation, text-to-speech, transcription, audio generation, video generation, MCP tools, A2A agents, AG-UI event streams, vector databases, search, rerank, and moderation. One package, one port, zero dependencies.",
"license": "MIT",
"keywords": [
Expand Down
45 changes: 43 additions & 2 deletions src/__tests__/fixture-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ describe("loadFixturesFromDir", () => {
expect(fixtures[1].match.userMessage).toBe("b");
});

it("does not recurse deeper than one level", () => {
it("recurses into nested subdirectories (full depth)", () => {
const subDir = join(tmpDir, "level1");
const deepDir = join(subDir, "level2");
mkdirSync(subDir);
Expand All @@ -531,8 +531,49 @@ describe("loadFixturesFromDir", () => {
});

const fixtures = loadFixturesFromDir(tmpDir);
expect(fixtures).toHaveLength(1);
expect(fixtures).toHaveLength(2);
expect(fixtures[0].match.userMessage).toBe("one");
expect(fixtures[1].match.userMessage).toBe("two");
});

it("recurses into deeply nested directories (3+ levels)", () => {
// Simulate showcase layout: showcase/aimock/d6/langgraph-python/chat-basic.json
const level1 = join(tmpDir, "d6");
const level2 = join(level1, "langgraph-python");
mkdirSync(level1);
mkdirSync(level2);
writeJson(tmpDir, "root.json", {
fixtures: [{ match: { userMessage: "root" }, response: { content: "R" } }],
});
writeJson(level1, "mid.json", {
fixtures: [{ match: { userMessage: "mid" }, response: { content: "M" } }],
});
writeJson(level2, "deep.json", {
fixtures: [{ match: { userMessage: "deep" }, response: { content: "D" } }],
});

const fixtures = loadFixturesFromDir(tmpDir);
expect(fixtures).toHaveLength(3);
// Root files first, then subdirectories in sorted order, recursively
expect(fixtures[0].match.userMessage).toBe("root");
expect(fixtures[1].match.userMessage).toBe("mid");
expect(fixtures[2].match.userMessage).toBe("deep");
});

it("recurses 4 levels deep", () => {
const l1 = join(tmpDir, "a");
const l2 = join(l1, "b");
const l3 = join(l2, "c");
mkdirSync(l1);
mkdirSync(l2);
mkdirSync(l3);
writeJson(l3, "fixture.json", {
fixtures: [{ match: { userMessage: "4-deep" }, response: { content: "found" } }],
});

const fixtures = loadFixturesFromDir(tmpDir);
expect(fixtures).toHaveLength(1);
expect(fixtures[0].match.userMessage).toBe("4-deep");
});

it("top-level JSON files come before subdirectory fixtures", () => {
Expand Down
37 changes: 3 additions & 34 deletions src/fixture-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,42 +175,11 @@ export function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[]
fixtures.push(...loadFixtureFile(filePath, logger));
}

// Recurse one level into subdirectories to support snapshot-style layouts
// where the recorder writes to <fixturePath>/<testId>/<provider>.json.
// Recurse into all subdirectories (full depth) to support nested layouts
// like showcase/aimock/d6/<integration>/<feature>.json.
subdirs.sort();
for (const sub of subdirs) {
const subPath = join(dirPath, sub);
let subEntries: string[];
try {
subEntries = readdirSync(subPath);
} catch (err) {
warn(logger, `Could not read subdirectory ${subPath}:`, err);
continue;
}
const subJsonFiles: string[] = [];
for (const subName of subEntries) {
const subFullPath = join(subPath, subName);
try {
if (statSync(subFullPath).isDirectory()) {
// Only one level of recursion — skip deeper nesting
continue;
}
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code !== "ENOENT") {
warn(logger, `Could not stat ${subFullPath}:`, err);
}
continue;
}
if (subName.endsWith(".json")) {
subJsonFiles.push(subName);
}
}
subJsonFiles.sort();
for (const subName of subJsonFiles) {
const filePath = join(subPath, subName);
fixtures.push(...loadFixtureFile(filePath, logger));
}
fixtures.push(...loadFixturesFromDir(join(dirPath, sub), logger));
}

return fixtures;
Expand Down
Loading