diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 36bd608..3bc3de6 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "source": { "source": "npm", "package": "@copilotkit/aimock", - "version": "^1.27.0" + "version": "^1.27.1" }, "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 c02577c..c80e672 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "aimock", - "version": "1.27.0", + "version": "1.27.1", "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 93a0892..c46c481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [1.27.1] - 2026-05-22 + ### Fixed - **Router** — systemMessage array exact-match logic was unsatisfiable for 2+ needles; collapsed to substring matching. Added `elevenlabs-tts` and `translation` to endpoint compatibility filter. @@ -13,6 +15,10 @@ - **Helpers** — extended `resolveUsage` with Gemini-native token fields. Preserved error cause in `resolveResponse` factory rethrow. `buildEmbeddingResponse` accepts optional usage. `extractFormField` escapes regex metacharacters. - **Drift test infra** — retry logging with body consumption, broadened `redactUrl` to cover `api_key`/`apikey`/`token`/`access_token` patterns, URL threaded into error messages with redaction, `parseDataOnlySSE` [DONE] filter fix, `parseTypedSSE` multi-line data handling with null guards. - **Drift collector** — invoke vitest directly via npx to avoid pnpm stdout prefix breaking JSON parse; classify raw stack traces as infrastructure errors instead of crashing. +- **AG-UI config loader** — removed `/.*/` catch-all regex fallback when `match.message` is absent; fixtures without a message pattern no longer shadow other fixtures. +- **AG-UI input validation** — runtime check that `input.messages` is an array after JSON parse; returns 400 instead of confusing downstream 404. +- **AG-UI SSE writer** — `writeAGUIEventStream` uses logger abstraction instead of `console.warn`; handles non-Error throws. +- **Drift test helpers** — `parseDataOnlySSE` handles multi-line data blocks, aligned with `providers.ts` implementation. ## [1.27.0] - 2026-05-20 diff --git a/charts/aimock/Chart.yaml b/charts/aimock/Chart.yaml index 0de02f9..5d39bd6 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.0" +appVersion: "1.27.1" diff --git a/package.json b/package.json index ecf94fb..9f9c624 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@copilotkit/aimock", - "version": "1.27.0", + "version": "1.27.1", "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": [ diff --git a/src/__tests__/agui-mock.test.ts b/src/__tests__/agui-mock.test.ts index a78cb23..75d9928 100644 --- a/src/__tests__/agui-mock.test.ts +++ b/src/__tests__/agui-mock.test.ts @@ -102,7 +102,9 @@ function postRaw( function aguiInput(userMessage: string, extra?: Partial): AGUIRunAgentInput { return { - messages: [{ role: "user", content: userMessage }], + threadId: "test-thread", + runId: "test-run", + messages: [{ id: "msg-1", role: "user", content: userMessage }], ...extra, }; } diff --git a/src/__tests__/drift/helpers.ts b/src/__tests__/drift/helpers.ts index 5b1cbb4..b8ff82b 100644 --- a/src/__tests__/drift/helpers.ts +++ b/src/__tests__/drift/helpers.ts @@ -87,9 +87,17 @@ export async function httpPostRaw( /** Parse data-only SSE blocks (OpenAI Chat Completions, Gemini). */ export function parseDataOnlySSE(body: string): object[] { return body + .replace(/\r\n/g, "\n") .split("\n\n") - .filter((block) => block.startsWith("data: ") && !block.includes("[DONE]")) - .map((block) => JSON.parse(block.slice(6))); + .filter((block) => block.startsWith("data: ") && block.trim() !== "data: [DONE]") + .map((block) => { + // Rejoin continuation lines (data split across multiple lines) + const json = block + .split("\n") + .map((line) => (line.startsWith("data: ") ? line.slice(6) : line)) + .join(""); + return JSON.parse(json); + }); } /** Parse typed SSE blocks with event: + data: (Anthropic, OpenAI Responses). */ diff --git a/src/agui-handler.ts b/src/agui-handler.ts index acdc115..1c5606c 100644 --- a/src/agui-handler.ts +++ b/src/agui-handler.ts @@ -5,6 +5,7 @@ import * as http from "node:http"; import { randomUUID } from "node:crypto"; +import type { Logger } from "./logger.js"; import type { AGUIRunAgentInput, AGUIFixtureMatch, @@ -597,7 +598,7 @@ export function buildReasoningEncryptedValue( export async function writeAGUIEventStream( res: http.ServerResponse, events: AGUIEvent[], - opts?: { delayMs?: number; signal?: AbortSignal }, + opts?: { delayMs?: number; signal?: AbortSignal; logger?: Logger }, ): Promise { const delayMs = opts?.delayMs ?? 0; @@ -616,9 +617,25 @@ export async function writeAGUIEventStream( res.write(`data: ${JSON.stringify(stamped)}\n\n`); } catch (err) { if (err instanceof TypeError || err instanceof RangeError) { - console.warn("AG-UI SSE write failed (serialization):", (err as Error).message); + const msg = (err as Error).message; + if (opts?.logger) { + opts.logger.warn("AG-UI SSE write failed (serialization):", msg); + } else { + console.warn("AG-UI SSE write failed (serialization):", msg); + } } else if (err instanceof Error) { - console.warn("AG-UI SSE write failed:", err.message); + if (opts?.logger) { + opts.logger.warn("AG-UI SSE write failed:", err.message); + } else { + console.warn("AG-UI SSE write failed:", err.message); + } + } else { + const msg = String(err); + if (opts?.logger) { + opts.logger.warn("AG-UI SSE write failed:", msg); + } else { + console.warn("AG-UI SSE write failed:", msg); + } } break; } diff --git a/src/agui-mock.ts b/src/agui-mock.ts index 192b2f8..2da50e1 100644 --- a/src/agui-mock.ts +++ b/src/agui-mock.ts @@ -169,6 +169,17 @@ export class AGUIMock implements Mountable { return true; } + if (input.messages !== undefined && !Array.isArray(input.messages)) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: "Invalid input: 'messages' must be an array when provided", + }), + ); + this.journalRequest(req, pathname, 400); + return true; + } + const fixture = findFixture(input, this.fixtures); if (fixture) { diff --git a/src/config-loader.ts b/src/config-loader.ts index 2c26f71..a02e450 100644 --- a/src/config-loader.ts +++ b/src/config-loader.ts @@ -8,6 +8,7 @@ import type { ChaosConfig, RecordConfig } from "./types.js"; import type { MCPToolDefinition, MCPPromptDefinition } from "./mcp-types.js"; import type { A2AAgentDefinition, A2APart, A2AArtifact, A2AStreamEvent } from "./a2a-types.js"; import type { AGUIEvent } from "./agui-types.js"; +import { buildTextResponse } from "./agui-handler.js"; import { VectorMock } from "./vector-mock.js"; import type { QueryResult } from "./vector-types.js"; import { Logger } from "./logger.js"; @@ -226,7 +227,22 @@ export async function startFromConfig( ); } if (f.text) { - agui.onMessage(f.match.message ?? /.*/, f.text, { delayMs: f.delayMs }); + if (f.match.message !== undefined) { + agui.onMessage(f.match.message, f.text, { delayMs: f.delayMs }); + } else { + // No message pattern — register via addFixture so it only matches + // on other criteria (toolCallId, toolName, stateKey) instead of + // becoming a catch-all that matches every request. + agui.addFixture({ + match: { + toolCallId: f.match.toolCallId, + toolName: f.match.toolName, + stateKey: f.match.stateKey, + }, + events: buildTextResponse(f.text), + delayMs: f.delayMs, + }); + } } else if (f.events) { agui.addFixture({ match: {