@copilotkit/aimock 1.42.0 → 1.43.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 +56 -0
- package/README.md +102 -16
- package/dist/agui-mock.cjs +1 -1
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +1 -1
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-recorder.cjs +33 -11
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.d.cts +10 -1
- package/dist/agui-recorder.d.cts.map +1 -1
- package/dist/agui-recorder.d.ts +10 -1
- package/dist/agui-recorder.d.ts.map +1 -1
- package/dist/agui-recorder.js +33 -11
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-types.d.cts +1 -0
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts +1 -0
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +38 -10
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts +5 -0
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts +5 -0
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +38 -10
- package/dist/aimock-cli.js.map +1 -1
- package/dist/batches.cjs +458 -0
- package/dist/batches.cjs.map +1 -0
- package/dist/batches.d.cts +57 -0
- package/dist/batches.d.cts.map +1 -0
- package/dist/batches.d.ts +57 -0
- package/dist/batches.d.ts.map +1 -0
- package/dist/batches.js +453 -0
- package/dist/batches.js.map +1 -0
- package/dist/bedrock-converse.cjs +12 -8
- 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 +14 -10
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +10 -8
- 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 +12 -10
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +3 -3
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.js +4 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +622 -72
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts +164 -7
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts +164 -7
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +613 -74
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +42 -23
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +42 -23
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +10 -8
- 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 +12 -10
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/convert.cjs +1 -1
- package/dist/convert.cjs.map +1 -1
- package/dist/convert.js +1 -1
- package/dist/convert.js.map +1 -1
- package/dist/elevenlabs-audio.cjs +3 -3
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.js +4 -4
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +1053 -0
- package/dist/elevenlabs-voice.cjs.map +1 -0
- package/dist/elevenlabs-voice.d.cts +72 -0
- package/dist/elevenlabs-voice.d.cts.map +1 -0
- package/dist/elevenlabs-voice.d.ts +72 -0
- package/dist/elevenlabs-voice.d.ts.map +1 -0
- package/dist/elevenlabs-voice.js +1048 -0
- package/dist/elevenlabs-voice.js.map +1 -0
- package/dist/embeddings.cjs +8 -4
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +9 -5
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +3 -3
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.js +4 -4
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +28 -4
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts +16 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts +16 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +28 -5
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +1071 -0
- package/dist/files.cjs.map +1 -0
- package/dist/files.d.cts +68 -0
- package/dist/files.d.cts.map +1 -0
- package/dist/files.d.ts +68 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +1059 -0
- package/dist/files.js.map +1 -0
- package/dist/fine-tuning.cjs +1051 -0
- package/dist/fine-tuning.cjs.map +1 -0
- package/dist/fine-tuning.d.cts +96 -0
- package/dist/fine-tuning.d.cts.map +1 -0
- package/dist/fine-tuning.d.ts +96 -0
- package/dist/fine-tuning.d.ts.map +1 -0
- package/dist/fine-tuning.js +1044 -0
- package/dist/fine-tuning.js.map +1 -0
- package/dist/fixture-loader.cjs +0 -0
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +37 -5
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +37 -5
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +0 -0
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +2 -2
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +3 -3
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +13 -7
- 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 +15 -9
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +11 -7
- 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 +13 -9
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -14
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.js +15 -15
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +229 -7
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +38 -2
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +38 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +221 -8
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +54 -123
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +55 -124
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +41 -8
- package/dist/index.d.cts +12 -7
- package/dist/index.d.ts +12 -7
- package/dist/index.js +14 -10
- package/dist/jest.cjs +14 -7
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +3 -2
- package/dist/jest.d.cts.map +1 -1
- package/dist/jest.d.ts +3 -2
- package/dist/jest.d.ts.map +1 -1
- package/dist/jest.js +14 -7
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +41 -4
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +44 -3
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +44 -3
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +41 -5
- package/dist/journal.js.map +1 -1
- package/dist/live-fixture.cjs +1192 -0
- package/dist/live-fixture.cjs.map +1 -0
- package/dist/live-fixture.js +1187 -0
- package/dist/live-fixture.js.map +1 -0
- package/dist/live-recorder.cjs +256 -0
- package/dist/live-recorder.cjs.map +1 -0
- package/dist/live-recorder.js +254 -0
- package/dist/live-recorder.js.map +1 -0
- package/dist/live-replay.cjs +286 -0
- package/dist/live-replay.cjs.map +1 -0
- package/dist/live-replay.js +285 -0
- package/dist/live-replay.js.map +1 -0
- package/dist/live-sanitize.cjs +196 -0
- package/dist/live-sanitize.cjs.map +1 -0
- package/dist/live-sanitize.js +196 -0
- package/dist/live-sanitize.js.map +1 -0
- package/dist/live-types.d.cts +71 -0
- package/dist/live-types.d.cts.map +1 -0
- package/dist/live-types.d.ts +71 -0
- package/dist/live-types.d.ts.map +1 -0
- package/dist/llmock.cjs +52 -41
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +16 -5
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +16 -5
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +53 -42
- package/dist/llmock.js.map +1 -1
- package/dist/messages.cjs +11 -7
- 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 +13 -9
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +258 -5
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts +3 -1
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts +3 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +255 -6
- package/dist/metrics.js.map +1 -1
- package/dist/moderation.cjs +10 -1
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.d.cts.map +1 -1
- package/dist/moderation.d.ts.map +1 -1
- package/dist/moderation.js +10 -1
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +5 -5
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.js +6 -6
- package/dist/ollama.js.map +1 -1
- package/dist/openrouter-video.cjs +5 -5
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.js +6 -6
- package/dist/openrouter-video.js.map +1 -1
- package/dist/recorder.cjs +45 -9
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +43 -11
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +4 -3
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.js +5 -5
- package/dist/responses.js.map +1 -1
- package/dist/router.cjs +20 -33
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +4 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +4 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +20 -34
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +701 -625
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +1 -0
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +704 -628
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +2 -2
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +3 -3
- package/dist/speech.js.map +1 -1
- package/dist/transcription.cjs +4 -4
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.js +5 -5
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +119 -16
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +119 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cli.cjs +925 -0
- package/dist/validate-cli.cjs.map +1 -0
- package/dist/validate-cli.js +924 -0
- package/dist/validate-cli.js.map +1 -0
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/veo-video.cjs +13 -13
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +14 -14
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +4 -4
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +5 -5
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +13 -6
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +2 -1
- package/dist/vitest.d.cts.map +1 -1
- package/dist/vitest.d.ts +2 -1
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +14 -7
- package/dist/vitest.js.map +1 -1
- package/dist/watcher.cjs +2 -1
- package/dist/watcher.cjs.map +1 -1
- package/dist/watcher.js +2 -1
- package/dist/watcher.js.map +1 -1
- package/dist/ws-framing.cjs +150 -35
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +18 -3
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +18 -3
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +151 -36
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +2 -2
- package/dist/ws-gemini-live.d.ts +2 -2
- package/dist/ws-gemini-live.js +2 -2
- package/dist/ws-live.cjs +401 -0
- package/dist/ws-live.cjs.map +1 -0
- package/dist/ws-live.js +400 -0
- package/dist/ws-live.js.map +1 -0
- package/dist/ws-realtime.cjs +46 -2
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +2 -2
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +46 -2
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +2 -2
- package/dist/ws-responses.js +2 -2
- package/dist/ws-upstream.cjs +118 -0
- package/dist/ws-upstream.cjs.map +1 -0
- package/dist/ws-upstream.js +115 -0
- package/dist/ws-upstream.js.map +1 -0
- package/fixtures/openai-live-client.json +390 -0
- package/fixtures/openai-live-managed.json +709 -0
- package/package.json +1 -1
package/dist/chaos.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chaos.cjs","names":["resolveTestId"],"sources":["../src/chaos.ts"],"sourcesContent":["/**\n * Chaos testing support for LLMock.\n *\n * Provides probabilistic failure injection — requests can be dropped (500),\n * returned with malformed JSON, or have the connection forcibly disconnected.\n *\n * Precedence: per-request headers > fixture-level config > server-level defaults\n * (and the server level is itself scoped per `X-Test-Id` — see `ChaosScope`).\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChaosAction,\n ChaosConfig,\n ChaosDefaults,\n ChaosScope,\n ChatCompletionRequest,\n Fixture,\n} from \"./types.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { resolveTestId } from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\n/**\n * Narrow the server-defaults argument. A `ChaosScope` is distinguishable from a\n * `ChaosConfig` by construction: the latter only ever carries the three rates.\n */\nexport function isChaosScope(defaults: ChaosDefaults): defaults is ChaosScope {\n return \"base\" in defaults || \"byTestId\" in defaults;\n}\n\n/**\n * Pick the server-level chaos config that applies to THIS request: the override\n * installed for its testId, else the server-wide baseline. The testId is\n * resolved by `resolveTestId` — the same helper `getTestId` and the control API\n * use — so a harness that tags by `?testId=` lands in the same scope its chaos\n * override was stored under, and the two sides cannot disagree.\n */\nfunction resolveScopedDefaults(\n serverDefaults: ChaosDefaults | undefined,\n rawHeaders?: http.IncomingHttpHeaders,\n url?: string,\n): ChaosConfig | undefined {\n if (!serverDefaults) return undefined;\n if (!isChaosScope(serverDefaults)) return serverDefaults;\n const scoped = serverDefaults.byTestId?.get(resolveTestId(rawHeaders ?? {}, url));\n if (scoped) return scoped;\n return serverDefaults.base;\n}\n\n/**\n * Resolve chaos config from headers, fixture, and server defaults.\n * Header values override fixture values, which override server defaults\n * (which are themselves per-testId — see `ChaosScope`).\n */\nfunction resolveChaosConfig(\n fixture: Fixture | null,\n serverDefaults?: ChaosDefaults,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n url?: string,\n): ChaosConfig {\n const base: ChaosConfig = { ...resolveScopedDefaults(serverDefaults, rawHeaders, url) };\n\n // Fixture-level overrides server defaults\n if (fixture?.chaos) {\n if (fixture.chaos.dropRate !== undefined) base.dropRate = fixture.chaos.dropRate;\n if (fixture.chaos.malformedRate !== undefined) base.malformedRate = fixture.chaos.malformedRate;\n if (fixture.chaos.disconnectRate !== undefined)\n base.disconnectRate = fixture.chaos.disconnectRate;\n }\n\n // Header overrides everything\n if (rawHeaders) {\n const dropHeader = rawHeaders[\"x-aimock-chaos-drop\"];\n const malformedHeader = rawHeaders[\"x-aimock-chaos-malformed\"];\n const disconnectHeader = rawHeaders[\"x-aimock-chaos-disconnect\"];\n\n if (typeof dropHeader === \"string\") {\n const val = parseFloat(dropHeader);\n if (isNaN(val)) {\n logger?.warn(`[chaos] x-aimock-chaos-drop: invalid value \"${dropHeader}\", ignoring`);\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(`[chaos] x-aimock-chaos-drop: value ${val} out of range [0,1], clamping`);\n }\n base.dropRate = Math.min(1, Math.max(0, val));\n }\n }\n if (typeof malformedHeader === \"string\") {\n const val = parseFloat(malformedHeader);\n if (isNaN(val)) {\n logger?.warn(\n `[chaos] x-aimock-chaos-malformed: invalid value \"${malformedHeader}\", ignoring`,\n );\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(\n `[chaos] x-aimock-chaos-malformed: value ${val} out of range [0,1], clamping`,\n );\n }\n base.malformedRate = Math.min(1, Math.max(0, val));\n }\n }\n if (typeof disconnectHeader === \"string\") {\n const val = parseFloat(disconnectHeader);\n if (isNaN(val)) {\n logger?.warn(\n `[chaos] x-aimock-chaos-disconnect: invalid value \"${disconnectHeader}\", ignoring`,\n );\n } else {\n if (val < 0 || val > 1) {\n logger?.warn(\n `[chaos] x-aimock-chaos-disconnect: value ${val} out of range [0,1], clamping`,\n );\n }\n base.disconnectRate = Math.min(1, Math.max(0, val));\n }\n }\n }\n\n // Clamp all resolved rates to [0, 1] regardless of source.\n // Header values are already clamped above; this covers fixture-level and server defaults.\n if (base.dropRate !== undefined) base.dropRate = Math.min(1, Math.max(0, base.dropRate));\n if (base.malformedRate !== undefined)\n base.malformedRate = Math.min(1, Math.max(0, base.malformedRate));\n if (base.disconnectRate !== undefined)\n base.disconnectRate = Math.min(1, Math.max(0, base.disconnectRate));\n\n return base;\n}\n\n/**\n * Evaluate chaos config and return the triggered action, or null if none.\n * Checks in order: drop, malformed, disconnect — first hit wins.\n */\nexport function evaluateChaos(\n fixture: Fixture | null,\n serverDefaults?: ChaosDefaults,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n url?: string,\n): ChaosAction | null {\n const config = resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger, url);\n\n if (config.dropRate !== undefined && config.dropRate > 0 && Math.random() < config.dropRate) {\n return \"drop\";\n }\n if (\n config.malformedRate !== undefined &&\n config.malformedRate > 0 &&\n Math.random() < config.malformedRate\n ) {\n return \"malformed\";\n }\n if (\n config.disconnectRate !== undefined &&\n config.disconnectRate > 0 &&\n Math.random() < config.disconnectRate\n ) {\n return \"disconnect\";\n }\n\n return null;\n}\n\ninterface ChaosJournalContext {\n method: string;\n path: string;\n headers: Record<string, string>;\n body: ChatCompletionRequest | null;\n}\n\n/**\n * Apply chaos to a request. Returns true if chaos was applied (caller should\n * return early), false if the request should proceed normally.\n *\n * `requestUrl` is the RAW `req.url` (query string included) — chaos scoping\n * resolves the testId from it exactly as `getTestId` does, so `?testId=` tagged\n * traffic is not silently unscoped. It is required, not optional, so a new\n * handler cannot forget it.\n *\n * `source` is required so the invariant \"this handler only applies chaos in\n * the <X> phase\" is enforced at the type level. A future handler that grows\n * a proxy path MUST pass `\"proxy\"` explicitly; the default can't drift silently.\n */\nexport function applyChaos(\n res: http.ServerResponse,\n fixture: Fixture | null,\n serverDefaults: ChaosDefaults | undefined,\n rawHeaders: http.IncomingHttpHeaders,\n requestUrl: string | undefined,\n journal: Journal,\n context: ChaosJournalContext,\n source: \"fixture\" | \"proxy\" | \"internal\",\n registry?: MetricsRegistry,\n logger?: Logger,\n): boolean {\n const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger, requestUrl);\n if (!action) return false;\n applyChaosAction(action, res, fixture, journal, context, source, registry);\n return true;\n}\n\n/**\n * Apply a specific (already-rolled) chaos action. Exposed so callers that roll\n * the dice themselves can dispatch without re-rolling — important when the\n * caller wants to branch on the action before committing (e.g. pre-flight vs.\n * post-response phases).\n *\n * `source` is required (not optional) so callers can't silently omit it on\n * one branch and journal an ambiguous entry. Pass `\"fixture\"` when a fixture\n * matched (or would have) and `\"proxy\"` when the request was headed for the\n * proxy path.\n */\nexport function applyChaosAction(\n action: ChaosAction,\n res: http.ServerResponse,\n fixture: Fixture | null,\n journal: Journal,\n context: ChaosJournalContext,\n source: \"fixture\" | \"proxy\" | \"internal\",\n registry?: MetricsRegistry,\n): void {\n if (registry) {\n registry.incrementCounter(\"aimock_chaos_triggered_total\", { action, source });\n }\n\n switch (action) {\n case \"drop\": {\n journal.add({\n ...context,\n response: { status: 500, fixture, chaosAction: \"drop\", source },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Chaos: request dropped\",\n type: \"server_error\",\n code: \"chaos_drop\",\n },\n }),\n );\n return;\n }\n case \"malformed\": {\n journal.add({\n ...context,\n response: { status: 200, fixture, chaosAction: \"malformed\", source },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\"{malformed json: <<<chaos>>>\");\n return;\n }\n case \"disconnect\": {\n journal.add({\n ...context,\n response: { status: 0, fixture, chaosAction: \"disconnect\", source },\n });\n res.destroy();\n return;\n }\n default: {\n const _exhaustive: never = action;\n void _exhaustive;\n return;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chaos.cjs","names":["resolveTestId","describeMatch","DEFAULT_TEST_ID","delay"],"sources":["../src/chaos.ts"],"sourcesContent":["/**\n * Chaos testing support for LLMock.\n *\n * Provides probabilistic failure injection — requests can be dropped (500),\n * returned with malformed JSON, rejected with a 429 rate limit, or have the\n * connection forcibly disconnected — plus a deterministic (non-probabilistic)\n * `latencyMs` delay applied before any of those are rolled.\n *\n * Precedence: per-request headers > fixture-level config > server-level defaults.\n * Those three levels merge FIELD-WISE — each level overrides only the fields it\n * sets. The server level is itself scoped per `X-Test-Id`, and that scope lookup\n * is a SELECTION, not a merge: a per-testId override replaces the server-wide\n * baseline wholesale. See `ChaosScope` in types.ts.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChaosAction,\n ChaosConfig,\n ChaosDefaults,\n ChaosScope,\n ChatCompletionRequest,\n Fixture,\n} from \"./types.js\";\nimport { delay, writeErrorResponse } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nimport { describeMatch, resolveTestId } from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\n/**\n * Narrow the server-defaults argument. The discrimination is STRUCTURAL: a\n * `ChaosConfig` only ever carries the fault fields (`dropRate`,\n * `malformedRate`, `rateLimitRate`, `disconnectRate`, `latencyMs`), so the\n * presence of a `base` / `byTestId` key means a `ChaosScope`.\n *\n * Both members are optional, so `{}` satisfies BOTH types and is reported here\n * as a config, not a scope. That is not a bug to route around: an empty scope\n * and an empty config resolve to exactly the same thing (no chaos, nothing to\n * select), so no caller can observe which branch ran. The server's own defaults\n * getter (`src/server.ts`) always sets `byTestId`, so a real server's value is\n * never the ambiguous one. A THIRD variant, or a scope whose emptiness became\n * observable, would need a tagged discriminant instead of this shape test.\n */\nexport function isChaosScope(defaults: ChaosDefaults): defaults is ChaosScope {\n return \"base\" in defaults || \"byTestId\" in defaults;\n}\n\n/**\n * Pick the server-level chaos config that applies to THIS request: the override\n * installed for its testId, else the server-wide baseline. The testId is\n * resolved by `resolveTestId` — the same helper `getTestId` and the control API\n * use — so a harness that tags by `?testId=` lands in the same scope its chaos\n * override was stored under, and the two sides cannot disagree.\n *\n * SELECTION, NOT MERGE. A scoped override REPLACES the baseline wholesale; it\n * is not layered field-wise over it. A server started with `--chaos-latency\n * 500` whose test installs `{ dropRate: 1 }` sees drops with NO latency on that\n * test's traffic — restate `latencyMs` in the override to keep it. See\n * `ChaosScope` in types.ts for why replacement is the contract (it is what\n * separates `POST /__aimock/chaos {}` from `DELETE`), and note that the two\n * links above this one — fixture and headers, applied below — DO merge\n * field-wise. `POST`/`GET /__aimock/chaos` echo the config actually in effect\n * for the scope, and the control API warns when an install drops a field that\n * was in effect, so nothing is lost silently.\n *\n * PRESENCE of the entry is what selects, not its truthiness: the lookup asks\n * `has`, not `get`. A map built with the key mapped to `undefined` (or to an\n * empty object) is an override that was explicitly installed and says \"no\n * chaos for this test\" — the `POST /__aimock/chaos {}` case — so it must win\n * over `base` exactly like a populated one. Testing `get(...)` truthiness would\n * quietly demote that one shape back to the baseline, which is the merge\n * behaviour this contract exists to rule out.\n */\nfunction resolveScopedDefaults(\n serverDefaults: ChaosDefaults | undefined,\n rawHeaders?: http.IncomingHttpHeaders,\n url?: string,\n): ChaosConfig | undefined {\n if (!serverDefaults) return undefined;\n if (!isChaosScope(serverDefaults)) return serverDefaults;\n const testId = resolveTestId(rawHeaders ?? {}, url);\n if (serverDefaults.byTestId?.has(testId)) return serverDefaults.byTestId.get(testId) ?? {};\n return serverDefaults.base;\n}\n\n/**\n * Every chaos field, with its accepted upper bound and its header spelling.\n * ONE table: every source — request header, fixture chaos, server default, the\n * `POST /__aimock/chaos` control API and the `--chaos-*` CLI flags — resolves a\n * field through this entry via {@link parseChaosField}, so no source can drift\n * onto its own bound or its own idea of which fields take a whole number.\n *\n * Exported for the two surfaces that live in other modules (`src/server.ts`'s\n * control API, `src/cli.ts`'s flag parsing); in-module callers go through\n * {@link parseChaosField} too.\n */\nexport const CHAOS_FIELDS = {\n dropRate: { header: \"x-aimock-chaos-drop\", max: 1, integer: false },\n malformedRate: { header: \"x-aimock-chaos-malformed\", max: 1, integer: false },\n disconnectRate: { header: \"x-aimock-chaos-disconnect\", max: 1, integer: false },\n rateLimitRate: { header: \"x-aimock-chaos-ratelimit\", max: 1, integer: false },\n latencyMs: { header: \"x-aimock-chaos-latency\", max: 30000, integer: true },\n} as const satisfies Record<string, { header: string; max: number; integer: boolean }>;\n\nexport type ChaosField = keyof typeof CHAOS_FIELDS;\n\nexport const CHAOS_FIELD_NAMES = Object.keys(CHAOS_FIELDS) as ChaosField[];\n\n/**\n * A plain decimal RATE: digits with an optional fractional part, and nothing\n * else. Deliberately NOT what `Number()` accepts — `Number` is a full-string\n * parse (unlike `parseFloat`, which would take the `0.5` out of `\"0.5abc\"`),\n * but it is also a JS *literal* parse, so it happily reads `\"0x1e\"` as 30,\n * `\"0b1\"` as 1, `\"1e3\"` as 1000 and `\"Infinity\"` as Infinity. None of those is\n * a rate a caller meant to write, and `0x1` silently becoming `dropRate: 1`\n * turns a typo into a 100 % outage. A leading `+`/`-` sign, `_` separators and\n * exponents are rejected for the same reason.\n */\nconst DECIMAL_RATE = /^(?:\\d+(?:\\.\\d*)?|\\.\\d+)$/;\n\n/** A non-negative decimal INTEGER — the only shape a millisecond count takes. */\nconst DECIMAL_INTEGER = /^\\d+$/;\n\n/**\n * The ONE way a chaos number is parsed, whatever its source.\n *\n * Two shapes, picked per field by the `CHAOS_FIELDS` table: a RATE is a plain\n * decimal in `[0, max]` (max is always 1), and an INTEGER field (`latencyMs`)\n * is a whole, non-negative count of milliseconds in `[0, max]`. `integer`\n * defaults to false so a caller that omits it gets the rate grammar.\n *\n * The VALUE rules apply to every source alike: `NaN` and non-finite values, a\n * fractional value for an integer field, negative zero, and anything outside\n * `[0, max]` are all rejected (returned as `undefined`) whether they arrived as\n * text or as a typed number. Nothing is ever clamped — a value the caller did\n * not ask for is never substituted for the one they did. Negative zero is\n * rejected rather than folded to `0` so that it matches its own text form\n * (`\"-0\"`, which the sign-less grammar already refuses) and so that no `-0`\n * escapes into a config object that later gets echoed back over JSON.\n *\n * The GRAMMAR above — the two regexes — is a rule about TEXT, and only a string\n * input passes through it: trailing garbage (`\"0.5abc\"`), non-numeric text\n * (`\"banana\"`), the empty string, a sign (`\"+1\"`, `\"-0\"`) and the JS\n * numeric-literal forms that are not plain decimals (`\"0x1e\"`, `\"0b1\"`,\n * `\"1e3\"`, `\"Infinity\"`) are rejected as spellings. A fixture or JSON body that\n * writes `latencyMs: 1e3` has ALREADY been through a JSON/JS parser and hands\n * this function the number `1000`, indistinguishable from a literal `1000` — so\n * it is accepted, while the header `\"1e3\"` is not. That asymmetry is the point,\n * not a gap: the grammar exists to stop an ambiguous *spelling* on the wire\n * (`0x1` silently becoming `dropRate: 1` turns a typo into a 100 % outage),\n * and a value that reached us as a number has no spelling left to police.\n */\nexport function parseChaosNumber(raw: unknown, max: number, integer = false): number | undefined {\n let value: number;\n if (typeof raw === \"number\") {\n value = raw;\n } else if (typeof raw === \"string\") {\n const text = raw.trim();\n if (!(integer ? DECIMAL_INTEGER : DECIMAL_RATE).test(text)) return undefined;\n value = Number(text);\n } else {\n return undefined;\n }\n if (!Number.isFinite(value)) return undefined;\n if (integer && !Number.isInteger(value)) return undefined;\n if (Object.is(value, -0)) return undefined;\n if (value < 0 || value > max) return undefined;\n return value;\n}\n\n/**\n * Parse a value for a NAMED chaos field, taking its bound and its integer-ness\n * from `CHAOS_FIELDS` instead of from the call site. Every entry point should\n * call this rather than `parseChaosNumber` with hand-written limits — that is\n * what makes the \"ONE table\" claim above true instead of aspirational.\n */\nexport function parseChaosField(field: ChaosField, raw: unknown): number | undefined {\n return parseChaosNumber(raw, CHAOS_FIELDS[field].max, CHAOS_FIELDS[field].integer);\n}\n\n/**\n * The single value a repeated header carries. Node models a repeat two ways —\n * as a `string[]`, and (for most headers) folded into one comma-joined string —\n * and ONE rule covers both shapes, so the two can never diverge: split on\n * commas, trim, drop empties, and the FIRST NON-EMPTY value wins.\n *\n * \"Non-empty\" is the load-bearing word. `x-aimock-chaos-latency: ` followed by\n * `x-aimock-chaos-latency: 500` reaches the handler as `\",500\"` or\n * `[\"\", \"500\"]` depending on the shape; taking element `[0]` blindly made the\n * folded form warn \"rejected latencyMs value \\\"\\\"\" while the array form went\n * silently absent — two behaviours for one wire-level request. An empty value\n * carries no instruction, so it is skipped rather than rejected.\n *\n * A repeat whose values DISAGREE is a real misconfiguration (two different\n * rates asked for on one request), so it warns once and the first wins.\n * \"Disagree\" is judged on what the values MEAN, not how they are spelled:\n * `0.5, 0.50` is one rate written twice and says nothing worth warning about,\n * whereas a TEXT comparison called it a conflict. Values the parser rejects\n * have no meaning to compare, so they stand for themselves — two different\n * typos still disagree, and a typo still disagrees with a valid number.\n */\nfunction firstHeaderValue(\n raw: string | string[] | undefined,\n field: ChaosField,\n logger?: Logger,\n): string | undefined {\n const parts = Array.isArray(raw) ? raw : typeof raw === \"string\" ? [raw] : [];\n const values = parts\n .flatMap((part) => part.split(\",\"))\n .map((part) => part.trim())\n .filter((part) => part !== \"\");\n const first = values[0];\n if (first === undefined) return undefined;\n const firstMeaning = headerValueMeaning(first, field);\n if (values.some((value) => headerValueMeaning(value, field) !== firstMeaning)) {\n logger?.warn(\n `[chaos] ${CHAOS_FIELDS[field].header}: repeated header with conflicting values ${JSON.stringify(values)} — using the first (${JSON.stringify(first)})`,\n );\n }\n return first;\n}\n\n/**\n * What one spelling of a header value MEANS, as a comparable token: the parsed\n * number when the grammar accepts it, else the raw text. Prefixed so a rejected\n * literal `\"0.5\"` could never compare equal to the parsed number 0.5.\n */\nfunction headerValueMeaning(text: string, field: ChaosField): string {\n const { max, integer } = CHAOS_FIELDS[field];\n const value = parseChaosNumber(text, max, integer);\n return value === undefined ? `raw:${text}` : `num:${value}`;\n}\n\n/**\n * Where a chaos value came from, for the warning that rejects it.\n *\n * `label` is what the line CALLS the source and names the specific fixture or\n * testId scope, not just its kind: with the latch below, only the FIRST of N\n * bad values is ever printed, so a line reading \"fixture chaos\" alone leaves\n * the reader to guess which of N fixtures carries the typo.\n *\n * `latchKey` identifies that source for the latch and is absent for sources\n * that are never latched (per-request headers). It is deliberately NOT the\n * label: two fixtures can summarise identically (colliding predicates or\n * regexes) and must still each get their line.\n */\ninterface ChaosSource {\n label: string;\n latchKey?: string;\n}\n\n/** Placeholder for a level that carries no config, so nothing can be warned about. */\nconst NO_SOURCE: ChaosSource = { label: \"(no chaos config)\" };\n\n/**\n * Static chaos config (a fixture's `chaos`, a server default) is re-read on\n * EVERY request, so an invalid value there used to warn on every request for\n * the life of the process — thousands of identical lines for one typo. Latch\n * those: once per distinct source+field+value, PER SERVER. Per-request HEADERS\n * are dynamic input and keep warning per request, because each request really\n * is a new caller making a new mistake.\n *\n * PER SERVER, not per process. The latch hangs off the server's `Logger` — one\n * is constructed per `createServer` and every handler warns through\n * `defaults.logger`, so the logger IS the server instance as far as this\n * module can see it. A module-global `Set` instead meant two `LLMock`s in one\n * process shared one latch: the second server never reported its own bad\n * value, and one server's `resetChaosWarnings` re-armed the other's. A\n * `WeakMap` so a stopped server's keys go with it — nothing else holds them.\n *\n * Cleared by `resetChaosWarnings`, which `performFullReset` calls alongside the\n * other per-server state, so a suite that resets between tests sees the warning\n * again rather than inheriting the previous test's latch.\n */\nconst warnedStaticRejections = new WeakMap<Logger, Set<string>>();\n\n/**\n * Cap on distinct latched keys per server. Runtime chaos installs (`POST\n * /__aimock/chaos`, once per test in a long-lived harness) can mint unboundedly\n * many distinct bad values, and a latch that only ever grows is a leak. Past\n * the cap the OLDEST key is evicted (a `Set` iterates in insertion order), so\n * the worst case is that a long-since-warned typo warns a second time — never\n * unbounded memory.\n */\nconst MAX_LATCHED_REJECTIONS = 64;\n\n/**\n * Clear ONE server's static-config rejection latch, so the next request reports\n * a bad static value again. See `warnedStaticRejections`.\n *\n * Takes that server's logger because the logger is what the latch is keyed by.\n * There is deliberately no \"clear every server\" form: `performFullReset` is an\n * isolation barrier for the server being reset, and reaching into a concurrent\n * server's latch is exactly the cross-talk this scoping removes.\n *\n * Does NOT clear the `applyChaos` deprecation notice (`warnedSyncApplyChaos`),\n * which is once per process by design — see the latch there.\n */\nexport function resetChaosWarnings(logger: Logger): void {\n warnedStaticRejections.delete(logger);\n}\n\nfunction warnRejected(source: ChaosSource, field: ChaosField, raw: unknown, logger?: Logger): void {\n if (!logger) return;\n if (source.latchKey !== undefined) {\n // Latched only once a logger is actually present, so a warn-less call can\n // never swallow the one line a configured logger was going to emit.\n const key = `${source.latchKey}\\u0000${field}\\u0000${JSON.stringify(raw)}`;\n let latched = warnedStaticRejections.get(logger);\n if (!latched) {\n latched = new Set<string>();\n warnedStaticRejections.set(logger, latched);\n }\n if (latched.has(key)) return;\n if (latched.size >= MAX_LATCHED_REJECTIONS) {\n const oldest = latched.values().next();\n if (!oldest.done) latched.delete(oldest.value);\n }\n latched.add(key);\n }\n const shape = CHAOS_FIELDS[field].integer ? \"a whole number of ms\" : \"a number\";\n logger.warn(\n `[chaos] ${source.label}: rejected ${field} value ${JSON.stringify(raw)} — must be ${shape} in [0,${CHAOS_FIELDS[field].max}]; ignoring it`,\n );\n}\n\n/**\n * Serial number per fixture OBJECT, minted on demand, so a fixture's latch key\n * is its identity rather than its (possibly colliding) description. Same reason\n * `router.ts` keys its relaxed-turnIndex throttle by object identity. A\n * `WeakMap`, so the serial dies with the fixture.\n */\nconst fixtureSerials = new WeakMap<Fixture, number>();\nlet nextFixtureSerial = 0;\n\n/** Name and latch-identify the fixture whose `chaos` block is being read. */\nfunction fixtureSource(fixture: Fixture): ChaosSource {\n let serial = fixtureSerials.get(fixture);\n if (serial === undefined) {\n serial = nextFixtureSerial++;\n fixtureSerials.set(fixture, serial);\n }\n return {\n label: `fixture chaos ${describeMatch(fixture.match, -1)}`,\n latchKey: `fixture#${serial}`,\n };\n}\n\n/**\n * Name and latch-identify the server-level config this request resolved to. A\n * per-testId override and the server-wide baseline are DIFFERENT sources — a\n * typo in one must not latch the other silent — so the testId is part of both\n * the label and the key.\n *\n * Mirrors the selection in `resolveScopedDefaults` (which is the authority):\n * the baseline is the scope's `DEFAULT_TEST_ID` entry, so an untagged request\n * reads as the plain server default rather than an override named after an\n * internal sentinel.\n */\nfunction serverSource(\n serverDefaults: ChaosDefaults | undefined,\n rawHeaders: http.IncomingHttpHeaders | undefined,\n url: string | undefined,\n): ChaosSource {\n if (serverDefaults && isChaosScope(serverDefaults)) {\n const testId = resolveTestId(rawHeaders ?? {}, url);\n if (testId !== DEFAULT_TEST_ID && serverDefaults.byTestId?.has(testId)) {\n return {\n label: `server chaos default (testId ${JSON.stringify(testId)})`,\n latchKey: `server:${testId}`,\n };\n }\n }\n return { label: \"server chaos default\", latchKey: \"server\" };\n}\n\n/** Read one field from the request headers. */\nfunction readHeaderField(\n rawHeaders: http.IncomingHttpHeaders | undefined,\n field: ChaosField,\n logger?: Logger,\n): number | undefined {\n const text = firstHeaderValue(rawHeaders?.[CHAOS_FIELDS[field].header], field, logger);\n if (text === undefined) return undefined;\n const value = parseChaosField(field, text);\n // A header is dynamic per-request input: warn EVERY time, never latched —\n // hence no `latchKey`.\n if (value === undefined) warnRejected({ label: CHAOS_FIELDS[field].header }, field, text, logger);\n return value;\n}\n\n/** Read one field from a config object — a fixture's chaos, or the server default. */\nfunction readConfigField(\n config: ChaosConfig | undefined,\n field: ChaosField,\n source: ChaosSource,\n logger?: Logger,\n): number | undefined {\n const raw = config?.[field];\n if (raw === undefined) return undefined;\n const value = parseChaosField(field, raw);\n // Static config, re-read every request: warn once per distinct bad value,\n // per source, per server.\n if (value === undefined) warnRejected(source, field, raw, logger);\n return value;\n}\n\n/**\n * Resolve chaos config from headers, fixture, and server defaults.\n * Header values override fixture values, which override server defaults —\n * field-wise at each step, so a level that sets only `dropRate` leaves the\n * level below's `latencyMs` intact. The server defaults are themselves selected\n * per-testId first, and THAT step replaces rather than merges (see\n * `resolveScopedDefaults` and `ChaosScope`).\n *\n * Every source goes through the same parser and the same ONE out-of-range\n * policy — reject and warn, never clamp (see `ChaosConfig`). A rejected value\n * is simply not set, so the next level of precedence applies. All three levels\n * are parsed even when a higher one wins, so a bad value is always reported\n * rather than hidden by the override above it.\n *\n * Exported so a caller that needs BOTH the latency delay and the terminal\n * action resolves once per request and threads the result through, instead of\n * parsing (and re-warning about) the same headers twice on the hot path.\n */\nexport function resolveChaosConfig(\n fixture: Fixture | null,\n serverDefaults?: ChaosDefaults,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n url?: string,\n): ChaosConfig {\n const serverConfig = resolveScopedDefaults(serverDefaults, rawHeaders, url);\n const resolved: ChaosConfig = {};\n // Resolved once, not per field: minting a fixture serial and re-resolving the\n // testId five times per request would be pure hot-path waste. `NO_SOURCE` is\n // never read — a config-less level returns before it touches the source.\n const fixtureFrom = fixture ? fixtureSource(fixture) : NO_SOURCE;\n const serverFrom = serverConfig ? serverSource(serverDefaults, rawHeaders, url) : NO_SOURCE;\n\n for (const field of CHAOS_FIELD_NAMES) {\n const fromHeader = readHeaderField(rawHeaders, field, logger);\n const fromFixture = readConfigField(fixture?.chaos, field, fixtureFrom, logger);\n const fromServer = readConfigField(serverConfig, field, serverFrom, logger);\n const value = fromHeader ?? fromFixture ?? fromServer;\n if (value !== undefined) resolved[field] = value;\n }\n\n return resolved;\n}\n\n/**\n * Resolve the deterministic latency delay (ms) for this request.\n * Precedence is header > fixture > server, same as the rates. Returns 0\n * when no latency is configured. Exported so async handlers can await the\n * delay BEFORE evaluating terminal chaos actions.\n */\nexport function resolveChaosLatencyMs(\n fixture: Fixture | null,\n serverDefaults?: ChaosDefaults,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n url?: string,\n resolved?: ChaosConfig,\n): number {\n const config = resolved ?? resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger, url);\n return config.latencyMs ?? 0;\n}\n\n/**\n * Why a response can no longer carry chaos bytes. These are NOT the same\n * event, and they do NOT foreclose the same actions — a caller that treats\n * them alike either logs a lie or skips work it could still have done:\n *\n * - `\"chaos-disconnected\"` — WE destroyed it, via the `disconnect` chaos\n * action. From the outside that is indistinguishable from a client hang-up,\n * so the response is marked on the way out and reported apart from one.\n * - `\"already-ended\"` — the body was ended by someone else on this handler.\n * The client got a response, just not ours. Checked BEFORE `destroyed`,\n * because a normally-ended response whose connection is torn down afterwards\n * is an ended response, not a disconnect; the old order blamed the client\n * for a response it had received in full.\n * - `\"client-gone\"` — the socket was destroyed with nothing ended on it. The\n * client hung up; whatever we were about to send is undeliverable and must\n * not be journalled.\n * - `\"headers-sent\"` — a status line is already committed. NOT death: the\n * connection may well still be healthy. Only the actions that need a status\n * line of their own (`drop`, `malformed`, `rateLimit`) are foreclosed —\n * `disconnect` is a bare `res.destroy()` and still works, and the\n * deterministic latency is still meaningful.\n *\n * The first three are collectively \"gone\": see {@link responseGone}.\n */\nexport type ResponseUnwritableReason =\n | \"chaos-disconnected\"\n | \"already-ended\"\n | \"client-gone\"\n | \"headers-sent\";\n\n/**\n * Responses this module destroyed itself, through the `disconnect` chaos\n * action. A `WeakSet` rather than a marker property on the response: it adds\n * nothing to the object's shape (so a `ServerResponse` test double stays a\n * faithful stand-in) and the entry goes away with the response.\n */\nconst chaosDisconnectedResponses = new WeakSet<http.ServerResponse>();\n\n/**\n * The reason this response can no longer carry a fresh status line, or `null`\n * while it can. Checked most-specific first: our own teardown, then an ended\n * body, then a destroyed socket, then a committed status line.\n */\nexport function responseUnwritableReason(\n res: http.ServerResponse,\n): ResponseUnwritableReason | null {\n if (chaosDisconnectedResponses.has(res)) return \"chaos-disconnected\";\n if (res.writableEnded) return \"already-ended\";\n if (res.destroyed) return \"client-gone\";\n if (res.headersSent) return \"headers-sent\";\n return null;\n}\n\n/**\n * The reason this response is GONE — dead for every purpose, chaos or not —\n * or `null` while anything at all can still be done with it. `\"headers-sent\"`\n * is deliberately NOT one of these.\n */\nexport function responseGoneReason(\n res: http.ServerResponse,\n): Exclude<ResponseUnwritableReason, \"headers-sent\"> | null {\n const reason = responseUnwritableReason(res);\n return reason === null || reason === \"headers-sent\" ? null : reason;\n}\n\n/**\n * True once nothing can be delivered on this response at all: the client hung\n * up, the body was already ended, or our own `disconnect` action destroyed it.\n *\n * This is the predicate the streaming surfaces guard their post-`await` writes\n * with (`grok-video.ts`, `veo-video.ts` — which hand-rolled\n * `res.destroyed || res.writableEnded`, i.e. this minus the chaos-teardown\n * case), and the one the SPLIT chaos gates in `src/server.ts` — the ones that\n * call `awaitChaosLatency` and `evaluateChaos` separately instead of going\n * through `applyChaosAsync` — have to re-check themselves after the await.\n * Without that check a client that hangs up mid-delay still gets a full\n * response built, \"served\" into a dead socket and JOURNALLED (C13).\n */\nexport function responseGone(res: http.ServerResponse): boolean {\n return responseGoneReason(res) !== null;\n}\n\n/**\n * True once the response can no longer carry a FRESH STATUS LINE, for any of\n * the reasons {@link responseUnwritableReason} enumerates — {@link\n * responseGone} plus `headers-sent`. Guard a `writeHead` with this; decide\n * whether there is any point continuing at all with `responseGone`.\n */\nexport function responseUnwritable(res: http.ServerResponse): boolean {\n return responseUnwritableReason(res) !== null;\n}\n\n/** Human-readable gloss per reason, so a skipped chaos action logs what actually happened. */\nconst UNWRITABLE_DESCRIPTION: Record<ResponseUnwritableReason, string> = {\n \"chaos-disconnected\": \"the chaos disconnect action had already destroyed it\",\n \"already-ended\": \"the response was already ended\",\n \"client-gone\": \"the client disconnected\",\n \"headers-sent\": \"a status line was already committed\",\n};\n\n/** The gloss for one reason. Exported so the split gates in `src/server.ts` log the same words. */\nexport function describeUnwritableReason(reason: ResponseUnwritableReason): string {\n return UNWRITABLE_DESCRIPTION[reason];\n}\n\n/**\n * What {@link applyChaosAsync} reports back. Read it as the ANSWER TO ONE\n * QUESTION — \"must the caller stop?\" — because that is how every call site\n * spells it: `if (await applyChaosAsync(...)) return;`.\n *\n * - `false` — the caller CARRIES ON and serves the request. Either no chaos\n * fired, or the action that was rolled could not be applied and was skipped\n * without touching the response (a body action on a headers-sent response).\n * A skip must never abandon a healthy body the handler was about to finish.\n * - `\"handled\"` — a terminal chaos action was applied and journalled; the\n * response is spoken for.\n * - `\"unwritable\"` — the response was GONE by the time the latency delay\n * finished (client hung up, body already ended, or our own `disconnect`\n * destroyed it). Nothing was written, nothing was journalled, no metric was\n * counted — and there is nothing left to serve, so the caller stops.\n *\n * Both non-`false` members are truthy strings, so every existing call site\n * keeps its meaning while callers that want the reason can read it.\n */\nexport type ChaosAsyncOutcome = false | \"handled\" | \"unwritable\";\n\n/**\n * Whether {@link applyChaosAction} actually did the thing. `\"skipped\"` means\n * the response could not take that particular action (see\n * {@link ResponseUnwritableReason}) and NOTHING happened: no write, no journal\n * entry, no metric. It is not a failure — it is the caller's cue that the\n * request is still its to finish.\n */\nexport type ChaosActionOutcome = \"applied\" | \"skipped\";\n\n/**\n * Await the deterministic latency delay configured for this request, if any.\n * Fixed (never randomised or jittered) so replay stays deterministic. Returns\n * immediately when no latency is configured. Handlers that roll the chaos dice\n * themselves (branching on the action before dispatching) call this first;\n * handlers that just need \"delay, then maybe fail\" use `applyChaosAsync`.\n *\n * Pass `res` so a client that hangs up mid-delay CANCELS the pending timer\n * (via the cancellable `delay(ms, signal)` helper) instead of leaving it armed\n * for up to the 30s cap. The wait resolves early on disconnect — callers must\n * re-check the response is still writable afterwards, which every chaos write\n * path does through `responseUnwritable`.\n *\n * The disconnect signal is taken from the RESPONSE, not the request: an\n * `IncomingMessage` is auto-destroyed as soon as its body has been fully read,\n * so `req.destroyed` / `req`'s `close` event fire on every healthy request and\n * would cancel the delay for everyone. `res`'s `close` before a write can only\n * mean the connection was terminated prematurely.\n */\nexport async function awaitChaosLatency(\n fixture: Fixture | null,\n serverDefaults?: ChaosDefaults,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n url?: string,\n res?: http.ServerResponse,\n resolved?: ChaosConfig,\n): Promise<void> {\n const delayMs = resolveChaosLatencyMs(fixture, serverDefaults, rawHeaders, logger, url, resolved);\n if (delayMs <= 0) return;\n\n // `res` is optional: without anything to listen on, fall back to a plain wait.\n if (!res) {\n await delay(delayMs);\n return;\n }\n\n const controller = new AbortController();\n const onClose = (): void => controller.abort();\n // Subscribe and unsubscribe must be guarded the SAME way: a test double may\n // carry only part of the EventEmitter surface, and an unconditional\n // `res.off(...)` in the `finally` would throw a TypeError out of an\n // otherwise-normal completion — turning the delay into a rejection. So we\n // only ever remove what we actually added, through the remover we verified\n // exists at subscribe time (`off` is the modern alias of `removeListener`;\n // a double carrying neither simply keeps the short-lived per-request\n // listener rather than blowing up the caller).\n let unsubscribe: (() => void) | undefined;\n // GONE, not merely unwritable: a response whose status line is already\n // committed is still live, and its configured delay is still meaningful.\n // Aborting on `headersSent` zeroed the latency for every post-headers gate.\n if (responseGone(res)) {\n controller.abort();\n } else if (typeof res.once === \"function\") {\n res.once(\"close\", onClose);\n if (typeof res.off === \"function\") unsubscribe = () => res.off(\"close\", onClose);\n else if (typeof res.removeListener === \"function\")\n unsubscribe = () => res.removeListener(\"close\", onClose);\n }\n try {\n await delay(delayMs, controller.signal);\n } finally {\n unsubscribe?.();\n }\n}\n\n/**\n * Evaluate chaos config and return the triggered action, or null if none.\n *\n * THE RATES DO NOT ROLL INDEPENDENTLY. Each is a separate `Math.random()` draw\n * taken in a fixed order — drop → malformed → rateLimit → disconnect — and the\n * first draw that hits returns immediately, so the ones after it are never\n * rolled at all. A rate is therefore the probability that ITS draw hits GIVEN\n * that no earlier rate fired, not the share of requests that end in that fault.\n * Only `dropRate`, first in the chain, is unconditional.\n *\n * Worked example: `{ dropRate: 0.5, rateLimitRate: 0.5 }` yields ~50 % drops\n * and ~25 % rate limits (0.5 x 0.5 — half the requests never reach the second\n * draw), not 50 % of each. To exercise two faults at their nominal frequency,\n * configure them on separate requests rather than in one config.\n */\nexport function evaluateChaos(\n fixture: Fixture | null,\n serverDefaults?: ChaosDefaults,\n rawHeaders?: http.IncomingHttpHeaders,\n logger?: Logger,\n url?: string,\n resolved?: ChaosConfig,\n): ChaosAction | null {\n const config = resolved ?? resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger, url);\n\n if (config.dropRate !== undefined && config.dropRate > 0 && Math.random() < config.dropRate) {\n return \"drop\";\n }\n if (\n config.malformedRate !== undefined &&\n config.malformedRate > 0 &&\n Math.random() < config.malformedRate\n ) {\n return \"malformed\";\n }\n if (\n config.rateLimitRate !== undefined &&\n config.rateLimitRate > 0 &&\n Math.random() < config.rateLimitRate\n ) {\n return \"rateLimit\";\n }\n if (\n config.disconnectRate !== undefined &&\n config.disconnectRate > 0 &&\n Math.random() < config.disconnectRate\n ) {\n return \"disconnect\";\n }\n\n return null;\n}\n\n/**\n * Async chaos entrypoint: awaits the deterministic latency delay (when\n * configured) BEFORE rolling terminal actions. Returns true when a terminal\n * action fired (caller returns early), false to proceed.\n *\n * This is the ONLY entrypoint that honours `latencyMs`, so it is the one every\n * route handler must use: `src/__tests__/chaos-async-call-sites.test.ts` fails\n * the build if any module outside this file rolls chaos without an awaited\n * latency dominating the roll. That guard skips this file wholesale — it\n * defines the primitives — so the `applyChaos(...)` delegation below is neither\n * checked by it nor a control for it. The guard proves it is not vacuous its\n * own way: synthetic fixtures for each evasion shape, a floor on the number of\n * files scanned and chaos imports resolved, and a reconciliation of its name\n * sets against this file's real exports.\n *\n * The delay is tied to the response socket, so a client that disconnects during\n * it cancels the timer. When that happens (or the response went away for any\n * other reason while we waited) the dice are never rolled: returns\n * `\"unwritable\"` so the caller returns early, with nothing written and nothing\n * journalled — a response that was never sent must not appear in the journal.\n *\n * A response that merely has its HEADERS SENT is NOT gone, and is not\n * short-circuited: the delay is still awaited and the dice are still rolled.\n * `disconnect` needs no status line and is applied; the three body actions\n * are skipped and the call reports `false`, because the caller still has a\n * healthy response to finish and `if (outcome) return;` would abandon it.\n *\n * The return value is a {@link ChaosAsyncOutcome}, not a boolean, so callers\n * can tell \"chaos fired\" from \"the response was already dead\" — two outcomes\n * the old `true` conflated. Both are truthy, so the standard call shape\n * `if (await applyChaosAsync(...)) return;` keeps working unchanged; a caller\n * that cares compares against `\"handled\"` / `\"unwritable\"`.\n */\nexport async function applyChaosAsync(\n res: http.ServerResponse,\n fixture: Fixture | null,\n serverDefaults: ChaosDefaults | undefined,\n rawHeaders: http.IncomingHttpHeaders,\n requestUrl: string | undefined,\n journal: Journal,\n context: ChaosJournalContext,\n source: \"fixture\" | \"proxy\" | \"internal\",\n registry?: MetricsRegistry,\n logger?: Logger,\n): Promise<ChaosAsyncOutcome> {\n // Resolve ONCE per request: the latency await and the terminal-action roll\n // share the same config, so headers are parsed (and warned about) exactly once.\n const resolved = resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger, requestUrl);\n await awaitChaosLatency(fixture, serverDefaults, rawHeaders, logger, requestUrl, res, resolved);\n // Only a GONE response short-circuits here. `headers-sent` falls through to\n // the roll: `disconnect` still applies to it, and a body action that cannot\n // is skipped inside `applyChaosAction`, which reports `false` so the caller\n // finishes the body it was already streaming.\n const gone = responseGoneReason(res);\n if (gone !== null) {\n logger?.debug(\n `[chaos] not applying chaos to ${context.method} ${context.path}: ` +\n `${UNWRITABLE_DESCRIPTION[gone]} — nothing written, nothing journalled`,\n );\n return \"unwritable\";\n }\n return applyChaos(\n res,\n fixture,\n serverDefaults,\n rawHeaders,\n requestUrl,\n journal,\n context,\n source,\n registry,\n logger,\n resolved,\n )\n ? \"handled\"\n : false;\n}\n\ninterface ChaosJournalContext {\n method: string;\n path: string;\n headers: Record<string, string>;\n body: ChatCompletionRequest | null;\n /**\n * Service tag copied onto the chaos-path journal entry, so a faulted request\n * stays selectable by `GET /__aimock/journal?service=<name>` exactly like the\n * handler's own non-chaos entries. Handlers that tag their normal journal\n * writes MUST pass it here too, or their chaos traffic falls out of that\n * filter. Optional because the OpenAI-surface handlers journal untagged.\n */\n service?: string;\n}\n\n/**\n * Apply chaos to a request. Returns true if chaos was applied (caller should\n * return early), false if the request should proceed normally.\n *\n * Synchronous, and therefore SKIPS the configured `latencyMs` delay — it cannot\n * await. It IS exported (the async wrapper and this module's tests call it by\n * name), but it is not the public surface: no handler may call it —\n * `src/__tests__/chaos-async-call-sites.test.ts` fails the build if one does —\n * and the barrel deliberately publishes `applyChaosDeprecated` under the name\n * `applyChaos` instead, so library consumers who still call it are told about\n * the latency gap. See both below.\n *\n * `requestUrl` is the RAW `req.url` (query string included) — chaos scoping\n * resolves the testId from it exactly as `getTestId` does, so `?testId=` tagged\n * traffic is not silently unscoped. It is required, not optional, so a new\n * handler cannot forget it.\n *\n * `source` is required so the invariant \"this handler only applies chaos in\n * the <X> phase\" is enforced at the type level. A future handler that grows\n * a proxy path MUST pass `\"proxy\"` explicitly; the default can't drift silently.\n */\nexport function applyChaos(\n res: http.ServerResponse,\n fixture: Fixture | null,\n serverDefaults: ChaosDefaults | undefined,\n rawHeaders: http.IncomingHttpHeaders,\n requestUrl: string | undefined,\n journal: Journal,\n context: ChaosJournalContext,\n source: \"fixture\" | \"proxy\" | \"internal\",\n registry?: MetricsRegistry,\n logger?: Logger,\n resolved?: ChaosConfig,\n): boolean {\n const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger, requestUrl, resolved);\n if (!action) return false;\n // \"Rolled\" is not \"applied\": an action the response could not take is a\n // no-op, and reporting it as a hit would make the caller abandon a request\n // nothing has answered.\n return (\n applyChaosAction(action, res, fixture, journal, context, source, registry, logger) === \"applied\"\n );\n}\n\n/**\n * One-shot latch for the deprecation notice below: warn once per process, not\n * per request.\n *\n * PER PROCESS, and deliberately not cleared by `resetChaosWarnings` /\n * `performFullReset` — unlike `warnedStaticRejections`, which IS per server and\n * IS cleared. The two latches guard different kinds of fact. A rejected static\n * chaos value describes THIS server's configuration, which a reset may change,\n * so a reset suite must hear about it again. This notice describes the\n * EMBEDDER'S CODE — a call site that still imports the sync `applyChaos` — and\n * a `reset()` cannot change that: re-emitting it per test would print the same\n * unchanged migration instruction once per test with nothing new to say. The\n * deprecation policy (`docs/deprecation-policy/index.html`) is about which\n * upstream endpoints aimock keeps mocking and says nothing about notice\n * frequency, so the choice is ours; the message states it (\"Warned once per\n * process.\") so nobody reads the single line as \"it only happened once\".\n */\nlet warnedSyncApplyChaos = false;\n\n/**\n * Public, deprecated wrapper around the synchronous {@link applyChaos}. The\n * barrel (`src/index.ts`) exports this under the name `applyChaos`, which has\n * been part of the published API since v1.10.0 — removing it outright would\n * break embedders silently, so it stays and says so instead.\n *\n * @deprecated Use {@link applyChaosAsync}. The synchronous form cannot await,\n * so it silently ignores the configured chaos latency (`latencyMs`,\n * `--chaos-latency`, `x-aimock-chaos-latency`): the fault rates still roll, but\n * no delay is injected. Every fault behaviour is otherwise identical.\n */\nexport function applyChaosDeprecated(\n res: http.ServerResponse,\n fixture: Fixture | null,\n serverDefaults: ChaosDefaults | undefined,\n rawHeaders: http.IncomingHttpHeaders,\n requestUrl: string | undefined,\n journal: Journal,\n context: ChaosJournalContext,\n source: \"fixture\" | \"proxy\" | \"internal\",\n registry?: MetricsRegistry,\n logger?: Logger,\n): boolean {\n if (!warnedSyncApplyChaos) {\n warnedSyncApplyChaos = true;\n const notice =\n \"applyChaos() is deprecated and ignores the configured chaos latency \" +\n \"(latencyMs / --chaos-latency / x-aimock-chaos-latency): it is synchronous and \" +\n \"cannot await the delay. Use `await applyChaosAsync(...)` instead — same arguments, \" +\n \"same fault behaviour, plus the latency. (Warned once per process.)\";\n // Route through the caller's logger when there is one: every other line\n // this module emits honours it, and a raw console.warn ignores the\n // configured log level and bypasses whatever the embedder captures. The\n // `console.warn` fallback (with the prefix `Logger` would have added) keeps\n // the notice visible for the many call sites that pass no logger at all.\n if (logger) logger.warn(notice);\n else console.warn(`[aimock] ${notice}`);\n }\n return applyChaos(\n res,\n fixture,\n serverDefaults,\n rawHeaders,\n requestUrl,\n journal,\n context,\n source,\n registry,\n logger,\n );\n}\n\n/**\n * Apply a specific (already-rolled) chaos action. Exposed so callers that roll\n * the dice themselves can dispatch without re-rolling — important when the\n * caller wants to branch on the action before committing (e.g. pre-flight vs.\n * post-response phases).\n *\n * `source` is required (not optional) so callers can't silently omit it on\n * one branch and journal an ambiguous entry. Pass `\"fixture\"` when a fixture\n * matched (or would have), `\"proxy\"` when the request was headed for the proxy\n * path, and `\"internal\"` — the value most call sites pass — for aimock's own\n * built-in endpoints, which are neither.\n *\n * A chaos action can be dispatched an arbitrarily long time after the request\n * arrived (up to the 30s latency cap), by which point the client may be gone.\n * Writing to a destroyed socket is silently lost, so counting the action and\n * journalling a status the client never received would be a lie. When the\n * response cannot take THIS action this is a no-op: no metric, no journal\n * entry, no write, and the return value says `\"skipped\"`. The journal\n * vocabulary has no \"aborted\" chaos status — the entry is omitted rather than\n * invented.\n *\n * \"Cannot take this action\" is per-action, not one bit. A GONE response\n * (client hung up, body ended, already chaos-destroyed) forecloses everything.\n * A response whose HEADERS ARE SENT forecloses only the three actions that\n * write a status line of their own — `disconnect` is a bare `res.destroy()`\n * and is applied normally.\n *\n * `logger` is REQUIRED (not optional, not defaulted) because every branch\n * above can decline to act, and a skip nobody can see is the bug this\n * parameter exists to prevent: one call site had quietly omitted it. It is\n * typed `Logger | undefined` rather than `Logger` because the public,\n * deprecated `applyChaos` wrapper's own logger is optional and threads through\n * here — arity is what the compiler enforces, and that is what catches the\n * omission.\n */\nexport function applyChaosAction(\n action: ChaosAction,\n res: http.ServerResponse,\n fixture: Fixture | null,\n journal: Journal,\n context: ChaosJournalContext,\n source: \"fixture\" | \"proxy\" | \"internal\",\n registry: MetricsRegistry | undefined,\n logger: Logger | undefined,\n): ChaosActionOutcome {\n const unwritable = responseUnwritableReason(res);\n // `headers-sent` blocks only the actions that need a status line; every\n // other reason means the response is gone and blocks all of them.\n if (unwritable !== null && (unwritable !== \"headers-sent\" || action !== \"disconnect\")) {\n logger?.debug(\n `[chaos] skipping ${action} on ${context.method} ${context.path}: ` +\n `${UNWRITABLE_DESCRIPTION[unwritable]} — no metric, no journal entry, no write`,\n );\n return \"skipped\";\n }\n\n switch (action) {\n // Every case journals AFTER its write, for the same reason the metric\n // below is counted after: a `writeHead` that throws (a socket torn down\n // between the guard above and the call) must not leave behind a journal\n // entry for a response the client never received.\n case \"drop\": {\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Chaos: request dropped\",\n type: \"server_error\",\n code: \"chaos_drop\",\n },\n }),\n );\n journal.add({\n ...context,\n response: { status: 500, fixture, chaosAction: \"drop\", source },\n });\n break;\n }\n case \"malformed\": {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\"{malformed json: <<<chaos>>>\");\n journal.add({\n ...context,\n response: { status: 200, fixture, chaosAction: \"malformed\", source },\n });\n break;\n }\n case \"rateLimit\": {\n // Route through writeErrorResponse() so the chaos 429 carries the same\n // OpenAI-shaped rate-limit header set (Retry-After + RATE_LIMIT_HEADERS)\n // as every other 429 in the repo, instead of a second hand-rolled set.\n writeErrorResponse(\n res,\n 429,\n JSON.stringify({\n error: {\n message: \"Chaos: rate limit exceeded\",\n type: \"rate_limit_error\",\n code: \"chaos_ratelimit\",\n },\n }),\n );\n journal.add({\n ...context,\n response: { status: 429, fixture, chaosAction: \"rateLimit\", source },\n });\n break;\n }\n case \"disconnect\": {\n // The status the client actually saw: the one already on the wire when\n // the response was committed before this teardown, else 0 (nothing was\n // ever sent). Journalling 0 for a committed response claimed the client\n // got no status line when it had.\n const status = res.headersSent ? res.statusCode : 0;\n // Marked BEFORE the teardown so anything reacting to `close` already\n // sees this as OUR disconnect rather than a client hang-up.\n chaosDisconnectedResponses.add(res);\n res.destroy();\n journal.add({\n ...context,\n response: { status, fixture, chaosAction: \"disconnect\", source },\n });\n break;\n }\n default: {\n const _exhaustive: never = action;\n void _exhaustive;\n return \"skipped\";\n }\n }\n\n // Counted AFTER the write, never before: a write that throws (a socket torn\n // down between the guard above and the call) must not leave behind a metric\n // for bytes that never went out.\n registry?.incrementCounter(\"aimock_chaos_triggered_total\", { action, source });\n return \"applied\";\n}\n"],"mappings":"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"}
|
package/dist/chaos.d.cts
CHANGED
|
@@ -1,26 +1,147 @@
|
|
|
1
1
|
import { Journal } from "./journal.cjs";
|
|
2
2
|
import { Logger } from "./logger.cjs";
|
|
3
3
|
import { MetricsRegistry } from "./metrics.cjs";
|
|
4
|
-
import { ChaosAction, ChaosDefaults, ChatCompletionRequest, Fixture } from "./types.cjs";
|
|
4
|
+
import { ChaosAction, ChaosConfig, ChaosDefaults, ChatCompletionRequest, Fixture } from "./types.cjs";
|
|
5
5
|
import * as http$1 from "node:http";
|
|
6
6
|
|
|
7
7
|
//#region src/chaos.d.ts
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the deterministic latency delay (ms) for this request.
|
|
11
|
+
* Precedence is header > fixture > server, same as the rates. Returns 0
|
|
12
|
+
* when no latency is configured. Exported so async handlers can await the
|
|
13
|
+
* delay BEFORE evaluating terminal chaos actions.
|
|
14
|
+
*/
|
|
15
|
+
declare function resolveChaosLatencyMs(fixture: Fixture | null, serverDefaults?: ChaosDefaults, rawHeaders?: http$1.IncomingHttpHeaders, logger?: Logger, url?: string, resolved?: ChaosConfig): number;
|
|
16
|
+
/**
|
|
17
|
+
* Why a response can no longer carry chaos bytes. These are NOT the same
|
|
18
|
+
* event, and they do NOT foreclose the same actions — a caller that treats
|
|
19
|
+
* them alike either logs a lie or skips work it could still have done:
|
|
20
|
+
*
|
|
21
|
+
* - `"chaos-disconnected"` — WE destroyed it, via the `disconnect` chaos
|
|
22
|
+
* action. From the outside that is indistinguishable from a client hang-up,
|
|
23
|
+
* so the response is marked on the way out and reported apart from one.
|
|
24
|
+
* - `"already-ended"` — the body was ended by someone else on this handler.
|
|
25
|
+
* The client got a response, just not ours. Checked BEFORE `destroyed`,
|
|
26
|
+
* because a normally-ended response whose connection is torn down afterwards
|
|
27
|
+
* is an ended response, not a disconnect; the old order blamed the client
|
|
28
|
+
* for a response it had received in full.
|
|
29
|
+
* - `"client-gone"` — the socket was destroyed with nothing ended on it. The
|
|
30
|
+
* client hung up; whatever we were about to send is undeliverable and must
|
|
31
|
+
* not be journalled.
|
|
32
|
+
* - `"headers-sent"` — a status line is already committed. NOT death: the
|
|
33
|
+
* connection may well still be healthy. Only the actions that need a status
|
|
34
|
+
* line of their own (`drop`, `malformed`, `rateLimit`) are foreclosed —
|
|
35
|
+
* `disconnect` is a bare `res.destroy()` and still works, and the
|
|
36
|
+
* deterministic latency is still meaningful.
|
|
37
|
+
*
|
|
38
|
+
* The first three are collectively "gone": see {@link responseGone}.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What {@link applyChaosAsync} reports back. Read it as the ANSWER TO ONE
|
|
43
|
+
* QUESTION — "must the caller stop?" — because that is how every call site
|
|
44
|
+
* spells it: `if (await applyChaosAsync(...)) return;`.
|
|
45
|
+
*
|
|
46
|
+
* - `false` — the caller CARRIES ON and serves the request. Either no chaos
|
|
47
|
+
* fired, or the action that was rolled could not be applied and was skipped
|
|
48
|
+
* without touching the response (a body action on a headers-sent response).
|
|
49
|
+
* A skip must never abandon a healthy body the handler was about to finish.
|
|
50
|
+
* - `"handled"` — a terminal chaos action was applied and journalled; the
|
|
51
|
+
* response is spoken for.
|
|
52
|
+
* - `"unwritable"` — the response was GONE by the time the latency delay
|
|
53
|
+
* finished (client hung up, body already ended, or our own `disconnect`
|
|
54
|
+
* destroyed it). Nothing was written, nothing was journalled, no metric was
|
|
55
|
+
* counted — and there is nothing left to serve, so the caller stops.
|
|
56
|
+
*
|
|
57
|
+
* Both non-`false` members are truthy strings, so every existing call site
|
|
58
|
+
* keeps its meaning while callers that want the reason can read it.
|
|
59
|
+
*/
|
|
60
|
+
type ChaosAsyncOutcome = false | "handled" | "unwritable";
|
|
61
|
+
/**
|
|
62
|
+
* Whether {@link applyChaosAction} actually did the thing. `"skipped"` means
|
|
63
|
+
* the response could not take that particular action (see
|
|
64
|
+
* {@link ResponseUnwritableReason}) and NOTHING happened: no write, no journal
|
|
65
|
+
* entry, no metric. It is not a failure — it is the caller's cue that the
|
|
66
|
+
* request is still its to finish.
|
|
67
|
+
*/
|
|
68
|
+
|
|
9
69
|
/**
|
|
10
70
|
* Evaluate chaos config and return the triggered action, or null if none.
|
|
11
|
-
*
|
|
71
|
+
*
|
|
72
|
+
* THE RATES DO NOT ROLL INDEPENDENTLY. Each is a separate `Math.random()` draw
|
|
73
|
+
* taken in a fixed order — drop → malformed → rateLimit → disconnect — and the
|
|
74
|
+
* first draw that hits returns immediately, so the ones after it are never
|
|
75
|
+
* rolled at all. A rate is therefore the probability that ITS draw hits GIVEN
|
|
76
|
+
* that no earlier rate fired, not the share of requests that end in that fault.
|
|
77
|
+
* Only `dropRate`, first in the chain, is unconditional.
|
|
78
|
+
*
|
|
79
|
+
* Worked example: `{ dropRate: 0.5, rateLimitRate: 0.5 }` yields ~50 % drops
|
|
80
|
+
* and ~25 % rate limits (0.5 x 0.5 — half the requests never reach the second
|
|
81
|
+
* draw), not 50 % of each. To exercise two faults at their nominal frequency,
|
|
82
|
+
* configure them on separate requests rather than in one config.
|
|
83
|
+
*/
|
|
84
|
+
declare function evaluateChaos(fixture: Fixture | null, serverDefaults?: ChaosDefaults, rawHeaders?: http$1.IncomingHttpHeaders, logger?: Logger, url?: string, resolved?: ChaosConfig): ChaosAction | null;
|
|
85
|
+
/**
|
|
86
|
+
* Async chaos entrypoint: awaits the deterministic latency delay (when
|
|
87
|
+
* configured) BEFORE rolling terminal actions. Returns true when a terminal
|
|
88
|
+
* action fired (caller returns early), false to proceed.
|
|
89
|
+
*
|
|
90
|
+
* This is the ONLY entrypoint that honours `latencyMs`, so it is the one every
|
|
91
|
+
* route handler must use: `src/__tests__/chaos-async-call-sites.test.ts` fails
|
|
92
|
+
* the build if any module outside this file rolls chaos without an awaited
|
|
93
|
+
* latency dominating the roll. That guard skips this file wholesale — it
|
|
94
|
+
* defines the primitives — so the `applyChaos(...)` delegation below is neither
|
|
95
|
+
* checked by it nor a control for it. The guard proves it is not vacuous its
|
|
96
|
+
* own way: synthetic fixtures for each evasion shape, a floor on the number of
|
|
97
|
+
* files scanned and chaos imports resolved, and a reconciliation of its name
|
|
98
|
+
* sets against this file's real exports.
|
|
99
|
+
*
|
|
100
|
+
* The delay is tied to the response socket, so a client that disconnects during
|
|
101
|
+
* it cancels the timer. When that happens (or the response went away for any
|
|
102
|
+
* other reason while we waited) the dice are never rolled: returns
|
|
103
|
+
* `"unwritable"` so the caller returns early, with nothing written and nothing
|
|
104
|
+
* journalled — a response that was never sent must not appear in the journal.
|
|
105
|
+
*
|
|
106
|
+
* A response that merely has its HEADERS SENT is NOT gone, and is not
|
|
107
|
+
* short-circuited: the delay is still awaited and the dice are still rolled.
|
|
108
|
+
* `disconnect` needs no status line and is applied; the three body actions
|
|
109
|
+
* are skipped and the call reports `false`, because the caller still has a
|
|
110
|
+
* healthy response to finish and `if (outcome) return;` would abandon it.
|
|
111
|
+
*
|
|
112
|
+
* The return value is a {@link ChaosAsyncOutcome}, not a boolean, so callers
|
|
113
|
+
* can tell "chaos fired" from "the response was already dead" — two outcomes
|
|
114
|
+
* the old `true` conflated. Both are truthy, so the standard call shape
|
|
115
|
+
* `if (await applyChaosAsync(...)) return;` keeps working unchanged; a caller
|
|
116
|
+
* that cares compares against `"handled"` / `"unwritable"`.
|
|
12
117
|
*/
|
|
13
|
-
declare function
|
|
118
|
+
declare function applyChaosAsync(res: http$1.ServerResponse, fixture: Fixture | null, serverDefaults: ChaosDefaults | undefined, rawHeaders: http$1.IncomingHttpHeaders, requestUrl: string | undefined, journal: Journal, context: ChaosJournalContext, source: "fixture" | "proxy" | "internal", registry?: MetricsRegistry, logger?: Logger): Promise<ChaosAsyncOutcome>;
|
|
14
119
|
interface ChaosJournalContext {
|
|
15
120
|
method: string;
|
|
16
121
|
path: string;
|
|
17
122
|
headers: Record<string, string>;
|
|
18
123
|
body: ChatCompletionRequest | null;
|
|
124
|
+
/**
|
|
125
|
+
* Service tag copied onto the chaos-path journal entry, so a faulted request
|
|
126
|
+
* stays selectable by `GET /__aimock/journal?service=<name>` exactly like the
|
|
127
|
+
* handler's own non-chaos entries. Handlers that tag their normal journal
|
|
128
|
+
* writes MUST pass it here too, or their chaos traffic falls out of that
|
|
129
|
+
* filter. Optional because the OpenAI-surface handlers journal untagged.
|
|
130
|
+
*/
|
|
131
|
+
service?: string;
|
|
19
132
|
}
|
|
20
133
|
/**
|
|
21
134
|
* Apply chaos to a request. Returns true if chaos was applied (caller should
|
|
22
135
|
* return early), false if the request should proceed normally.
|
|
23
136
|
*
|
|
137
|
+
* Synchronous, and therefore SKIPS the configured `latencyMs` delay — it cannot
|
|
138
|
+
* await. It IS exported (the async wrapper and this module's tests call it by
|
|
139
|
+
* name), but it is not the public surface: no handler may call it —
|
|
140
|
+
* `src/__tests__/chaos-async-call-sites.test.ts` fails the build if one does —
|
|
141
|
+
* and the barrel deliberately publishes `applyChaosDeprecated` under the name
|
|
142
|
+
* `applyChaos` instead, so library consumers who still call it are told about
|
|
143
|
+
* the latency gap. See both below.
|
|
144
|
+
*
|
|
24
145
|
* `requestUrl` is the RAW `req.url` (query string included) — chaos scoping
|
|
25
146
|
* resolves the testId from it exactly as `getTestId` does, so `?testId=` tagged
|
|
26
147
|
* traffic is not silently unscoped. It is required, not optional, so a new
|
|
@@ -30,7 +151,19 @@ interface ChaosJournalContext {
|
|
|
30
151
|
* the <X> phase" is enforced at the type level. A future handler that grows
|
|
31
152
|
* a proxy path MUST pass `"proxy"` explicitly; the default can't drift silently.
|
|
32
153
|
*/
|
|
33
|
-
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Public, deprecated wrapper around the synchronous {@link applyChaos}. The
|
|
157
|
+
* barrel (`src/index.ts`) exports this under the name `applyChaos`, which has
|
|
158
|
+
* been part of the published API since v1.10.0 — removing it outright would
|
|
159
|
+
* break embedders silently, so it stays and says so instead.
|
|
160
|
+
*
|
|
161
|
+
* @deprecated Use {@link applyChaosAsync}. The synchronous form cannot await,
|
|
162
|
+
* so it silently ignores the configured chaos latency (`latencyMs`,
|
|
163
|
+
* `--chaos-latency`, `x-aimock-chaos-latency`): the fault rates still roll, but
|
|
164
|
+
* no delay is injected. Every fault behaviour is otherwise identical.
|
|
165
|
+
*/
|
|
166
|
+
declare function applyChaosDeprecated(res: http$1.ServerResponse, fixture: Fixture | null, serverDefaults: ChaosDefaults | undefined, rawHeaders: http$1.IncomingHttpHeaders, requestUrl: string | undefined, journal: Journal, context: ChaosJournalContext, source: "fixture" | "proxy" | "internal", registry?: MetricsRegistry, logger?: Logger): boolean;
|
|
34
167
|
/**
|
|
35
168
|
* Apply a specific (already-rolled) chaos action. Exposed so callers that roll
|
|
36
169
|
* the dice themselves can dispatch without re-rolling — important when the
|
|
@@ -39,9 +172,33 @@ declare function applyChaos(res: http$1.ServerResponse, fixture: Fixture | null,
|
|
|
39
172
|
*
|
|
40
173
|
* `source` is required (not optional) so callers can't silently omit it on
|
|
41
174
|
* one branch and journal an ambiguous entry. Pass `"fixture"` when a fixture
|
|
42
|
-
* matched (or would have)
|
|
43
|
-
*
|
|
175
|
+
* matched (or would have), `"proxy"` when the request was headed for the proxy
|
|
176
|
+
* path, and `"internal"` — the value most call sites pass — for aimock's own
|
|
177
|
+
* built-in endpoints, which are neither.
|
|
178
|
+
*
|
|
179
|
+
* A chaos action can be dispatched an arbitrarily long time after the request
|
|
180
|
+
* arrived (up to the 30s latency cap), by which point the client may be gone.
|
|
181
|
+
* Writing to a destroyed socket is silently lost, so counting the action and
|
|
182
|
+
* journalling a status the client never received would be a lie. When the
|
|
183
|
+
* response cannot take THIS action this is a no-op: no metric, no journal
|
|
184
|
+
* entry, no write, and the return value says `"skipped"`. The journal
|
|
185
|
+
* vocabulary has no "aborted" chaos status — the entry is omitted rather than
|
|
186
|
+
* invented.
|
|
187
|
+
*
|
|
188
|
+
* "Cannot take this action" is per-action, not one bit. A GONE response
|
|
189
|
+
* (client hung up, body ended, already chaos-destroyed) forecloses everything.
|
|
190
|
+
* A response whose HEADERS ARE SENT forecloses only the three actions that
|
|
191
|
+
* write a status line of their own — `disconnect` is a bare `res.destroy()`
|
|
192
|
+
* and is applied normally.
|
|
193
|
+
*
|
|
194
|
+
* `logger` is REQUIRED (not optional, not defaulted) because every branch
|
|
195
|
+
* above can decline to act, and a skip nobody can see is the bug this
|
|
196
|
+
* parameter exists to prevent: one call site had quietly omitted it. It is
|
|
197
|
+
* typed `Logger | undefined` rather than `Logger` because the public,
|
|
198
|
+
* deprecated `applyChaos` wrapper's own logger is optional and threads through
|
|
199
|
+
* here — arity is what the compiler enforces, and that is what catches the
|
|
200
|
+
* omission.
|
|
44
201
|
*/
|
|
45
202
|
//#endregion
|
|
46
|
-
export {
|
|
203
|
+
export { ChaosAsyncOutcome, applyChaosAsync, applyChaosDeprecated, evaluateChaos, resolveChaosLatencyMs };
|
|
47
204
|
//# sourceMappingURL=chaos.d.cts.map
|
package/dist/chaos.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chaos.d.cts","names":[],"sources":["../src/chaos.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chaos.d.cts","names":[],"sources":["../src/chaos.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;iBA2cgB,qBAAA,UACL,iCACQ,4BACJ,MAAA,CAAK,8BACT,iCAEE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAkID,iBAAA;;;;;;;;;;;;;;;;;;;;;;;;iBA4FI,aAAA,UACL,iCACQ,4BACJ,MAAA,CAAK,8BACT,iCAEE,cACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgEmB,eAAA,MACf,MAAA,CAAK,yBACD,gCACO,uCACJ,MAAA,CAAK,8DAER,kBACA,0EAEE,0BACF,SACR,QAAQ;UAkCD,mBAAA;;;WAGC;QACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqFQ,oBAAA,MACT,MAAA,CAAK,yBACD,gCACO,uCACJ,MAAA,CAAK,8DAER,kBACA,0EAEE,0BACF"}
|
package/dist/chaos.d.ts
CHANGED
|
@@ -1,26 +1,147 @@
|
|
|
1
1
|
import { Journal } from "./journal.js";
|
|
2
2
|
import { Logger } from "./logger.js";
|
|
3
3
|
import { MetricsRegistry } from "./metrics.js";
|
|
4
|
-
import { ChaosAction, ChaosDefaults, ChatCompletionRequest, Fixture } from "./types.js";
|
|
4
|
+
import { ChaosAction, ChaosConfig, ChaosDefaults, ChatCompletionRequest, Fixture } from "./types.js";
|
|
5
5
|
import * as http$1 from "node:http";
|
|
6
6
|
|
|
7
7
|
//#region src/chaos.d.ts
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the deterministic latency delay (ms) for this request.
|
|
11
|
+
* Precedence is header > fixture > server, same as the rates. Returns 0
|
|
12
|
+
* when no latency is configured. Exported so async handlers can await the
|
|
13
|
+
* delay BEFORE evaluating terminal chaos actions.
|
|
14
|
+
*/
|
|
15
|
+
declare function resolveChaosLatencyMs(fixture: Fixture | null, serverDefaults?: ChaosDefaults, rawHeaders?: http$1.IncomingHttpHeaders, logger?: Logger, url?: string, resolved?: ChaosConfig): number;
|
|
16
|
+
/**
|
|
17
|
+
* Why a response can no longer carry chaos bytes. These are NOT the same
|
|
18
|
+
* event, and they do NOT foreclose the same actions — a caller that treats
|
|
19
|
+
* them alike either logs a lie or skips work it could still have done:
|
|
20
|
+
*
|
|
21
|
+
* - `"chaos-disconnected"` — WE destroyed it, via the `disconnect` chaos
|
|
22
|
+
* action. From the outside that is indistinguishable from a client hang-up,
|
|
23
|
+
* so the response is marked on the way out and reported apart from one.
|
|
24
|
+
* - `"already-ended"` — the body was ended by someone else on this handler.
|
|
25
|
+
* The client got a response, just not ours. Checked BEFORE `destroyed`,
|
|
26
|
+
* because a normally-ended response whose connection is torn down afterwards
|
|
27
|
+
* is an ended response, not a disconnect; the old order blamed the client
|
|
28
|
+
* for a response it had received in full.
|
|
29
|
+
* - `"client-gone"` — the socket was destroyed with nothing ended on it. The
|
|
30
|
+
* client hung up; whatever we were about to send is undeliverable and must
|
|
31
|
+
* not be journalled.
|
|
32
|
+
* - `"headers-sent"` — a status line is already committed. NOT death: the
|
|
33
|
+
* connection may well still be healthy. Only the actions that need a status
|
|
34
|
+
* line of their own (`drop`, `malformed`, `rateLimit`) are foreclosed —
|
|
35
|
+
* `disconnect` is a bare `res.destroy()` and still works, and the
|
|
36
|
+
* deterministic latency is still meaningful.
|
|
37
|
+
*
|
|
38
|
+
* The first three are collectively "gone": see {@link responseGone}.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What {@link applyChaosAsync} reports back. Read it as the ANSWER TO ONE
|
|
43
|
+
* QUESTION — "must the caller stop?" — because that is how every call site
|
|
44
|
+
* spells it: `if (await applyChaosAsync(...)) return;`.
|
|
45
|
+
*
|
|
46
|
+
* - `false` — the caller CARRIES ON and serves the request. Either no chaos
|
|
47
|
+
* fired, or the action that was rolled could not be applied and was skipped
|
|
48
|
+
* without touching the response (a body action on a headers-sent response).
|
|
49
|
+
* A skip must never abandon a healthy body the handler was about to finish.
|
|
50
|
+
* - `"handled"` — a terminal chaos action was applied and journalled; the
|
|
51
|
+
* response is spoken for.
|
|
52
|
+
* - `"unwritable"` — the response was GONE by the time the latency delay
|
|
53
|
+
* finished (client hung up, body already ended, or our own `disconnect`
|
|
54
|
+
* destroyed it). Nothing was written, nothing was journalled, no metric was
|
|
55
|
+
* counted — and there is nothing left to serve, so the caller stops.
|
|
56
|
+
*
|
|
57
|
+
* Both non-`false` members are truthy strings, so every existing call site
|
|
58
|
+
* keeps its meaning while callers that want the reason can read it.
|
|
59
|
+
*/
|
|
60
|
+
type ChaosAsyncOutcome = false | "handled" | "unwritable";
|
|
61
|
+
/**
|
|
62
|
+
* Whether {@link applyChaosAction} actually did the thing. `"skipped"` means
|
|
63
|
+
* the response could not take that particular action (see
|
|
64
|
+
* {@link ResponseUnwritableReason}) and NOTHING happened: no write, no journal
|
|
65
|
+
* entry, no metric. It is not a failure — it is the caller's cue that the
|
|
66
|
+
* request is still its to finish.
|
|
67
|
+
*/
|
|
68
|
+
|
|
9
69
|
/**
|
|
10
70
|
* Evaluate chaos config and return the triggered action, or null if none.
|
|
11
|
-
*
|
|
71
|
+
*
|
|
72
|
+
* THE RATES DO NOT ROLL INDEPENDENTLY. Each is a separate `Math.random()` draw
|
|
73
|
+
* taken in a fixed order — drop → malformed → rateLimit → disconnect — and the
|
|
74
|
+
* first draw that hits returns immediately, so the ones after it are never
|
|
75
|
+
* rolled at all. A rate is therefore the probability that ITS draw hits GIVEN
|
|
76
|
+
* that no earlier rate fired, not the share of requests that end in that fault.
|
|
77
|
+
* Only `dropRate`, first in the chain, is unconditional.
|
|
78
|
+
*
|
|
79
|
+
* Worked example: `{ dropRate: 0.5, rateLimitRate: 0.5 }` yields ~50 % drops
|
|
80
|
+
* and ~25 % rate limits (0.5 x 0.5 — half the requests never reach the second
|
|
81
|
+
* draw), not 50 % of each. To exercise two faults at their nominal frequency,
|
|
82
|
+
* configure them on separate requests rather than in one config.
|
|
83
|
+
*/
|
|
84
|
+
declare function evaluateChaos(fixture: Fixture | null, serverDefaults?: ChaosDefaults, rawHeaders?: http$1.IncomingHttpHeaders, logger?: Logger, url?: string, resolved?: ChaosConfig): ChaosAction | null;
|
|
85
|
+
/**
|
|
86
|
+
* Async chaos entrypoint: awaits the deterministic latency delay (when
|
|
87
|
+
* configured) BEFORE rolling terminal actions. Returns true when a terminal
|
|
88
|
+
* action fired (caller returns early), false to proceed.
|
|
89
|
+
*
|
|
90
|
+
* This is the ONLY entrypoint that honours `latencyMs`, so it is the one every
|
|
91
|
+
* route handler must use: `src/__tests__/chaos-async-call-sites.test.ts` fails
|
|
92
|
+
* the build if any module outside this file rolls chaos without an awaited
|
|
93
|
+
* latency dominating the roll. That guard skips this file wholesale — it
|
|
94
|
+
* defines the primitives — so the `applyChaos(...)` delegation below is neither
|
|
95
|
+
* checked by it nor a control for it. The guard proves it is not vacuous its
|
|
96
|
+
* own way: synthetic fixtures for each evasion shape, a floor on the number of
|
|
97
|
+
* files scanned and chaos imports resolved, and a reconciliation of its name
|
|
98
|
+
* sets against this file's real exports.
|
|
99
|
+
*
|
|
100
|
+
* The delay is tied to the response socket, so a client that disconnects during
|
|
101
|
+
* it cancels the timer. When that happens (or the response went away for any
|
|
102
|
+
* other reason while we waited) the dice are never rolled: returns
|
|
103
|
+
* `"unwritable"` so the caller returns early, with nothing written and nothing
|
|
104
|
+
* journalled — a response that was never sent must not appear in the journal.
|
|
105
|
+
*
|
|
106
|
+
* A response that merely has its HEADERS SENT is NOT gone, and is not
|
|
107
|
+
* short-circuited: the delay is still awaited and the dice are still rolled.
|
|
108
|
+
* `disconnect` needs no status line and is applied; the three body actions
|
|
109
|
+
* are skipped and the call reports `false`, because the caller still has a
|
|
110
|
+
* healthy response to finish and `if (outcome) return;` would abandon it.
|
|
111
|
+
*
|
|
112
|
+
* The return value is a {@link ChaosAsyncOutcome}, not a boolean, so callers
|
|
113
|
+
* can tell "chaos fired" from "the response was already dead" — two outcomes
|
|
114
|
+
* the old `true` conflated. Both are truthy, so the standard call shape
|
|
115
|
+
* `if (await applyChaosAsync(...)) return;` keeps working unchanged; a caller
|
|
116
|
+
* that cares compares against `"handled"` / `"unwritable"`.
|
|
12
117
|
*/
|
|
13
|
-
declare function
|
|
118
|
+
declare function applyChaosAsync(res: http$1.ServerResponse, fixture: Fixture | null, serverDefaults: ChaosDefaults | undefined, rawHeaders: http$1.IncomingHttpHeaders, requestUrl: string | undefined, journal: Journal, context: ChaosJournalContext, source: "fixture" | "proxy" | "internal", registry?: MetricsRegistry, logger?: Logger): Promise<ChaosAsyncOutcome>;
|
|
14
119
|
interface ChaosJournalContext {
|
|
15
120
|
method: string;
|
|
16
121
|
path: string;
|
|
17
122
|
headers: Record<string, string>;
|
|
18
123
|
body: ChatCompletionRequest | null;
|
|
124
|
+
/**
|
|
125
|
+
* Service tag copied onto the chaos-path journal entry, so a faulted request
|
|
126
|
+
* stays selectable by `GET /__aimock/journal?service=<name>` exactly like the
|
|
127
|
+
* handler's own non-chaos entries. Handlers that tag their normal journal
|
|
128
|
+
* writes MUST pass it here too, or their chaos traffic falls out of that
|
|
129
|
+
* filter. Optional because the OpenAI-surface handlers journal untagged.
|
|
130
|
+
*/
|
|
131
|
+
service?: string;
|
|
19
132
|
}
|
|
20
133
|
/**
|
|
21
134
|
* Apply chaos to a request. Returns true if chaos was applied (caller should
|
|
22
135
|
* return early), false if the request should proceed normally.
|
|
23
136
|
*
|
|
137
|
+
* Synchronous, and therefore SKIPS the configured `latencyMs` delay — it cannot
|
|
138
|
+
* await. It IS exported (the async wrapper and this module's tests call it by
|
|
139
|
+
* name), but it is not the public surface: no handler may call it —
|
|
140
|
+
* `src/__tests__/chaos-async-call-sites.test.ts` fails the build if one does —
|
|
141
|
+
* and the barrel deliberately publishes `applyChaosDeprecated` under the name
|
|
142
|
+
* `applyChaos` instead, so library consumers who still call it are told about
|
|
143
|
+
* the latency gap. See both below.
|
|
144
|
+
*
|
|
24
145
|
* `requestUrl` is the RAW `req.url` (query string included) — chaos scoping
|
|
25
146
|
* resolves the testId from it exactly as `getTestId` does, so `?testId=` tagged
|
|
26
147
|
* traffic is not silently unscoped. It is required, not optional, so a new
|
|
@@ -30,7 +151,19 @@ interface ChaosJournalContext {
|
|
|
30
151
|
* the <X> phase" is enforced at the type level. A future handler that grows
|
|
31
152
|
* a proxy path MUST pass `"proxy"` explicitly; the default can't drift silently.
|
|
32
153
|
*/
|
|
33
|
-
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Public, deprecated wrapper around the synchronous {@link applyChaos}. The
|
|
157
|
+
* barrel (`src/index.ts`) exports this under the name `applyChaos`, which has
|
|
158
|
+
* been part of the published API since v1.10.0 — removing it outright would
|
|
159
|
+
* break embedders silently, so it stays and says so instead.
|
|
160
|
+
*
|
|
161
|
+
* @deprecated Use {@link applyChaosAsync}. The synchronous form cannot await,
|
|
162
|
+
* so it silently ignores the configured chaos latency (`latencyMs`,
|
|
163
|
+
* `--chaos-latency`, `x-aimock-chaos-latency`): the fault rates still roll, but
|
|
164
|
+
* no delay is injected. Every fault behaviour is otherwise identical.
|
|
165
|
+
*/
|
|
166
|
+
declare function applyChaosDeprecated(res: http$1.ServerResponse, fixture: Fixture | null, serverDefaults: ChaosDefaults | undefined, rawHeaders: http$1.IncomingHttpHeaders, requestUrl: string | undefined, journal: Journal, context: ChaosJournalContext, source: "fixture" | "proxy" | "internal", registry?: MetricsRegistry, logger?: Logger): boolean;
|
|
34
167
|
/**
|
|
35
168
|
* Apply a specific (already-rolled) chaos action. Exposed so callers that roll
|
|
36
169
|
* the dice themselves can dispatch without re-rolling — important when the
|
|
@@ -39,9 +172,33 @@ declare function applyChaos(res: http$1.ServerResponse, fixture: Fixture | null,
|
|
|
39
172
|
*
|
|
40
173
|
* `source` is required (not optional) so callers can't silently omit it on
|
|
41
174
|
* one branch and journal an ambiguous entry. Pass `"fixture"` when a fixture
|
|
42
|
-
* matched (or would have)
|
|
43
|
-
*
|
|
175
|
+
* matched (or would have), `"proxy"` when the request was headed for the proxy
|
|
176
|
+
* path, and `"internal"` — the value most call sites pass — for aimock's own
|
|
177
|
+
* built-in endpoints, which are neither.
|
|
178
|
+
*
|
|
179
|
+
* A chaos action can be dispatched an arbitrarily long time after the request
|
|
180
|
+
* arrived (up to the 30s latency cap), by which point the client may be gone.
|
|
181
|
+
* Writing to a destroyed socket is silently lost, so counting the action and
|
|
182
|
+
* journalling a status the client never received would be a lie. When the
|
|
183
|
+
* response cannot take THIS action this is a no-op: no metric, no journal
|
|
184
|
+
* entry, no write, and the return value says `"skipped"`. The journal
|
|
185
|
+
* vocabulary has no "aborted" chaos status — the entry is omitted rather than
|
|
186
|
+
* invented.
|
|
187
|
+
*
|
|
188
|
+
* "Cannot take this action" is per-action, not one bit. A GONE response
|
|
189
|
+
* (client hung up, body ended, already chaos-destroyed) forecloses everything.
|
|
190
|
+
* A response whose HEADERS ARE SENT forecloses only the three actions that
|
|
191
|
+
* write a status line of their own — `disconnect` is a bare `res.destroy()`
|
|
192
|
+
* and is applied normally.
|
|
193
|
+
*
|
|
194
|
+
* `logger` is REQUIRED (not optional, not defaulted) because every branch
|
|
195
|
+
* above can decline to act, and a skip nobody can see is the bug this
|
|
196
|
+
* parameter exists to prevent: one call site had quietly omitted it. It is
|
|
197
|
+
* typed `Logger | undefined` rather than `Logger` because the public,
|
|
198
|
+
* deprecated `applyChaos` wrapper's own logger is optional and threads through
|
|
199
|
+
* here — arity is what the compiler enforces, and that is what catches the
|
|
200
|
+
* omission.
|
|
44
201
|
*/
|
|
45
202
|
//#endregion
|
|
46
|
-
export {
|
|
203
|
+
export { ChaosAsyncOutcome, applyChaosAsync, applyChaosDeprecated, evaluateChaos, resolveChaosLatencyMs };
|
|
47
204
|
//# sourceMappingURL=chaos.d.ts.map
|
package/dist/chaos.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chaos.d.ts","names":[],"sources":["../src/chaos.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chaos.d.ts","names":[],"sources":["../src/chaos.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;iBA2cgB,qBAAA,UACL,iCACQ,4BACJ,MAAA,CAAK,8BACT,iCAEE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAkID,iBAAA;;;;;;;;;;;;;;;;;;;;;;;;iBA4FI,aAAA,UACL,iCACQ,4BACJ,MAAA,CAAK,8BACT,iCAEE,cACV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgEmB,eAAA,MACf,MAAA,CAAK,yBACD,gCACO,uCACJ,MAAA,CAAK,8DAER,kBACA,0EAEE,0BACF,SACR,QAAQ;UAkCD,mBAAA;;;WAGC;QACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqFQ,oBAAA,MACT,MAAA,CAAK,yBACD,gCACO,uCACJ,MAAA,CAAK,8DAER,kBACA,0EAEE,0BACF"}
|