@copilotkit/aimock 1.15.1 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/fixture-loader.cjs +19 -4
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +19 -4
- package/dist/fixture-loader.js.map +1 -1
- package/dist/journal.cjs +1 -1
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +1 -1
- package/dist/journal.js.map +1 -1
- package/dist/llmock.cjs +6 -0
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +1 -0
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +1 -0
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +6 -0
- package/dist/llmock.js.map +1 -1
- package/dist/router.cjs +6 -0
- package/dist/router.cjs.map +1 -1
- package/dist/router.js +6 -0
- package/dist/router.js.map +1 -1
- package/dist/types.d.cts +4 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/write-fixtures/SKILL.md +75 -49
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @copilotkit/aimock
|
|
2
2
|
|
|
3
|
+
## 1.16.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`turnIndex` match criterion**: Stateless conversation-depth matching — counts `role: "assistant"` messages in the request's message array. Use for multi-turn conversation flows in shared/deployed instances where `sequenceIndex` counters break under concurrency. `turnIndex: 0` matches the first turn (no prior assistant messages), `turnIndex: 1` the second, etc.
|
|
8
|
+
- **`hasToolResult` match criterion**: Stateless boolean — `true` when any `role: "tool"` message exists in the request, `false` when none do. Simplest option for 2-step HITL flows (tool call → tool result → follow-up).
|
|
9
|
+
- `onTurn(turn, pattern, response, opts)` convenience method on the programmatic API.
|
|
10
|
+
|
|
3
11
|
## 1.15.1
|
|
4
12
|
|
|
5
13
|
### Fixed
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Run them all on one port with `npx @copilotkit/aimock --config aimock.json`, or
|
|
|
48
48
|
## Features
|
|
49
49
|
|
|
50
50
|
- **[Record & Replay](https://aimock.copilotkit.dev/record-replay)** — Proxy real APIs, save as fixtures, replay deterministically forever
|
|
51
|
-
- **[Multi-turn Conversations](https://aimock.copilotkit.dev/multi-turn)** — Record and replay multi-turn traces with tool rounds; match distinct turns via `toolCallId`, `sequenceIndex`, or custom predicates
|
|
51
|
+
- **[Multi-turn Conversations](https://aimock.copilotkit.dev/multi-turn)** — Record and replay multi-turn traces with tool rounds; match distinct turns via `turnIndex`, `hasToolResult`, `toolCallId`, `sequenceIndex`, or custom predicates
|
|
52
52
|
- **[11 LLM Providers](https://aimock.copilotkit.dev/docs)** — OpenAI Chat, OpenAI Responses, OpenAI Realtime, Claude, Gemini, Gemini Live, Azure, Bedrock, Vertex AI, Ollama, Cohere — full streaming support
|
|
53
53
|
- **Multimedia APIs** — [image generation](https://aimock.copilotkit.dev/images) (DALL-E, Imagen), [text-to-speech](https://aimock.copilotkit.dev/speech), [audio transcription](https://aimock.copilotkit.dev/transcription), [video generation](https://aimock.copilotkit.dev/video)
|
|
54
54
|
- **[MCP](https://aimock.copilotkit.dev/mcp-mock) / [A2A](https://aimock.copilotkit.dev/a2a-mock) / [AG-UI](https://aimock.copilotkit.dev/agui-mock) / [Vector](https://aimock.copilotkit.dev/vector-mock)** — Mock every protocol your AI agents use
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-loader.d.cts","names":[],"sources":["../src/config-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAciB,aAAA,SAAsB;;AAAvC;AAIiB,UAAA,iBAAA,CAAiB;EASjB,GAAA,EAAA,MAAA;EAAgB,IAAA,EAAA,MAAA;UAEnB,CAAA,EAAA,MAAA;aAF2B,CAAA,EAAA,MAAA;EAAmB,IAAA,CAAA,EAAA,MAAA;EAM3C,IAAA,CAAA,EAAA,MAAS;;AAGhB,UATO,eAAA,SAAwB,mBAS/B,CAAA;QACI,CAAA,EAAA;IACF,QAAA,EATE,KASF,CAAA;MAAe,IAAA,EAAA,MAAA;MAGV,OAAA,EAAA;QAAgB,IAAA,EAAA,MAAA;QAEvB,IAAA,EAAA,MAAA;MACI,CAAA;IACH,CAAA,CAAA;EAAc,CAAA;AAIzB;AAAgC,UAhBf,SAAA,CAgBe;MACnB,CAAA,EAAA,MAAA;YACH,CAAA,EAAA;IACS,IAAA,EAAA,MAAA;IAHqB,OAAA,EAAA,MAAA;EAAkB,CAAA;EAMzC,KAAA,CAAA,EAnBP,aAmBgB,
|
|
1
|
+
{"version":3,"file":"config-loader.d.cts","names":[],"sources":["../src/config-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAciB,aAAA,SAAsB;;AAAvC;AAIiB,UAAA,iBAAA,CAAiB;EASjB,GAAA,EAAA,MAAA;EAAgB,IAAA,EAAA,MAAA;UAEnB,CAAA,EAAA,MAAA;aAF2B,CAAA,EAAA,MAAA;EAAmB,IAAA,CAAA,EAAA,MAAA;EAM3C,IAAA,CAAA,EAAA,MAAS;;AAGhB,UATO,eAAA,SAAwB,mBAS/B,CAAA;QACI,CAAA,EAAA;IACF,QAAA,EATE,KASF,CAAA;MAAe,IAAA,EAAA,MAAA;MAGV,OAAA,EAAA;QAAgB,IAAA,EAAA,MAAA;QAEvB,IAAA,EAAA,MAAA;MACI,CAAA;IACH,CAAA,CAAA;EAAc,CAAA;AAIzB;AAAgC,UAhBf,SAAA,CAgBe;MACnB,CAAA,EAAA,MAAA;YACH,CAAA,EAAA;IACS,IAAA,EAAA,MAAA;IAHqB,OAAA,EAAA,MAAA;EAAkB,CAAA;EAMzC,KAAA,CAAA,EAnBP,aAmBgB,EAAA;EAKT,SAAA,CAAA,EAvBH,iBAuBoB,EAAA;EAOjB,OAAA,CAAA,EA7BL,eA+BC,EAAA;AAGb;AAAuC,UA/BtB,gBAAA,CA+BsB;SAMxB,EAAA,MAAA;OAHH,CAAA,EAhCF,OAgCE,EAAA;WAKK,CAAA,EApCH,WAoCG,EAAA;EAAW,MAAA,CAAA,EAnCjB,cAmCiB,EAAA;EAGX,OAAA,CAAA,EAAA,MAAY;AAK7B;AAA6B,UAvCZ,cAAA,SAAuB,kBAuCX,CAAA;UAGjB,CAAA,EAzCC,gBAyCD,EAAA;OACC,CAAA,EAzCH,gBAyCG,EAAA;gBAEL,CAAA,EA1CW,gBA0CX,EAAA;;AAEC,UAzCQ,SAAA,CAyCR;MACE,CAAA,EAAA,MAAA;EAAY,MAAA,CAAA,EAxCZ,cAwCY,EAAA;AAQvB;AAKsB,UAlDL,iBAAA,CAkDoB;EAAA,KAAA,EAAA;IAC3B,OAAA,CAAA,EAAA,MAAA;IAEW,QAAA,CAAA,EAAA,MAAA;IAAlB,QAAA,CAAA,EAAA,MAAA;EAAO,CAAA;;WAlDC;;;UAIM,UAAA;;aAEJ;;UAGI,sBAAA;;;YAGL;;;eAGG;;iBAEE;;UAGA,YAAA;;gBAED;;UAGC,YAAA;;;YAGL;aACC;;QAEL;QACA;SACC;WACE;;;;;;;;;;;iBAQK,UAAA,sBAAgC;iBAK1B,eAAA,SACZ;;;IAEP;UAAkB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-loader.d.ts","names":[],"sources":["../src/config-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAciB,aAAA,SAAsB;;AAAvC;AAIiB,UAAA,iBAAA,CAAiB;EASjB,GAAA,EAAA,MAAA;EAAgB,IAAA,EAAA,MAAA;UAEnB,CAAA,EAAA,MAAA;aAF2B,CAAA,EAAA,MAAA;EAAmB,IAAA,CAAA,EAAA,MAAA;EAM3C,IAAA,CAAA,EAAA,MAAS;;AAGhB,UATO,eAAA,SAAwB,mBAS/B,CAAA;QACI,CAAA,EAAA;IACF,QAAA,EATE,KASF,CAAA;MAAe,IAAA,EAAA,MAAA;MAGV,OAAA,EAAA;QAAgB,IAAA,EAAA,MAAA;QAEvB,IAAA,EAAA,MAAA;MACI,CAAA;IACH,CAAA,CAAA;EAAc,CAAA;AAIzB;AAAgC,UAhBf,SAAA,CAgBe;MACnB,CAAA,EAAA,MAAA;YACH,CAAA,EAAA;IACS,IAAA,EAAA,MAAA;IAHqB,OAAA,EAAA,MAAA;EAAkB,CAAA;EAMzC,KAAA,CAAA,EAnBP,aAmBgB,
|
|
1
|
+
{"version":3,"file":"config-loader.d.ts","names":[],"sources":["../src/config-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAciB,aAAA,SAAsB;;AAAvC;AAIiB,UAAA,iBAAA,CAAiB;EASjB,GAAA,EAAA,MAAA;EAAgB,IAAA,EAAA,MAAA;UAEnB,CAAA,EAAA,MAAA;aAF2B,CAAA,EAAA,MAAA;EAAmB,IAAA,CAAA,EAAA,MAAA;EAM3C,IAAA,CAAA,EAAA,MAAS;;AAGhB,UATO,eAAA,SAAwB,mBAS/B,CAAA;QACI,CAAA,EAAA;IACF,QAAA,EATE,KASF,CAAA;MAAe,IAAA,EAAA,MAAA;MAGV,OAAA,EAAA;QAAgB,IAAA,EAAA,MAAA;QAEvB,IAAA,EAAA,MAAA;MACI,CAAA;IACH,CAAA,CAAA;EAAc,CAAA;AAIzB;AAAgC,UAhBf,SAAA,CAgBe;MACnB,CAAA,EAAA,MAAA;YACH,CAAA,EAAA;IACS,IAAA,EAAA,MAAA;IAHqB,OAAA,EAAA,MAAA;EAAkB,CAAA;EAMzC,KAAA,CAAA,EAnBP,aAmBgB,EAEf;EAGM,SAAA,CAAA,EAvBH,iBAuBoB,EAGvB;EAIM,OAAA,CAAA,EA7BL,eA+BC,EAAA;AAGb;AAAuC,UA/BtB,gBAAA,CA+BsB;SAMxB,EAAA,MAAA;OAHH,CAAA,EAhCF,OAgCE,EAAA;WAKK,CAAA,EApCH,WAoCG,EAAA;EAAW,MAAA,CAAA,EAnCjB,cAmCiB,EAAA;EAGX,OAAA,CAAA,EAAA,MAAY;AAK7B;AAA6B,UAvCZ,cAAA,SAAuB,kBAuCX,CAAA;UAGjB,CAAA,EAzCC,gBAyCD,EAAA;OACC,CAAA,EAzCH,gBAyCG,EAAA;gBAEL,CAAA,EA1CW,gBA0CX,EAAA;;AAEC,UAzCQ,SAAA,CAyCR;MACE,CAAA,EAAA,MAAA;EAAY,MAAA,CAAA,EAxCZ,cAwCY,EAAA;AAQvB;AAKsB,UAlDL,iBAAA,CAkDoB;EAAA,KAAA,EAAA;IAC3B,OAAA,CAAA,EAAA,MAAA;IAEW,QAAA,CAAA,EAAA,MAAA;IAAlB,QAAA,CAAA,EAAA,MAAA;EAAO,CAAA;;WAlDC;;;UAIM,UAAA;;aAEJ;;UAGI,sBAAA;;;YAGL;;;eAGG;;iBAEE;;UAGA,YAAA;;gBAED;;UAGC,YAAA;;;YAGL;aACC;;QAEL;QACA;SACC;WACE;;;;;;;;;;;iBAQK,UAAA,sBAAgC;iBAK1B,eAAA,SACZ;;;IAEP;UAAkB"}
|
package/dist/fixture-loader.cjs
CHANGED
|
@@ -31,7 +31,9 @@ function entryToFixture(entry) {
|
|
|
31
31
|
model: entry.match.model,
|
|
32
32
|
responseFormat: entry.match.responseFormat,
|
|
33
33
|
endpoint: entry.match.endpoint,
|
|
34
|
-
...entry.match.sequenceIndex !== void 0 && { sequenceIndex: entry.match.sequenceIndex }
|
|
34
|
+
...entry.match.sequenceIndex !== void 0 && { sequenceIndex: entry.match.sequenceIndex },
|
|
35
|
+
...entry.match.turnIndex !== void 0 && { turnIndex: entry.match.turnIndex },
|
|
36
|
+
...entry.match.hasToolResult !== void 0 && { hasToolResult: entry.match.hasToolResult }
|
|
35
37
|
},
|
|
36
38
|
response: normalizeResponse(entry.response),
|
|
37
39
|
...entry.latency !== void 0 && { latency: entry.latency },
|
|
@@ -342,18 +344,31 @@ function validateFixtures(fixtures) {
|
|
|
342
344
|
message: "chaos.disconnectRate must be between 0 and 1"
|
|
343
345
|
});
|
|
344
346
|
}
|
|
347
|
+
if (f.match.turnIndex !== void 0) {
|
|
348
|
+
if (typeof f.match.turnIndex !== "number" || f.match.turnIndex < 0 || !Number.isInteger(f.match.turnIndex)) results.push({
|
|
349
|
+
severity: "error",
|
|
350
|
+
fixtureIndex: i,
|
|
351
|
+
message: "match.turnIndex must be a non-negative integer"
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (f.match.hasToolResult !== void 0 && typeof f.match.hasToolResult !== "boolean") results.push({
|
|
355
|
+
severity: "error",
|
|
356
|
+
fixtureIndex: i,
|
|
357
|
+
message: `match.hasToolResult must be a boolean, got ${typeof f.match.hasToolResult}`
|
|
358
|
+
});
|
|
345
359
|
const um = f.match.userMessage;
|
|
346
360
|
if (typeof um === "string" && um) {
|
|
347
|
-
const
|
|
361
|
+
const dedupKey = `${um}|${f.match.turnIndex}|${f.match.hasToolResult}|${f.match.sequenceIndex}`;
|
|
362
|
+
const prev = seenUserMessages.get(dedupKey);
|
|
348
363
|
if (prev !== void 0) results.push({
|
|
349
364
|
severity: "warning",
|
|
350
365
|
fixtureIndex: i,
|
|
351
366
|
message: `duplicate userMessage '${um}' — shadows fixture ${prev}`
|
|
352
367
|
});
|
|
353
|
-
else seenUserMessages.set(
|
|
368
|
+
else seenUserMessages.set(dedupKey, i);
|
|
354
369
|
}
|
|
355
370
|
const match = f.match;
|
|
356
|
-
if (!(match.endpoint !== void 0 || match.userMessage !== void 0 || match.inputText !== void 0 || match.responseFormat !== void 0 || match.toolCallId !== void 0 || match.toolName !== void 0 || match.model !== void 0 || match.predicate !== void 0) && i < fixtures.length - 1) results.push({
|
|
371
|
+
if (!(match.endpoint !== void 0 || match.userMessage !== void 0 || match.inputText !== void 0 || match.responseFormat !== void 0 || match.toolCallId !== void 0 || match.toolName !== void 0 || match.model !== void 0 || match.predicate !== void 0 || match.turnIndex !== void 0 || match.hasToolResult !== void 0) && i < fixtures.length - 1) results.push({
|
|
357
372
|
severity: "warning",
|
|
358
373
|
fixtureIndex: i,
|
|
359
374
|
message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixture-loader.cjs","names":["isContentWithToolCallsResponse","isTextResponse","isToolCallResponse","isErrorResponse","isEmbeddingResponse","isImageResponse","isAudioResponse","isTranscriptionResponse","isVideoResponse"],"sources":["../src/fixture-loader.ts"],"sourcesContent":["import { readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type {\n Fixture,\n FixtureFile,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureResponse,\n ResponseOverrides,\n} from \"./types.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isEmbeddingResponse,\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n} from \"./helpers.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Auto-stringify object-valued `content` and `toolCalls[].arguments` fields.\n * This lets fixture authors write plain JSON objects instead of escaped strings.\n * All other fields (including ResponseOverrides) pass through unmodified.\n */\nexport function normalizeResponse(raw: FixtureFileResponse): FixtureResponse {\n // Shallow-clone so we don't mutate the parsed JSON input.\n const response = { ...raw } as Record<string, unknown>;\n\n // Auto-stringify object content (e.g. structured output)\n if (typeof response.content === \"object\" && response.content !== null) {\n response.content = JSON.stringify(response.content);\n }\n\n // Auto-stringify object arguments in toolCalls\n if (Array.isArray(response.toolCalls)) {\n response.toolCalls = (response.toolCalls as Array<Record<string, unknown>>).map((tc) => {\n if (typeof tc.arguments === \"object\" && tc.arguments !== null) {\n return { ...tc, arguments: JSON.stringify(tc.arguments) };\n }\n return tc;\n });\n }\n\n return response as unknown as FixtureResponse;\n}\n\nexport function entryToFixture(entry: FixtureFileEntry): Fixture {\n return {\n match: {\n userMessage: entry.match.userMessage,\n inputText: entry.match.inputText,\n toolCallId: entry.match.toolCallId,\n toolName: entry.match.toolName,\n model: entry.match.model,\n responseFormat: entry.match.responseFormat,\n endpoint: entry.match.endpoint,\n ...(entry.match.sequenceIndex !== undefined && { sequenceIndex: entry.match.sequenceIndex }),\n },\n response: normalizeResponse(entry.response),\n ...(entry.latency !== undefined && { latency: entry.latency }),\n ...(entry.chunkSize !== undefined && { chunkSize: entry.chunkSize }),\n ...(entry.truncateAfterChunks !== undefined && {\n truncateAfterChunks: entry.truncateAfterChunks,\n }),\n ...(entry.disconnectAfterMs !== undefined && { disconnectAfterMs: entry.disconnectAfterMs }),\n ...(entry.streamingProfile !== undefined && { streamingProfile: entry.streamingProfile }),\n ...(entry.chaos !== undefined && { chaos: entry.chaos }),\n };\n}\n\n// Logging helper — uses logger if provided, falls back to console.warn.\nfunction warn(logger: Logger | undefined, msg: string, ...rest: unknown[]): void {\n if (logger) {\n logger.warn(msg, ...rest);\n } else {\n console.warn(`[fixture-loader] ${msg}`, ...rest);\n }\n}\n\nexport function loadFixtureFile(filePath: string, logger?: Logger): Fixture[] {\n let raw: string;\n try {\n raw = readFileSync(filePath, \"utf-8\");\n } catch (err) {\n warn(logger, `Could not read file ${filePath}:`, err);\n return [];\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n warn(logger, `Invalid JSON in ${filePath}:`, err);\n return [];\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !Array.isArray((parsed as FixtureFile).fixtures)\n ) {\n warn(logger, `Missing or invalid \"fixtures\" array in ${filePath}`);\n return [];\n }\n\n return (parsed as FixtureFile).fixtures.map(entryToFixture);\n}\n\nexport function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[] {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n warn(logger, `Could not read directory ${dirPath}:`, err);\n return [];\n }\n\n const jsonFiles: string[] = [];\n for (const name of entries) {\n const fullPath = join(dirPath, name);\n try {\n if (statSync(fullPath).isDirectory()) {\n warn(logger, `Skipping subdirectory ${fullPath} (fixtures are not loaded recursively)`);\n continue;\n }\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n warn(logger, `Could not stat ${fullPath}:`, err);\n }\n continue;\n }\n if (name.endsWith(\".json\")) {\n jsonFiles.push(name);\n }\n }\n jsonFiles.sort();\n\n const fixtures: Fixture[] = [];\n for (const name of jsonFiles) {\n const filePath = join(dirPath, name);\n fixtures.push(...loadFixtureFile(filePath, logger));\n }\n\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture validation\n// ---------------------------------------------------------------------------\n\nexport interface ValidationResult {\n severity: \"error\" | \"warning\";\n fixtureIndex: number;\n message: string;\n}\n\nfunction validateReasoning(\n response: { reasoning?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.reasoning !== undefined) {\n if (typeof response.reasoning !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"reasoning must be a string\",\n });\n } else if (response.reasoning === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"reasoning is empty string — no reasoning events will be emitted\",\n });\n }\n }\n}\n\nfunction validateWebSearches(\n response: { webSearches?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.webSearches !== undefined) {\n if (!Array.isArray(response.webSearches)) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"webSearches must be an array of strings\",\n });\n } else if (response.webSearches.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"webSearches is empty array — no web search events will be emitted\",\n });\n } else {\n for (let j = 0; j < response.webSearches.length; j++) {\n if (typeof response.webSearches[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `webSearches[${j}] is not a string`,\n });\n break;\n }\n if (response.webSearches[j] === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: `webSearches[${j}] is empty string`,\n });\n }\n }\n }\n }\n}\n\nexport function validateFixtures(fixtures: Fixture[]): ValidationResult[] {\n const results: ValidationResult[] = [];\n\n const seenUserMessages = new Map<string, number>();\n\n for (let i = 0; i < fixtures.length; i++) {\n const f = fixtures[i];\n const response = f.response;\n\n // --- Error checks ---\n\n // Response type recognition\n // Note: isContentWithToolCallsResponse must be checked before isTextResponse\n // and isToolCallResponse since it is a structural superset of both.\n if (\n !isContentWithToolCallsResponse(response) &&\n !isTextResponse(response) &&\n !isToolCallResponse(response) &&\n !isErrorResponse(response) &&\n !isEmbeddingResponse(response) &&\n !isImageResponse(response) &&\n !isAudioResponse(response) &&\n !isTranscriptionResponse(response) &&\n !isVideoResponse(response)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message:\n \"response is not a recognized type (must have content, toolCalls, error, embedding, image, audio, transcription, or video)\",\n });\n }\n\n // Text response checks\n if (isTextResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // ContentWithToolCalls response checks\n if (isContentWithToolCallsResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // Tool call response checks\n if (isToolCallResponse(response)) {\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n }\n\n // Error response checks\n if (isErrorResponse(response)) {\n if (!response.error.message) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"error.message is empty\",\n });\n }\n if (response.status !== undefined && (response.status < 100 || response.status > 599)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code`,\n });\n }\n }\n\n // Embedding response checks\n if (isEmbeddingResponse(response)) {\n if (response.embedding.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"embedding array is empty\",\n });\n }\n for (let j = 0; j < response.embedding.length; j++) {\n if (typeof response.embedding[j] !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `embedding[${j}] is not a number`,\n });\n break; // one error is enough\n }\n }\n }\n\n // Validate ResponseOverrides fields\n if (\n isTextResponse(response) ||\n isToolCallResponse(response) ||\n isContentWithToolCallsResponse(response)\n ) {\n const r = response as ResponseOverrides;\n if (r.id !== undefined && typeof r.id !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"id\" must be a string, got ${typeof r.id}`,\n });\n }\n if (r.created !== undefined && (typeof r.created !== \"number\" || r.created < 0)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"created\" must be a non-negative number`,\n });\n }\n if (r.model !== undefined && typeof r.model !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"model\" must be a string, got ${typeof r.model}`,\n });\n }\n if (r.finishReason !== undefined && typeof r.finishReason !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"finishReason\" must be a string, got ${typeof r.finishReason}`,\n });\n }\n if (r.role !== undefined && typeof r.role !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"role\" must be a string, got ${typeof r.role}`,\n });\n }\n if (r.systemFingerprint !== undefined && typeof r.systemFingerprint !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"systemFingerprint\" must be a string, got ${typeof r.systemFingerprint}`,\n });\n }\n if (r.usage !== undefined) {\n if (typeof r.usage !== \"object\" || r.usage === null || Array.isArray(r.usage)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage\" must be an object`,\n });\n } else {\n // Check all known usage fields are numbers if present\n for (const key of Object.keys(r.usage)) {\n const val = (r.usage as Record<string, unknown>)[key];\n if (val !== undefined && typeof val !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}\" must be a number, got ${typeof val}`,\n });\n }\n }\n }\n }\n }\n\n // Numeric sanity checks\n if (f.latency !== undefined && f.latency < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"latency must be >= 0\",\n });\n }\n if (f.chunkSize !== undefined && f.chunkSize < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chunkSize must be >= 1\",\n });\n }\n if (f.truncateAfterChunks !== undefined && f.truncateAfterChunks < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"truncateAfterChunks must be >= 1\",\n });\n }\n if (f.disconnectAfterMs !== undefined && f.disconnectAfterMs < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"disconnectAfterMs must be >= 0\",\n });\n }\n if (f.streamingProfile !== undefined) {\n const sp = f.streamingProfile;\n if (sp.ttft !== undefined && sp.ttft < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.ttft must be >= 0\",\n });\n }\n if (sp.tps !== undefined && sp.tps <= 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.tps must be > 0\",\n });\n }\n if (sp.jitter !== undefined && (sp.jitter < 0 || sp.jitter > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.jitter must be between 0 and 1\",\n });\n }\n }\n if (f.chaos !== undefined) {\n const ch = f.chaos;\n if (ch.dropRate !== undefined && (ch.dropRate < 0 || ch.dropRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.dropRate must be between 0 and 1\",\n });\n }\n if (ch.malformedRate !== undefined && (ch.malformedRate < 0 || ch.malformedRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.malformedRate must be between 0 and 1\",\n });\n }\n if (ch.disconnectRate !== undefined && (ch.disconnectRate < 0 || ch.disconnectRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.disconnectRate must be between 0 and 1\",\n });\n }\n }\n\n // --- Warning checks ---\n\n // Duplicate userMessage shadowing\n const um = f.match.userMessage;\n if (typeof um === \"string\" && um) {\n const prev = seenUserMessages.get(um);\n if (prev !== undefined) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `duplicate userMessage '${um}' — shadows fixture ${prev}`,\n });\n } else {\n seenUserMessages.set(um, i);\n }\n }\n\n // Catch-all not in last position\n const match = f.match;\n const hasDiscriminator =\n match.endpoint !== undefined ||\n match.userMessage !== undefined ||\n match.inputText !== undefined ||\n match.responseFormat !== undefined ||\n match.toolCallId !== undefined ||\n match.toolName !== undefined ||\n match.model !== undefined ||\n match.predicate !== undefined;\n\n if (!hasDiscriminator && i < fixtures.length - 1) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fixture-loader.cjs","names":["isContentWithToolCallsResponse","isTextResponse","isToolCallResponse","isErrorResponse","isEmbeddingResponse","isImageResponse","isAudioResponse","isTranscriptionResponse","isVideoResponse"],"sources":["../src/fixture-loader.ts"],"sourcesContent":["import { readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type {\n Fixture,\n FixtureFile,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureResponse,\n ResponseOverrides,\n} from \"./types.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isEmbeddingResponse,\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n} from \"./helpers.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Auto-stringify object-valued `content` and `toolCalls[].arguments` fields.\n * This lets fixture authors write plain JSON objects instead of escaped strings.\n * All other fields (including ResponseOverrides) pass through unmodified.\n */\nexport function normalizeResponse(raw: FixtureFileResponse): FixtureResponse {\n // Shallow-clone so we don't mutate the parsed JSON input.\n const response = { ...raw } as Record<string, unknown>;\n\n // Auto-stringify object content (e.g. structured output)\n if (typeof response.content === \"object\" && response.content !== null) {\n response.content = JSON.stringify(response.content);\n }\n\n // Auto-stringify object arguments in toolCalls\n if (Array.isArray(response.toolCalls)) {\n response.toolCalls = (response.toolCalls as Array<Record<string, unknown>>).map((tc) => {\n if (typeof tc.arguments === \"object\" && tc.arguments !== null) {\n return { ...tc, arguments: JSON.stringify(tc.arguments) };\n }\n return tc;\n });\n }\n\n return response as unknown as FixtureResponse;\n}\n\nexport function entryToFixture(entry: FixtureFileEntry): Fixture {\n return {\n match: {\n userMessage: entry.match.userMessage,\n inputText: entry.match.inputText,\n toolCallId: entry.match.toolCallId,\n toolName: entry.match.toolName,\n model: entry.match.model,\n responseFormat: entry.match.responseFormat,\n endpoint: entry.match.endpoint,\n ...(entry.match.sequenceIndex !== undefined && { sequenceIndex: entry.match.sequenceIndex }),\n ...(entry.match.turnIndex !== undefined && {\n turnIndex: entry.match.turnIndex,\n }),\n ...(entry.match.hasToolResult !== undefined && {\n hasToolResult: entry.match.hasToolResult,\n }),\n },\n response: normalizeResponse(entry.response),\n ...(entry.latency !== undefined && { latency: entry.latency }),\n ...(entry.chunkSize !== undefined && { chunkSize: entry.chunkSize }),\n ...(entry.truncateAfterChunks !== undefined && {\n truncateAfterChunks: entry.truncateAfterChunks,\n }),\n ...(entry.disconnectAfterMs !== undefined && { disconnectAfterMs: entry.disconnectAfterMs }),\n ...(entry.streamingProfile !== undefined && { streamingProfile: entry.streamingProfile }),\n ...(entry.chaos !== undefined && { chaos: entry.chaos }),\n };\n}\n\n// Logging helper — uses logger if provided, falls back to console.warn.\nfunction warn(logger: Logger | undefined, msg: string, ...rest: unknown[]): void {\n if (logger) {\n logger.warn(msg, ...rest);\n } else {\n console.warn(`[fixture-loader] ${msg}`, ...rest);\n }\n}\n\nexport function loadFixtureFile(filePath: string, logger?: Logger): Fixture[] {\n let raw: string;\n try {\n raw = readFileSync(filePath, \"utf-8\");\n } catch (err) {\n warn(logger, `Could not read file ${filePath}:`, err);\n return [];\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n warn(logger, `Invalid JSON in ${filePath}:`, err);\n return [];\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !Array.isArray((parsed as FixtureFile).fixtures)\n ) {\n warn(logger, `Missing or invalid \"fixtures\" array in ${filePath}`);\n return [];\n }\n\n return (parsed as FixtureFile).fixtures.map(entryToFixture);\n}\n\nexport function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[] {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n warn(logger, `Could not read directory ${dirPath}:`, err);\n return [];\n }\n\n const jsonFiles: string[] = [];\n for (const name of entries) {\n const fullPath = join(dirPath, name);\n try {\n if (statSync(fullPath).isDirectory()) {\n warn(logger, `Skipping subdirectory ${fullPath} (fixtures are not loaded recursively)`);\n continue;\n }\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n warn(logger, `Could not stat ${fullPath}:`, err);\n }\n continue;\n }\n if (name.endsWith(\".json\")) {\n jsonFiles.push(name);\n }\n }\n jsonFiles.sort();\n\n const fixtures: Fixture[] = [];\n for (const name of jsonFiles) {\n const filePath = join(dirPath, name);\n fixtures.push(...loadFixtureFile(filePath, logger));\n }\n\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture validation\n// ---------------------------------------------------------------------------\n\nexport interface ValidationResult {\n severity: \"error\" | \"warning\";\n fixtureIndex: number;\n message: string;\n}\n\nfunction validateReasoning(\n response: { reasoning?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.reasoning !== undefined) {\n if (typeof response.reasoning !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"reasoning must be a string\",\n });\n } else if (response.reasoning === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"reasoning is empty string — no reasoning events will be emitted\",\n });\n }\n }\n}\n\nfunction validateWebSearches(\n response: { webSearches?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.webSearches !== undefined) {\n if (!Array.isArray(response.webSearches)) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"webSearches must be an array of strings\",\n });\n } else if (response.webSearches.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"webSearches is empty array — no web search events will be emitted\",\n });\n } else {\n for (let j = 0; j < response.webSearches.length; j++) {\n if (typeof response.webSearches[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `webSearches[${j}] is not a string`,\n });\n break;\n }\n if (response.webSearches[j] === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: `webSearches[${j}] is empty string`,\n });\n }\n }\n }\n }\n}\n\nexport function validateFixtures(fixtures: Fixture[]): ValidationResult[] {\n const results: ValidationResult[] = [];\n\n const seenUserMessages = new Map<string, number>();\n\n for (let i = 0; i < fixtures.length; i++) {\n const f = fixtures[i];\n const response = f.response;\n\n // --- Error checks ---\n\n // Response type recognition\n // Note: isContentWithToolCallsResponse must be checked before isTextResponse\n // and isToolCallResponse since it is a structural superset of both.\n if (\n !isContentWithToolCallsResponse(response) &&\n !isTextResponse(response) &&\n !isToolCallResponse(response) &&\n !isErrorResponse(response) &&\n !isEmbeddingResponse(response) &&\n !isImageResponse(response) &&\n !isAudioResponse(response) &&\n !isTranscriptionResponse(response) &&\n !isVideoResponse(response)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message:\n \"response is not a recognized type (must have content, toolCalls, error, embedding, image, audio, transcription, or video)\",\n });\n }\n\n // Text response checks\n if (isTextResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // ContentWithToolCalls response checks\n if (isContentWithToolCallsResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // Tool call response checks\n if (isToolCallResponse(response)) {\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n }\n\n // Error response checks\n if (isErrorResponse(response)) {\n if (!response.error.message) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"error.message is empty\",\n });\n }\n if (response.status !== undefined && (response.status < 100 || response.status > 599)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code`,\n });\n }\n }\n\n // Embedding response checks\n if (isEmbeddingResponse(response)) {\n if (response.embedding.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"embedding array is empty\",\n });\n }\n for (let j = 0; j < response.embedding.length; j++) {\n if (typeof response.embedding[j] !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `embedding[${j}] is not a number`,\n });\n break; // one error is enough\n }\n }\n }\n\n // Validate ResponseOverrides fields\n if (\n isTextResponse(response) ||\n isToolCallResponse(response) ||\n isContentWithToolCallsResponse(response)\n ) {\n const r = response as ResponseOverrides;\n if (r.id !== undefined && typeof r.id !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"id\" must be a string, got ${typeof r.id}`,\n });\n }\n if (r.created !== undefined && (typeof r.created !== \"number\" || r.created < 0)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"created\" must be a non-negative number`,\n });\n }\n if (r.model !== undefined && typeof r.model !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"model\" must be a string, got ${typeof r.model}`,\n });\n }\n if (r.finishReason !== undefined && typeof r.finishReason !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"finishReason\" must be a string, got ${typeof r.finishReason}`,\n });\n }\n if (r.role !== undefined && typeof r.role !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"role\" must be a string, got ${typeof r.role}`,\n });\n }\n if (r.systemFingerprint !== undefined && typeof r.systemFingerprint !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"systemFingerprint\" must be a string, got ${typeof r.systemFingerprint}`,\n });\n }\n if (r.usage !== undefined) {\n if (typeof r.usage !== \"object\" || r.usage === null || Array.isArray(r.usage)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage\" must be an object`,\n });\n } else {\n // Check all known usage fields are numbers if present\n for (const key of Object.keys(r.usage)) {\n const val = (r.usage as Record<string, unknown>)[key];\n if (val !== undefined && typeof val !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}\" must be a number, got ${typeof val}`,\n });\n }\n }\n }\n }\n }\n\n // Numeric sanity checks\n if (f.latency !== undefined && f.latency < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"latency must be >= 0\",\n });\n }\n if (f.chunkSize !== undefined && f.chunkSize < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chunkSize must be >= 1\",\n });\n }\n if (f.truncateAfterChunks !== undefined && f.truncateAfterChunks < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"truncateAfterChunks must be >= 1\",\n });\n }\n if (f.disconnectAfterMs !== undefined && f.disconnectAfterMs < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"disconnectAfterMs must be >= 0\",\n });\n }\n if (f.streamingProfile !== undefined) {\n const sp = f.streamingProfile;\n if (sp.ttft !== undefined && sp.ttft < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.ttft must be >= 0\",\n });\n }\n if (sp.tps !== undefined && sp.tps <= 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.tps must be > 0\",\n });\n }\n if (sp.jitter !== undefined && (sp.jitter < 0 || sp.jitter > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.jitter must be between 0 and 1\",\n });\n }\n }\n if (f.chaos !== undefined) {\n const ch = f.chaos;\n if (ch.dropRate !== undefined && (ch.dropRate < 0 || ch.dropRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.dropRate must be between 0 and 1\",\n });\n }\n if (ch.malformedRate !== undefined && (ch.malformedRate < 0 || ch.malformedRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.malformedRate must be between 0 and 1\",\n });\n }\n if (ch.disconnectRate !== undefined && (ch.disconnectRate < 0 || ch.disconnectRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.disconnectRate must be between 0 and 1\",\n });\n }\n }\n\n // Match field type checks\n if (f.match.turnIndex !== undefined) {\n if (\n typeof f.match.turnIndex !== \"number\" ||\n f.match.turnIndex < 0 ||\n !Number.isInteger(f.match.turnIndex)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.turnIndex must be a non-negative integer\",\n });\n }\n }\n if (f.match.hasToolResult !== undefined && typeof f.match.hasToolResult !== \"boolean\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.hasToolResult must be a boolean, got ${typeof f.match.hasToolResult}`,\n });\n }\n\n // --- Warning checks ---\n\n // Duplicate userMessage shadowing — include turnIndex, hasToolResult, and\n // sequenceIndex in the dedup key so that fixtures which share a userMessage\n // but differ on those fields are NOT considered duplicates.\n const um = f.match.userMessage;\n if (typeof um === \"string\" && um) {\n const dedupKey = `${um}|${f.match.turnIndex}|${f.match.hasToolResult}|${f.match.sequenceIndex}`;\n const prev = seenUserMessages.get(dedupKey);\n if (prev !== undefined) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `duplicate userMessage '${um}' — shadows fixture ${prev}`,\n });\n } else {\n seenUserMessages.set(dedupKey, i);\n }\n }\n\n // Catch-all not in last position\n const match = f.match;\n const hasDiscriminator =\n match.endpoint !== undefined ||\n match.userMessage !== undefined ||\n match.inputText !== undefined ||\n match.responseFormat !== undefined ||\n match.toolCallId !== undefined ||\n match.toolName !== undefined ||\n match.model !== undefined ||\n match.predicate !== undefined ||\n match.turnIndex !== undefined ||\n match.hasToolResult !== undefined;\n\n if (!hasDiscriminator && i < fixtures.length - 1) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixture-loader.d.cts","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;AA4BA;;;AAA6D,iBAA7C,iBAAA,CAA6C,GAAA,EAAtB,mBAAsB,CAAA,EAAA,eAAA;
|
|
1
|
+
{"version":3,"file":"fixture-loader.d.cts","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;AA4BA;;;AAA6D,iBAA7C,iBAAA,CAA6C,GAAA,EAAtB,mBAAsB,CAAA,EAAA,eAAA;AA6D7C,iBAAA,eAAA,CAAe,QAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAA4B,MAA5B,CAAA,EAAqC,OAArC,EAAA;AAAA,iBA6Bf,mBAAA,CA7Be,OAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA6B+B,MA7B/B,CAAA,EA6BwC,OA7BxC,EAAA;AAA4B,UAwE1C,gBAAA,CAxE0C;UAAS,EAAA,OAAA,GAAA,SAAA;EAAO,YAAA,EAAA,MAAA;EA6B3D,OAAA,EAAA,MAAA;;AAA8C,iBA+G9C,gBAAA,CA/G8C,QAAA,EA+GnB,OA/GmB,EAAA,CAAA,EA+GP,gBA/GO,EAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixture-loader.d.ts","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;AA4BA;;;AAA6D,iBAA7C,iBAAA,CAA6C,GAAA,EAAtB,mBAAsB,CAAA,EAAA,eAAA;
|
|
1
|
+
{"version":3,"file":"fixture-loader.d.ts","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;AA4BA;;;AAA6D,iBAA7C,iBAAA,CAA6C,GAAA,EAAtB,mBAAsB,CAAA,EAAA,eAAA;AA6D7C,iBAAA,eAAA,CAAe,QAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAA4B,MAA5B,CAAA,EAAqC,OAArC,EAAA;AAAA,iBA6Bf,mBAAA,CA7Be,OAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA6B+B,MA7B/B,CAAA,EA6BwC,OA7BxC,EAAA;AAA4B,UAwE1C,gBAAA,CAxE0C;UAAS,EAAA,OAAA,GAAA,SAAA;EAAO,YAAA,EAAA,MAAA;EA6B3D,OAAA,EAAA,MAAA;;AAA8C,iBA+G9C,gBAAA,CA/G8C,QAAA,EA+GnB,OA/GmB,EAAA,CAAA,EA+GP,gBA/GO,EAAA"}
|
package/dist/fixture-loader.js
CHANGED
|
@@ -30,7 +30,9 @@ function entryToFixture(entry) {
|
|
|
30
30
|
model: entry.match.model,
|
|
31
31
|
responseFormat: entry.match.responseFormat,
|
|
32
32
|
endpoint: entry.match.endpoint,
|
|
33
|
-
...entry.match.sequenceIndex !== void 0 && { sequenceIndex: entry.match.sequenceIndex }
|
|
33
|
+
...entry.match.sequenceIndex !== void 0 && { sequenceIndex: entry.match.sequenceIndex },
|
|
34
|
+
...entry.match.turnIndex !== void 0 && { turnIndex: entry.match.turnIndex },
|
|
35
|
+
...entry.match.hasToolResult !== void 0 && { hasToolResult: entry.match.hasToolResult }
|
|
34
36
|
},
|
|
35
37
|
response: normalizeResponse(entry.response),
|
|
36
38
|
...entry.latency !== void 0 && { latency: entry.latency },
|
|
@@ -341,18 +343,31 @@ function validateFixtures(fixtures) {
|
|
|
341
343
|
message: "chaos.disconnectRate must be between 0 and 1"
|
|
342
344
|
});
|
|
343
345
|
}
|
|
346
|
+
if (f.match.turnIndex !== void 0) {
|
|
347
|
+
if (typeof f.match.turnIndex !== "number" || f.match.turnIndex < 0 || !Number.isInteger(f.match.turnIndex)) results.push({
|
|
348
|
+
severity: "error",
|
|
349
|
+
fixtureIndex: i,
|
|
350
|
+
message: "match.turnIndex must be a non-negative integer"
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (f.match.hasToolResult !== void 0 && typeof f.match.hasToolResult !== "boolean") results.push({
|
|
354
|
+
severity: "error",
|
|
355
|
+
fixtureIndex: i,
|
|
356
|
+
message: `match.hasToolResult must be a boolean, got ${typeof f.match.hasToolResult}`
|
|
357
|
+
});
|
|
344
358
|
const um = f.match.userMessage;
|
|
345
359
|
if (typeof um === "string" && um) {
|
|
346
|
-
const
|
|
360
|
+
const dedupKey = `${um}|${f.match.turnIndex}|${f.match.hasToolResult}|${f.match.sequenceIndex}`;
|
|
361
|
+
const prev = seenUserMessages.get(dedupKey);
|
|
347
362
|
if (prev !== void 0) results.push({
|
|
348
363
|
severity: "warning",
|
|
349
364
|
fixtureIndex: i,
|
|
350
365
|
message: `duplicate userMessage '${um}' — shadows fixture ${prev}`
|
|
351
366
|
});
|
|
352
|
-
else seenUserMessages.set(
|
|
367
|
+
else seenUserMessages.set(dedupKey, i);
|
|
353
368
|
}
|
|
354
369
|
const match = f.match;
|
|
355
|
-
if (!(match.endpoint !== void 0 || match.userMessage !== void 0 || match.inputText !== void 0 || match.responseFormat !== void 0 || match.toolCallId !== void 0 || match.toolName !== void 0 || match.model !== void 0 || match.predicate !== void 0) && i < fixtures.length - 1) results.push({
|
|
370
|
+
if (!(match.endpoint !== void 0 || match.userMessage !== void 0 || match.inputText !== void 0 || match.responseFormat !== void 0 || match.toolCallId !== void 0 || match.toolName !== void 0 || match.model !== void 0 || match.predicate !== void 0 || match.turnIndex !== void 0 || match.hasToolResult !== void 0) && i < fixtures.length - 1) results.push({
|
|
356
371
|
severity: "warning",
|
|
357
372
|
fixtureIndex: i,
|
|
358
373
|
message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixture-loader.js","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":["import { readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type {\n Fixture,\n FixtureFile,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureResponse,\n ResponseOverrides,\n} from \"./types.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isEmbeddingResponse,\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n} from \"./helpers.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Auto-stringify object-valued `content` and `toolCalls[].arguments` fields.\n * This lets fixture authors write plain JSON objects instead of escaped strings.\n * All other fields (including ResponseOverrides) pass through unmodified.\n */\nexport function normalizeResponse(raw: FixtureFileResponse): FixtureResponse {\n // Shallow-clone so we don't mutate the parsed JSON input.\n const response = { ...raw } as Record<string, unknown>;\n\n // Auto-stringify object content (e.g. structured output)\n if (typeof response.content === \"object\" && response.content !== null) {\n response.content = JSON.stringify(response.content);\n }\n\n // Auto-stringify object arguments in toolCalls\n if (Array.isArray(response.toolCalls)) {\n response.toolCalls = (response.toolCalls as Array<Record<string, unknown>>).map((tc) => {\n if (typeof tc.arguments === \"object\" && tc.arguments !== null) {\n return { ...tc, arguments: JSON.stringify(tc.arguments) };\n }\n return tc;\n });\n }\n\n return response as unknown as FixtureResponse;\n}\n\nexport function entryToFixture(entry: FixtureFileEntry): Fixture {\n return {\n match: {\n userMessage: entry.match.userMessage,\n inputText: entry.match.inputText,\n toolCallId: entry.match.toolCallId,\n toolName: entry.match.toolName,\n model: entry.match.model,\n responseFormat: entry.match.responseFormat,\n endpoint: entry.match.endpoint,\n ...(entry.match.sequenceIndex !== undefined && { sequenceIndex: entry.match.sequenceIndex }),\n },\n response: normalizeResponse(entry.response),\n ...(entry.latency !== undefined && { latency: entry.latency }),\n ...(entry.chunkSize !== undefined && { chunkSize: entry.chunkSize }),\n ...(entry.truncateAfterChunks !== undefined && {\n truncateAfterChunks: entry.truncateAfterChunks,\n }),\n ...(entry.disconnectAfterMs !== undefined && { disconnectAfterMs: entry.disconnectAfterMs }),\n ...(entry.streamingProfile !== undefined && { streamingProfile: entry.streamingProfile }),\n ...(entry.chaos !== undefined && { chaos: entry.chaos }),\n };\n}\n\n// Logging helper — uses logger if provided, falls back to console.warn.\nfunction warn(logger: Logger | undefined, msg: string, ...rest: unknown[]): void {\n if (logger) {\n logger.warn(msg, ...rest);\n } else {\n console.warn(`[fixture-loader] ${msg}`, ...rest);\n }\n}\n\nexport function loadFixtureFile(filePath: string, logger?: Logger): Fixture[] {\n let raw: string;\n try {\n raw = readFileSync(filePath, \"utf-8\");\n } catch (err) {\n warn(logger, `Could not read file ${filePath}:`, err);\n return [];\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n warn(logger, `Invalid JSON in ${filePath}:`, err);\n return [];\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !Array.isArray((parsed as FixtureFile).fixtures)\n ) {\n warn(logger, `Missing or invalid \"fixtures\" array in ${filePath}`);\n return [];\n }\n\n return (parsed as FixtureFile).fixtures.map(entryToFixture);\n}\n\nexport function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[] {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n warn(logger, `Could not read directory ${dirPath}:`, err);\n return [];\n }\n\n const jsonFiles: string[] = [];\n for (const name of entries) {\n const fullPath = join(dirPath, name);\n try {\n if (statSync(fullPath).isDirectory()) {\n warn(logger, `Skipping subdirectory ${fullPath} (fixtures are not loaded recursively)`);\n continue;\n }\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n warn(logger, `Could not stat ${fullPath}:`, err);\n }\n continue;\n }\n if (name.endsWith(\".json\")) {\n jsonFiles.push(name);\n }\n }\n jsonFiles.sort();\n\n const fixtures: Fixture[] = [];\n for (const name of jsonFiles) {\n const filePath = join(dirPath, name);\n fixtures.push(...loadFixtureFile(filePath, logger));\n }\n\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture validation\n// ---------------------------------------------------------------------------\n\nexport interface ValidationResult {\n severity: \"error\" | \"warning\";\n fixtureIndex: number;\n message: string;\n}\n\nfunction validateReasoning(\n response: { reasoning?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.reasoning !== undefined) {\n if (typeof response.reasoning !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"reasoning must be a string\",\n });\n } else if (response.reasoning === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"reasoning is empty string — no reasoning events will be emitted\",\n });\n }\n }\n}\n\nfunction validateWebSearches(\n response: { webSearches?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.webSearches !== undefined) {\n if (!Array.isArray(response.webSearches)) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"webSearches must be an array of strings\",\n });\n } else if (response.webSearches.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"webSearches is empty array — no web search events will be emitted\",\n });\n } else {\n for (let j = 0; j < response.webSearches.length; j++) {\n if (typeof response.webSearches[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `webSearches[${j}] is not a string`,\n });\n break;\n }\n if (response.webSearches[j] === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: `webSearches[${j}] is empty string`,\n });\n }\n }\n }\n }\n}\n\nexport function validateFixtures(fixtures: Fixture[]): ValidationResult[] {\n const results: ValidationResult[] = [];\n\n const seenUserMessages = new Map<string, number>();\n\n for (let i = 0; i < fixtures.length; i++) {\n const f = fixtures[i];\n const response = f.response;\n\n // --- Error checks ---\n\n // Response type recognition\n // Note: isContentWithToolCallsResponse must be checked before isTextResponse\n // and isToolCallResponse since it is a structural superset of both.\n if (\n !isContentWithToolCallsResponse(response) &&\n !isTextResponse(response) &&\n !isToolCallResponse(response) &&\n !isErrorResponse(response) &&\n !isEmbeddingResponse(response) &&\n !isImageResponse(response) &&\n !isAudioResponse(response) &&\n !isTranscriptionResponse(response) &&\n !isVideoResponse(response)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message:\n \"response is not a recognized type (must have content, toolCalls, error, embedding, image, audio, transcription, or video)\",\n });\n }\n\n // Text response checks\n if (isTextResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // ContentWithToolCalls response checks\n if (isContentWithToolCallsResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // Tool call response checks\n if (isToolCallResponse(response)) {\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n }\n\n // Error response checks\n if (isErrorResponse(response)) {\n if (!response.error.message) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"error.message is empty\",\n });\n }\n if (response.status !== undefined && (response.status < 100 || response.status > 599)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code`,\n });\n }\n }\n\n // Embedding response checks\n if (isEmbeddingResponse(response)) {\n if (response.embedding.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"embedding array is empty\",\n });\n }\n for (let j = 0; j < response.embedding.length; j++) {\n if (typeof response.embedding[j] !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `embedding[${j}] is not a number`,\n });\n break; // one error is enough\n }\n }\n }\n\n // Validate ResponseOverrides fields\n if (\n isTextResponse(response) ||\n isToolCallResponse(response) ||\n isContentWithToolCallsResponse(response)\n ) {\n const r = response as ResponseOverrides;\n if (r.id !== undefined && typeof r.id !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"id\" must be a string, got ${typeof r.id}`,\n });\n }\n if (r.created !== undefined && (typeof r.created !== \"number\" || r.created < 0)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"created\" must be a non-negative number`,\n });\n }\n if (r.model !== undefined && typeof r.model !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"model\" must be a string, got ${typeof r.model}`,\n });\n }\n if (r.finishReason !== undefined && typeof r.finishReason !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"finishReason\" must be a string, got ${typeof r.finishReason}`,\n });\n }\n if (r.role !== undefined && typeof r.role !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"role\" must be a string, got ${typeof r.role}`,\n });\n }\n if (r.systemFingerprint !== undefined && typeof r.systemFingerprint !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"systemFingerprint\" must be a string, got ${typeof r.systemFingerprint}`,\n });\n }\n if (r.usage !== undefined) {\n if (typeof r.usage !== \"object\" || r.usage === null || Array.isArray(r.usage)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage\" must be an object`,\n });\n } else {\n // Check all known usage fields are numbers if present\n for (const key of Object.keys(r.usage)) {\n const val = (r.usage as Record<string, unknown>)[key];\n if (val !== undefined && typeof val !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}\" must be a number, got ${typeof val}`,\n });\n }\n }\n }\n }\n }\n\n // Numeric sanity checks\n if (f.latency !== undefined && f.latency < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"latency must be >= 0\",\n });\n }\n if (f.chunkSize !== undefined && f.chunkSize < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chunkSize must be >= 1\",\n });\n }\n if (f.truncateAfterChunks !== undefined && f.truncateAfterChunks < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"truncateAfterChunks must be >= 1\",\n });\n }\n if (f.disconnectAfterMs !== undefined && f.disconnectAfterMs < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"disconnectAfterMs must be >= 0\",\n });\n }\n if (f.streamingProfile !== undefined) {\n const sp = f.streamingProfile;\n if (sp.ttft !== undefined && sp.ttft < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.ttft must be >= 0\",\n });\n }\n if (sp.tps !== undefined && sp.tps <= 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.tps must be > 0\",\n });\n }\n if (sp.jitter !== undefined && (sp.jitter < 0 || sp.jitter > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.jitter must be between 0 and 1\",\n });\n }\n }\n if (f.chaos !== undefined) {\n const ch = f.chaos;\n if (ch.dropRate !== undefined && (ch.dropRate < 0 || ch.dropRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.dropRate must be between 0 and 1\",\n });\n }\n if (ch.malformedRate !== undefined && (ch.malformedRate < 0 || ch.malformedRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.malformedRate must be between 0 and 1\",\n });\n }\n if (ch.disconnectRate !== undefined && (ch.disconnectRate < 0 || ch.disconnectRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.disconnectRate must be between 0 and 1\",\n });\n }\n }\n\n // --- Warning checks ---\n\n // Duplicate userMessage shadowing\n const um = f.match.userMessage;\n if (typeof um === \"string\" && um) {\n const prev = seenUserMessages.get(um);\n if (prev !== undefined) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `duplicate userMessage '${um}' — shadows fixture ${prev}`,\n });\n } else {\n seenUserMessages.set(um, i);\n }\n }\n\n // Catch-all not in last position\n const match = f.match;\n const hasDiscriminator =\n match.endpoint !== undefined ||\n match.userMessage !== undefined ||\n match.inputText !== undefined ||\n match.responseFormat !== undefined ||\n match.toolCallId !== undefined ||\n match.toolName !== undefined ||\n match.model !== undefined ||\n match.predicate !== undefined;\n\n if (!hasDiscriminator && i < fixtures.length - 1) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fixture-loader.js","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":["import { readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type {\n Fixture,\n FixtureFile,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureResponse,\n ResponseOverrides,\n} from \"./types.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isEmbeddingResponse,\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n} from \"./helpers.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Auto-stringify object-valued `content` and `toolCalls[].arguments` fields.\n * This lets fixture authors write plain JSON objects instead of escaped strings.\n * All other fields (including ResponseOverrides) pass through unmodified.\n */\nexport function normalizeResponse(raw: FixtureFileResponse): FixtureResponse {\n // Shallow-clone so we don't mutate the parsed JSON input.\n const response = { ...raw } as Record<string, unknown>;\n\n // Auto-stringify object content (e.g. structured output)\n if (typeof response.content === \"object\" && response.content !== null) {\n response.content = JSON.stringify(response.content);\n }\n\n // Auto-stringify object arguments in toolCalls\n if (Array.isArray(response.toolCalls)) {\n response.toolCalls = (response.toolCalls as Array<Record<string, unknown>>).map((tc) => {\n if (typeof tc.arguments === \"object\" && tc.arguments !== null) {\n return { ...tc, arguments: JSON.stringify(tc.arguments) };\n }\n return tc;\n });\n }\n\n return response as unknown as FixtureResponse;\n}\n\nexport function entryToFixture(entry: FixtureFileEntry): Fixture {\n return {\n match: {\n userMessage: entry.match.userMessage,\n inputText: entry.match.inputText,\n toolCallId: entry.match.toolCallId,\n toolName: entry.match.toolName,\n model: entry.match.model,\n responseFormat: entry.match.responseFormat,\n endpoint: entry.match.endpoint,\n ...(entry.match.sequenceIndex !== undefined && { sequenceIndex: entry.match.sequenceIndex }),\n ...(entry.match.turnIndex !== undefined && {\n turnIndex: entry.match.turnIndex,\n }),\n ...(entry.match.hasToolResult !== undefined && {\n hasToolResult: entry.match.hasToolResult,\n }),\n },\n response: normalizeResponse(entry.response),\n ...(entry.latency !== undefined && { latency: entry.latency }),\n ...(entry.chunkSize !== undefined && { chunkSize: entry.chunkSize }),\n ...(entry.truncateAfterChunks !== undefined && {\n truncateAfterChunks: entry.truncateAfterChunks,\n }),\n ...(entry.disconnectAfterMs !== undefined && { disconnectAfterMs: entry.disconnectAfterMs }),\n ...(entry.streamingProfile !== undefined && { streamingProfile: entry.streamingProfile }),\n ...(entry.chaos !== undefined && { chaos: entry.chaos }),\n };\n}\n\n// Logging helper — uses logger if provided, falls back to console.warn.\nfunction warn(logger: Logger | undefined, msg: string, ...rest: unknown[]): void {\n if (logger) {\n logger.warn(msg, ...rest);\n } else {\n console.warn(`[fixture-loader] ${msg}`, ...rest);\n }\n}\n\nexport function loadFixtureFile(filePath: string, logger?: Logger): Fixture[] {\n let raw: string;\n try {\n raw = readFileSync(filePath, \"utf-8\");\n } catch (err) {\n warn(logger, `Could not read file ${filePath}:`, err);\n return [];\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n warn(logger, `Invalid JSON in ${filePath}:`, err);\n return [];\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !Array.isArray((parsed as FixtureFile).fixtures)\n ) {\n warn(logger, `Missing or invalid \"fixtures\" array in ${filePath}`);\n return [];\n }\n\n return (parsed as FixtureFile).fixtures.map(entryToFixture);\n}\n\nexport function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[] {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n warn(logger, `Could not read directory ${dirPath}:`, err);\n return [];\n }\n\n const jsonFiles: string[] = [];\n for (const name of entries) {\n const fullPath = join(dirPath, name);\n try {\n if (statSync(fullPath).isDirectory()) {\n warn(logger, `Skipping subdirectory ${fullPath} (fixtures are not loaded recursively)`);\n continue;\n }\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n warn(logger, `Could not stat ${fullPath}:`, err);\n }\n continue;\n }\n if (name.endsWith(\".json\")) {\n jsonFiles.push(name);\n }\n }\n jsonFiles.sort();\n\n const fixtures: Fixture[] = [];\n for (const name of jsonFiles) {\n const filePath = join(dirPath, name);\n fixtures.push(...loadFixtureFile(filePath, logger));\n }\n\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture validation\n// ---------------------------------------------------------------------------\n\nexport interface ValidationResult {\n severity: \"error\" | \"warning\";\n fixtureIndex: number;\n message: string;\n}\n\nfunction validateReasoning(\n response: { reasoning?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.reasoning !== undefined) {\n if (typeof response.reasoning !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"reasoning must be a string\",\n });\n } else if (response.reasoning === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"reasoning is empty string — no reasoning events will be emitted\",\n });\n }\n }\n}\n\nfunction validateWebSearches(\n response: { webSearches?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.webSearches !== undefined) {\n if (!Array.isArray(response.webSearches)) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"webSearches must be an array of strings\",\n });\n } else if (response.webSearches.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"webSearches is empty array — no web search events will be emitted\",\n });\n } else {\n for (let j = 0; j < response.webSearches.length; j++) {\n if (typeof response.webSearches[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `webSearches[${j}] is not a string`,\n });\n break;\n }\n if (response.webSearches[j] === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: `webSearches[${j}] is empty string`,\n });\n }\n }\n }\n }\n}\n\nexport function validateFixtures(fixtures: Fixture[]): ValidationResult[] {\n const results: ValidationResult[] = [];\n\n const seenUserMessages = new Map<string, number>();\n\n for (let i = 0; i < fixtures.length; i++) {\n const f = fixtures[i];\n const response = f.response;\n\n // --- Error checks ---\n\n // Response type recognition\n // Note: isContentWithToolCallsResponse must be checked before isTextResponse\n // and isToolCallResponse since it is a structural superset of both.\n if (\n !isContentWithToolCallsResponse(response) &&\n !isTextResponse(response) &&\n !isToolCallResponse(response) &&\n !isErrorResponse(response) &&\n !isEmbeddingResponse(response) &&\n !isImageResponse(response) &&\n !isAudioResponse(response) &&\n !isTranscriptionResponse(response) &&\n !isVideoResponse(response)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message:\n \"response is not a recognized type (must have content, toolCalls, error, embedding, image, audio, transcription, or video)\",\n });\n }\n\n // Text response checks\n if (isTextResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // ContentWithToolCalls response checks\n if (isContentWithToolCallsResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // Tool call response checks\n if (isToolCallResponse(response)) {\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n }\n\n // Error response checks\n if (isErrorResponse(response)) {\n if (!response.error.message) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"error.message is empty\",\n });\n }\n if (response.status !== undefined && (response.status < 100 || response.status > 599)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code`,\n });\n }\n }\n\n // Embedding response checks\n if (isEmbeddingResponse(response)) {\n if (response.embedding.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"embedding array is empty\",\n });\n }\n for (let j = 0; j < response.embedding.length; j++) {\n if (typeof response.embedding[j] !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `embedding[${j}] is not a number`,\n });\n break; // one error is enough\n }\n }\n }\n\n // Validate ResponseOverrides fields\n if (\n isTextResponse(response) ||\n isToolCallResponse(response) ||\n isContentWithToolCallsResponse(response)\n ) {\n const r = response as ResponseOverrides;\n if (r.id !== undefined && typeof r.id !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"id\" must be a string, got ${typeof r.id}`,\n });\n }\n if (r.created !== undefined && (typeof r.created !== \"number\" || r.created < 0)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"created\" must be a non-negative number`,\n });\n }\n if (r.model !== undefined && typeof r.model !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"model\" must be a string, got ${typeof r.model}`,\n });\n }\n if (r.finishReason !== undefined && typeof r.finishReason !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"finishReason\" must be a string, got ${typeof r.finishReason}`,\n });\n }\n if (r.role !== undefined && typeof r.role !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"role\" must be a string, got ${typeof r.role}`,\n });\n }\n if (r.systemFingerprint !== undefined && typeof r.systemFingerprint !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"systemFingerprint\" must be a string, got ${typeof r.systemFingerprint}`,\n });\n }\n if (r.usage !== undefined) {\n if (typeof r.usage !== \"object\" || r.usage === null || Array.isArray(r.usage)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage\" must be an object`,\n });\n } else {\n // Check all known usage fields are numbers if present\n for (const key of Object.keys(r.usage)) {\n const val = (r.usage as Record<string, unknown>)[key];\n if (val !== undefined && typeof val !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}\" must be a number, got ${typeof val}`,\n });\n }\n }\n }\n }\n }\n\n // Numeric sanity checks\n if (f.latency !== undefined && f.latency < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"latency must be >= 0\",\n });\n }\n if (f.chunkSize !== undefined && f.chunkSize < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chunkSize must be >= 1\",\n });\n }\n if (f.truncateAfterChunks !== undefined && f.truncateAfterChunks < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"truncateAfterChunks must be >= 1\",\n });\n }\n if (f.disconnectAfterMs !== undefined && f.disconnectAfterMs < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"disconnectAfterMs must be >= 0\",\n });\n }\n if (f.streamingProfile !== undefined) {\n const sp = f.streamingProfile;\n if (sp.ttft !== undefined && sp.ttft < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.ttft must be >= 0\",\n });\n }\n if (sp.tps !== undefined && sp.tps <= 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.tps must be > 0\",\n });\n }\n if (sp.jitter !== undefined && (sp.jitter < 0 || sp.jitter > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.jitter must be between 0 and 1\",\n });\n }\n }\n if (f.chaos !== undefined) {\n const ch = f.chaos;\n if (ch.dropRate !== undefined && (ch.dropRate < 0 || ch.dropRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.dropRate must be between 0 and 1\",\n });\n }\n if (ch.malformedRate !== undefined && (ch.malformedRate < 0 || ch.malformedRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.malformedRate must be between 0 and 1\",\n });\n }\n if (ch.disconnectRate !== undefined && (ch.disconnectRate < 0 || ch.disconnectRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.disconnectRate must be between 0 and 1\",\n });\n }\n }\n\n // Match field type checks\n if (f.match.turnIndex !== undefined) {\n if (\n typeof f.match.turnIndex !== \"number\" ||\n f.match.turnIndex < 0 ||\n !Number.isInteger(f.match.turnIndex)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.turnIndex must be a non-negative integer\",\n });\n }\n }\n if (f.match.hasToolResult !== undefined && typeof f.match.hasToolResult !== \"boolean\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.hasToolResult must be a boolean, got ${typeof f.match.hasToolResult}`,\n });\n }\n\n // --- Warning checks ---\n\n // Duplicate userMessage shadowing — include turnIndex, hasToolResult, and\n // sequenceIndex in the dedup key so that fixtures which share a userMessage\n // but differ on those fields are NOT considered duplicates.\n const um = f.match.userMessage;\n if (typeof um === \"string\" && um) {\n const dedupKey = `${um}|${f.match.turnIndex}|${f.match.hasToolResult}|${f.match.sequenceIndex}`;\n const prev = seenUserMessages.get(dedupKey);\n if (prev !== undefined) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `duplicate userMessage '${um}' — shadows fixture ${prev}`,\n });\n } else {\n seenUserMessages.set(dedupKey, i);\n }\n }\n\n // Catch-all not in last position\n const match = f.match;\n const hasDiscriminator =\n match.endpoint !== undefined ||\n match.userMessage !== undefined ||\n match.inputText !== undefined ||\n match.responseFormat !== undefined ||\n match.toolCallId !== undefined ||\n match.toolName !== undefined ||\n match.model !== undefined ||\n match.predicate !== undefined ||\n match.turnIndex !== undefined ||\n match.hasToolResult !== undefined;\n\n if (!hasDiscriminator && i < fixtures.length - 1) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
|
package/dist/journal.cjs
CHANGED
|
@@ -15,7 +15,7 @@ function fieldEqual(a, b) {
|
|
|
15
15
|
* (ignoring sequenceIndex). Used to group sequenced fixtures.
|
|
16
16
|
*/
|
|
17
17
|
function matchCriteriaEqual(a, b) {
|
|
18
|
-
return fieldEqual(a.userMessage, b.userMessage) && fieldEqual(a.inputText, b.inputText) && fieldEqual(a.toolCallId, b.toolCallId) && fieldEqual(a.toolName, b.toolName) && fieldEqual(a.model, b.model) && fieldEqual(a.responseFormat, b.responseFormat) && fieldEqual(a.predicate, b.predicate) && fieldEqual(a.endpoint, b.endpoint);
|
|
18
|
+
return fieldEqual(a.userMessage, b.userMessage) && fieldEqual(a.inputText, b.inputText) && fieldEqual(a.toolCallId, b.toolCallId) && fieldEqual(a.toolName, b.toolName) && fieldEqual(a.model, b.model) && fieldEqual(a.responseFormat, b.responseFormat) && fieldEqual(a.predicate, b.predicate) && fieldEqual(a.endpoint, b.endpoint) && fieldEqual(a.turnIndex, b.turnIndex) && fieldEqual(a.hasToolResult, b.hasToolResult);
|
|
19
19
|
}
|
|
20
20
|
var Journal = class {
|
|
21
21
|
entries = [];
|
package/dist/journal.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journal.cjs","names":["generateId"],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\n\n/** Sentinel testId used when no explicit test scope is provided. */\nexport const DEFAULT_TEST_ID = \"__default__\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n\n constructor(options: JournalOptions = {}) {\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n };\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":";;;;AAIA,MAAa,kBAAkB;;;;AAK/B,SAAS,WAAW,GAAY,GAAqB;AACnD,KAAI,aAAa,UAAU,aAAa,OACtC,QAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE;AAChD,QAAO,MAAM;;;;;;AAOf,SAAS,mBAAmB,GAAiB,GAA0B;AACrE,QACE,WAAW,EAAE,aAAa,EAAE,YAAY,IACxC,WAAW,EAAE,WAAW,EAAE,UAAU,IACpC,WAAW,EAAE,YAAY,EAAE,WAAW,IACtC,WAAW,EAAE,UAAU,EAAE,SAAS,IAClC,WAAW,EAAE,OAAO,EAAE,MAAM,IAC5B,WAAW,EAAE,gBAAgB,EAAE,eAAe,IAC9C,WAAW,EAAE,WAAW,EAAE,UAAU,IACpC,WAAW,EAAE,UAAU,EAAE,SAAS;;
|
|
1
|
+
{"version":3,"file":"journal.cjs","names":["generateId"],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\n\n/** Sentinel testId used when no explicit test scope is provided. */\nexport const DEFAULT_TEST_ID = \"__default__\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint) &&\n fieldEqual(a.turnIndex, b.turnIndex) &&\n fieldEqual(a.hasToolResult, b.hasToolResult)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n\n constructor(options: JournalOptions = {}) {\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n };\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":"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"}
|
package/dist/journal.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journal.d.cts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;cAIa,eAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"journal.d.cts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;cAIa,eAAA;AAAA,UA8BI,cAAA,CA9BW;EA8BX;AA0BjB;;;;;;;;;;;YAiDmC,CAAA,EAAA,MAAA;;;;;;;;;;;;cAjDtB,OAAA;;;;;wBAMU;;4BAUK,IAAI;aAInB,KAAK,oCAAoC;;;MAkBjB;aAOxB;yBAIY,UAAU;;;;;;;;gDAWa,IAAI;;;;;;;;gCA+BpB;sCAKnB,gCACc"}
|
package/dist/journal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journal.d.ts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;cAIa,eAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"journal.d.ts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;cAIa,eAAA;AAAA,UA8BI,cAAA,CA9BW;EA8BX;AA0BjB;;;;;;;;;;;YAiDmC,CAAA,EAAA,MAAA;;;;;;;;;;;;cAjDtB,OAAA;;;;;wBAMU;;4BAUK,IAAI;aAInB,KAAK,oCAAoC;;;MAkBjB;aAOxB;yBAIY,UAAU;;;;;;;;gDAWa,IAAI;;;;;;;;gCA+BpB;sCAKnB,gCACc"}
|
package/dist/journal.js
CHANGED
|
@@ -15,7 +15,7 @@ function fieldEqual(a, b) {
|
|
|
15
15
|
* (ignoring sequenceIndex). Used to group sequenced fixtures.
|
|
16
16
|
*/
|
|
17
17
|
function matchCriteriaEqual(a, b) {
|
|
18
|
-
return fieldEqual(a.userMessage, b.userMessage) && fieldEqual(a.inputText, b.inputText) && fieldEqual(a.toolCallId, b.toolCallId) && fieldEqual(a.toolName, b.toolName) && fieldEqual(a.model, b.model) && fieldEqual(a.responseFormat, b.responseFormat) && fieldEqual(a.predicate, b.predicate) && fieldEqual(a.endpoint, b.endpoint);
|
|
18
|
+
return fieldEqual(a.userMessage, b.userMessage) && fieldEqual(a.inputText, b.inputText) && fieldEqual(a.toolCallId, b.toolCallId) && fieldEqual(a.toolName, b.toolName) && fieldEqual(a.model, b.model) && fieldEqual(a.responseFormat, b.responseFormat) && fieldEqual(a.predicate, b.predicate) && fieldEqual(a.endpoint, b.endpoint) && fieldEqual(a.turnIndex, b.turnIndex) && fieldEqual(a.hasToolResult, b.hasToolResult);
|
|
19
19
|
}
|
|
20
20
|
var Journal = class {
|
|
21
21
|
entries = [];
|
package/dist/journal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journal.js","names":[],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\n\n/** Sentinel testId used when no explicit test scope is provided. */\nexport const DEFAULT_TEST_ID = \"__default__\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n\n constructor(options: JournalOptions = {}) {\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n };\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":";;;;AAIA,MAAa,kBAAkB;;;;AAK/B,SAAS,WAAW,GAAY,GAAqB;AACnD,KAAI,aAAa,UAAU,aAAa,OACtC,QAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE;AAChD,QAAO,MAAM;;;;;;AAOf,SAAS,mBAAmB,GAAiB,GAA0B;AACrE,QACE,WAAW,EAAE,aAAa,EAAE,YAAY,IACxC,WAAW,EAAE,WAAW,EAAE,UAAU,IACpC,WAAW,EAAE,YAAY,EAAE,WAAW,IACtC,WAAW,EAAE,UAAU,EAAE,SAAS,IAClC,WAAW,EAAE,OAAO,EAAE,MAAM,IAC5B,WAAW,EAAE,gBAAgB,EAAE,eAAe,IAC9C,WAAW,EAAE,WAAW,EAAE,UAAU,IACpC,WAAW,EAAE,UAAU,EAAE,SAAS;;
|
|
1
|
+
{"version":3,"file":"journal.js","names":[],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\n\n/** Sentinel testId used when no explicit test scope is provided. */\nexport const DEFAULT_TEST_ID = \"__default__\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint) &&\n fieldEqual(a.turnIndex, b.turnIndex) &&\n fieldEqual(a.hasToolResult, b.hasToolResult)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n\n constructor(options: JournalOptions = {}) {\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n };\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":";;;;AAIA,MAAa,kBAAkB;;;;AAK/B,SAAS,WAAW,GAAY,GAAqB;AACnD,KAAI,aAAa,UAAU,aAAa,OACtC,QAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE;AAChD,QAAO,MAAM;;;;;;AAOf,SAAS,mBAAmB,GAAiB,GAA0B;AACrE,QACE,WAAW,EAAE,aAAa,EAAE,YAAY,IACxC,WAAW,EAAE,WAAW,EAAE,UAAU,IACpC,WAAW,EAAE,YAAY,EAAE,WAAW,IACtC,WAAW,EAAE,UAAU,EAAE,SAAS,IAClC,WAAW,EAAE,OAAO,EAAE,MAAM,IAC5B,WAAW,EAAE,gBAAgB,EAAE,eAAe,IAC9C,WAAW,EAAE,WAAW,EAAE,UAAU,IACpC,WAAW,EAAE,UAAU,EAAE,SAAS,IAClC,WAAW,EAAE,WAAW,EAAE,UAAU,IACpC,WAAW,EAAE,eAAe,EAAE,cAAc;;AA8BhD,IAAa,UAAb,MAAqB;CACnB,AAAQ,UAA0B,EAAE;CACpC,AAAiB,6CAAgE,IAAI,KAAK;CAC1F,AAAiB;CACjB,AAAiB;CAEjB,YAAY,UAA0B,EAAE,EAAE;EAGxC,MAAM,MAAM,QAAQ;AACpB,OAAK,aAAa,QAAQ,UAAa,MAAM,IAAI,MAAM;EACvD,MAAM,YAAY,QAAQ;AAC1B,OAAK,0BAA0B,cAAc,UAAa,YAAY,IAAI,YAAY;;;CAIxF,IAAI,qBAA2C;AAC7C,SAAO,KAAK,6BAA6B,gBAAgB;;CAG3D,IAAI,OAA6D;EAC/D,MAAM,OAAqB;GACzB,IAAI,WAAW,MAAM;GACrB,WAAW,KAAK,KAAK;GACrB,GAAG;GACJ;AACD,OAAK,QAAQ,KAAK,KAAK;AAMvB,MAAI,KAAK,aAAa,KAAK,KAAK,QAAQ,SAAS,KAAK,WACpD,MAAK,QAAQ,OAAO;AAEtB,SAAO;;CAGT,OAAO,MAA2C;AAChD,MAAI,MAAM,UAAU,OAClB,QAAO,KAAK,QAAQ,MAAM,CAAC,KAAK,MAAM;AAExC,SAAO,KAAK,QAAQ,OAAO;;CAG7B,UAA+B;AAC7B,SAAO,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,KAAK,QAAQ,SAAS,KAAK;;CAG3E,cAAc,SAAkC;AAC9C,SAAO,KAAK,QAAQ,QAAQ,MAAM,EAAE,SAAS,YAAY,QAAQ;;;;;;;;;CAUnE,6BAA6B,QAAsC;AACjE,SAAO,KAAK,2BAA2B,IAAI,OAAO,oBAAI,IAAI,KAAK;;;;;;;;CASjE,AAAQ,qCAAqC,QAAsC;EACjF,IAAI,SAAS,KAAK,2BAA2B,IAAI,OAAO;AACxD,MAAI,CAAC,QAAQ;AACX,4BAAS,IAAI,KAAK;AAClB,QAAK,2BAA2B,IAAI,QAAQ,OAAO;AAInD,OACE,KAAK,0BAA0B,KAC/B,KAAK,2BAA2B,OAAO,KAAK,yBAC5C;IACA,MAAM,SAAS,KAAK,2BAA2B,MAAM,CAAC,MAAM,CAAC;AAC7D,QAAI,WAAW,OACb,MAAK,2BAA2B,OAAO,OAAO;;;AAIpD,SAAO;;CAGT,qBAAqB,SAAkB,SAAS,iBAAyB;AACvE,SAAO,KAAK,6BAA6B,OAAO,CAAC,IAAI,QAAQ,IAAI;;CAGnE,2BACE,SACA,aACA,SAAS,iBACH;EACN,MAAM,SAAS,KAAK,qCAAqC,OAAO;AAChE,SAAO,IAAI,UAAU,OAAO,IAAI,QAAQ,IAAI,KAAK,EAAE;AAEnD,MAAI,QAAQ,MAAM,kBAAkB,UAAa,YAC/C,MAAK,MAAM,WAAW,aAAa;AACjC,OAAI,YAAY,QAAS;AACzB,OAAI,QAAQ,MAAM,kBAAkB,OAAW;AAC/C,OAAI,mBAAmB,QAAQ,OAAO,QAAQ,MAAM,CAClD,QAAO,IAAI,UAAU,OAAO,IAAI,QAAQ,IAAI,KAAK,EAAE;;;CAM3D,iBAAiB,QAAuB;AACtC,MAAI,WAAW,OACb,MAAK,2BAA2B,OAAO,OAAO;MAE9C,MAAK,2BAA2B,OAAO;;CAI3C,QAAc;AACZ,OAAK,UAAU,EAAE;AACjB,OAAK,2BAA2B,OAAO;;CAGzC,IAAI,OAAe;AACjB,SAAO,KAAK,QAAQ"}
|
package/dist/llmock.cjs
CHANGED
|
@@ -77,6 +77,12 @@ var LLMock = class LLMock {
|
|
|
77
77
|
onToolResult(id, response, opts) {
|
|
78
78
|
return this.on({ toolCallId: id }, response, opts);
|
|
79
79
|
}
|
|
80
|
+
onTurn(turn, pattern, response, opts) {
|
|
81
|
+
return this.on({
|
|
82
|
+
userMessage: pattern,
|
|
83
|
+
turnIndex: turn
|
|
84
|
+
}, response, opts);
|
|
85
|
+
}
|
|
80
86
|
onImage(prompt, response) {
|
|
81
87
|
return this.addFixture({
|
|
82
88
|
match: {
|