@copilotkit/aimock 1.43.0 → 1.45.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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +14 -2
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +31 -5
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +31 -5
- package/dist/aimock-cli.js.map +1 -1
- package/dist/bedrock-converse.cjs +344 -43
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +345 -44
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +195 -42
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +196 -43
- package/dist/bedrock.js.map +1 -1
- package/dist/chaos.cjs +9 -5
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +10 -7
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +238 -9
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +242 -13
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +159 -72
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +160 -73
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +63 -5
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +37 -4
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +37 -4
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +63 -5
- package/dist/config-loader.js.map +1 -1
- package/dist/constants.cjs +8 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +6 -2
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +8 -1
- package/dist/constants.js.map +1 -1
- package/dist/echo-text.cjs +65 -0
- package/dist/echo-text.cjs.map +1 -0
- package/dist/echo-text.d.cts +22 -0
- package/dist/echo-text.d.cts.map +1 -0
- package/dist/echo-text.d.ts +22 -0
- package/dist/echo-text.d.ts.map +1 -0
- package/dist/echo-text.js +65 -0
- package/dist/echo-text.js.map +1 -0
- package/dist/embeddings.cjs +18 -1
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +18 -1
- package/dist/embeddings.js.map +1 -1
- package/dist/fakes-plugin.cjs +131 -0
- package/dist/fakes-plugin.cjs.map +1 -0
- package/dist/fakes-plugin.d.cts +23 -0
- package/dist/fakes-plugin.d.cts.map +1 -0
- package/dist/fakes-plugin.d.ts +23 -0
- package/dist/fakes-plugin.d.ts.map +1 -0
- package/dist/fakes-plugin.js +127 -0
- package/dist/fakes-plugin.js.map +1 -0
- package/dist/files.cjs +9 -11
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +10 -13
- package/dist/files.js.map +1 -1
- package/dist/fine-tuning.cjs +1 -19
- package/dist/fine-tuning.cjs.map +1 -1
- package/dist/fine-tuning.d.cts.map +1 -1
- package/dist/fine-tuning.d.ts.map +1 -1
- package/dist/fine-tuning.js +2 -20
- package/dist/fine-tuning.js.map +1 -1
- package/dist/fixture-loader-services.cjs +116 -0
- package/dist/fixture-loader-services.cjs.map +1 -0
- package/dist/fixture-loader-services.d.cts +48 -0
- package/dist/fixture-loader-services.d.cts.map +1 -0
- package/dist/fixture-loader-services.d.ts +48 -0
- package/dist/fixture-loader-services.d.ts.map +1 -0
- package/dist/fixture-loader-services.js +114 -0
- package/dist/fixture-loader-services.js.map +1 -0
- package/dist/fixture-loader.cjs +552 -29
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +140 -2
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +140 -2
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +542 -31
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +42 -2
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +43 -3
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +288 -52
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +289 -53
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +234 -38
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +235 -39
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +1157 -12
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +85 -4
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +85 -4
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +1113 -14
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +3 -2
- package/dist/images.cjs.map +1 -1
- package/dist/images.js +3 -2
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +39 -1
- package/dist/index.d.cts +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.js +11 -4
- package/dist/jest.cjs +43 -14
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +47 -5
- package/dist/jest.d.cts.map +1 -1
- package/dist/jest.d.ts +47 -5
- package/dist/jest.d.ts.map +1 -1
- package/dist/jest.js +45 -16
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +155 -13
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +54 -4
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +54 -4
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +155 -13
- package/dist/journal.js.map +1 -1
- package/dist/live-sanitize.cjs +4 -32
- package/dist/live-sanitize.cjs.map +1 -1
- package/dist/live-sanitize.js +4 -32
- package/dist/live-sanitize.js.map +1 -1
- package/dist/llmock.cjs +204 -39
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +51 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +51 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +206 -41
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes-mount.cjs +217 -0
- package/dist/mcp-fakes-mount.cjs.map +1 -0
- package/dist/mcp-fakes-mount.js +211 -0
- package/dist/mcp-fakes-mount.js.map +1 -0
- package/dist/mcp-fakes-report.cjs +111 -0
- package/dist/mcp-fakes-report.cjs.map +1 -0
- package/dist/mcp-fakes-report.d.cts +40 -0
- package/dist/mcp-fakes-report.d.cts.map +1 -0
- package/dist/mcp-fakes-report.d.ts +40 -0
- package/dist/mcp-fakes-report.d.ts.map +1 -0
- package/dist/mcp-fakes-report.js +107 -0
- package/dist/mcp-fakes-report.js.map +1 -0
- package/dist/mcp-fakes.cjs +1942 -0
- package/dist/mcp-fakes.cjs.map +1 -0
- package/dist/mcp-fakes.d.cts +504 -0
- package/dist/mcp-fakes.d.cts.map +1 -0
- package/dist/mcp-fakes.d.ts +504 -0
- package/dist/mcp-fakes.d.ts.map +1 -0
- package/dist/mcp-fakes.js +1937 -0
- package/dist/mcp-fakes.js.map +1 -0
- package/dist/mcp-handler.cjs +534 -27
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.d.cts +49 -0
- package/dist/mcp-handler.d.cts.map +1 -0
- package/dist/mcp-handler.d.ts +49 -0
- package/dist/mcp-handler.d.ts.map +1 -0
- package/dist/mcp-handler.js +530 -28
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-mock.cjs +326 -47
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts +97 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts +97 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +327 -48
- package/dist/mcp-mock.js.map +1 -1
- package/dist/mcp-recorder.cjs +1224 -0
- package/dist/mcp-recorder.cjs.map +1 -0
- package/dist/mcp-recorder.d.cts +108 -0
- package/dist/mcp-recorder.d.cts.map +1 -0
- package/dist/mcp-recorder.d.ts +108 -0
- package/dist/mcp-recorder.d.ts.map +1 -0
- package/dist/mcp-recorder.js +1215 -0
- package/dist/mcp-recorder.js.map +1 -0
- package/dist/mcp-stub.cjs +5 -1
- package/dist/mcp-stub.d.cts +3 -1
- package/dist/mcp-stub.d.ts +3 -1
- package/dist/mcp-stub.js +3 -1
- package/dist/mcp-types.d.cts +6 -0
- package/dist/mcp-types.d.ts +6 -0
- package/dist/message-text.cjs +192 -0
- package/dist/message-text.cjs.map +1 -0
- package/dist/message-text.d.cts +31 -0
- package/dist/message-text.d.cts.map +1 -0
- package/dist/message-text.d.ts +31 -0
- package/dist/message-text.d.ts.map +1 -0
- package/dist/message-text.js +184 -0
- package/dist/message-text.js.map +1 -0
- package/dist/messages.cjs +400 -56
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +401 -58
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +46 -66
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts +14 -6
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts +14 -6
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +32 -40
- package/dist/metrics.js.map +1 -1
- package/dist/misbehavior.cjs +1523 -0
- package/dist/misbehavior.cjs.map +1 -0
- package/dist/misbehavior.d.cts +43 -0
- package/dist/misbehavior.d.cts.map +1 -0
- package/dist/misbehavior.d.ts +43 -0
- package/dist/misbehavior.d.ts.map +1 -0
- package/dist/misbehavior.js +1511 -0
- package/dist/misbehavior.js.map +1 -0
- package/dist/ollama.cjs +315 -113
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +316 -114
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +3405 -0
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +30 -0
- package/dist/openapi.d.cts.map +1 -0
- package/dist/openapi.d.ts +30 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +3404 -0
- package/dist/openapi.js.map +1 -0
- package/dist/openrouter-video.cjs +4 -4
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +4 -4
- package/dist/openrouter-video.js.map +1 -1
- package/dist/record-sanitize.cjs +231 -0
- package/dist/record-sanitize.cjs.map +1 -0
- package/dist/record-sanitize.js +224 -0
- package/dist/record-sanitize.js.map +1 -0
- package/dist/recorder.cjs +115 -6
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +2 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts +2 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +116 -8
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +701 -166
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +5 -13
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +5 -13
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +699 -169
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +951 -0
- package/dist/route-registry.cjs.map +1 -0
- package/dist/route-registry.d.cts +33 -0
- package/dist/route-registry.d.cts.map +1 -0
- package/dist/route-registry.d.ts +33 -0
- package/dist/route-registry.d.ts.map +1 -0
- package/dist/route-registry.js +866 -0
- package/dist/route-registry.js.map +1 -0
- package/dist/router.cjs +24 -2
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +25 -4
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +1159 -264
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +4 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1067 -172
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +4 -2
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +4 -2
- package/dist/speech.js.map +1 -1
- package/dist/stream-collapse.cjs +236 -34
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +39 -4
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +39 -4
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +235 -35
- package/dist/stream-collapse.js.map +1 -1
- package/dist/suite.cjs +1 -1
- package/dist/suite.js +1 -1
- package/dist/transcription.cjs +2 -2
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +2 -2
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs +18 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +516 -5
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +516 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-cli.cjs +123 -33
- package/dist/validate-cli.cjs.map +1 -1
- package/dist/validate-cli.js +125 -35
- package/dist/validate-cli.js.map +1 -1
- package/dist/vector-stores.cjs +1180 -0
- package/dist/vector-stores.cjs.map +1 -0
- package/dist/vector-stores.d.cts +106 -0
- package/dist/vector-stores.d.cts.map +1 -0
- package/dist/vector-stores.d.ts +106 -0
- package/dist/vector-stores.d.ts.map +1 -0
- package/dist/vector-stores.js +1164 -0
- package/dist/vector-stores.js.map +1 -0
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/version.cjs +45 -0
- package/dist/version.cjs.map +1 -0
- package/dist/version.js +44 -0
- package/dist/version.js.map +1 -0
- package/dist/video.cjs +19 -0
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +19 -0
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +77 -16
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +38 -5
- package/dist/vitest.d.cts.map +1 -1
- package/dist/vitest.d.ts +38 -5
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +78 -17
- package/dist/vitest.js.map +1 -1
- package/dist/watcher.cjs +6 -2
- package/dist/watcher.cjs.map +1 -1
- package/dist/watcher.js +6 -2
- package/dist/watcher.js.map +1 -1
- package/dist/ws-gemini-live.cjs +210 -101
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +9 -3
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +9 -3
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +211 -102
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +175 -62
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts +8 -3
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +8 -3
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +176 -63
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +121 -16
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts +8 -12
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts +8 -12
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +123 -18
- package/dist/ws-responses.js.map +1 -1
- package/package.json +13 -2
- package/skills/write-fixtures/SKILL.md +179 -21
|
@@ -0,0 +1,1511 @@
|
|
|
1
|
+
import { assertCustomToolCalls, assertResponsesToolCalls, isCombinedFixtureResponse, isErrorResponse, isTextResponse, isToolCallResponse, keepAuthoredToolArguments, rebuildOrderedBlocks, requireEmittedFunctionToolCalls, resolveFixtureBlocks, resolveServedBlockOutcome, resolveTestId, servedToolCalls, strictToolArgumentsEnabled, toolArgsForWire, toolCallFixtureBlock, withServedToolCalls } from "./helpers.js";
|
|
2
|
+
import { parseChaosNumber } from "./chaos.js";
|
|
3
|
+
|
|
4
|
+
//#region src/misbehavior.ts
|
|
5
|
+
function normalizeDirectConstraints(schema) {
|
|
6
|
+
if (!isObject(schema)) return void 0;
|
|
7
|
+
if (schema.properties !== void 0 && !isObject(schema.properties)) return void 0;
|
|
8
|
+
if (schema.required !== void 0 && (!Array.isArray(schema.required) || !schema.required.every((name) => typeof name === "string"))) return void 0;
|
|
9
|
+
if (schema.patternProperties !== void 0 && !isObject(schema.patternProperties)) return void 0;
|
|
10
|
+
return {
|
|
11
|
+
properties: schema.properties ?? {},
|
|
12
|
+
required: schema.required ?? [],
|
|
13
|
+
...Object.hasOwn(schema, "additionalProperties") ? { additionalProperties: schema.additionalProperties } : {},
|
|
14
|
+
...schema.patternProperties !== void 0 ? { patternProperties: schema.patternProperties } : {}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Private tools-only input also permits native Cohere definitions before adaptation.
|
|
19
|
+
* With `scope`, a function tool matches only when its `namespace` equals
|
|
20
|
+
* `scope.namespace` (an absent namespace matches only un-namespaced tools), so
|
|
21
|
+
* an OpenAI Responses call never takes another namespace's same-named schema.
|
|
22
|
+
*/
|
|
23
|
+
function normalizeDirectToolSchema(request, toolName, scope) {
|
|
24
|
+
for (const tool of request.tools ?? []) {
|
|
25
|
+
if (!isObject(tool)) continue;
|
|
26
|
+
if (tool.type === "function" && isObject(tool.function) && tool.function.name === toolName) {
|
|
27
|
+
if (scope && tool.namespace !== scope.namespace) continue;
|
|
28
|
+
return normalizeDirectConstraints(tool.function.parameters);
|
|
29
|
+
}
|
|
30
|
+
if (tool.name !== toolName || !Object.hasOwn(tool, "parameter_definitions")) continue;
|
|
31
|
+
const definitions = tool.parameter_definitions;
|
|
32
|
+
if (!isObject(definitions)) return void 0;
|
|
33
|
+
const required = [];
|
|
34
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
35
|
+
if (!isObject(definition)) return void 0;
|
|
36
|
+
if (definition.required !== void 0 && typeof definition.required !== "boolean") return void 0;
|
|
37
|
+
if (definition.required === true) required.push(name);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
properties: definitions,
|
|
41
|
+
required
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The schema of a fault's target call. Only OpenAI Responses serves a call's
|
|
47
|
+
* `namespace`, so only there does the lookup resolve (namespace, name).
|
|
48
|
+
*/
|
|
49
|
+
function targetToolSchema(context, target) {
|
|
50
|
+
return context.wire === "openai-responses" ? normalizeDirectToolSchema(context.request, target.name, { namespace: target.namespace }) : normalizeDirectToolSchema(context.request, target.name);
|
|
51
|
+
}
|
|
52
|
+
function normalizeDirectType(value) {
|
|
53
|
+
if (typeof value !== "string") return void 0;
|
|
54
|
+
const type = value.toLowerCase();
|
|
55
|
+
switch (type) {
|
|
56
|
+
case "string":
|
|
57
|
+
case "number":
|
|
58
|
+
case "integer":
|
|
59
|
+
case "boolean":
|
|
60
|
+
case "array":
|
|
61
|
+
case "object":
|
|
62
|
+
case "null": return type;
|
|
63
|
+
default: return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function directPropertyTypes(propertySchema) {
|
|
67
|
+
if (!isObject(propertySchema)) return void 0;
|
|
68
|
+
const rawTypes = Array.isArray(propertySchema.type) ? propertySchema.type : [propertySchema.type];
|
|
69
|
+
if (rawTypes.length === 0) return void 0;
|
|
70
|
+
const types = [];
|
|
71
|
+
for (const rawType of rawTypes) {
|
|
72
|
+
const type = normalizeDirectType(rawType);
|
|
73
|
+
if (type === void 0) return void 0;
|
|
74
|
+
types.push(type);
|
|
75
|
+
}
|
|
76
|
+
return types;
|
|
77
|
+
}
|
|
78
|
+
function acceptsDirectType(value, type) {
|
|
79
|
+
switch (type) {
|
|
80
|
+
case "null": return value === null;
|
|
81
|
+
case "array": return Array.isArray(value);
|
|
82
|
+
case "object": return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
83
|
+
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
84
|
+
case "number": return typeof value === "number" && Number.isFinite(value);
|
|
85
|
+
default: return typeof value === type;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function invalidJsonArguments(canonical, style) {
|
|
89
|
+
let candidate = canonical;
|
|
90
|
+
if (style === "truncated") candidate = canonical.slice(0, Math.max(1, Math.floor(canonical.length / 2)));
|
|
91
|
+
else if (style === "trailing-comma" && canonical.endsWith("}")) candidate = `${canonical.slice(0, -1)},}`;
|
|
92
|
+
else if (style === "single-quotes") candidate = canonical.replace(/\\[\s\S]|"/g, (token) => token === "\"" ? "'" : token);
|
|
93
|
+
if (candidate === canonical) return void 0;
|
|
94
|
+
try {
|
|
95
|
+
JSON.parse(candidate);
|
|
96
|
+
return;
|
|
97
|
+
} catch {
|
|
98
|
+
return candidate;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The ordered blocks a fault sees. Custom tool calls and namespaces are kept so
|
|
103
|
+
* an OpenAI Responses rewrite serves them unchanged; the planner never reaches
|
|
104
|
+
* here with a custom call on any other wire.
|
|
105
|
+
*/
|
|
106
|
+
function faultBlockOutcome(response) {
|
|
107
|
+
return isCombinedFixtureResponse(response) ? resolveServedBlockOutcome(response) : void 0;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Index of a tool fault's target: the first function call, or the first one
|
|
111
|
+
* named `tool`. A custom tool call has no JSON arguments, so it is never a
|
|
112
|
+
* fault target; -1 when no function call qualifies.
|
|
113
|
+
*/
|
|
114
|
+
function faultTargetIndex(calls, tool) {
|
|
115
|
+
return calls.findIndex((call) => call.type !== "custom" && (tool === void 0 || call.name === tool));
|
|
116
|
+
}
|
|
117
|
+
/** The function call at `index`, or undefined when absent or custom. */
|
|
118
|
+
function functionCallAt(calls, index) {
|
|
119
|
+
const call = calls[index];
|
|
120
|
+
return call !== void 0 && call.type !== "custom" ? call : void 0;
|
|
121
|
+
}
|
|
122
|
+
/** Replace a response's tool calls one-for-one, rebuilding ordered blocks when present. */
|
|
123
|
+
function rewriteFaultToolCalls(response, outcome, toolCalls) {
|
|
124
|
+
if (!outcome) return withServedToolCalls(response, toolCalls);
|
|
125
|
+
return withServedToolCalls({
|
|
126
|
+
...response,
|
|
127
|
+
content: outcome.content
|
|
128
|
+
}, toolCalls, rebuildOrderedBlocks(outcome.ordered, toolCalls));
|
|
129
|
+
}
|
|
130
|
+
/** Prepare K1 without selecting a fault, spending counters, or mutating input. */
|
|
131
|
+
function prepareInvalidJsonCandidate(context, fault) {
|
|
132
|
+
const response = context.response;
|
|
133
|
+
const combined = isCombinedFixtureResponse(response);
|
|
134
|
+
if (isErrorResponse(response) || !(combined || isToolCallResponse(response))) return {
|
|
135
|
+
kind: "not-applicable",
|
|
136
|
+
detail: "Response has no tool calls"
|
|
137
|
+
};
|
|
138
|
+
const outcome = faultBlockOutcome(response);
|
|
139
|
+
const calls = outcome?.toolCalls ?? servedToolCalls(response);
|
|
140
|
+
const index = faultTargetIndex(calls, fault.tool);
|
|
141
|
+
const target = functionCallAt(calls, index);
|
|
142
|
+
if (!target) return {
|
|
143
|
+
kind: "not-applicable",
|
|
144
|
+
detail: "Target tool call is absent"
|
|
145
|
+
};
|
|
146
|
+
const args = toolArgsForWire(target);
|
|
147
|
+
const canonical = args.kind === "parsed" ? args.text : args.raw;
|
|
148
|
+
const style = fault.style ?? MISBEHAVIOR_CATALOG[fault.fault].defaults.style;
|
|
149
|
+
const argumentsText = invalidJsonArguments(canonical, style);
|
|
150
|
+
if (argumentsText === void 0) return {
|
|
151
|
+
kind: "not-applicable",
|
|
152
|
+
detail: `${style} cannot produce changed invalid JSON`
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
kind: "ready",
|
|
156
|
+
candidate: {
|
|
157
|
+
response: rewriteFaultToolCalls(response, outcome, calls.map((call, callIndex) => callIndex === index && call.type !== "custom" ? {
|
|
158
|
+
...call,
|
|
159
|
+
arguments: argumentsText
|
|
160
|
+
} : { ...call })),
|
|
161
|
+
target: {
|
|
162
|
+
tool: target.name,
|
|
163
|
+
index
|
|
164
|
+
},
|
|
165
|
+
detail: style
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** Prepare K2 missing-required without selecting a fault or spending counters. */
|
|
170
|
+
function prepareMissingRequiredCandidate(context, fault) {
|
|
171
|
+
const response = context.response;
|
|
172
|
+
const combined = isCombinedFixtureResponse(response);
|
|
173
|
+
if (isErrorResponse(response) || !(combined || isToolCallResponse(response))) return {
|
|
174
|
+
kind: "not-applicable",
|
|
175
|
+
detail: "Response has no tool calls"
|
|
176
|
+
};
|
|
177
|
+
const outcome = faultBlockOutcome(response);
|
|
178
|
+
const calls = outcome?.toolCalls ?? servedToolCalls(response);
|
|
179
|
+
const index = faultTargetIndex(calls, fault.tool);
|
|
180
|
+
const target = functionCallAt(calls, index);
|
|
181
|
+
if (!target) return {
|
|
182
|
+
kind: "not-applicable",
|
|
183
|
+
detail: "Target tool call is absent"
|
|
184
|
+
};
|
|
185
|
+
const args = toolArgsForWire(target);
|
|
186
|
+
if (args.kind !== "parsed" || !isObject(args.value)) return {
|
|
187
|
+
kind: "not-applicable",
|
|
188
|
+
detail: "Arguments must parse to an object"
|
|
189
|
+
};
|
|
190
|
+
const value = args.value;
|
|
191
|
+
const schema = targetToolSchema(context, target);
|
|
192
|
+
if (!schema) return {
|
|
193
|
+
kind: "not-applicable",
|
|
194
|
+
detail: "Target tool has no direct schema"
|
|
195
|
+
};
|
|
196
|
+
const property = fault.property ?? schema.required.find((name) => Object.hasOwn(value, name));
|
|
197
|
+
if (property === void 0 || !schema.required.includes(property) || !Object.hasOwn(value, property)) return {
|
|
198
|
+
kind: "not-applicable",
|
|
199
|
+
detail: "No present required property can be removed"
|
|
200
|
+
};
|
|
201
|
+
const copied = { ...value };
|
|
202
|
+
delete copied[property];
|
|
203
|
+
const argumentsText = JSON.stringify(copied);
|
|
204
|
+
return {
|
|
205
|
+
kind: "ready",
|
|
206
|
+
candidate: {
|
|
207
|
+
response: rewriteFaultToolCalls(response, outcome, calls.map((call, callIndex) => callIndex === index && call.type !== "custom" ? {
|
|
208
|
+
...call,
|
|
209
|
+
arguments: argumentsText
|
|
210
|
+
} : { ...call })),
|
|
211
|
+
target: {
|
|
212
|
+
tool: target.name,
|
|
213
|
+
index
|
|
214
|
+
},
|
|
215
|
+
detail: "missing-required"
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function wrongTypeReplacement(type) {
|
|
220
|
+
switch (type) {
|
|
221
|
+
case "string":
|
|
222
|
+
case "null": return 12345;
|
|
223
|
+
case "number":
|
|
224
|
+
case "integer": return "not-a-number";
|
|
225
|
+
case "boolean": return "true";
|
|
226
|
+
case "array": return {};
|
|
227
|
+
case "object": return "[object]";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Prepare K2 wrong-type without selecting a fault or spending counters. */
|
|
231
|
+
function prepareWrongTypeCandidate(context, fault) {
|
|
232
|
+
const response = context.response;
|
|
233
|
+
const combined = isCombinedFixtureResponse(response);
|
|
234
|
+
if (isErrorResponse(response) || !(combined || isToolCallResponse(response))) return {
|
|
235
|
+
kind: "not-applicable",
|
|
236
|
+
detail: "Response has no tool calls"
|
|
237
|
+
};
|
|
238
|
+
const outcome = faultBlockOutcome(response);
|
|
239
|
+
const calls = outcome?.toolCalls ?? servedToolCalls(response);
|
|
240
|
+
const index = faultTargetIndex(calls, fault.tool);
|
|
241
|
+
const target = functionCallAt(calls, index);
|
|
242
|
+
if (!target) return {
|
|
243
|
+
kind: "not-applicable",
|
|
244
|
+
detail: "Target tool call is absent"
|
|
245
|
+
};
|
|
246
|
+
const args = toolArgsForWire(target);
|
|
247
|
+
if (args.kind !== "parsed" || !isObject(args.value)) return {
|
|
248
|
+
kind: "not-applicable",
|
|
249
|
+
detail: "Arguments must parse to an object"
|
|
250
|
+
};
|
|
251
|
+
const value = args.value;
|
|
252
|
+
const schema = targetToolSchema(context, target);
|
|
253
|
+
if (!schema) return {
|
|
254
|
+
kind: "not-applicable",
|
|
255
|
+
detail: "Target tool has no direct schema"
|
|
256
|
+
};
|
|
257
|
+
const names = fault.property === void 0 ? Object.keys(schema.properties) : [fault.property];
|
|
258
|
+
let mutation;
|
|
259
|
+
for (const property of names) {
|
|
260
|
+
if (!Object.hasOwn(value, property) || !Object.hasOwn(schema.properties, property)) continue;
|
|
261
|
+
const propertySchema = schema.properties[property];
|
|
262
|
+
const types = directPropertyTypes(propertySchema);
|
|
263
|
+
if (!types) continue;
|
|
264
|
+
const replacement = (!(isObject(propertySchema) && Array.isArray(propertySchema.type)) ? [wrongTypeReplacement(types[0])] : [
|
|
265
|
+
12345,
|
|
266
|
+
"not-a-number",
|
|
267
|
+
"true",
|
|
268
|
+
{},
|
|
269
|
+
"[object]",
|
|
270
|
+
null
|
|
271
|
+
]).find((candidate) => types.every((type) => !acceptsDirectType(candidate, type)));
|
|
272
|
+
if (replacement === void 0 || JSON.stringify(replacement) === JSON.stringify(value[property])) continue;
|
|
273
|
+
mutation = {
|
|
274
|
+
property,
|
|
275
|
+
replacement
|
|
276
|
+
};
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
if (!mutation) return {
|
|
280
|
+
kind: "not-applicable",
|
|
281
|
+
detail: "No present declared property has a provably wrong replacement"
|
|
282
|
+
};
|
|
283
|
+
const argumentsText = JSON.stringify({
|
|
284
|
+
...value,
|
|
285
|
+
[mutation.property]: mutation.replacement
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
kind: "ready",
|
|
289
|
+
candidate: {
|
|
290
|
+
response: rewriteFaultToolCalls(response, outcome, calls.map((call, callIndex) => callIndex === index && call.type !== "custom" ? {
|
|
291
|
+
...call,
|
|
292
|
+
arguments: argumentsText
|
|
293
|
+
} : { ...call })),
|
|
294
|
+
target: {
|
|
295
|
+
tool: target.name,
|
|
296
|
+
index
|
|
297
|
+
},
|
|
298
|
+
detail: "wrong-type"
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/** Prepare K2 extra-property only when direct constraints prove the addition invalid. */
|
|
303
|
+
function prepareExtraPropertyCandidate(context, fault) {
|
|
304
|
+
const response = context.response;
|
|
305
|
+
const combined = isCombinedFixtureResponse(response);
|
|
306
|
+
if (isErrorResponse(response) || !(combined || isToolCallResponse(response))) return {
|
|
307
|
+
kind: "not-applicable",
|
|
308
|
+
detail: "Response has no tool calls"
|
|
309
|
+
};
|
|
310
|
+
const outcome = faultBlockOutcome(response);
|
|
311
|
+
const calls = outcome?.toolCalls ?? servedToolCalls(response);
|
|
312
|
+
const index = faultTargetIndex(calls, fault.tool);
|
|
313
|
+
const target = functionCallAt(calls, index);
|
|
314
|
+
if (!target) return {
|
|
315
|
+
kind: "not-applicable",
|
|
316
|
+
detail: "Target tool call is absent"
|
|
317
|
+
};
|
|
318
|
+
const args = toolArgsForWire(target);
|
|
319
|
+
if (args.kind !== "parsed" || !isObject(args.value)) return {
|
|
320
|
+
kind: "not-applicable",
|
|
321
|
+
detail: "Arguments must parse to an object"
|
|
322
|
+
};
|
|
323
|
+
const value = args.value;
|
|
324
|
+
const schema = targetToolSchema(context, target);
|
|
325
|
+
if (!schema || schema.additionalProperties !== false) return {
|
|
326
|
+
kind: "not-applicable",
|
|
327
|
+
detail: "Target schema does not forbid additional properties"
|
|
328
|
+
};
|
|
329
|
+
const property = fault.property ?? "__aimock_extra";
|
|
330
|
+
if (Object.hasOwn(schema.properties, property) || Object.hasOwn(value, property)) return {
|
|
331
|
+
kind: "not-applicable",
|
|
332
|
+
detail: "Extra property name is already declared or present"
|
|
333
|
+
};
|
|
334
|
+
for (const [pattern, patternSchema] of Object.entries(schema.patternProperties ?? {})) try {
|
|
335
|
+
if (new RegExp(pattern).test(property) && patternSchema !== false) return {
|
|
336
|
+
kind: "not-applicable",
|
|
337
|
+
detail: "A pattern covers the extra property name"
|
|
338
|
+
};
|
|
339
|
+
} catch {
|
|
340
|
+
return {
|
|
341
|
+
kind: "not-applicable",
|
|
342
|
+
detail: "A property pattern cannot be evaluated"
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const argumentsText = JSON.stringify({
|
|
346
|
+
...value,
|
|
347
|
+
[property]: true
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
kind: "ready",
|
|
351
|
+
candidate: {
|
|
352
|
+
response: rewriteFaultToolCalls(response, outcome, calls.map((call, callIndex) => callIndex === index && call.type !== "custom" ? {
|
|
353
|
+
...call,
|
|
354
|
+
arguments: argumentsText
|
|
355
|
+
} : { ...call })),
|
|
356
|
+
target: {
|
|
357
|
+
tool: target.name,
|
|
358
|
+
index
|
|
359
|
+
},
|
|
360
|
+
detail: "extra-property"
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/** Prepare K2 enum-mismatch without selecting a fault or spending counters. */
|
|
365
|
+
function prepareEnumMismatchCandidate(context, fault) {
|
|
366
|
+
const response = context.response;
|
|
367
|
+
const combined = isCombinedFixtureResponse(response);
|
|
368
|
+
if (isErrorResponse(response) || !(combined || isToolCallResponse(response))) return {
|
|
369
|
+
kind: "not-applicable",
|
|
370
|
+
detail: "Response has no tool calls"
|
|
371
|
+
};
|
|
372
|
+
const outcome = faultBlockOutcome(response);
|
|
373
|
+
const calls = outcome?.toolCalls ?? servedToolCalls(response);
|
|
374
|
+
const index = faultTargetIndex(calls, fault.tool);
|
|
375
|
+
const target = functionCallAt(calls, index);
|
|
376
|
+
if (!target) return {
|
|
377
|
+
kind: "not-applicable",
|
|
378
|
+
detail: "Target tool call is absent"
|
|
379
|
+
};
|
|
380
|
+
const args = toolArgsForWire(target);
|
|
381
|
+
if (args.kind !== "parsed" || !isObject(args.value)) return {
|
|
382
|
+
kind: "not-applicable",
|
|
383
|
+
detail: "Arguments must parse to an object"
|
|
384
|
+
};
|
|
385
|
+
const value = args.value;
|
|
386
|
+
const schema = targetToolSchema(context, target);
|
|
387
|
+
if (!schema) return {
|
|
388
|
+
kind: "not-applicable",
|
|
389
|
+
detail: "Target tool has no direct schema"
|
|
390
|
+
};
|
|
391
|
+
const names = fault.property === void 0 ? Object.keys(schema.properties) : [fault.property];
|
|
392
|
+
let mutation;
|
|
393
|
+
for (const property of names) {
|
|
394
|
+
if (!Object.hasOwn(value, property) || !Object.hasOwn(schema.properties, property)) continue;
|
|
395
|
+
const propertySchema = schema.properties[property];
|
|
396
|
+
if (!isObject(propertySchema) || !Array.isArray(propertySchema.enum)) continue;
|
|
397
|
+
let replacement = "__aimock_not_in_enum";
|
|
398
|
+
let suffix = 2;
|
|
399
|
+
while (propertySchema.enum.includes(replacement)) replacement = `__aimock_not_in_enum_${suffix++}`;
|
|
400
|
+
if (replacement === value[property]) continue;
|
|
401
|
+
mutation = {
|
|
402
|
+
property,
|
|
403
|
+
replacement
|
|
404
|
+
};
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
if (!mutation) return {
|
|
408
|
+
kind: "not-applicable",
|
|
409
|
+
detail: "No present declared enum property has a changed out-of-enum replacement"
|
|
410
|
+
};
|
|
411
|
+
const argumentsText = JSON.stringify({
|
|
412
|
+
...value,
|
|
413
|
+
[mutation.property]: mutation.replacement
|
|
414
|
+
});
|
|
415
|
+
return {
|
|
416
|
+
kind: "ready",
|
|
417
|
+
candidate: {
|
|
418
|
+
response: rewriteFaultToolCalls(response, outcome, calls.map((call, callIndex) => callIndex === index && call.type !== "custom" ? {
|
|
419
|
+
...call,
|
|
420
|
+
arguments: argumentsText
|
|
421
|
+
} : { ...call })),
|
|
422
|
+
target: {
|
|
423
|
+
tool: target.name,
|
|
424
|
+
index
|
|
425
|
+
},
|
|
426
|
+
detail: "enum-mismatch"
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/** Prepare K2 not-object without selecting a fault or spending counters. */
|
|
431
|
+
function prepareNotObjectCandidate(context, fault) {
|
|
432
|
+
if (!supportsMisbehavior(context.wire, {
|
|
433
|
+
...fault,
|
|
434
|
+
violation: "not-object"
|
|
435
|
+
}, context.stream)) return {
|
|
436
|
+
kind: "not-applicable",
|
|
437
|
+
detail: "not-object is unsupported on this wire/output mode"
|
|
438
|
+
};
|
|
439
|
+
const response = context.response;
|
|
440
|
+
const combined = isCombinedFixtureResponse(response);
|
|
441
|
+
if (isErrorResponse(response) || !(combined || isToolCallResponse(response))) return {
|
|
442
|
+
kind: "not-applicable",
|
|
443
|
+
detail: "Response has no tool calls"
|
|
444
|
+
};
|
|
445
|
+
const outcome = faultBlockOutcome(response);
|
|
446
|
+
const calls = outcome?.toolCalls ?? servedToolCalls(response);
|
|
447
|
+
const index = faultTargetIndex(calls, fault.tool);
|
|
448
|
+
const target = functionCallAt(calls, index);
|
|
449
|
+
if (!target) return {
|
|
450
|
+
kind: "not-applicable",
|
|
451
|
+
detail: "Target tool call is absent"
|
|
452
|
+
};
|
|
453
|
+
const args = toolArgsForWire(target);
|
|
454
|
+
if (args.kind !== "parsed" || !isObject(args.value)) return {
|
|
455
|
+
kind: "not-applicable",
|
|
456
|
+
detail: "Arguments must parse to an object"
|
|
457
|
+
};
|
|
458
|
+
const argumentsText = JSON.stringify(args.text);
|
|
459
|
+
return {
|
|
460
|
+
kind: "ready",
|
|
461
|
+
candidate: {
|
|
462
|
+
response: rewriteFaultToolCalls(response, outcome, calls.map((call, callIndex) => callIndex === index && call.type !== "custom" ? {
|
|
463
|
+
...call,
|
|
464
|
+
arguments: argumentsText
|
|
465
|
+
} : { ...call })),
|
|
466
|
+
target: {
|
|
467
|
+
tool: target.name,
|
|
468
|
+
index
|
|
469
|
+
},
|
|
470
|
+
detail: "not-object"
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const fixtureMisbehaviorPositions = /* @__PURE__ */ new WeakMap();
|
|
475
|
+
function setFixtureMisbehaviorPosition(fixture, position) {
|
|
476
|
+
fixtureMisbehaviorPositions.set(fixture, position);
|
|
477
|
+
}
|
|
478
|
+
function getFixtureMisbehaviorPosition(fixture) {
|
|
479
|
+
return fixtureMisbehaviorPositions.get(fixture);
|
|
480
|
+
}
|
|
481
|
+
function copyFixtureMisbehaviorPosition(from, to) {
|
|
482
|
+
const position = getFixtureMisbehaviorPosition(from);
|
|
483
|
+
if (position !== void 0) setFixtureMisbehaviorPosition(to, position);
|
|
484
|
+
}
|
|
485
|
+
/** Shared by fixture identity and the seeded planner; hash UTF-8 bytes, not UTF-16 units. */
|
|
486
|
+
function fnv1a32(text) {
|
|
487
|
+
let hash = 2166136261;
|
|
488
|
+
for (const byte of new TextEncoder().encode(text)) hash = Math.imul(hash ^ byte, 16777619);
|
|
489
|
+
return hash >>> 0;
|
|
490
|
+
}
|
|
491
|
+
/** Canonicalize the declared match/config domain without shortening strings or arrays. */
|
|
492
|
+
function canonicalIdentityValue(value) {
|
|
493
|
+
if (value instanceof RegExp) return { $regexp: value.toString() };
|
|
494
|
+
if (typeof value === "function") return "fn";
|
|
495
|
+
if (Array.isArray(value)) return value.map(canonicalIdentityValue);
|
|
496
|
+
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, canonicalIdentityValue(child)]));
|
|
497
|
+
return value;
|
|
498
|
+
}
|
|
499
|
+
/** Ingress must assign a logical load/addition position before planning a fixture fault. */
|
|
500
|
+
function fixtureMisbehaviorSourceKey(fixture, config) {
|
|
501
|
+
const position = getFixtureMisbehaviorPosition(fixture);
|
|
502
|
+
if (!position) throw new Error("Missing fixture misbehavior position: internal invariant");
|
|
503
|
+
const canonicalMatch = JSON.stringify(canonicalIdentityValue(fixture.match));
|
|
504
|
+
const seedlessConfig = Object.fromEntries(Object.entries(config).filter(([key]) => key !== "seed"));
|
|
505
|
+
return `fixture:${fnv1a32(`${canonicalMatch}|${JSON.stringify(canonicalIdentityValue(seedlessConfig))}|${position}`)}`;
|
|
506
|
+
}
|
|
507
|
+
const string = (value) => typeof value === "string";
|
|
508
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
509
|
+
const choices = (...values) => (value) => typeof value === "string" && values.includes(value);
|
|
510
|
+
/** Single catalog for accepted parameters and their defaults. Defaults are applied by the planner. */
|
|
511
|
+
const MISBEHAVIOR_CATALOG = {
|
|
512
|
+
"tool-args-invalid-json": {
|
|
513
|
+
parameters: { style: choices("truncated", "trailing-comma", "single-quotes") },
|
|
514
|
+
defaults: { style: "truncated" }
|
|
515
|
+
},
|
|
516
|
+
"tool-args-schema-violation": {
|
|
517
|
+
parameters: {
|
|
518
|
+
violation: choices("missing-required", "wrong-type", "extra-property", "enum-mismatch", "not-object"),
|
|
519
|
+
property: string
|
|
520
|
+
},
|
|
521
|
+
defaults: { violation: "missing-required" }
|
|
522
|
+
},
|
|
523
|
+
"tool-unknown-name": {
|
|
524
|
+
parameters: { name: string },
|
|
525
|
+
defaults: {}
|
|
526
|
+
},
|
|
527
|
+
"tool-call-id-duplicate": {
|
|
528
|
+
parameters: {},
|
|
529
|
+
defaults: {}
|
|
530
|
+
},
|
|
531
|
+
"stop-length-mid-tool": {
|
|
532
|
+
parameters: { at: (v) => typeof v === "number" && finite(v) && v > 0 && v < 1 },
|
|
533
|
+
defaults: { at: .5 }
|
|
534
|
+
},
|
|
535
|
+
"empty-response": {
|
|
536
|
+
parameters: {},
|
|
537
|
+
defaults: {}
|
|
538
|
+
},
|
|
539
|
+
refusal: {
|
|
540
|
+
parameters: {
|
|
541
|
+
message: string,
|
|
542
|
+
category: (v) => v === null || string(v)
|
|
543
|
+
},
|
|
544
|
+
defaults: {
|
|
545
|
+
message: "I can't help with that.",
|
|
546
|
+
category: null
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
"content-filter": {
|
|
550
|
+
parameters: {},
|
|
551
|
+
defaults: {}
|
|
552
|
+
},
|
|
553
|
+
"reasoning-only": {
|
|
554
|
+
parameters: { reasoning: string },
|
|
555
|
+
defaults: {}
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
/** Engine stage: all nine faults are supported only on OpenAI Chat. */
|
|
559
|
+
const WIRE_SUPPORT = {
|
|
560
|
+
"openai-chat": {
|
|
561
|
+
faults: {
|
|
562
|
+
"tool-args-invalid-json": "yes",
|
|
563
|
+
"tool-args-schema-violation": "yes",
|
|
564
|
+
"tool-unknown-name": "yes",
|
|
565
|
+
"tool-call-id-duplicate": "yes",
|
|
566
|
+
"stop-length-mid-tool": "yes",
|
|
567
|
+
"empty-response": "yes",
|
|
568
|
+
refusal: "yes",
|
|
569
|
+
"content-filter": "yes",
|
|
570
|
+
"reasoning-only": "yes"
|
|
571
|
+
},
|
|
572
|
+
refusalCategory: false,
|
|
573
|
+
notObject: {
|
|
574
|
+
stream: true,
|
|
575
|
+
nonstream: true
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
"openai-responses": {
|
|
579
|
+
faults: {
|
|
580
|
+
"tool-args-invalid-json": "yes",
|
|
581
|
+
"tool-args-schema-violation": "yes",
|
|
582
|
+
"tool-unknown-name": "yes",
|
|
583
|
+
"tool-call-id-duplicate": "yes",
|
|
584
|
+
"stop-length-mid-tool": "yes",
|
|
585
|
+
"empty-response": "yes",
|
|
586
|
+
refusal: "yes",
|
|
587
|
+
"content-filter": "yes",
|
|
588
|
+
"reasoning-only": "yes"
|
|
589
|
+
},
|
|
590
|
+
refusalCategory: false,
|
|
591
|
+
notObject: {
|
|
592
|
+
stream: true,
|
|
593
|
+
nonstream: true
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
"openai-realtime": {
|
|
597
|
+
faults: {
|
|
598
|
+
"tool-args-invalid-json": "yes",
|
|
599
|
+
"tool-args-schema-violation": "yes",
|
|
600
|
+
"tool-unknown-name": "yes",
|
|
601
|
+
"tool-call-id-duplicate": "yes",
|
|
602
|
+
"stop-length-mid-tool": "yes",
|
|
603
|
+
"empty-response": "yes",
|
|
604
|
+
refusal: "n/a",
|
|
605
|
+
"content-filter": "yes",
|
|
606
|
+
"reasoning-only": "n/a"
|
|
607
|
+
},
|
|
608
|
+
refusalCategory: false,
|
|
609
|
+
notObject: {
|
|
610
|
+
stream: true,
|
|
611
|
+
nonstream: true
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
anthropic: {
|
|
615
|
+
faults: {
|
|
616
|
+
"tool-args-invalid-json": "stream-only",
|
|
617
|
+
"tool-args-schema-violation": "yes",
|
|
618
|
+
"tool-unknown-name": "yes",
|
|
619
|
+
"tool-call-id-duplicate": "yes",
|
|
620
|
+
"stop-length-mid-tool": "yes",
|
|
621
|
+
"empty-response": "yes",
|
|
622
|
+
refusal: "yes",
|
|
623
|
+
"content-filter": "n/a",
|
|
624
|
+
"reasoning-only": "yes"
|
|
625
|
+
},
|
|
626
|
+
refusalCategory: true,
|
|
627
|
+
notObject: {
|
|
628
|
+
stream: true,
|
|
629
|
+
nonstream: false
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
"bedrock-invoke": {
|
|
633
|
+
faults: {
|
|
634
|
+
"tool-args-invalid-json": "stream-only",
|
|
635
|
+
"tool-args-schema-violation": "yes",
|
|
636
|
+
"tool-unknown-name": "yes",
|
|
637
|
+
"tool-call-id-duplicate": "yes",
|
|
638
|
+
"stop-length-mid-tool": "yes",
|
|
639
|
+
"empty-response": "yes",
|
|
640
|
+
refusal: "yes",
|
|
641
|
+
"content-filter": "n/a",
|
|
642
|
+
"reasoning-only": "yes"
|
|
643
|
+
},
|
|
644
|
+
refusalCategory: true,
|
|
645
|
+
notObject: {
|
|
646
|
+
stream: true,
|
|
647
|
+
nonstream: false
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
"bedrock-converse": {
|
|
651
|
+
faults: {
|
|
652
|
+
"tool-args-invalid-json": "yes",
|
|
653
|
+
"tool-args-schema-violation": "yes",
|
|
654
|
+
"tool-unknown-name": "yes",
|
|
655
|
+
"tool-call-id-duplicate": "yes",
|
|
656
|
+
"stop-length-mid-tool": "yes",
|
|
657
|
+
"empty-response": "yes",
|
|
658
|
+
refusal: "n/a",
|
|
659
|
+
"content-filter": "yes",
|
|
660
|
+
"reasoning-only": "yes"
|
|
661
|
+
},
|
|
662
|
+
refusalCategory: false,
|
|
663
|
+
notObject: {
|
|
664
|
+
stream: true,
|
|
665
|
+
nonstream: false
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
gemini: {
|
|
669
|
+
faults: {
|
|
670
|
+
"tool-args-invalid-json": "yes",
|
|
671
|
+
"tool-args-schema-violation": "yes",
|
|
672
|
+
"tool-unknown-name": "yes",
|
|
673
|
+
"tool-call-id-duplicate": "yes",
|
|
674
|
+
"stop-length-mid-tool": "yes",
|
|
675
|
+
"empty-response": "yes",
|
|
676
|
+
refusal: "n/a",
|
|
677
|
+
"content-filter": "yes",
|
|
678
|
+
"reasoning-only": "yes"
|
|
679
|
+
},
|
|
680
|
+
refusalCategory: false,
|
|
681
|
+
notObject: {
|
|
682
|
+
stream: false,
|
|
683
|
+
nonstream: false
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
"gemini-live": {
|
|
687
|
+
faults: {
|
|
688
|
+
"tool-args-invalid-json": "yes",
|
|
689
|
+
"tool-args-schema-violation": "yes",
|
|
690
|
+
"tool-unknown-name": "yes",
|
|
691
|
+
"tool-call-id-duplicate": "yes",
|
|
692
|
+
"stop-length-mid-tool": "n/a",
|
|
693
|
+
"empty-response": "yes",
|
|
694
|
+
refusal: "n/a",
|
|
695
|
+
"content-filter": "n/a",
|
|
696
|
+
"reasoning-only": "n/a"
|
|
697
|
+
},
|
|
698
|
+
refusalCategory: false,
|
|
699
|
+
notObject: {
|
|
700
|
+
stream: false,
|
|
701
|
+
nonstream: false
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
"gemini-interactions": {
|
|
705
|
+
faults: {
|
|
706
|
+
"tool-args-invalid-json": "stream-only",
|
|
707
|
+
"tool-args-schema-violation": "yes",
|
|
708
|
+
"tool-unknown-name": "yes",
|
|
709
|
+
"tool-call-id-duplicate": "yes",
|
|
710
|
+
"stop-length-mid-tool": "yes",
|
|
711
|
+
"empty-response": "yes",
|
|
712
|
+
refusal: "n/a",
|
|
713
|
+
"content-filter": "n/a",
|
|
714
|
+
"reasoning-only": "yes"
|
|
715
|
+
},
|
|
716
|
+
refusalCategory: false,
|
|
717
|
+
notObject: {
|
|
718
|
+
stream: true,
|
|
719
|
+
nonstream: false
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
cohere: {
|
|
723
|
+
faults: {
|
|
724
|
+
"tool-args-invalid-json": "yes",
|
|
725
|
+
"tool-args-schema-violation": "yes",
|
|
726
|
+
"tool-unknown-name": "yes",
|
|
727
|
+
"tool-call-id-duplicate": "yes",
|
|
728
|
+
"stop-length-mid-tool": "yes",
|
|
729
|
+
"empty-response": "yes",
|
|
730
|
+
refusal: "n/a",
|
|
731
|
+
"content-filter": "n/a",
|
|
732
|
+
"reasoning-only": "n/a"
|
|
733
|
+
},
|
|
734
|
+
refusalCategory: false,
|
|
735
|
+
notObject: {
|
|
736
|
+
stream: true,
|
|
737
|
+
nonstream: true
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
ollama: {
|
|
741
|
+
faults: {
|
|
742
|
+
"tool-args-invalid-json": "yes",
|
|
743
|
+
"tool-args-schema-violation": "yes",
|
|
744
|
+
"tool-unknown-name": "yes",
|
|
745
|
+
"tool-call-id-duplicate": "n/a",
|
|
746
|
+
"stop-length-mid-tool": "n/a",
|
|
747
|
+
"empty-response": "yes",
|
|
748
|
+
refusal: "n/a",
|
|
749
|
+
"content-filter": "n/a",
|
|
750
|
+
"reasoning-only": "yes"
|
|
751
|
+
},
|
|
752
|
+
refusalCategory: false,
|
|
753
|
+
notObject: {
|
|
754
|
+
stream: false,
|
|
755
|
+
nonstream: false
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
function supportsMisbehavior(wire, fault, stream) {
|
|
760
|
+
if (!isWireId(wire)) return false;
|
|
761
|
+
const support = WIRE_SUPPORT[wire];
|
|
762
|
+
const mode = stream ? "stream" : "nonstream";
|
|
763
|
+
const base = support.faults[fault.fault];
|
|
764
|
+
if (base === "n/a" || base === "stream-only" && !stream) return false;
|
|
765
|
+
if (fault.fault === "refusal" && Object.hasOwn(fault, "category") && !support.refusalCategory) return false;
|
|
766
|
+
return fault.fault !== "tool-args-schema-violation" || fault.violation !== "not-object" || support.notObject[mode];
|
|
767
|
+
}
|
|
768
|
+
function isWireId(value) {
|
|
769
|
+
return typeof value === "string" && Object.hasOwn(WIRE_SUPPORT, value);
|
|
770
|
+
}
|
|
771
|
+
function isFaultId(value) {
|
|
772
|
+
return typeof value === "string" && Object.hasOwn(MISBEHAVIOR_CATALOG, value);
|
|
773
|
+
}
|
|
774
|
+
function isObject(value) {
|
|
775
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
776
|
+
}
|
|
777
|
+
/** Error reporting must not invoke user-authored primitive conversion hooks. */
|
|
778
|
+
function describeInvalidValue(value) {
|
|
779
|
+
try {
|
|
780
|
+
const text = JSON.stringify(value) ?? typeof value;
|
|
781
|
+
return text.length > 200 ? `${text.slice(0, 200)}…` : text;
|
|
782
|
+
} catch {
|
|
783
|
+
return "[unserializable value]";
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function failure(rule, path, value) {
|
|
787
|
+
return {
|
|
788
|
+
ok: false,
|
|
789
|
+
issue: {
|
|
790
|
+
rule,
|
|
791
|
+
path,
|
|
792
|
+
value,
|
|
793
|
+
message: `${rule} at ${path}: invalid value ${describeInvalidValue(value)}`
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
const common = {
|
|
798
|
+
rate: (v) => parseChaosNumber(v, 1) !== void 0,
|
|
799
|
+
times: (v) => typeof v === "number" && Number.isInteger(v) && v > 0,
|
|
800
|
+
tool: string,
|
|
801
|
+
providers: (v) => Array.isArray(v)
|
|
802
|
+
};
|
|
803
|
+
/** Strict object/shorthand parser; wire applicability is evaluated after scope/provider selection. */
|
|
804
|
+
function parseMisbehavior(input, path = "misbehavior") {
|
|
805
|
+
if (typeof input === "string") return isFaultId(input) ? {
|
|
806
|
+
ok: true,
|
|
807
|
+
config: { faults: [{ fault: input }] }
|
|
808
|
+
} : failure("misbehavior/bad-value", path, input);
|
|
809
|
+
if (!isObject(input)) return failure("misbehavior/bad-value", path, input);
|
|
810
|
+
for (const key of Object.keys(input)) if (key !== "seed" && key !== "faults") return failure("misbehavior/unknown-key", `${path}.${key}`, input[key]);
|
|
811
|
+
if (Object.hasOwn(input, "seed") && input.seed !== "random" && !(typeof input.seed === "number" && Number.isInteger(input.seed))) return failure("misbehavior/bad-value", `${path}.seed`, input.seed);
|
|
812
|
+
if (!Array.isArray(input.faults)) return failure("misbehavior/bad-value", `${path}.faults`, input.faults);
|
|
813
|
+
const faults = [];
|
|
814
|
+
for (let index = 0; index < input.faults.length; index++) {
|
|
815
|
+
const entry = input.faults[index];
|
|
816
|
+
const entryPath = `${path}.faults[${index}]`;
|
|
817
|
+
if (!isObject(entry)) return failure("misbehavior/bad-value", entryPath, entry);
|
|
818
|
+
if (!isFaultId(entry.fault)) return failure("misbehavior/bad-value", `${entryPath}.fault`, entry.fault);
|
|
819
|
+
const parameters = MISBEHAVIOR_CATALOG[entry.fault].parameters;
|
|
820
|
+
for (const key of Object.keys(entry)) {
|
|
821
|
+
if (key === "fault") continue;
|
|
822
|
+
const check = Object.hasOwn(common, key) ? common[key] : Object.hasOwn(parameters, key) ? parameters[key] : void 0;
|
|
823
|
+
if (!check) return failure("misbehavior/unknown-key", `${entryPath}.${key}`, entry[key]);
|
|
824
|
+
if (!check(entry[key])) return failure("misbehavior/bad-value", `${entryPath}.${key}`, entry[key]);
|
|
825
|
+
}
|
|
826
|
+
if (Array.isArray(entry.providers)) {
|
|
827
|
+
for (let i = 0; i < entry.providers.length; i++) if (!isWireId(entry.providers[i])) return failure("misbehavior/bad-value", `${entryPath}.providers[${i}]`, entry.providers[i]);
|
|
828
|
+
}
|
|
829
|
+
faults.push({
|
|
830
|
+
...entry,
|
|
831
|
+
...Object.hasOwn(entry, "rate") ? { rate: parseChaosNumber(entry.rate, 1) } : {},
|
|
832
|
+
...Array.isArray(entry.providers) ? { providers: [...entry.providers] } : {}
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
ok: true,
|
|
837
|
+
config: {
|
|
838
|
+
...typeof input.seed === "number" || input.seed === "random" ? { seed: input.seed } : {},
|
|
839
|
+
faults
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* One fault followed by semicolon-delimited scalar key=value pairs. Values are
|
|
845
|
+
* literal text: the first equals sign separates the key, with no unquoting.
|
|
846
|
+
* Node's repeated-header comma joining must still parse as one whole grammar.
|
|
847
|
+
*/
|
|
848
|
+
function parseMisbehaviorHeader(value) {
|
|
849
|
+
if (value === void 0) return void 0;
|
|
850
|
+
const path = "headers.x-aimock-misbehavior";
|
|
851
|
+
const [fault, ...pairs] = (Array.isArray(value) ? value.join(", ") : value).split(";").map((part) => part.trim());
|
|
852
|
+
if (!isFaultId(fault)) return failure("misbehavior/bad-value", path, value);
|
|
853
|
+
const entry = { fault };
|
|
854
|
+
for (const pair of pairs) {
|
|
855
|
+
const separator = pair.indexOf("=");
|
|
856
|
+
const key = pair.slice(0, separator).trim();
|
|
857
|
+
if (separator < 0 || !key) return failure("misbehavior/bad-value", path, pair);
|
|
858
|
+
const fieldPath = `${path}.faults[0].${key}`;
|
|
859
|
+
if (key === "fault" || key === "times" || key === "providers") return failure("misbehavior/unknown-key", fieldPath, pair);
|
|
860
|
+
if (Object.hasOwn(entry, key)) return failure("misbehavior/bad-value", fieldPath, pair);
|
|
861
|
+
const scalar = pair.slice(separator + 1).trim();
|
|
862
|
+
const parsed = key === "at" ? parseChaosNumber(scalar, 1) ?? scalar : scalar;
|
|
863
|
+
Object.defineProperty(entry, key, {
|
|
864
|
+
value: parsed,
|
|
865
|
+
enumerable: true
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
return parseMisbehavior({ faults: [entry] }, path);
|
|
869
|
+
}
|
|
870
|
+
/** Only statically decidable S3/S4 checks; no factories or request schemas are evaluated. */
|
|
871
|
+
function validateFixtureMisbehavior(fixture, path = "misbehavior") {
|
|
872
|
+
if (fixture.misbehavior === void 0) return void 0;
|
|
873
|
+
const parsed = parseMisbehavior(fixture.misbehavior, path);
|
|
874
|
+
if (!parsed.ok) return parsed.issue;
|
|
875
|
+
const endpoint = fixture.match.endpoint;
|
|
876
|
+
const wires = Object.keys(WIRE_SUPPORT).filter(isWireId).filter((wire) => endpoint === "realtime" ? wire === "openai-realtime" : endpoint === "chat" ? wire !== "openai-realtime" : true);
|
|
877
|
+
const nonchat = endpoint !== void 0 && endpoint !== "chat" && endpoint !== "realtime";
|
|
878
|
+
const response = fixture.response;
|
|
879
|
+
for (const [index, fault] of parsed.config.faults.entries()) {
|
|
880
|
+
const possible = wires.filter((wire) => fault.providers === void 0 || fault.providers.includes(wire));
|
|
881
|
+
if (possible.length === 0) continue;
|
|
882
|
+
let reason;
|
|
883
|
+
if (nonchat) reason = "misbehavior/not-applicable";
|
|
884
|
+
else if (typeof response !== "function") {
|
|
885
|
+
const combined = isCombinedFixtureResponse(response);
|
|
886
|
+
if (isErrorResponse(response) || !(combined || isTextResponse(response) || isToolCallResponse(response))) reason = "misbehavior/not-applicable";
|
|
887
|
+
else {
|
|
888
|
+
const calls = (combined ? resolveServedBlockOutcome(response)?.toolCalls : void 0) ?? ("toolCalls" in response ? servedToolCalls(response) : []);
|
|
889
|
+
const toolFault = fault.fault.startsWith("tool-") || fault.fault === "stop-length-mid-tool";
|
|
890
|
+
const target = toolFault ? functionCallAt(calls, faultTargetIndex(calls, fault.tool)) : void 0;
|
|
891
|
+
if (toolFault && !target) reason = "misbehavior/not-applicable";
|
|
892
|
+
else if (fault.tool !== void 0 && !calls.some((call) => call.name === fault.tool && call.type !== "custom")) reason = "misbehavior/not-applicable";
|
|
893
|
+
else if (target) {
|
|
894
|
+
const args = toolArgsForWire(target);
|
|
895
|
+
const canonical = args.kind === "parsed" ? args.text : args.raw;
|
|
896
|
+
if (fault.fault === "tool-args-schema-violation") if (args.kind !== "parsed" || args.value === null || typeof args.value !== "object" || Array.isArray(args.value)) reason = "misbehavior/not-applicable";
|
|
897
|
+
else {
|
|
898
|
+
const violation = fault.violation ?? MISBEHAVIOR_CATALOG[fault.fault].defaults.violation;
|
|
899
|
+
if (violation === "extra-property") {
|
|
900
|
+
if (Object.hasOwn(args.value, fault.property ?? "__aimock_extra")) reason = "misbehavior/not-applicable";
|
|
901
|
+
} else if (violation !== "not-object") {
|
|
902
|
+
if (!(fault.property === void 0 ? Object.keys(args.value).length > 0 : Object.hasOwn(args.value, fault.property))) reason = "misbehavior/not-applicable";
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
else if (fault.fault === "stop-length-mid-tool") {
|
|
906
|
+
if (Math.max(1, Math.floor(canonical.length * (fault.at ?? MISBEHAVIOR_CATALOG[fault.fault].defaults.at))) >= canonical.length) reason = "misbehavior/not-applicable";
|
|
907
|
+
} else if (fault.fault === "tool-args-invalid-json") {
|
|
908
|
+
if (invalidJsonArguments(canonical, fault.style ?? MISBEHAVIOR_CATALOG[fault.fault].defaults.style) === void 0) reason = "misbehavior/not-applicable";
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (!reason && !possible.some((wire) => supportsMisbehavior(wire, fault, false) || supportsMisbehavior(wire, fault, true))) reason = "misbehavior/unsupported-on-wire";
|
|
914
|
+
if (reason) {
|
|
915
|
+
const entryPath = `${path}.faults[${index}]`;
|
|
916
|
+
return {
|
|
917
|
+
rule: reason,
|
|
918
|
+
path: entryPath,
|
|
919
|
+
value: fault,
|
|
920
|
+
message: `${reason} at ${entryPath}: ${fault.fault} cannot apply to this fixture`
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/** Prepare K3 without selecting a fault, spending counters, or mutating input. */
|
|
926
|
+
function prepareUnknownNameCandidate(context, fault) {
|
|
927
|
+
const response = context.response;
|
|
928
|
+
const combined = isCombinedFixtureResponse(response);
|
|
929
|
+
if (isErrorResponse(response) || !(combined || isToolCallResponse(response))) return {
|
|
930
|
+
kind: "not-applicable",
|
|
931
|
+
detail: "Response has no tool calls"
|
|
932
|
+
};
|
|
933
|
+
const outcome = faultBlockOutcome(response);
|
|
934
|
+
const calls = outcome?.toolCalls ?? servedToolCalls(response);
|
|
935
|
+
const index = faultTargetIndex(calls, fault.tool);
|
|
936
|
+
const target = functionCallAt(calls, index);
|
|
937
|
+
if (!target) return {
|
|
938
|
+
kind: "not-applicable",
|
|
939
|
+
detail: "Target tool call is absent"
|
|
940
|
+
};
|
|
941
|
+
const inScope = (tool) => context.wire !== "openai-responses" || tool.namespace === target.namespace;
|
|
942
|
+
const declared = new Set((context.request.tools ?? []).filter(inScope).map((tool) => tool.function.name));
|
|
943
|
+
let name = fault.name ?? `${target.name}_v2`;
|
|
944
|
+
if (fault.name !== void 0 && declared.has(name)) return {
|
|
945
|
+
kind: "not-applicable",
|
|
946
|
+
detail: "Explicit replacement tool name is declared in the request"
|
|
947
|
+
};
|
|
948
|
+
if (fault.name === void 0) {
|
|
949
|
+
const base = name;
|
|
950
|
+
for (let suffix = 2; declared.has(name); suffix++) name = `${base}_${suffix}`;
|
|
951
|
+
}
|
|
952
|
+
if (name === target.name) return {
|
|
953
|
+
kind: "not-applicable",
|
|
954
|
+
detail: "Replacement tool name is unchanged"
|
|
955
|
+
};
|
|
956
|
+
return {
|
|
957
|
+
kind: "ready",
|
|
958
|
+
candidate: {
|
|
959
|
+
response: rewriteFaultToolCalls(response, outcome, calls.map((call, callIndex) => callIndex === index ? {
|
|
960
|
+
...call,
|
|
961
|
+
name
|
|
962
|
+
} : { ...call })),
|
|
963
|
+
target: {
|
|
964
|
+
tool: target.name,
|
|
965
|
+
index
|
|
966
|
+
},
|
|
967
|
+
detail: name
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
/** Prepare shared identity without allocating the renderer's opaque call IDs. */
|
|
972
|
+
function prepareDuplicateIdCandidate(context, fault) {
|
|
973
|
+
if (context.toolCallIdMode !== "authored-nonempty" && typeof context.emitsToolCallIds !== "boolean") throw new TypeError("K4 preparation requires the selected output mode's emitsToolCallIds capability");
|
|
974
|
+
if (context.toolCallIdMode === void 0 && !context.emitsToolCallIds) return {
|
|
975
|
+
kind: "not-applicable",
|
|
976
|
+
detail: "Current output mode does not emit tool call IDs"
|
|
977
|
+
};
|
|
978
|
+
const response = context.response;
|
|
979
|
+
const combined = isCombinedFixtureResponse(response);
|
|
980
|
+
if (isErrorResponse(response) || !(combined || isToolCallResponse(response))) return {
|
|
981
|
+
kind: "not-applicable",
|
|
982
|
+
detail: "Response has no tool calls"
|
|
983
|
+
};
|
|
984
|
+
const outcome = faultBlockOutcome(response);
|
|
985
|
+
const calls = outcome?.toolCalls ?? servedToolCalls(response);
|
|
986
|
+
const sourceIndex = faultTargetIndex(calls, fault.tool);
|
|
987
|
+
const source = functionCallAt(calls, sourceIndex);
|
|
988
|
+
if (!source) return {
|
|
989
|
+
kind: "not-applicable",
|
|
990
|
+
detail: "Target tool call is absent"
|
|
991
|
+
};
|
|
992
|
+
if (context.toolCallIdMode === "authored-nonempty") {
|
|
993
|
+
const emitsToolCallIds = Boolean(source.id);
|
|
994
|
+
if (context.emitsToolCallIds !== void 0 && context.emitsToolCallIds !== emitsToolCallIds) throw new Error("Conflicting current output ID capabilities: internal invariant");
|
|
995
|
+
if (!emitsToolCallIds) return {
|
|
996
|
+
kind: "not-applicable",
|
|
997
|
+
detail: "Current output mode does not emit target tool call ID"
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
let destinationIndex = -1;
|
|
1001
|
+
for (let step = 1; step < calls.length && destinationIndex === -1; step++) if (functionCallAt(calls, (sourceIndex + step) % calls.length)) destinationIndex = (sourceIndex + step) % calls.length;
|
|
1002
|
+
const inserted = destinationIndex === -1;
|
|
1003
|
+
const toolCalls = calls.map((call) => ({ ...call }));
|
|
1004
|
+
if (inserted) {
|
|
1005
|
+
destinationIndex = sourceIndex + 1;
|
|
1006
|
+
toolCalls.splice(destinationIndex, 0, { ...source });
|
|
1007
|
+
}
|
|
1008
|
+
if (source.id === void 0) delete toolCalls[destinationIndex].id;
|
|
1009
|
+
else toolCalls[destinationIndex].id = source.id;
|
|
1010
|
+
let rewritten = withServedToolCalls(response, toolCalls);
|
|
1011
|
+
if (outcome) {
|
|
1012
|
+
let callIndex = 0;
|
|
1013
|
+
const blocks = outcome.ordered.flatMap((block) => {
|
|
1014
|
+
if (block.type === "text") return [{ ...block }];
|
|
1015
|
+
const isSource = callIndex === sourceIndex;
|
|
1016
|
+
const rewrittenBlock = toolCallFixtureBlock(toolCalls[callIndex++]);
|
|
1017
|
+
if (!(inserted && isSource)) return [rewrittenBlock];
|
|
1018
|
+
return [rewrittenBlock, toolCallFixtureBlock(toolCalls[callIndex++])];
|
|
1019
|
+
});
|
|
1020
|
+
rewritten = withServedToolCalls({
|
|
1021
|
+
...response,
|
|
1022
|
+
content: outcome.content
|
|
1023
|
+
}, toolCalls, blocks);
|
|
1024
|
+
}
|
|
1025
|
+
return {
|
|
1026
|
+
kind: "ready",
|
|
1027
|
+
candidate: {
|
|
1028
|
+
response: rewritten,
|
|
1029
|
+
target: {
|
|
1030
|
+
tool: source.name,
|
|
1031
|
+
index: sourceIndex
|
|
1032
|
+
},
|
|
1033
|
+
duplicateId: {
|
|
1034
|
+
sourceIndex,
|
|
1035
|
+
destinationIndex
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
/** Prepare a strict argument prefix without selecting a fault or spending counters. */
|
|
1041
|
+
function prepareLengthCandidate(context, fault) {
|
|
1042
|
+
const response = context.response;
|
|
1043
|
+
const combined = isCombinedFixtureResponse(response);
|
|
1044
|
+
if (isErrorResponse(response) || !(combined || isToolCallResponse(response))) return {
|
|
1045
|
+
kind: "not-applicable",
|
|
1046
|
+
detail: "Response has no tool calls"
|
|
1047
|
+
};
|
|
1048
|
+
const outcome = faultBlockOutcome(response);
|
|
1049
|
+
const calls = outcome?.toolCalls ?? servedToolCalls(response);
|
|
1050
|
+
const index = faultTargetIndex(calls, fault.tool);
|
|
1051
|
+
const target = functionCallAt(calls, index);
|
|
1052
|
+
if (!target) return {
|
|
1053
|
+
kind: "not-applicable",
|
|
1054
|
+
detail: "Target tool call is absent"
|
|
1055
|
+
};
|
|
1056
|
+
const args = toolArgsForWire(target);
|
|
1057
|
+
const canonical = args.kind === "parsed" ? args.text : args.raw;
|
|
1058
|
+
const at = fault.at ?? MISBEHAVIOR_CATALOG[fault.fault].defaults.at;
|
|
1059
|
+
const cut = Math.max(1, Math.floor(canonical.length * at));
|
|
1060
|
+
if (canonical.length < 2 || !(cut < canonical.length)) return {
|
|
1061
|
+
kind: "not-applicable",
|
|
1062
|
+
detail: "Arguments have no strict nonempty proper prefix"
|
|
1063
|
+
};
|
|
1064
|
+
const toolCalls = calls.slice(0, index + 1).map((call, callIndex) => callIndex === index && call.type !== "custom" ? {
|
|
1065
|
+
...call,
|
|
1066
|
+
arguments: canonical.slice(0, cut)
|
|
1067
|
+
} : { ...call });
|
|
1068
|
+
let rewritten = withServedToolCalls(response, toolCalls);
|
|
1069
|
+
if (outcome) {
|
|
1070
|
+
const blocks = [];
|
|
1071
|
+
let callIndex = 0;
|
|
1072
|
+
let content = "";
|
|
1073
|
+
for (const block of outcome.ordered) if (block.type === "text") {
|
|
1074
|
+
content += block.text;
|
|
1075
|
+
blocks.push({ ...block });
|
|
1076
|
+
} else {
|
|
1077
|
+
blocks.push(toolCallFixtureBlock(toolCalls[callIndex]));
|
|
1078
|
+
if (callIndex++ === index) break;
|
|
1079
|
+
}
|
|
1080
|
+
rewritten = withServedToolCalls({
|
|
1081
|
+
...response,
|
|
1082
|
+
content
|
|
1083
|
+
}, toolCalls, blocks);
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
kind: "ready",
|
|
1087
|
+
candidate: {
|
|
1088
|
+
response: rewritten,
|
|
1089
|
+
stop: "length",
|
|
1090
|
+
target: {
|
|
1091
|
+
tool: target.name,
|
|
1092
|
+
index
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
/** Remove all chat output channels while preserving response metadata. */
|
|
1098
|
+
function clearChatOutput(response) {
|
|
1099
|
+
const cleared = {
|
|
1100
|
+
...response,
|
|
1101
|
+
content: ""
|
|
1102
|
+
};
|
|
1103
|
+
delete cleared.blocks;
|
|
1104
|
+
delete cleared.responsesBlocks;
|
|
1105
|
+
delete cleared.toolCalls;
|
|
1106
|
+
delete cleared.customToolCalls;
|
|
1107
|
+
delete cleared.reasoning;
|
|
1108
|
+
delete cleared.reasoningSignature;
|
|
1109
|
+
delete cleared.redactedThinking;
|
|
1110
|
+
delete cleared.webSearches;
|
|
1111
|
+
delete cleared.finishReason;
|
|
1112
|
+
delete cleared.nativeFinishReason;
|
|
1113
|
+
return cleared;
|
|
1114
|
+
}
|
|
1115
|
+
/** Prepare an empty normal completion without mutating the resolved response. */
|
|
1116
|
+
function prepareEmptyCandidate(context, fault) {
|
|
1117
|
+
const response = context.response;
|
|
1118
|
+
if (isErrorResponse(response) || !(isCombinedFixtureResponse(response) || isTextResponse(response) || isToolCallResponse(response))) return {
|
|
1119
|
+
kind: "not-applicable",
|
|
1120
|
+
detail: "Response is not a chat response"
|
|
1121
|
+
};
|
|
1122
|
+
if (fault.tool !== void 0) {
|
|
1123
|
+
const calls = (isCombinedFixtureResponse(response) ? resolveServedBlockOutcome(response)?.toolCalls : void 0) ?? ("toolCalls" in response ? servedToolCalls(response) : []);
|
|
1124
|
+
if (!calls.some((call) => call.name === fault.tool && call.type !== "custom")) return {
|
|
1125
|
+
kind: "not-applicable",
|
|
1126
|
+
detail: calls.some((call) => call.name === fault.tool) ? "Target tool call is a custom tool call" : "Target tool call is absent"
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
return {
|
|
1130
|
+
kind: "ready",
|
|
1131
|
+
candidate: {
|
|
1132
|
+
response: clearChatOutput(response),
|
|
1133
|
+
stop: "stop"
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
/** Prepare structured refusal data; the planner checks support on the original fault. */
|
|
1138
|
+
function prepareRefusalCandidate(context, fault) {
|
|
1139
|
+
const empty = prepareEmptyCandidate(context, {
|
|
1140
|
+
fault: "empty-response",
|
|
1141
|
+
tool: fault.tool
|
|
1142
|
+
});
|
|
1143
|
+
if (empty.kind !== "ready") return empty;
|
|
1144
|
+
return {
|
|
1145
|
+
kind: "ready",
|
|
1146
|
+
candidate: {
|
|
1147
|
+
response: empty.candidate.response,
|
|
1148
|
+
stop: "refusal",
|
|
1149
|
+
refusal: fault.message ?? MISBEHAVIOR_CATALOG.refusal.defaults.message,
|
|
1150
|
+
refusalCategory: fault.category ?? MISBEHAVIOR_CATALOG.refusal.defaults.category
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
/** Prepare withheld output; the planner checks support on the original fault. */
|
|
1155
|
+
function prepareContentFilterCandidate(context, fault) {
|
|
1156
|
+
const empty = prepareEmptyCandidate(context, {
|
|
1157
|
+
fault: "empty-response",
|
|
1158
|
+
tool: fault.tool
|
|
1159
|
+
});
|
|
1160
|
+
if (empty.kind !== "ready") return empty;
|
|
1161
|
+
return {
|
|
1162
|
+
kind: "ready",
|
|
1163
|
+
candidate: {
|
|
1164
|
+
response: empty.candidate.response,
|
|
1165
|
+
stop: "content_filter"
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
/** Prepare reasoning-only output; the planner checks support on the original fault. */
|
|
1170
|
+
function prepareReasoningOnlyCandidate(context, fault) {
|
|
1171
|
+
const empty = prepareEmptyCandidate(context, {
|
|
1172
|
+
fault: "empty-response",
|
|
1173
|
+
tool: fault.tool
|
|
1174
|
+
});
|
|
1175
|
+
if (empty.kind !== "ready") return empty;
|
|
1176
|
+
const fixtureReasoning = "reasoning" in context.response ? context.response.reasoning : void 0;
|
|
1177
|
+
return {
|
|
1178
|
+
kind: "ready",
|
|
1179
|
+
candidate: {
|
|
1180
|
+
response: empty.candidate.response,
|
|
1181
|
+
stop: "length",
|
|
1182
|
+
reasoning: fault.reasoning ?? fixtureReasoning ?? "Thinking..."
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
/** Dispatch only after the planner has checked authored parameter/wire support. */
|
|
1187
|
+
function prepareMisbehaviorCandidate(context, fault) {
|
|
1188
|
+
switch (fault.fault) {
|
|
1189
|
+
case "tool-args-invalid-json": return prepareInvalidJsonCandidate(context, fault);
|
|
1190
|
+
case "tool-args-schema-violation": {
|
|
1191
|
+
const violation = fault.violation ?? "missing-required";
|
|
1192
|
+
switch (violation) {
|
|
1193
|
+
case "missing-required": return prepareMissingRequiredCandidate(context, fault);
|
|
1194
|
+
case "wrong-type": return prepareWrongTypeCandidate(context, fault);
|
|
1195
|
+
case "extra-property": return prepareExtraPropertyCandidate(context, fault);
|
|
1196
|
+
case "enum-mismatch": return prepareEnumMismatchCandidate(context, fault);
|
|
1197
|
+
case "not-object": return prepareNotObjectCandidate(context, fault);
|
|
1198
|
+
default: return assertUnreachableMisbehavior(violation);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
case "tool-unknown-name": return prepareUnknownNameCandidate(context, fault);
|
|
1202
|
+
case "tool-call-id-duplicate": {
|
|
1203
|
+
if (context.toolCallIdMode === "authored-nonempty") return prepareDuplicateIdCandidate({
|
|
1204
|
+
...context,
|
|
1205
|
+
toolCallIdMode: context.toolCallIdMode
|
|
1206
|
+
}, fault);
|
|
1207
|
+
const emitsToolCallIds = context.wire === "openai-chat" ? true : context.emitsToolCallIds;
|
|
1208
|
+
if (emitsToolCallIds === void 0) throw new Error("Missing current output ID capability: internal invariant");
|
|
1209
|
+
return prepareDuplicateIdCandidate({
|
|
1210
|
+
...context,
|
|
1211
|
+
emitsToolCallIds
|
|
1212
|
+
}, fault);
|
|
1213
|
+
}
|
|
1214
|
+
case "stop-length-mid-tool": return prepareLengthCandidate(context, fault);
|
|
1215
|
+
case "empty-response": return prepareEmptyCandidate(context, fault);
|
|
1216
|
+
case "refusal": return prepareRefusalCandidate(context, fault);
|
|
1217
|
+
case "content-filter": return prepareContentFilterCandidate(context, fault);
|
|
1218
|
+
case "reasoning-only": return prepareReasoningOnlyCandidate(context, fault);
|
|
1219
|
+
default: return assertUnreachableMisbehavior(fault);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function assertUnreachableMisbehavior(value) {
|
|
1223
|
+
throw new Error(`Unknown misbehavior candidate: ${String(value)}`);
|
|
1224
|
+
}
|
|
1225
|
+
let processMisbehaviorSeed;
|
|
1226
|
+
/** Shared with config inspection so a random run can be replayed with this numeric seed. */
|
|
1227
|
+
function resolveMisbehaviorSeed(seed, logger) {
|
|
1228
|
+
if (seed !== "random") return seed ?? 0;
|
|
1229
|
+
if (processMisbehaviorSeed === void 0) {
|
|
1230
|
+
processMisbehaviorSeed = Math.floor(Math.random() * 4294967296);
|
|
1231
|
+
logger.info(`Misbehavior random seed: ${processMisbehaviorSeed}`);
|
|
1232
|
+
}
|
|
1233
|
+
return processMisbehaviorSeed;
|
|
1234
|
+
}
|
|
1235
|
+
function misbehaviorRoll(seed) {
|
|
1236
|
+
let value = seed + 1831565813;
|
|
1237
|
+
value = Math.imul(value ^ value >>> 15, value | 1);
|
|
1238
|
+
value ^= value + Math.imul(value ^ value >>> 7, value | 61);
|
|
1239
|
+
return ((value ^ value >>> 14) >>> 0) / 4294967296;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Whether the wire's normal path would serve this response's blocks and tool
|
|
1243
|
+
* calls, or reject the fixture. Mirrors that path: non-empty ordered `blocks`
|
|
1244
|
+
* are authoritative (the legacy `toolCalls` is then never read), only the
|
|
1245
|
+
* OpenAI Responses API may carry a custom tool call, and every entry must be
|
|
1246
|
+
* well formed. A wire that carries arguments as an object also rejects a
|
|
1247
|
+
* function call whose `arguments` are not valid JSON (see
|
|
1248
|
+
* {@link rejectsInvalidToolArguments}). A rejection is either a coded fixture
|
|
1249
|
+
* tool-call error or the plain Error for a malformed text block or an unknown
|
|
1250
|
+
* block type; all of them mean the wire cannot serve the fixture.
|
|
1251
|
+
*/
|
|
1252
|
+
function servesFixture(wire, stream, response) {
|
|
1253
|
+
if (typeof response !== "object" || response === null) return true;
|
|
1254
|
+
const blocks = "blocks" in response ? response.blocks : void 0;
|
|
1255
|
+
let functionCalls = [];
|
|
1256
|
+
try {
|
|
1257
|
+
if (wire === "openai-responses") {
|
|
1258
|
+
if (!resolveServedBlockOutcome(response) && "toolCalls" in response) {
|
|
1259
|
+
assertCustomToolCalls(response);
|
|
1260
|
+
assertResponsesToolCalls(servedToolCalls(response));
|
|
1261
|
+
}
|
|
1262
|
+
} else if ("toolCalls" in response || "blocks" in response || "responsesBlocks" in response) {
|
|
1263
|
+
functionCalls = requireEmittedFunctionToolCalls(response, wire);
|
|
1264
|
+
if (Array.isArray(blocks) && blocks.length > 0) functionCalls = resolveFixtureBlocks(blocks).filter((block) => block.type === "toolCall");
|
|
1265
|
+
}
|
|
1266
|
+
} catch {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
return !(strictToolArgumentsEnabled() ? rejectsInvalidToolArguments(wire, stream) : replacesInvalidToolArguments(wire)) || functionCalls.every((call) => toolArgsForWire(call).kind === "parsed");
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Whether the normal path of this wire serves invalid-JSON `arguments` as
|
|
1273
|
+
* `{}` / `"{}"` (the default, as in 1.44.0) instead of the authored text. The
|
|
1274
|
+
* OpenAI wires pass the authored text through unchanged.
|
|
1275
|
+
*/
|
|
1276
|
+
function replacesInvalidToolArguments(wire) {
|
|
1277
|
+
return rejectsInvalidToolArguments(wire, false) || wire === "cohere";
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Whether this wire and output mode carry tool arguments as an object, so the
|
|
1281
|
+
* normal path must parse a fixture's string `arguments` and, under
|
|
1282
|
+
* `strictToolArguments`, answers aimock_invalid_tool_arguments when they are
|
|
1283
|
+
* not valid JSON. The other modes
|
|
1284
|
+
* pass the authored string through unchanged (the OpenAI wires, Cohere, and
|
|
1285
|
+
* the streaming Anthropic, Bedrock and Gemini Interactions paths, which send
|
|
1286
|
+
* arguments as a JSON-string fragment).
|
|
1287
|
+
*/
|
|
1288
|
+
function rejectsInvalidToolArguments(wire, stream) {
|
|
1289
|
+
switch (wire) {
|
|
1290
|
+
case "gemini":
|
|
1291
|
+
case "gemini-live":
|
|
1292
|
+
case "ollama": return true;
|
|
1293
|
+
case "anthropic":
|
|
1294
|
+
case "bedrock-invoke":
|
|
1295
|
+
case "bedrock-converse":
|
|
1296
|
+
case "gemini-interactions": return !stream;
|
|
1297
|
+
default: return false;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
/** Complete preparation precedes selection; every candidate sees the original response. */
|
|
1301
|
+
function planMisbehavior(input) {
|
|
1302
|
+
const { wire, fixture, defaults } = input;
|
|
1303
|
+
if (!defaults.misbehavior) return { kind: "skipped" };
|
|
1304
|
+
const testId = resolveTestId(input.rawHeaders, input.url);
|
|
1305
|
+
let source;
|
|
1306
|
+
let parsed = parseMisbehaviorHeader(input.rawHeaders["x-aimock-misbehavior"]);
|
|
1307
|
+
if (parsed !== void 0) source = "header";
|
|
1308
|
+
else if (fixture.misbehavior !== void 0) {
|
|
1309
|
+
source = "fixture";
|
|
1310
|
+
parsed = parseMisbehavior(fixture.misbehavior);
|
|
1311
|
+
} else if (defaults.misbehavior?.byTestId.has(testId)) {
|
|
1312
|
+
source = "scope";
|
|
1313
|
+
parsed = parseMisbehavior(defaults.misbehavior.byTestId.get(testId));
|
|
1314
|
+
} else if (defaults.misbehavior?.baseline !== void 0) {
|
|
1315
|
+
source = "server";
|
|
1316
|
+
parsed = parseMisbehavior(defaults.misbehavior.baseline);
|
|
1317
|
+
} else return { kind: "skipped" };
|
|
1318
|
+
if (!parsed.ok) return {
|
|
1319
|
+
kind: "error",
|
|
1320
|
+
status: source === "header" ? 400 : 501,
|
|
1321
|
+
code: source === "header" ? "aimock_misbehavior_invalid" : "aimock_misbehavior_not_applicable",
|
|
1322
|
+
message: `${wire}: ${parsed.issue.message}`,
|
|
1323
|
+
summary: {
|
|
1324
|
+
applied: false,
|
|
1325
|
+
source,
|
|
1326
|
+
wire,
|
|
1327
|
+
evaluations: []
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
if (!servesFixture(wire, input.stream, input.response)) return { kind: "skipped" };
|
|
1331
|
+
const config = parsed.config;
|
|
1332
|
+
const explicit = source === "header" || source === "fixture";
|
|
1333
|
+
const evaluations = [];
|
|
1334
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
1335
|
+
const details = /* @__PURE__ */ new Map();
|
|
1336
|
+
const summaryFor = (row) => ({
|
|
1337
|
+
applied: row?.outcome === "applied",
|
|
1338
|
+
source,
|
|
1339
|
+
wire,
|
|
1340
|
+
evaluations: [...evaluations].sort((a, b) => a.entryIndex - b.entryIndex),
|
|
1341
|
+
...row ? {
|
|
1342
|
+
fault: row.fault,
|
|
1343
|
+
...row.reason !== void 0 ? { reason: row.reason } : {},
|
|
1344
|
+
...row.ordinal !== void 0 ? { ordinal: row.ordinal } : {},
|
|
1345
|
+
...details.has(row.entryIndex) ? { detail: details.get(row.entryIndex) } : {}
|
|
1346
|
+
} : { reason: "disabled" }
|
|
1347
|
+
});
|
|
1348
|
+
for (const [entryIndex, fault] of config.faults.entries()) {
|
|
1349
|
+
if (fault.providers !== void 0 && !fault.providers.includes(wire)) {
|
|
1350
|
+
evaluations.push({
|
|
1351
|
+
entryIndex,
|
|
1352
|
+
fault: fault.fault,
|
|
1353
|
+
outcome: "skipped",
|
|
1354
|
+
reason: "provider-excluded"
|
|
1355
|
+
});
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
let reason;
|
|
1359
|
+
let detail;
|
|
1360
|
+
const response = input.response;
|
|
1361
|
+
const combined = isCombinedFixtureResponse(response);
|
|
1362
|
+
if (isErrorResponse(response) || !(combined || isTextResponse(response) || isToolCallResponse(response))) {
|
|
1363
|
+
reason = "not-applicable";
|
|
1364
|
+
detail = "Response is not a chat response";
|
|
1365
|
+
} else if (input.faultRenderingAvailable === false || !supportsMisbehavior(wire, fault, input.stream)) {
|
|
1366
|
+
reason = "unsupported-on-wire";
|
|
1367
|
+
detail = "Fault or authored parameter is unsupported in this wire/output mode";
|
|
1368
|
+
} else {
|
|
1369
|
+
const prepared = prepareMisbehaviorCandidate(input, fault);
|
|
1370
|
+
if (prepared.kind === "ready") candidates.set(entryIndex, prepared.candidate);
|
|
1371
|
+
else {
|
|
1372
|
+
reason = "not-applicable";
|
|
1373
|
+
detail = prepared.detail;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (reason) {
|
|
1377
|
+
if (detail !== void 0) details.set(entryIndex, detail);
|
|
1378
|
+
const row = {
|
|
1379
|
+
entryIndex,
|
|
1380
|
+
fault: fault.fault,
|
|
1381
|
+
outcome: explicit ? "error" : "skipped",
|
|
1382
|
+
reason
|
|
1383
|
+
};
|
|
1384
|
+
evaluations.push(row);
|
|
1385
|
+
if (explicit) return {
|
|
1386
|
+
kind: "error",
|
|
1387
|
+
status: 501,
|
|
1388
|
+
code: reason === "unsupported-on-wire" ? "aimock_misbehavior_unsupported" : "aimock_misbehavior_not_applicable",
|
|
1389
|
+
message: `${fault.fault} on ${wire}: ${reason}: ${detail}`,
|
|
1390
|
+
summary: summaryFor(row)
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (candidates.size > 0) {
|
|
1395
|
+
const counters = defaults.misbehaviorCounters;
|
|
1396
|
+
if (!counters) throw new Error("Missing misbehavior journal counters: internal invariant");
|
|
1397
|
+
const sourceKey = source === "fixture" ? fixtureMisbehaviorSourceKey(fixture, config) : source === "scope" ? `scope:${testId}` : source;
|
|
1398
|
+
const seed = resolveMisbehaviorSeed(config.seed, defaults.logger);
|
|
1399
|
+
for (const [entryIndex, candidate] of candidates) {
|
|
1400
|
+
const fault = config.faults[entryIndex];
|
|
1401
|
+
const key = {
|
|
1402
|
+
testId,
|
|
1403
|
+
sourceKey,
|
|
1404
|
+
entryIndex
|
|
1405
|
+
};
|
|
1406
|
+
if (fault.times !== void 0 && counters.getFiringCount(key) >= fault.times) {
|
|
1407
|
+
evaluations.push({
|
|
1408
|
+
entryIndex,
|
|
1409
|
+
fault: fault.fault,
|
|
1410
|
+
outcome: "skipped",
|
|
1411
|
+
reason: "times-exhausted"
|
|
1412
|
+
});
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
const ordinal = counters.nextOrdinal(key);
|
|
1416
|
+
if (misbehaviorRoll(fnv1a32(`${seed}|${testId}|${sourceKey}|${entryIndex}|${ordinal}`)) >= (fault.rate ?? 1)) {
|
|
1417
|
+
evaluations.push({
|
|
1418
|
+
entryIndex,
|
|
1419
|
+
fault: fault.fault,
|
|
1420
|
+
outcome: "skipped",
|
|
1421
|
+
reason: "not-rolled",
|
|
1422
|
+
ordinal
|
|
1423
|
+
});
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
counters.recordFiring(key);
|
|
1427
|
+
const row = {
|
|
1428
|
+
entryIndex,
|
|
1429
|
+
fault: fault.fault,
|
|
1430
|
+
outcome: "applied",
|
|
1431
|
+
ordinal
|
|
1432
|
+
};
|
|
1433
|
+
evaluations.push(row);
|
|
1434
|
+
keepAuthoredToolArguments();
|
|
1435
|
+
return {
|
|
1436
|
+
kind: "applied",
|
|
1437
|
+
...candidate,
|
|
1438
|
+
summary: {
|
|
1439
|
+
...summaryFor(row),
|
|
1440
|
+
...candidate.target !== void 0 ? { target: candidate.target } : {},
|
|
1441
|
+
...candidate.detail !== void 0 ? { detail: candidate.detail } : {}
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return {
|
|
1447
|
+
kind: "skipped",
|
|
1448
|
+
summary: summaryFor([...evaluations].sort((a, b) => a.entryIndex - b.entryIndex).at(-1))
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
const recordedMisbehaviorEntries = /* @__PURE__ */ new WeakSet();
|
|
1452
|
+
const warnedUnsupportedMisbehavior = /* @__PURE__ */ new WeakMap();
|
|
1453
|
+
/** Record prepared output on the existing request; this is not a delivery receipt. */
|
|
1454
|
+
function recordMisbehaviorOutcome({ entry, summary, defaults, testId }) {
|
|
1455
|
+
if (!summary || recordedMisbehaviorEntries.has(entry)) return;
|
|
1456
|
+
if (summary.applied && !Array.isArray(summary.servedToolCalls)) throw new Error("Applied misbehavior requires prepared servedToolCalls before journaling");
|
|
1457
|
+
if (summary.evaluations.length > 0 && summary.wire === void 0) throw new Error("Misbehavior evaluations require a wire before journaling");
|
|
1458
|
+
entry.response.misbehavior = summary;
|
|
1459
|
+
recordedMisbehaviorEntries.add(entry);
|
|
1460
|
+
for (const evaluation of summary.evaluations) {
|
|
1461
|
+
if (summary.wire === void 0) continue;
|
|
1462
|
+
const outcome = evaluation.reason ? `${evaluation.outcome}:${evaluation.reason}` : evaluation.outcome;
|
|
1463
|
+
defaults.registry?.incrementCounter("aimock_misbehavior_total", {
|
|
1464
|
+
fault: evaluation.fault,
|
|
1465
|
+
wire: summary.wire,
|
|
1466
|
+
outcome
|
|
1467
|
+
});
|
|
1468
|
+
const message = `[misbehavior] ${evaluation.fault} ${outcome} on ${summary.wire} (testId=${testId})`;
|
|
1469
|
+
if (evaluation.outcome === "error") defaults.logger.error(message);
|
|
1470
|
+
else if (evaluation.outcome === "applied") defaults.logger.info(message);
|
|
1471
|
+
else if (evaluation.reason === "unsupported-on-wire") {
|
|
1472
|
+
let warned = warnedUnsupportedMisbehavior.get(defaults.logger);
|
|
1473
|
+
if (!warned) {
|
|
1474
|
+
warned = /* @__PURE__ */ new Set();
|
|
1475
|
+
warnedUnsupportedMisbehavior.set(defaults.logger, warned);
|
|
1476
|
+
}
|
|
1477
|
+
const key = JSON.stringify([
|
|
1478
|
+
testId,
|
|
1479
|
+
evaluation.fault,
|
|
1480
|
+
summary.wire
|
|
1481
|
+
]);
|
|
1482
|
+
if (!warned.has(key)) {
|
|
1483
|
+
warned.add(key);
|
|
1484
|
+
defaults.logger.warn(message);
|
|
1485
|
+
}
|
|
1486
|
+
} else defaults.logger.debug(message);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
/** Resolve configuration presence only; request boundaries validate headers before bypassing. */
|
|
1490
|
+
function resolveMisbehaviorShortCircuit(input) {
|
|
1491
|
+
const { wire, fixture, defaults, rawHeaders, url, reason } = input;
|
|
1492
|
+
if (!defaults.misbehavior) return void 0;
|
|
1493
|
+
const testId = resolveTestId(rawHeaders, url);
|
|
1494
|
+
let source;
|
|
1495
|
+
if (rawHeaders["x-aimock-misbehavior"] !== void 0) source = "header";
|
|
1496
|
+
else if (fixture?.misbehavior !== void 0) source = "fixture";
|
|
1497
|
+
else if (defaults.misbehavior?.byTestId.has(testId)) source = "scope";
|
|
1498
|
+
else if (defaults.misbehavior?.baseline !== void 0) source = "server";
|
|
1499
|
+
else return void 0;
|
|
1500
|
+
return {
|
|
1501
|
+
applied: false,
|
|
1502
|
+
source,
|
|
1503
|
+
wire,
|
|
1504
|
+
reason,
|
|
1505
|
+
evaluations: []
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
//#endregion
|
|
1510
|
+
export { MISBEHAVIOR_CATALOG, WIRE_SUPPORT, copyFixtureMisbehaviorPosition, getFixtureMisbehaviorPosition, normalizeDirectToolSchema, parseMisbehavior, parseMisbehaviorHeader, planMisbehavior, recordMisbehaviorOutcome, resolveMisbehaviorSeed, resolveMisbehaviorShortCircuit, setFixtureMisbehaviorPosition, validateFixtureMisbehavior };
|
|
1511
|
+
//# sourceMappingURL=misbehavior.js.map
|