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.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"
}
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.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"
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

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.0"
appVersion: "1.27.1"
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.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": [
Expand Down
4 changes: 3 additions & 1 deletion src/__tests__/agui-mock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ function postRaw(

function aguiInput(userMessage: string, extra?: Partial<AGUIRunAgentInput>): AGUIRunAgentInput {
return {
messages: [{ role: "user", content: userMessage }],
threadId: "test-thread",
runId: "test-run",
messages: [{ id: "msg-1", role: "user", content: userMessage }],
...extra,
};
}
Expand Down
12 changes: 10 additions & 2 deletions src/__tests__/drift/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down
23 changes: 20 additions & 3 deletions src/agui-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<void> {
const delayMs = opts?.delayMs ?? 0;

Expand All @@ -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;
}
Expand Down
11 changes: 11 additions & 0 deletions src/agui-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
18 changes: 17 additions & 1 deletion src/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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: {
Expand Down
Loading