From e895e0c75381d2032c6e135d04b4b9a0095a1960 Mon Sep 17 00:00:00 2001 From: Jordan Ritter Date: Tue, 26 May 2026 17:39:20 -0700 Subject: [PATCH 1/3] fix: enable full recursive directory traversal in fixture loader The fixture loader previously capped recursion at 2 levels (root + immediate subdirectories). This was insufficient for the per-integration showcase layout where fixtures live at paths like d6/langgraph-python/chat-basic.json (3 levels deep). Replace the manual 2-level loop with a self-recursive call to loadFixturesFromDir(), which naturally walks the entire directory tree. Backward compatible: all existing single-file and 2-level callers continue to work identically. --- src/__tests__/fixture-loader.test.ts | 45 ++++++++++++++++++++++++++-- src/fixture-loader.ts | 37 ++--------------------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/__tests__/fixture-loader.test.ts b/src/__tests__/fixture-loader.test.ts index fca5178..cc0d0b4 100644 --- a/src/__tests__/fixture-loader.test.ts +++ b/src/__tests__/fixture-loader.test.ts @@ -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); @@ -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", () => { diff --git a/src/fixture-loader.ts b/src/fixture-loader.ts index 4f80b06..5caa8d1 100644 --- a/src/fixture-loader.ts +++ b/src/fixture-loader.ts @@ -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 //.json. + // Recurse into all subdirectories (full depth) to support nested layouts + // like showcase/aimock/d6//.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; From d9f5b38ae8d4e1803bb2f11dd838a3d8e9fa4dca Mon Sep 17 00:00:00 2001 From: Jordan Ritter Date: Tue, 26 May 2026 17:42:38 -0700 Subject: [PATCH 2/3] fix: add git binary to production Docker image The Railway startCommand now uses git sparse-checkout to fetch showcase fixtures at boot. node:22-alpine does not include git, so this adds apk add --no-cache git (~15MB) to the production stage. --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1f7e0ec..e27bdf5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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/ From b54125fc931bad1866c9e46c69f1790746e813e9 Mon Sep 17 00:00:00 2001 From: Jordan Ritter Date: Tue, 26 May 2026 17:43:06 -0700 Subject: [PATCH 3/3] chore: release v1.27.2 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 10 ++++++++++ charts/aimock/Chart.yaml | 2 +- package.json | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3bc3de6..c1e0f55 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index c80e672..ebcfd5e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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" diff --git a/CHANGELOG.md b/CHANGELOG.md index c46c481..289cc58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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//.json`. + +### Changed + +- **Docker image** — added `git` binary to production stage for sparse-checkout fixture fetching at boot. + ## [1.27.1] - 2026-05-22 ### Fixed diff --git a/charts/aimock/Chart.yaml b/charts/aimock/Chart.yaml index 5d39bd6..a2c505b 100644 --- a/charts/aimock/Chart.yaml +++ b/charts/aimock/Chart.yaml @@ -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" diff --git a/package.json b/package.json index 9f9c624..25d05b3 100644 --- a/package.json +++ b/package.json @@ -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": [