{"version":3,"file":"chaos.cjs","names":[],"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 */\n\nimport type * as http from \"node:http\";\nimport type { ChaosAction, ChaosConfig, ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\n/**\n * Resolve chaos config from headers, fixture, and server defaults.\n * Header values override fixture values, which override server defaults.\n */\nfunction resolveChaosConfig(\n  fixture: Fixture | null,\n  serverDefaults?: ChaosConfig,\n  rawHeaders?: http.IncomingHttpHeaders,\n  logger?: Logger,\n): ChaosConfig {\n  const base: ChaosConfig = { ...serverDefaults };\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?: ChaosConfig,\n  rawHeaders?: http.IncomingHttpHeaders,\n  logger?: Logger,\n): ChaosAction | null {\n  const config = resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger);\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;\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 */\nexport function applyChaos(\n  res: http.ServerResponse,\n  fixture: Fixture | null,\n  serverDefaults: ChaosConfig | undefined,\n  rawHeaders: http.IncomingHttpHeaders,\n  journal: Journal,\n  context: ChaosJournalContext,\n  registry?: MetricsRegistry,\n  logger?: Logger,\n): boolean {\n  const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger);\n  if (!action) return false;\n\n  if (registry) {\n    registry.incrementCounter(\"aimock_chaos_triggered_total\", { action });\n  }\n\n  switch (action) {\n    case \"drop\": {\n      journal.add({\n        ...context,\n        response: { status: 500, fixture, chaosAction: \"drop\" },\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 true;\n    }\n    case \"malformed\": {\n      journal.add({\n        ...context,\n        response: { status: 200, fixture, chaosAction: \"malformed\" },\n      });\n      res.writeHead(200, { \"Content-Type\": \"application/json\" });\n      res.end(\"{malformed json: <<<chaos>>>\");\n      return true;\n    }\n    case \"disconnect\": {\n      journal.add({\n        ...context,\n        response: { status: 0, fixture, chaosAction: \"disconnect\" },\n      });\n      res.destroy();\n      return true;\n    }\n    default: {\n      const _exhaustive: never = action;\n      void _exhaustive;\n      return false;\n    }\n  }\n}\n"],"mappings":"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"}