@copilotkit/aimock 1.26.0 → 1.26.1

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.
Files changed (150) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +13 -0
  4. package/README.md +1 -0
  5. package/dist/agui-types.d.cts.map +1 -1
  6. package/dist/bedrock-converse.cjs +2 -0
  7. package/dist/bedrock-converse.cjs.map +1 -1
  8. package/dist/bedrock-converse.d.cts.map +1 -1
  9. package/dist/bedrock-converse.d.ts.map +1 -1
  10. package/dist/bedrock-converse.js +3 -1
  11. package/dist/bedrock-converse.js.map +1 -1
  12. package/dist/bedrock.cjs +2 -0
  13. package/dist/bedrock.cjs.map +1 -1
  14. package/dist/bedrock.d.cts.map +1 -1
  15. package/dist/bedrock.d.ts.map +1 -1
  16. package/dist/bedrock.js +3 -1
  17. package/dist/bedrock.js.map +1 -1
  18. package/dist/cohere.cjs +3 -1
  19. package/dist/cohere.cjs.map +1 -1
  20. package/dist/cohere.d.cts.map +1 -1
  21. package/dist/cohere.d.ts.map +1 -1
  22. package/dist/cohere.js +4 -2
  23. package/dist/cohere.js.map +1 -1
  24. package/dist/elevenlabs-audio.cjs +4 -2
  25. package/dist/elevenlabs-audio.cjs.map +1 -1
  26. package/dist/elevenlabs-audio.d.cts.map +1 -1
  27. package/dist/elevenlabs-audio.d.ts.map +1 -1
  28. package/dist/elevenlabs-audio.js +5 -3
  29. package/dist/elevenlabs-audio.js.map +1 -1
  30. package/dist/embeddings.cjs +2 -1
  31. package/dist/embeddings.cjs.map +1 -1
  32. package/dist/embeddings.d.cts.map +1 -1
  33. package/dist/embeddings.d.ts.map +1 -1
  34. package/dist/embeddings.js +3 -2
  35. package/dist/embeddings.js.map +1 -1
  36. package/dist/fal-audio.cjs +4 -2
  37. package/dist/fal-audio.cjs.map +1 -1
  38. package/dist/fal-audio.d.cts.map +1 -1
  39. package/dist/fal-audio.d.ts.map +1 -1
  40. package/dist/fal-audio.js +5 -3
  41. package/dist/fal-audio.js.map +1 -1
  42. package/dist/fal.cjs +2 -1
  43. package/dist/fal.cjs.map +1 -1
  44. package/dist/fal.d.cts.map +1 -1
  45. package/dist/fal.d.ts.map +1 -1
  46. package/dist/fal.js +3 -2
  47. package/dist/fal.js.map +1 -1
  48. package/dist/fixture-loader.cjs +9 -3
  49. package/dist/fixture-loader.cjs.map +1 -1
  50. package/dist/fixture-loader.d.cts.map +1 -1
  51. package/dist/fixture-loader.d.ts.map +1 -1
  52. package/dist/fixture-loader.js +9 -3
  53. package/dist/fixture-loader.js.map +1 -1
  54. package/dist/gemini-embeddings.cjs +2 -1
  55. package/dist/gemini-embeddings.cjs.map +1 -1
  56. package/dist/gemini-embeddings.js +3 -2
  57. package/dist/gemini-embeddings.js.map +1 -1
  58. package/dist/gemini-interactions.cjs +1 -0
  59. package/dist/gemini-interactions.cjs.map +1 -1
  60. package/dist/gemini-interactions.d.cts.map +1 -1
  61. package/dist/gemini-interactions.d.ts.map +1 -1
  62. package/dist/gemini-interactions.js +2 -1
  63. package/dist/gemini-interactions.js.map +1 -1
  64. package/dist/gemini.cjs +1 -0
  65. package/dist/gemini.cjs.map +1 -1
  66. package/dist/gemini.d.cts.map +1 -1
  67. package/dist/gemini.d.ts.map +1 -1
  68. package/dist/gemini.js +2 -1
  69. package/dist/gemini.js.map +1 -1
  70. package/dist/helpers.cjs +7 -0
  71. package/dist/helpers.cjs.map +1 -1
  72. package/dist/helpers.d.cts.map +1 -1
  73. package/dist/helpers.d.ts.map +1 -1
  74. package/dist/helpers.js +7 -1
  75. package/dist/helpers.js.map +1 -1
  76. package/dist/images.cjs +6 -5
  77. package/dist/images.cjs.map +1 -1
  78. package/dist/images.d.cts.map +1 -1
  79. package/dist/images.d.ts.map +1 -1
  80. package/dist/images.js +7 -6
  81. package/dist/images.js.map +1 -1
  82. package/dist/messages.cjs +1 -0
  83. package/dist/messages.cjs.map +1 -1
  84. package/dist/messages.d.cts.map +1 -1
  85. package/dist/messages.d.ts.map +1 -1
  86. package/dist/messages.js +2 -1
  87. package/dist/messages.js.map +1 -1
  88. package/dist/ollama.cjs +4 -1
  89. package/dist/ollama.cjs.map +1 -1
  90. package/dist/ollama.d.cts.map +1 -1
  91. package/dist/ollama.d.ts.map +1 -1
  92. package/dist/ollama.js +5 -2
  93. package/dist/ollama.js.map +1 -1
  94. package/dist/recorder.cjs +4 -3
  95. package/dist/recorder.cjs.map +1 -1
  96. package/dist/recorder.js +4 -3
  97. package/dist/recorder.js.map +1 -1
  98. package/dist/responses.cjs +1 -0
  99. package/dist/responses.cjs.map +1 -1
  100. package/dist/responses.d.cts.map +1 -1
  101. package/dist/responses.d.ts.map +1 -1
  102. package/dist/responses.js +2 -1
  103. package/dist/responses.js.map +1 -1
  104. package/dist/router.cjs +3 -0
  105. package/dist/router.cjs.map +1 -1
  106. package/dist/router.js +3 -0
  107. package/dist/router.js.map +1 -1
  108. package/dist/server.cjs +1 -0
  109. package/dist/server.cjs.map +1 -1
  110. package/dist/server.d.cts.map +1 -1
  111. package/dist/server.d.ts.map +1 -1
  112. package/dist/server.js +2 -1
  113. package/dist/server.js.map +1 -1
  114. package/dist/speech.cjs +2 -1
  115. package/dist/speech.cjs.map +1 -1
  116. package/dist/speech.d.cts.map +1 -1
  117. package/dist/speech.d.ts.map +1 -1
  118. package/dist/speech.js +3 -2
  119. package/dist/speech.js.map +1 -1
  120. package/dist/transcription.cjs +2 -1
  121. package/dist/transcription.cjs.map +1 -1
  122. package/dist/transcription.d.cts.map +1 -1
  123. package/dist/transcription.d.ts.map +1 -1
  124. package/dist/transcription.js +3 -2
  125. package/dist/transcription.js.map +1 -1
  126. package/dist/types.d.cts +4 -0
  127. package/dist/types.d.cts.map +1 -1
  128. package/dist/types.d.ts +4 -0
  129. package/dist/types.d.ts.map +1 -1
  130. package/dist/vector-types.d.cts.map +1 -1
  131. package/dist/vector-types.d.ts.map +1 -1
  132. package/dist/video.cjs +2 -1
  133. package/dist/video.cjs.map +1 -1
  134. package/dist/video.d.cts.map +1 -1
  135. package/dist/video.d.ts.map +1 -1
  136. package/dist/video.js +3 -2
  137. package/dist/video.js.map +1 -1
  138. package/dist/ws-gemini-live.cjs +4 -1
  139. package/dist/ws-gemini-live.cjs.map +1 -1
  140. package/dist/ws-gemini-live.js +4 -1
  141. package/dist/ws-gemini-live.js.map +1 -1
  142. package/dist/ws-realtime.cjs +4 -1
  143. package/dist/ws-realtime.cjs.map +1 -1
  144. package/dist/ws-realtime.js +4 -1
  145. package/dist/ws-realtime.js.map +1 -1
  146. package/dist/ws-responses.cjs +2 -0
  147. package/dist/ws-responses.cjs.map +1 -1
  148. package/dist/ws-responses.js +2 -0
  149. package/dist/ws-responses.js.map +1 -1
  150. package/package.json +1 -1
package/dist/fal.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fal.js","names":[],"sources":["../src/fal.ts"],"sourcesContent":["import type http from \"node:http\";\nimport crypto from \"node:crypto\";\nimport type {\n ChatCompletionRequest,\n FalQueueConfig,\n Fixture,\n HandlerDefaults,\n ImageItem,\n ImageResponse,\n RawJSONResponse,\n VideoResponse,\n} from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n serializeErrorResponse,\n isJSONResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { matchFixture } from \"./router.js\";\nimport { buildFixtureMatch, persistFixture, proxyAndRecord } from \"./recorder.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\nimport type { Journal } from \"./journal.js\";\nimport { audioToFalFile } from \"./fal-audio.js\";\n\n// ─── FalQueueState (TTL + bounded) ───────────────────────────────────────\n\nconst FAL_QUEUE_MAX_ENTRIES = 10_000;\nconst FAL_QUEUE_TTL_MS = 3_600_000; // 1 hour\n\ntype FalQueueStatus = \"IN_QUEUE\" | \"IN_PROGRESS\" | \"COMPLETED\" | \"CANCELLED\";\n\ninterface FalQueueJob {\n requestId: string;\n modelId: string;\n status: FalQueueStatus;\n result: unknown;\n /** Number of `/status` (or `/{id}`) polls the caller has made against this job. */\n pollCount: number;\n /** Poll-count threshold for `IN_QUEUE → IN_PROGRESS` transition. */\n pollsBeforeInProgress: number;\n /** Poll-count threshold for `IN_PROGRESS → COMPLETED` transition. */\n pollsBeforeCompleted: number;\n submittedAt: number;\n completedAt: number | null;\n /** State-transition log entries surfaced in the `/status` response. */\n logs: Array<{ timestamp: string; level: string; message: string }>;\n createdAt: number;\n}\n\ninterface FalQueueEntry {\n job: FalQueueJob;\n createdAt: number;\n}\n\n/**\n * Per-testId queue state for the general fal handler. Mirrors FalJobMap from\n * fal-audio.ts but stores arbitrary JSON payloads instead of audio file\n * objects, so it can serve any fal model (image, video, motion, music, etc.).\n */\nexport class FalQueueStateMap {\n private readonly entries = new Map<string, FalQueueEntry>();\n\n get(key: string): FalQueueJob | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > FAL_QUEUE_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.job;\n }\n\n set(key: string, job: FalQueueJob): void {\n this.entries.set(key, { job, createdAt: Date.now() });\n if (this.entries.size > FAL_QUEUE_MAX_ENTRIES) {\n const excess = this.entries.size - FAL_QUEUE_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\nexport const falQueueStates = new FalQueueStateMap();\n\n// ─── Typed-response → fal envelope converters ───────────────────────────\n\nfunction extractExtension(url: string, fallback: string): { fileName: string; ext: string } {\n const segment = url.split(\"?\")[0].split(\"#\")[0].split(\"/\").pop() ?? \"\";\n const fileName = segment.length > 0 ? segment : \"\";\n const dotIdx = fileName.lastIndexOf(\".\");\n const ext = dotIdx >= 0 ? fileName.slice(dotIdx + 1).toLowerCase() : fallback;\n return { fileName, ext };\n}\n\nfunction imageItemToFalImage(item: ImageItem, index: number): Record<string, unknown> {\n const url = item.url ?? `https://mock.fal.media/files/generated_image_${index}.png`;\n const { ext } = extractExtension(url, \"png\");\n const contentType = ext === \"jpg\" || ext === \"jpeg\" ? \"image/jpeg\" : `image/${ext}`;\n return {\n url,\n width: 1024,\n height: 1024,\n content_type: contentType,\n };\n}\n\n/**\n * Translate an `ImageResponse` fixture into fal's image envelope shape:\n * `{ images: [...], timings, seed, has_nsfw_concepts, prompt }`.\n * Used by `LLMock.onFalImage` to keep callers from re-deriving the wire shape.\n */\nexport function imageResponseToFalJson(response: ImageResponse): Record<string, unknown> {\n const items = response.images ?? (response.image ? [response.image] : []);\n const images = items.map((item, i) => imageItemToFalImage(item, i));\n return {\n images,\n timings: { inference: 0 },\n seed: 0,\n has_nsfw_concepts: images.map(() => false),\n prompt: \"\",\n };\n}\n\n/**\n * Translate a `VideoResponse` fixture into fal's video envelope shape:\n * `{ video: { url, content_type, file_name, file_size }, seed }`.\n */\nexport function videoResponseToFalJson(response: VideoResponse): Record<string, unknown> {\n const url = response.video.url ?? \"https://mock.fal.media/files/generated_video.mp4\";\n const { fileName, ext } = extractExtension(url, \"mp4\");\n return {\n video: {\n url,\n content_type: `video/${ext}`,\n file_name: fileName || \"generated_video.mp4\",\n file_size: 0,\n },\n seed: 0,\n };\n}\n\n// ─── Queue progression ─────────────────────────────────────────────────\n\nfunction resolveProgression(config: FalQueueConfig | undefined): {\n pollsBeforeInProgress: number;\n pollsBeforeCompleted: number;\n} {\n const pollsBeforeInProgress = config?.pollsBeforeInProgress ?? 0;\n const explicitCompleted = config?.pollsBeforeCompleted;\n // When only pollsBeforeInProgress is set, default pollsBeforeCompleted to one\n // poll later so the job actually passes through IN_PROGRESS. When the caller\n // sets both explicitly, clamp completed >= inProgress so a misconfigured\n // pair (e.g. completed < inProgress) can't silently skip the IN_PROGRESS\n // transition. When neither is set, both stay 0 (completes on submit).\n let pollsBeforeCompleted: number;\n if (explicitCompleted != null) {\n pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);\n } else if (config?.pollsBeforeInProgress != null) {\n pollsBeforeCompleted = pollsBeforeInProgress + 1;\n } else {\n pollsBeforeCompleted = 0;\n }\n return { pollsBeforeInProgress, pollsBeforeCompleted };\n}\n\n/**\n * Mutates a job in place to advance its state on a status/result poll.\n * IN_QUEUE → IN_PROGRESS → COMPLETED based on poll-count thresholds. No-op\n * once COMPLETED or CANCELLED.\n */\nfunction advanceJob(job: FalQueueJob): void {\n if (job.status === \"COMPLETED\" || job.status === \"CANCELLED\") return;\n\n job.pollCount += 1;\n // Check IN_PROGRESS before COMPLETED so a job whose thresholds are equal\n // still spends one poll in IN_PROGRESS instead of jumping straight to\n // COMPLETED.\n if (job.status === \"IN_QUEUE\" && job.pollCount >= job.pollsBeforeInProgress) {\n job.status = \"IN_PROGRESS\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job started processing.\",\n });\n } else if (job.pollCount >= job.pollsBeforeCompleted) {\n job.status = \"COMPLETED\";\n job.completedAt = Date.now();\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job completed.\",\n });\n }\n}\n\nfunction queuePosition(job: FalQueueJob): number {\n if (job.status !== \"IN_QUEUE\") return 0;\n return Math.max(0, job.pollsBeforeInProgress - job.pollCount);\n}\n\nfunction statusResponseBody(job: FalQueueJob): Record<string, unknown> {\n const body: Record<string, unknown> = {\n status: job.status,\n request_id: job.requestId,\n response_url: `https://${FAL_HOSTS.queue}/${job.modelId}/requests/${job.requestId}`,\n logs: job.logs,\n };\n if (job.status === \"IN_QUEUE\" || job.status === \"IN_PROGRESS\") {\n body.queue_position = queuePosition(job);\n }\n if (job.status === \"COMPLETED\" && job.completedAt != null) {\n body.metrics = {\n inference_time: (job.completedAt - job.submittedAt) / 1000,\n };\n }\n return body;\n}\n\n// ─── Hosts and routing ──────────────────────────────────────────────────\n\nconst FAL_HOSTS = {\n queue: \"queue.fal.run\",\n sync: \"fal.run\",\n storage: \"rest.fal.ai\",\n storageAlpha: \"rest.alpha.fal.ai\",\n gateway: \"gateway.fal.ai\",\n} as const;\n\nconst QUEUE_REQUESTS_RE = /^(.+)\\/requests\\/([^/]+)(\\/status|\\/cancel)?$/;\nconst STORAGE_INITIATE_PATH = \"/storage/upload/initiate\";\n\nfunction stripFalPrefix(pathname: string): string {\n const stripped = pathname.replace(/^\\/fal/, \"\");\n return stripped.length > 0 ? stripped : \"/\";\n}\n\nfunction extractPromptFromBody(body: unknown): string {\n if (!body || typeof body !== \"object\") return \"\";\n const obj = body as Record<string, unknown>;\n if (typeof obj.prompt === \"string\") return obj.prompt;\n if (typeof obj.text === \"string\") return obj.text;\n const input = obj.input;\n if (input && typeof input === \"object\") {\n const inputObj = input as Record<string, unknown>;\n if (typeof inputObj.prompt === \"string\") return inputObj.prompt;\n if (typeof inputObj.text === \"string\") return inputObj.text;\n }\n return \"\";\n}\n\ninterface ParsedFalPath {\n modelId: string;\n requestId?: string;\n action?: \"status\" | \"cancel\" | \"result\";\n}\n\nfunction parseFalPath(stripped: string): ParsedFalPath | null {\n if (!stripped.startsWith(\"/\")) return null;\n const trimmed = stripped.replace(/^\\/+/, \"\");\n if (!trimmed) return null;\n\n const m = QUEUE_REQUESTS_RE.exec(`/${trimmed}`);\n if (m) {\n const modelId = m[1].replace(/^\\/+/, \"\");\n const action = m[3] === \"/status\" ? \"status\" : m[3] === \"/cancel\" ? \"cancel\" : \"result\";\n return { modelId, requestId: m[2], action };\n }\n return { modelId: trimmed };\n}\n\nexport type HandleFalOutcome = \"handled\" | \"passthrough\";\n\ninterface FalRouteInfo {\n kind: \"queue-submit\" | \"queue-status\" | \"queue-result\" | \"queue-cancel\" | \"sync-run\" | \"storage\";\n modelId?: string;\n requestId?: string;\n targetHost: string;\n}\n\nfunction classifyRoute(\n req: http.IncomingMessage,\n pathname: string,\n targetHost: string,\n): FalRouteInfo | null {\n const stripped = stripFalPrefix(pathname);\n\n if (targetHost === FAL_HOSTS.storage || targetHost === FAL_HOSTS.storageAlpha) {\n if (req.method === \"POST\" && stripped === STORAGE_INITIATE_PATH) {\n return { kind: \"storage\", targetHost };\n }\n return null;\n }\n\n const parsed = parseFalPath(stripped);\n if (!parsed) return null;\n\n if (targetHost === FAL_HOSTS.queue) {\n if (parsed.requestId) {\n if (parsed.action === \"status\" && req.method === \"GET\") {\n return {\n kind: \"queue-status\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"cancel\" && req.method === \"PUT\") {\n return {\n kind: \"queue-cancel\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"result\" && req.method === \"GET\") {\n return {\n kind: \"queue-result\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n return null;\n }\n if (req.method === \"POST\") {\n return { kind: \"queue-submit\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n if (targetHost === FAL_HOSTS.sync) {\n if (req.method === \"POST\" && parsed.modelId) {\n return { kind: \"sync-run\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n return null;\n}\n\n/**\n * General fal.ai handler. Routes by `x-fal-target-host` header (the convention\n * used by `@fal-ai/client`'s server-side requestMiddleware workaround for the\n * fact that `proxyUrl` is browser-only).\n *\n * Returns `\"passthrough\"` when the request does not look like a host-mirrored\n * fal call, so the caller can fall back to the legacy `/fal/queue/...` and\n * `/fal/run/...` audio routes.\n */\nexport async function handleFal(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n journal: Journal,\n): Promise<HandleFalOutcome> {\n const targetHostHeader = req.headers[\"x-fal-target-host\"];\n const targetHost = Array.isArray(targetHostHeader) ? targetHostHeader[0] : targetHostHeader;\n if (!targetHost) return \"passthrough\";\n\n const route = classifyRoute(req, pathname, targetHost);\n if (!route) return \"passthrough\";\n\n const testId = getTestId(req);\n const stateKey = (id: string) => `${testId}:${id}`;\n\n switch (route.kind) {\n case \"queue-status\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n advanceJob(job);\n writeJson(req, res, 200, statusResponseBody(job), pathname, journal);\n return \"handled\";\n }\n\n case \"queue-result\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n // Callers may fetch result without first polling status — advance so\n // tests that skip the status check still reach completion.\n advanceJob(job);\n if (job.status !== \"COMPLETED\") {\n writeJson(req, res, 202, statusResponseBody(job), pathname, journal);\n return \"handled\";\n }\n writeJson(req, res, 200, job.result, pathname, journal);\n return \"handled\";\n }\n\n case \"queue-cancel\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"NOT_FOUND\" }));\n return \"handled\";\n }\n if (job.status === \"COMPLETED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ALREADY_COMPLETED\" }));\n return \"handled\";\n }\n if (job.status === \"CANCELLED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n job.status = \"CANCELLED\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job cancelled.\",\n });\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n\n case \"storage\": {\n let filename = \"upload.bin\";\n try {\n const parsed = body ? (JSON.parse(body) as Record<string, unknown>) : {};\n if (typeof parsed.filename === \"string\") filename = parsed.filename;\n if (typeof parsed.file_name === \"string\") filename = parsed.file_name;\n } catch {\n // ignore — stub doesn't require a structured body\n }\n const fileId = crypto.randomUUID();\n const responseBody = {\n upload_url: `https://${route.targetHost}/storage/upload/${fileId}`,\n file_url: `https://${route.targetHost}/files/${fileId}/${filename}`,\n };\n writeJson(req, res, 200, responseBody, pathname, journal);\n return \"handled\";\n }\n\n case \"queue-submit\":\n case \"sync-run\": {\n const modelId = route.modelId!;\n let parsedBody: Record<string, unknown> | null;\n try {\n parsedBody = parseBody(body);\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"Invalid JSON body\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: detail,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return \"handled\";\n }\n const prompt = extractPromptFromBody(parsedBody);\n const syntheticReq: ChatCompletionRequest = {\n model: modelId,\n messages: [{ role: \"user\", content: prompt || JSON.stringify(parsedBody ?? {}) }],\n _endpointType: \"fal\",\n };\n\n const matchCounts = journal.getFixtureMatchCountsForTest(testId);\n const fixture = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n if (defaults.record) {\n const effectiveDefaults = withFalUpstream(defaults, route.targetHost);\n // queue-submit must walk the queue upstream (submit → poll status →\n // get result) before persisting, so the fixture stores the FINAL job\n // body, not the IN_QUEUE envelope. sync-run is already a single\n // request/response cycle and the generic recorder handles it.\n if (route.kind === \"queue-submit\") {\n const outcome = await proxyAndRecordFalQueueSubmit({\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath: stripFalPrefix(pathname),\n body,\n fixtures,\n defaults: effectiveDefaults,\n stateKey,\n journal,\n });\n if (outcome === \"handled\") return \"handled\";\n // outcome === \"no_upstream\" — fall through to strict/404\n } else {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"fal\",\n stripFalPrefix(pathname),\n fixtures,\n effectiveDefaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return \"handled\";\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return \"handled\";\n }\n }\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return \"handled\";\n }\n\n let payload: unknown;\n if (isJSONResponse(response)) {\n payload = (response as RawJSONResponse).json;\n } else if (isAudioResponse(response)) {\n payload = audioToFalFile(response);\n } else {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Fixture response is not JSON or audio for fal endpoint\",\n type: \"server_error\",\n },\n }),\n );\n return \"handled\";\n }\n\n if (route.kind === \"sync-run\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n return \"handled\";\n }\n\n const requestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId,\n modelId,\n status: initialStatus,\n result: payload,\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [\n {\n timestamp: new Date(now).toISOString(),\n level: \"INFO\",\n message: \"Job enqueued.\",\n },\n ],\n createdAt: now,\n };\n falQueueStates.set(stateKey(requestId), job);\n const envelope = {\n request_id: requestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/cancel`,\n queue_position: queuePosition(job),\n };\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n }\n }\n}\n\nfunction parseBody(raw: string): Record<string, unknown> | null {\n if (!raw.trim()) return null;\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"unknown\";\n throw new Error(`Malformed JSON: ${detail}`);\n }\n}\n\n// ─── Queue-walk recording ──────────────────────────────────────────────\n//\n// The fal queue protocol surfaces three endpoints — submit (POST), status\n// (GET, polled), and result (GET) — but at the fixture layer we only store ONE\n// thing: the FINAL job body. A naive `proxyAndRecord` against submit would\n// persist the IN_QUEUE envelope, which is useless to replay (the SDK polls\n// status and then reads the result body, expecting `{ images: [...] }`-shaped\n// model output, not an envelope). So during recording we walk the upstream\n// queue ourselves, capture the result body, and write THAT as the fixture —\n// then synthesise the local envelope the same way the replay path does.\n\nconst DEFAULT_FAL_POLL_INTERVAL_MS = 1000;\n// Video generations (kling, veo, runway, etc.) routinely take 5–10 minutes\n// on the upstream queue; 15 min gives headroom without trapping a genuinely\n// hung job indefinitely.\nconst DEFAULT_FAL_TIMEOUT_MS = 900_000;\n\n// Hop-by-hop and client-set headers excluded from upstream forwarding.\n// Mirrors STRIP_HEADERS in recorder.ts.\nconst FAL_STRIP_FORWARD_HEADERS = new Set([\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authorization\",\n \"proxy-authenticate\",\n \"host\",\n \"content-length\",\n \"cookie\",\n \"accept-encoding\",\n]);\n\nexport function buildFalForwardHeaders(req: http.IncomingMessage): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [name, val] of Object.entries(req.headers)) {\n if (val === undefined) continue;\n if (FAL_STRIP_FORWARD_HEADERS.has(name.toLowerCase())) continue;\n out[name] = Array.isArray(val) ? val.join(\", \") : val;\n }\n return out;\n}\n\n/**\n * Walk a fal-shaped queue protocol upstream: POST submit, poll status until\n * COMPLETED, GET final result body. Returns the parsed final body so the caller\n * can persist it as the fixture and seed local queue state.\n *\n * Decoupled from the route layer so the legacy `/fal/queue/submit/{model}`\n * audio path (`fal-audio.ts`) can reuse the same logic.\n */\nexport async function walkFalQueue(args: {\n upstreamBase: string;\n submitPath: string;\n body: string;\n headers: Record<string, string>;\n pollIntervalMs?: number;\n timeoutMs?: number;\n /**\n * Build the status-poll URL from `request_id` when upstream's submit\n * response doesn't return a usable `status_url`. The legacy path uses\n * aimock-internal `/fal/queue/requests/<id>/status` rather than fal.ai's\n * `/<model>/requests/<id>/status` layout.\n */\n fallbackStatusPath: (requestId: string) => string;\n fallbackResultPath: (requestId: string) => string;\n}): Promise<unknown> {\n const {\n upstreamBase,\n submitPath,\n body,\n headers,\n pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS,\n timeoutMs = DEFAULT_FAL_TIMEOUT_MS,\n fallbackStatusPath,\n fallbackResultPath,\n } = args;\n\n const deadline = Date.now() + timeoutMs;\n\n // ── 1. POST submit ────────────────────────────────────────────────\n const submitUrl = resolveUpstreamUrl(upstreamBase, submitPath);\n const submitRes = await fetch(submitUrl, { method: \"POST\", headers, body });\n const submitText = await submitRes.text();\n if (!submitRes.ok) {\n throw new Error(`Submit ${submitRes.status}: ${submitText.slice(0, 200)}`);\n }\n const submitJson = parseJsonOrThrow(submitText, \"Submit\");\n const env = submitJson as Record<string, unknown>;\n const upstreamRequestId = String(env.request_id ?? \"\").trim();\n if (!upstreamRequestId) {\n throw new Error(\"Submit response missing request_id\");\n }\n\n // Prefer the URLs upstream returned — a proxy in front of fal.ai may sit on\n // a different host than the canonical `queue.fal.run` — and only fall back\n // to constructed paths if the envelope omits them.\n const envStatusUrl = env.status_url;\n const envResponseUrl = env.response_url;\n const statusUrl =\n typeof envStatusUrl === \"string\" && envStatusUrl\n ? new URL(envStatusUrl)\n : resolveUpstreamUrl(upstreamBase, fallbackStatusPath(upstreamRequestId));\n const resultUrl =\n typeof envResponseUrl === \"string\" && envResponseUrl\n ? new URL(envResponseUrl)\n : resolveUpstreamUrl(upstreamBase, fallbackResultPath(upstreamRequestId));\n\n // ── 2. Poll status until COMPLETED ───────────────────────────────\n while (true) {\n if (Date.now() > deadline) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n const statusRes = await fetch(statusUrl, { headers });\n const statusText = await statusRes.text();\n if (!statusRes.ok) {\n throw new Error(`Status ${statusRes.status}: ${statusText.slice(0, 200)}`);\n }\n const statusJson = parseJsonOrThrow(statusText, \"Status\") as Record<string, unknown>;\n const s = String(statusJson.status ?? \"\");\n if (s === \"COMPLETED\") break;\n if (s === \"FAILED\" || s === \"ERROR\" || s === \"CANCELLED\") {\n throw new Error(`Upstream job terminated with status ${s}`);\n }\n const remaining = deadline - Date.now();\n const sleep = Math.min(pollIntervalMs, Math.max(0, remaining));\n if (sleep <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n await new Promise<void>((r) => setTimeout(r, sleep));\n }\n\n // ── 3. GET final result ──────────────────────────────────────────\n const resultRes = await fetch(resultUrl, { headers });\n const resultText = await resultRes.text();\n if (!resultRes.ok) {\n throw new Error(`Result ${resultRes.status}: ${resultText.slice(0, 200)}`);\n }\n return parseJsonOrThrow(resultText, \"Result\");\n}\n\nasync function proxyAndRecordFalQueueSubmit(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n syntheticReq: ChatCompletionRequest;\n modelId: string;\n pathname: string;\n strippedPath: string;\n body: string;\n fixtures: Fixture[];\n defaults: HandlerDefaults;\n stateKey: (id: string) => string;\n journal: Journal;\n}): Promise<\"handled\" | \"no_upstream\"> {\n const {\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath,\n body,\n fixtures,\n defaults,\n stateKey,\n journal,\n } = args;\n\n const record = defaults.record;\n if (!record) return \"no_upstream\";\n const upstreamBase = record.providers.fal;\n if (!upstreamBase) {\n defaults.logger.warn(`No upstream URL configured for provider \"fal\" — cannot proxy`);\n return \"no_upstream\";\n }\n\n defaults.logger.warn(`NO FIXTURE MATCH — walking fal queue at ${upstreamBase}${strippedPath}`);\n\n let finalBody: unknown;\n try {\n finalBody = await walkFalQueue({\n upstreamBase,\n submitPath: strippedPath,\n body,\n headers: buildFalForwardHeaders(req),\n pollIntervalMs: record.fal?.pollIntervalMs,\n timeoutMs: record.fal?.timeoutMs,\n fallbackStatusPath: (id) => `${modelId}/requests/${id}/status`,\n fallbackResultPath: (id) => `${modelId}/requests/${id}`,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown queue-walk error\";\n defaults.logger.error(`fal queue-walk proxy failed: ${msg}`);\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 502, fixture: null, source: \"proxy\" },\n });\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n return \"handled\";\n }\n\n // ── 4. Persist fixture using the FINAL body, not the submit envelope ──\n const matchRequest = defaults.requestTransform\n ? defaults.requestTransform(syntheticReq)\n : syntheticReq;\n const fixture: Fixture = {\n match: buildFixtureMatch(matchRequest, record),\n response: { json: finalBody, status: 200 },\n };\n persistFixture({\n record,\n providerKey: \"fal\",\n testId: getTestId(req),\n fixture,\n fixtures,\n logger: defaults.logger,\n });\n\n // ── 5. Synthesise envelope + seed state (same shape as the replay path) ──\n const newRequestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId: newRequestId,\n modelId,\n status: initialStatus,\n result: finalBody,\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [{ timestamp: new Date(now).toISOString(), level: \"INFO\", message: \"Job enqueued.\" }],\n createdAt: now,\n };\n falQueueStates.set(stateKey(newRequestId), job);\n const envelope = {\n request_id: newRequestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/cancel`,\n queue_position: queuePosition(job),\n };\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null, source: \"proxy\" },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n}\n\nfunction parseJsonOrThrow(text: string, label: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n throw new Error(`${label} returned non-JSON: ${text.slice(0, 200)}`);\n }\n}\n\nfunction withFalUpstream(defaults: HandlerDefaults, targetHost: string): HandlerDefaults {\n if (!defaults.record) return defaults;\n // Respect an explicit record.providers.fal — tests and dev configs need to\n // point at a stub upstream. Only synthesise from the header when the user\n // didn't configure one (the \"or omit upstream URL — it's in the request\n // hostname\" mode from the issue).\n if (defaults.record.providers.fal) return defaults;\n return {\n ...defaults,\n record: {\n ...defaults.record,\n providers: {\n ...defaults.record.providers,\n fal: `https://${targetHost}`,\n },\n },\n };\n}\n\nfunction writeJson(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n pathname: string,\n journal: Journal,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction respondNotFound(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n journal: Journal,\n requestId: string,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Request ${requestId} not found`, type: \"not_found\" },\n }),\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fal.js","names":[],"sources":["../src/fal.ts"],"sourcesContent":["import type http from \"node:http\";\nimport crypto from \"node:crypto\";\nimport type {\n ChatCompletionRequest,\n FalQueueConfig,\n Fixture,\n HandlerDefaults,\n ImageItem,\n ImageResponse,\n RawJSONResponse,\n VideoResponse,\n} from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n serializeErrorResponse,\n isJSONResponse,\n flattenHeaders,\n getContext,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { matchFixture } from \"./router.js\";\nimport { buildFixtureMatch, persistFixture, proxyAndRecord } from \"./recorder.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\nimport type { Journal } from \"./journal.js\";\nimport { audioToFalFile } from \"./fal-audio.js\";\n\n// ─── FalQueueState (TTL + bounded) ───────────────────────────────────────\n\nconst FAL_QUEUE_MAX_ENTRIES = 10_000;\nconst FAL_QUEUE_TTL_MS = 3_600_000; // 1 hour\n\ntype FalQueueStatus = \"IN_QUEUE\" | \"IN_PROGRESS\" | \"COMPLETED\" | \"CANCELLED\";\n\ninterface FalQueueJob {\n requestId: string;\n modelId: string;\n status: FalQueueStatus;\n result: unknown;\n /** Number of `/status` (or `/{id}`) polls the caller has made against this job. */\n pollCount: number;\n /** Poll-count threshold for `IN_QUEUE → IN_PROGRESS` transition. */\n pollsBeforeInProgress: number;\n /** Poll-count threshold for `IN_PROGRESS → COMPLETED` transition. */\n pollsBeforeCompleted: number;\n submittedAt: number;\n completedAt: number | null;\n /** State-transition log entries surfaced in the `/status` response. */\n logs: Array<{ timestamp: string; level: string; message: string }>;\n createdAt: number;\n}\n\ninterface FalQueueEntry {\n job: FalQueueJob;\n createdAt: number;\n}\n\n/**\n * Per-testId queue state for the general fal handler. Mirrors FalJobMap from\n * fal-audio.ts but stores arbitrary JSON payloads instead of audio file\n * objects, so it can serve any fal model (image, video, motion, music, etc.).\n */\nexport class FalQueueStateMap {\n private readonly entries = new Map<string, FalQueueEntry>();\n\n get(key: string): FalQueueJob | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > FAL_QUEUE_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.job;\n }\n\n set(key: string, job: FalQueueJob): void {\n this.entries.set(key, { job, createdAt: Date.now() });\n if (this.entries.size > FAL_QUEUE_MAX_ENTRIES) {\n const excess = this.entries.size - FAL_QUEUE_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\nexport const falQueueStates = new FalQueueStateMap();\n\n// ─── Typed-response → fal envelope converters ───────────────────────────\n\nfunction extractExtension(url: string, fallback: string): { fileName: string; ext: string } {\n const segment = url.split(\"?\")[0].split(\"#\")[0].split(\"/\").pop() ?? \"\";\n const fileName = segment.length > 0 ? segment : \"\";\n const dotIdx = fileName.lastIndexOf(\".\");\n const ext = dotIdx >= 0 ? fileName.slice(dotIdx + 1).toLowerCase() : fallback;\n return { fileName, ext };\n}\n\nfunction imageItemToFalImage(item: ImageItem, index: number): Record<string, unknown> {\n const url = item.url ?? `https://mock.fal.media/files/generated_image_${index}.png`;\n const { ext } = extractExtension(url, \"png\");\n const contentType = ext === \"jpg\" || ext === \"jpeg\" ? \"image/jpeg\" : `image/${ext}`;\n return {\n url,\n width: 1024,\n height: 1024,\n content_type: contentType,\n };\n}\n\n/**\n * Translate an `ImageResponse` fixture into fal's image envelope shape:\n * `{ images: [...], timings, seed, has_nsfw_concepts, prompt }`.\n * Used by `LLMock.onFalImage` to keep callers from re-deriving the wire shape.\n */\nexport function imageResponseToFalJson(response: ImageResponse): Record<string, unknown> {\n const items = response.images ?? (response.image ? [response.image] : []);\n const images = items.map((item, i) => imageItemToFalImage(item, i));\n return {\n images,\n timings: { inference: 0 },\n seed: 0,\n has_nsfw_concepts: images.map(() => false),\n prompt: \"\",\n };\n}\n\n/**\n * Translate a `VideoResponse` fixture into fal's video envelope shape:\n * `{ video: { url, content_type, file_name, file_size }, seed }`.\n */\nexport function videoResponseToFalJson(response: VideoResponse): Record<string, unknown> {\n const url = response.video.url ?? \"https://mock.fal.media/files/generated_video.mp4\";\n const { fileName, ext } = extractExtension(url, \"mp4\");\n return {\n video: {\n url,\n content_type: `video/${ext}`,\n file_name: fileName || \"generated_video.mp4\",\n file_size: 0,\n },\n seed: 0,\n };\n}\n\n// ─── Queue progression ─────────────────────────────────────────────────\n\nfunction resolveProgression(config: FalQueueConfig | undefined): {\n pollsBeforeInProgress: number;\n pollsBeforeCompleted: number;\n} {\n const pollsBeforeInProgress = config?.pollsBeforeInProgress ?? 0;\n const explicitCompleted = config?.pollsBeforeCompleted;\n // When only pollsBeforeInProgress is set, default pollsBeforeCompleted to one\n // poll later so the job actually passes through IN_PROGRESS. When the caller\n // sets both explicitly, clamp completed >= inProgress so a misconfigured\n // pair (e.g. completed < inProgress) can't silently skip the IN_PROGRESS\n // transition. When neither is set, both stay 0 (completes on submit).\n let pollsBeforeCompleted: number;\n if (explicitCompleted != null) {\n pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);\n } else if (config?.pollsBeforeInProgress != null) {\n pollsBeforeCompleted = pollsBeforeInProgress + 1;\n } else {\n pollsBeforeCompleted = 0;\n }\n return { pollsBeforeInProgress, pollsBeforeCompleted };\n}\n\n/**\n * Mutates a job in place to advance its state on a status/result poll.\n * IN_QUEUE → IN_PROGRESS → COMPLETED based on poll-count thresholds. No-op\n * once COMPLETED or CANCELLED.\n */\nfunction advanceJob(job: FalQueueJob): void {\n if (job.status === \"COMPLETED\" || job.status === \"CANCELLED\") return;\n\n job.pollCount += 1;\n // Check IN_PROGRESS before COMPLETED so a job whose thresholds are equal\n // still spends one poll in IN_PROGRESS instead of jumping straight to\n // COMPLETED.\n if (job.status === \"IN_QUEUE\" && job.pollCount >= job.pollsBeforeInProgress) {\n job.status = \"IN_PROGRESS\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job started processing.\",\n });\n } else if (job.pollCount >= job.pollsBeforeCompleted) {\n job.status = \"COMPLETED\";\n job.completedAt = Date.now();\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job completed.\",\n });\n }\n}\n\nfunction queuePosition(job: FalQueueJob): number {\n if (job.status !== \"IN_QUEUE\") return 0;\n return Math.max(0, job.pollsBeforeInProgress - job.pollCount);\n}\n\nfunction statusResponseBody(job: FalQueueJob): Record<string, unknown> {\n const body: Record<string, unknown> = {\n status: job.status,\n request_id: job.requestId,\n response_url: `https://${FAL_HOSTS.queue}/${job.modelId}/requests/${job.requestId}`,\n logs: job.logs,\n };\n if (job.status === \"IN_QUEUE\" || job.status === \"IN_PROGRESS\") {\n body.queue_position = queuePosition(job);\n }\n if (job.status === \"COMPLETED\" && job.completedAt != null) {\n body.metrics = {\n inference_time: (job.completedAt - job.submittedAt) / 1000,\n };\n }\n return body;\n}\n\n// ─── Hosts and routing ──────────────────────────────────────────────────\n\nconst FAL_HOSTS = {\n queue: \"queue.fal.run\",\n sync: \"fal.run\",\n storage: \"rest.fal.ai\",\n storageAlpha: \"rest.alpha.fal.ai\",\n gateway: \"gateway.fal.ai\",\n} as const;\n\nconst QUEUE_REQUESTS_RE = /^(.+)\\/requests\\/([^/]+)(\\/status|\\/cancel)?$/;\nconst STORAGE_INITIATE_PATH = \"/storage/upload/initiate\";\n\nfunction stripFalPrefix(pathname: string): string {\n const stripped = pathname.replace(/^\\/fal/, \"\");\n return stripped.length > 0 ? stripped : \"/\";\n}\n\nfunction extractPromptFromBody(body: unknown): string {\n if (!body || typeof body !== \"object\") return \"\";\n const obj = body as Record<string, unknown>;\n if (typeof obj.prompt === \"string\") return obj.prompt;\n if (typeof obj.text === \"string\") return obj.text;\n const input = obj.input;\n if (input && typeof input === \"object\") {\n const inputObj = input as Record<string, unknown>;\n if (typeof inputObj.prompt === \"string\") return inputObj.prompt;\n if (typeof inputObj.text === \"string\") return inputObj.text;\n }\n return \"\";\n}\n\ninterface ParsedFalPath {\n modelId: string;\n requestId?: string;\n action?: \"status\" | \"cancel\" | \"result\";\n}\n\nfunction parseFalPath(stripped: string): ParsedFalPath | null {\n if (!stripped.startsWith(\"/\")) return null;\n const trimmed = stripped.replace(/^\\/+/, \"\");\n if (!trimmed) return null;\n\n const m = QUEUE_REQUESTS_RE.exec(`/${trimmed}`);\n if (m) {\n const modelId = m[1].replace(/^\\/+/, \"\");\n const action = m[3] === \"/status\" ? \"status\" : m[3] === \"/cancel\" ? \"cancel\" : \"result\";\n return { modelId, requestId: m[2], action };\n }\n return { modelId: trimmed };\n}\n\nexport type HandleFalOutcome = \"handled\" | \"passthrough\";\n\ninterface FalRouteInfo {\n kind: \"queue-submit\" | \"queue-status\" | \"queue-result\" | \"queue-cancel\" | \"sync-run\" | \"storage\";\n modelId?: string;\n requestId?: string;\n targetHost: string;\n}\n\nfunction classifyRoute(\n req: http.IncomingMessage,\n pathname: string,\n targetHost: string,\n): FalRouteInfo | null {\n const stripped = stripFalPrefix(pathname);\n\n if (targetHost === FAL_HOSTS.storage || targetHost === FAL_HOSTS.storageAlpha) {\n if (req.method === \"POST\" && stripped === STORAGE_INITIATE_PATH) {\n return { kind: \"storage\", targetHost };\n }\n return null;\n }\n\n const parsed = parseFalPath(stripped);\n if (!parsed) return null;\n\n if (targetHost === FAL_HOSTS.queue) {\n if (parsed.requestId) {\n if (parsed.action === \"status\" && req.method === \"GET\") {\n return {\n kind: \"queue-status\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"cancel\" && req.method === \"PUT\") {\n return {\n kind: \"queue-cancel\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"result\" && req.method === \"GET\") {\n return {\n kind: \"queue-result\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n return null;\n }\n if (req.method === \"POST\") {\n return { kind: \"queue-submit\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n if (targetHost === FAL_HOSTS.sync) {\n if (req.method === \"POST\" && parsed.modelId) {\n return { kind: \"sync-run\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n return null;\n}\n\n/**\n * General fal.ai handler. Routes by `x-fal-target-host` header (the convention\n * used by `@fal-ai/client`'s server-side requestMiddleware workaround for the\n * fact that `proxyUrl` is browser-only).\n *\n * Returns `\"passthrough\"` when the request does not look like a host-mirrored\n * fal call, so the caller can fall back to the legacy `/fal/queue/...` and\n * `/fal/run/...` audio routes.\n */\nexport async function handleFal(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n journal: Journal,\n): Promise<HandleFalOutcome> {\n const targetHostHeader = req.headers[\"x-fal-target-host\"];\n const targetHost = Array.isArray(targetHostHeader) ? targetHostHeader[0] : targetHostHeader;\n if (!targetHost) return \"passthrough\";\n\n const route = classifyRoute(req, pathname, targetHost);\n if (!route) return \"passthrough\";\n\n const testId = getTestId(req);\n const stateKey = (id: string) => `${testId}:${id}`;\n\n switch (route.kind) {\n case \"queue-status\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n advanceJob(job);\n writeJson(req, res, 200, statusResponseBody(job), pathname, journal);\n return \"handled\";\n }\n\n case \"queue-result\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n // Callers may fetch result without first polling status — advance so\n // tests that skip the status check still reach completion.\n advanceJob(job);\n if (job.status !== \"COMPLETED\") {\n writeJson(req, res, 202, statusResponseBody(job), pathname, journal);\n return \"handled\";\n }\n writeJson(req, res, 200, job.result, pathname, journal);\n return \"handled\";\n }\n\n case \"queue-cancel\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"NOT_FOUND\" }));\n return \"handled\";\n }\n if (job.status === \"COMPLETED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ALREADY_COMPLETED\" }));\n return \"handled\";\n }\n if (job.status === \"CANCELLED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n job.status = \"CANCELLED\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job cancelled.\",\n });\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n\n case \"storage\": {\n let filename = \"upload.bin\";\n try {\n const parsed = body ? (JSON.parse(body) as Record<string, unknown>) : {};\n if (typeof parsed.filename === \"string\") filename = parsed.filename;\n if (typeof parsed.file_name === \"string\") filename = parsed.file_name;\n } catch {\n // ignore — stub doesn't require a structured body\n }\n const fileId = crypto.randomUUID();\n const responseBody = {\n upload_url: `https://${route.targetHost}/storage/upload/${fileId}`,\n file_url: `https://${route.targetHost}/files/${fileId}/${filename}`,\n };\n writeJson(req, res, 200, responseBody, pathname, journal);\n return \"handled\";\n }\n\n case \"queue-submit\":\n case \"sync-run\": {\n const modelId = route.modelId!;\n let parsedBody: Record<string, unknown> | null;\n try {\n parsedBody = parseBody(body);\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"Invalid JSON body\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: detail,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return \"handled\";\n }\n const prompt = extractPromptFromBody(parsedBody);\n const syntheticReq: ChatCompletionRequest = {\n model: modelId,\n messages: [{ role: \"user\", content: prompt || JSON.stringify(parsedBody ?? {}) }],\n _endpointType: \"fal\",\n _context: getContext(req),\n };\n\n const matchCounts = journal.getFixtureMatchCountsForTest(testId);\n const fixture = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n if (defaults.record) {\n const effectiveDefaults = withFalUpstream(defaults, route.targetHost);\n // queue-submit must walk the queue upstream (submit → poll status →\n // get result) before persisting, so the fixture stores the FINAL job\n // body, not the IN_QUEUE envelope. sync-run is already a single\n // request/response cycle and the generic recorder handles it.\n if (route.kind === \"queue-submit\") {\n const outcome = await proxyAndRecordFalQueueSubmit({\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath: stripFalPrefix(pathname),\n body,\n fixtures,\n defaults: effectiveDefaults,\n stateKey,\n journal,\n });\n if (outcome === \"handled\") return \"handled\";\n // outcome === \"no_upstream\" — fall through to strict/404\n } else {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"fal\",\n stripFalPrefix(pathname),\n fixtures,\n effectiveDefaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return \"handled\";\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return \"handled\";\n }\n }\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return \"handled\";\n }\n\n let payload: unknown;\n if (isJSONResponse(response)) {\n payload = (response as RawJSONResponse).json;\n } else if (isAudioResponse(response)) {\n payload = audioToFalFile(response);\n } else {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Fixture response is not JSON or audio for fal endpoint\",\n type: \"server_error\",\n },\n }),\n );\n return \"handled\";\n }\n\n if (route.kind === \"sync-run\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n return \"handled\";\n }\n\n const requestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId,\n modelId,\n status: initialStatus,\n result: payload,\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [\n {\n timestamp: new Date(now).toISOString(),\n level: \"INFO\",\n message: \"Job enqueued.\",\n },\n ],\n createdAt: now,\n };\n falQueueStates.set(stateKey(requestId), job);\n const envelope = {\n request_id: requestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/cancel`,\n queue_position: queuePosition(job),\n };\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n }\n }\n}\n\nfunction parseBody(raw: string): Record<string, unknown> | null {\n if (!raw.trim()) return null;\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"unknown\";\n throw new Error(`Malformed JSON: ${detail}`);\n }\n}\n\n// ─── Queue-walk recording ──────────────────────────────────────────────\n//\n// The fal queue protocol surfaces three endpoints — submit (POST), status\n// (GET, polled), and result (GET) — but at the fixture layer we only store ONE\n// thing: the FINAL job body. A naive `proxyAndRecord` against submit would\n// persist the IN_QUEUE envelope, which is useless to replay (the SDK polls\n// status and then reads the result body, expecting `{ images: [...] }`-shaped\n// model output, not an envelope). So during recording we walk the upstream\n// queue ourselves, capture the result body, and write THAT as the fixture —\n// then synthesise the local envelope the same way the replay path does.\n\nconst DEFAULT_FAL_POLL_INTERVAL_MS = 1000;\n// Video generations (kling, veo, runway, etc.) routinely take 5–10 minutes\n// on the upstream queue; 15 min gives headroom without trapping a genuinely\n// hung job indefinitely.\nconst DEFAULT_FAL_TIMEOUT_MS = 900_000;\n\n// Hop-by-hop and client-set headers excluded from upstream forwarding.\n// Mirrors STRIP_HEADERS in recorder.ts.\nconst FAL_STRIP_FORWARD_HEADERS = new Set([\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authorization\",\n \"proxy-authenticate\",\n \"host\",\n \"content-length\",\n \"cookie\",\n \"accept-encoding\",\n]);\n\nexport function buildFalForwardHeaders(req: http.IncomingMessage): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [name, val] of Object.entries(req.headers)) {\n if (val === undefined) continue;\n if (FAL_STRIP_FORWARD_HEADERS.has(name.toLowerCase())) continue;\n out[name] = Array.isArray(val) ? val.join(\", \") : val;\n }\n return out;\n}\n\n/**\n * Walk a fal-shaped queue protocol upstream: POST submit, poll status until\n * COMPLETED, GET final result body. Returns the parsed final body so the caller\n * can persist it as the fixture and seed local queue state.\n *\n * Decoupled from the route layer so the legacy `/fal/queue/submit/{model}`\n * audio path (`fal-audio.ts`) can reuse the same logic.\n */\nexport async function walkFalQueue(args: {\n upstreamBase: string;\n submitPath: string;\n body: string;\n headers: Record<string, string>;\n pollIntervalMs?: number;\n timeoutMs?: number;\n /**\n * Build the status-poll URL from `request_id` when upstream's submit\n * response doesn't return a usable `status_url`. The legacy path uses\n * aimock-internal `/fal/queue/requests/<id>/status` rather than fal.ai's\n * `/<model>/requests/<id>/status` layout.\n */\n fallbackStatusPath: (requestId: string) => string;\n fallbackResultPath: (requestId: string) => string;\n}): Promise<unknown> {\n const {\n upstreamBase,\n submitPath,\n body,\n headers,\n pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS,\n timeoutMs = DEFAULT_FAL_TIMEOUT_MS,\n fallbackStatusPath,\n fallbackResultPath,\n } = args;\n\n const deadline = Date.now() + timeoutMs;\n\n // ── 1. POST submit ────────────────────────────────────────────────\n const submitUrl = resolveUpstreamUrl(upstreamBase, submitPath);\n const submitRes = await fetch(submitUrl, { method: \"POST\", headers, body });\n const submitText = await submitRes.text();\n if (!submitRes.ok) {\n throw new Error(`Submit ${submitRes.status}: ${submitText.slice(0, 200)}`);\n }\n const submitJson = parseJsonOrThrow(submitText, \"Submit\");\n const env = submitJson as Record<string, unknown>;\n const upstreamRequestId = String(env.request_id ?? \"\").trim();\n if (!upstreamRequestId) {\n throw new Error(\"Submit response missing request_id\");\n }\n\n // Prefer the URLs upstream returned — a proxy in front of fal.ai may sit on\n // a different host than the canonical `queue.fal.run` — and only fall back\n // to constructed paths if the envelope omits them.\n const envStatusUrl = env.status_url;\n const envResponseUrl = env.response_url;\n const statusUrl =\n typeof envStatusUrl === \"string\" && envStatusUrl\n ? new URL(envStatusUrl)\n : resolveUpstreamUrl(upstreamBase, fallbackStatusPath(upstreamRequestId));\n const resultUrl =\n typeof envResponseUrl === \"string\" && envResponseUrl\n ? new URL(envResponseUrl)\n : resolveUpstreamUrl(upstreamBase, fallbackResultPath(upstreamRequestId));\n\n // ── 2. Poll status until COMPLETED ───────────────────────────────\n while (true) {\n if (Date.now() > deadline) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n const statusRes = await fetch(statusUrl, { headers });\n const statusText = await statusRes.text();\n if (!statusRes.ok) {\n throw new Error(`Status ${statusRes.status}: ${statusText.slice(0, 200)}`);\n }\n const statusJson = parseJsonOrThrow(statusText, \"Status\") as Record<string, unknown>;\n const s = String(statusJson.status ?? \"\");\n if (s === \"COMPLETED\") break;\n if (s === \"FAILED\" || s === \"ERROR\" || s === \"CANCELLED\") {\n throw new Error(`Upstream job terminated with status ${s}`);\n }\n const remaining = deadline - Date.now();\n const sleep = Math.min(pollIntervalMs, Math.max(0, remaining));\n if (sleep <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n await new Promise<void>((r) => setTimeout(r, sleep));\n }\n\n // ── 3. GET final result ──────────────────────────────────────────\n const resultRes = await fetch(resultUrl, { headers });\n const resultText = await resultRes.text();\n if (!resultRes.ok) {\n throw new Error(`Result ${resultRes.status}: ${resultText.slice(0, 200)}`);\n }\n return parseJsonOrThrow(resultText, \"Result\");\n}\n\nasync function proxyAndRecordFalQueueSubmit(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n syntheticReq: ChatCompletionRequest;\n modelId: string;\n pathname: string;\n strippedPath: string;\n body: string;\n fixtures: Fixture[];\n defaults: HandlerDefaults;\n stateKey: (id: string) => string;\n journal: Journal;\n}): Promise<\"handled\" | \"no_upstream\"> {\n const {\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath,\n body,\n fixtures,\n defaults,\n stateKey,\n journal,\n } = args;\n\n const record = defaults.record;\n if (!record) return \"no_upstream\";\n const upstreamBase = record.providers.fal;\n if (!upstreamBase) {\n defaults.logger.warn(`No upstream URL configured for provider \"fal\" — cannot proxy`);\n return \"no_upstream\";\n }\n\n defaults.logger.warn(`NO FIXTURE MATCH — walking fal queue at ${upstreamBase}${strippedPath}`);\n\n let finalBody: unknown;\n try {\n finalBody = await walkFalQueue({\n upstreamBase,\n submitPath: strippedPath,\n body,\n headers: buildFalForwardHeaders(req),\n pollIntervalMs: record.fal?.pollIntervalMs,\n timeoutMs: record.fal?.timeoutMs,\n fallbackStatusPath: (id) => `${modelId}/requests/${id}/status`,\n fallbackResultPath: (id) => `${modelId}/requests/${id}`,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown queue-walk error\";\n defaults.logger.error(`fal queue-walk proxy failed: ${msg}`);\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 502, fixture: null, source: \"proxy\" },\n });\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n return \"handled\";\n }\n\n // ── 4. Persist fixture using the FINAL body, not the submit envelope ──\n const matchRequest = defaults.requestTransform\n ? defaults.requestTransform(syntheticReq)\n : syntheticReq;\n const fixture: Fixture = {\n match: buildFixtureMatch(matchRequest, record),\n response: { json: finalBody, status: 200 },\n };\n persistFixture({\n record,\n providerKey: \"fal\",\n testId: getTestId(req),\n fixture,\n fixtures,\n logger: defaults.logger,\n });\n\n // ── 5. Synthesise envelope + seed state (same shape as the replay path) ──\n const newRequestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId: newRequestId,\n modelId,\n status: initialStatus,\n result: finalBody,\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [{ timestamp: new Date(now).toISOString(), level: \"INFO\", message: \"Job enqueued.\" }],\n createdAt: now,\n };\n falQueueStates.set(stateKey(newRequestId), job);\n const envelope = {\n request_id: newRequestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/cancel`,\n queue_position: queuePosition(job),\n };\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null, source: \"proxy\" },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n}\n\nfunction parseJsonOrThrow(text: string, label: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n throw new Error(`${label} returned non-JSON: ${text.slice(0, 200)}`);\n }\n}\n\nfunction withFalUpstream(defaults: HandlerDefaults, targetHost: string): HandlerDefaults {\n if (!defaults.record) return defaults;\n // Respect an explicit record.providers.fal — tests and dev configs need to\n // point at a stub upstream. Only synthesise from the header when the user\n // didn't configure one (the \"or omit upstream URL — it's in the request\n // hostname\" mode from the issue).\n if (defaults.record.providers.fal) return defaults;\n return {\n ...defaults,\n record: {\n ...defaults.record,\n providers: {\n ...defaults.record.providers,\n fal: `https://${targetHost}`,\n },\n },\n };\n}\n\nfunction writeJson(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n pathname: string,\n journal: Journal,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction respondNotFound(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n journal: Journal,\n requestId: string,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Request ${requestId} not found`, type: \"not_found\" },\n }),\n );\n}\n"],"mappings":"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"}
@@ -34,7 +34,8 @@ function entryToFixture(entry, logger) {
34
34
  endpoint: entry.match.endpoint,
35
35
  ...entry.match.sequenceIndex !== void 0 && { sequenceIndex: entry.match.sequenceIndex },
36
36
  ...entry.match.turnIndex !== void 0 && { turnIndex: entry.match.turnIndex },
37
- ...entry.match.hasToolResult !== void 0 && { hasToolResult: entry.match.hasToolResult }
37
+ ...entry.match.hasToolResult !== void 0 && { hasToolResult: entry.match.hasToolResult },
38
+ ...entry.match.context !== void 0 && { context: entry.match.context }
38
39
  },
39
40
  response: normalizeResponse(entry.response),
40
41
  ...entry.latency !== void 0 && { latency: entry.latency },
@@ -441,9 +442,14 @@ function validateFixtures(fixtures) {
441
442
  message: `match.systemMessage must be a string or string[], got ${typeof sm}`
442
443
  });
443
444
  }
445
+ if (f.match.context !== void 0 && typeof f.match.context !== "string") results.push({
446
+ severity: "error",
447
+ fixtureIndex: i,
448
+ message: `match.context must be a string, got ${typeof f.match.context}`
449
+ });
444
450
  const um = f.match.userMessage;
445
451
  if (typeof um === "string" && um) {
446
- const dedupKey = `${um}|${f.match.turnIndex}|${f.match.hasToolResult}|${f.match.sequenceIndex}`;
452
+ const dedupKey = `${um}|${f.match.turnIndex}|${f.match.hasToolResult}|${f.match.sequenceIndex}|${f.match.context}`;
447
453
  const prev = seenUserMessages.get(dedupKey);
448
454
  if (prev !== void 0) results.push({
449
455
  severity: "warning",
@@ -453,7 +459,7 @@ function validateFixtures(fixtures) {
453
459
  else seenUserMessages.set(dedupKey, i);
454
460
  }
455
461
  const match = f.match;
456
- if (!(match.endpoint !== void 0 || match.userMessage !== void 0 || match.systemMessage !== void 0 || match.inputText !== void 0 || match.responseFormat !== void 0 || match.toolCallId !== void 0 || match.toolName !== void 0 || match.model !== void 0 || match.predicate !== void 0 || match.turnIndex !== void 0 || match.sequenceIndex !== void 0 || match.hasToolResult !== void 0) && i < fixtures.length - 1) results.push({
462
+ if (!(match.endpoint !== void 0 || match.context !== void 0 || match.userMessage !== void 0 || match.systemMessage !== void 0 || match.inputText !== void 0 || match.responseFormat !== void 0 || match.toolCallId !== void 0 || match.toolName !== void 0 || match.model !== void 0 || match.predicate !== void 0 || match.turnIndex !== void 0 || match.sequenceIndex !== void 0 || match.hasToolResult !== void 0) && i < fixtures.length - 1) results.push({
457
463
  severity: "warning",
458
464
  fixtureIndex: i,
459
465
  message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`
@@ -1 +1 @@
1
- {"version":3,"file":"fixture-loader.cjs","names":["isContentWithToolCallsResponse","isTextResponse","isToolCallResponse","isErrorResponse","isEmbeddingResponse","isImageResponse","isAudioResponse","isTranscriptionResponse","isVideoResponse","isJSONResponse"],"sources":["../src/fixture-loader.ts"],"sourcesContent":["import { readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type {\n Fixture,\n FixtureFile,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureResponse,\n ResponseOverrides,\n} from \"./types.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isEmbeddingResponse,\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n} from \"./helpers.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Auto-stringify object-valued `content` and `toolCalls[].arguments` fields.\n * This lets fixture authors write plain JSON objects instead of escaped strings.\n * All other fields (including ResponseOverrides) pass through unmodified.\n */\nexport function normalizeResponse(raw: FixtureFileResponse): FixtureResponse {\n // Shallow-clone so we don't mutate the parsed JSON input.\n const response = { ...raw } as Record<string, unknown>;\n\n // Auto-stringify object content (e.g. structured output)\n if (typeof response.content === \"object\" && response.content !== null) {\n response.content = JSON.stringify(response.content);\n }\n\n // Auto-stringify object arguments in toolCalls\n if (Array.isArray(response.toolCalls)) {\n response.toolCalls = (response.toolCalls as Array<Record<string, unknown>>).map((tc) => {\n if (typeof tc.arguments === \"object\" && tc.arguments !== null) {\n return { ...tc, arguments: JSON.stringify(tc.arguments) };\n }\n return tc;\n });\n }\n\n return response as unknown as FixtureResponse;\n}\n\nexport function entryToFixture(entry: FixtureFileEntry, logger?: Logger): Fixture {\n const fixture: Fixture = {\n match: {\n userMessage: entry.match.userMessage,\n systemMessage: entry.match.systemMessage,\n inputText: entry.match.inputText,\n toolCallId: entry.match.toolCallId,\n toolName: entry.match.toolName,\n model: entry.match.model,\n responseFormat: entry.match.responseFormat,\n endpoint: entry.match.endpoint,\n ...(entry.match.sequenceIndex !== undefined && { sequenceIndex: entry.match.sequenceIndex }),\n ...(entry.match.turnIndex !== undefined && {\n turnIndex: entry.match.turnIndex,\n }),\n ...(entry.match.hasToolResult !== undefined && {\n hasToolResult: entry.match.hasToolResult,\n }),\n },\n response: normalizeResponse(entry.response),\n ...(entry.latency !== undefined && { latency: entry.latency }),\n ...(entry.chunkSize !== undefined && { chunkSize: entry.chunkSize }),\n ...(entry.truncateAfterChunks !== undefined && {\n truncateAfterChunks: entry.truncateAfterChunks,\n }),\n ...(entry.disconnectAfterMs !== undefined && { disconnectAfterMs: entry.disconnectAfterMs }),\n ...(entry.streamingProfile !== undefined && { streamingProfile: entry.streamingProfile }),\n ...(entry.recordedTimings !== undefined && { recordedTimings: entry.recordedTimings }),\n ...(entry.replaySpeed != null && { replaySpeed: entry.replaySpeed }),\n ...(entry.chaos !== undefined && { chaos: entry.chaos }),\n ...(entry.metadata !== undefined && { metadata: entry.metadata }),\n };\n\n // Sanitize recordedTimings to guard against NaN or negative values that\n // would silently degrade replay timing calculations.\n if (fixture.recordedTimings) {\n const rt = fixture.recordedTimings;\n if (!Number.isFinite(rt.ttftMs) || rt.ttftMs < 0) rt.ttftMs = 0;\n rt.interChunkDelaysMs = rt.interChunkDelaysMs.filter((d) => Number.isFinite(d) && d >= 0);\n if (!Number.isFinite(rt.totalDurationMs) || rt.totalDurationMs < 0) rt.totalDurationMs = 0;\n }\n\n if (fixture.replaySpeed != null && fixture.replaySpeed <= 0) {\n logger?.warn(`Fixture replaySpeed must be positive, got ${fixture.replaySpeed}. Ignoring.`);\n delete fixture.replaySpeed;\n }\n\n return fixture;\n}\n\n// Logging helper — uses logger if provided, falls back to console.warn.\nfunction warn(logger: Logger | undefined, msg: string, ...rest: unknown[]): void {\n if (logger) {\n logger.warn(msg, ...rest);\n } else {\n console.warn(`[fixture-loader] ${msg}`, ...rest);\n }\n}\n\nexport function loadFixtureFile(filePath: string, logger?: Logger): Fixture[] {\n let raw: string;\n try {\n raw = readFileSync(filePath, \"utf-8\");\n } catch (err) {\n warn(logger, `Could not read file ${filePath}:`, err);\n return [];\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n warn(logger, `Invalid JSON in ${filePath}:`, err);\n return [];\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !Array.isArray((parsed as FixtureFile).fixtures)\n ) {\n warn(logger, `Missing or invalid \"fixtures\" array in ${filePath}`);\n return [];\n }\n\n return (parsed as FixtureFile).fixtures.map((e) => entryToFixture(e, logger));\n}\n\nexport function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[] {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n warn(logger, `Could not read directory ${dirPath}:`, err);\n return [];\n }\n\n const jsonFiles: string[] = [];\n const subdirs: string[] = [];\n for (const name of entries) {\n const fullPath = join(dirPath, name);\n try {\n if (statSync(fullPath).isDirectory()) {\n subdirs.push(name);\n continue;\n }\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n warn(logger, `Could not stat ${fullPath}:`, err);\n }\n continue;\n }\n if (name.endsWith(\".json\")) {\n jsonFiles.push(name);\n }\n }\n jsonFiles.sort();\n\n const fixtures: Fixture[] = [];\n for (const name of jsonFiles) {\n const filePath = join(dirPath, name);\n fixtures.push(...loadFixtureFile(filePath, logger));\n }\n\n // Recurse one level into subdirectories to support snapshot-style layouts\n // where the recorder writes to <fixturePath>/<testId>/<provider>.json.\n subdirs.sort();\n for (const sub of subdirs) {\n const subPath = join(dirPath, sub);\n let subEntries: string[];\n try {\n subEntries = readdirSync(subPath);\n } catch (err) {\n warn(logger, `Could not read subdirectory ${subPath}:`, err);\n continue;\n }\n const subJsonFiles: string[] = [];\n for (const subName of subEntries) {\n const subFullPath = join(subPath, subName);\n try {\n if (statSync(subFullPath).isDirectory()) {\n // Only one level of recursion — skip deeper nesting\n continue;\n }\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n warn(logger, `Could not stat ${subFullPath}:`, err);\n }\n continue;\n }\n if (subName.endsWith(\".json\")) {\n subJsonFiles.push(subName);\n }\n }\n subJsonFiles.sort();\n for (const subName of subJsonFiles) {\n const filePath = join(subPath, subName);\n fixtures.push(...loadFixtureFile(filePath, logger));\n }\n }\n\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture validation\n// ---------------------------------------------------------------------------\n\nexport interface ValidationResult {\n severity: \"error\" | \"warning\";\n fixtureIndex: number;\n message: string;\n}\n\nfunction validateReasoning(\n response: { reasoning?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.reasoning !== undefined) {\n if (typeof response.reasoning !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"reasoning must be a string\",\n });\n } else if (response.reasoning === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"reasoning is empty string — no reasoning events will be emitted\",\n });\n }\n }\n}\n\nfunction validateWebSearches(\n response: { webSearches?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.webSearches !== undefined) {\n if (!Array.isArray(response.webSearches)) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"webSearches must be an array of strings\",\n });\n } else if (response.webSearches.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"webSearches is empty array — no web search events will be emitted\",\n });\n } else {\n for (let j = 0; j < response.webSearches.length; j++) {\n if (typeof response.webSearches[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `webSearches[${j}] is not a string`,\n });\n break;\n }\n if (response.webSearches[j] === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: `webSearches[${j}] is empty string`,\n });\n }\n }\n }\n }\n}\n\nexport function validateFixtures(fixtures: Fixture[]): ValidationResult[] {\n const results: ValidationResult[] = [];\n\n const seenUserMessages = new Map<string, number>();\n\n for (let i = 0; i < fixtures.length; i++) {\n const f = fixtures[i];\n const response = f.response;\n\n // Skip response-shape validation for function responses — they are\n // evaluated at runtime so we cannot statically inspect them.\n if (typeof response === \"function\") {\n // Still validate match fields and numeric options below.\n } else {\n // --- Error checks ---\n\n // Response type recognition\n // Note: isContentWithToolCallsResponse must be checked before isTextResponse\n // and isToolCallResponse since it is a structural superset of both.\n if (\n !isContentWithToolCallsResponse(response) &&\n !isTextResponse(response) &&\n !isToolCallResponse(response) &&\n !isErrorResponse(response) &&\n !isEmbeddingResponse(response) &&\n !isImageResponse(response) &&\n !isAudioResponse(response) &&\n !isTranscriptionResponse(response) &&\n !isVideoResponse(response) &&\n !isJSONResponse(response)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message:\n \"response is not a recognized type (must have content, toolCalls, error, embedding, image, audio, transcription, video, or json)\",\n });\n }\n\n // Text response checks\n if (isTextResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // ContentWithToolCalls response checks\n if (isContentWithToolCallsResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // Tool call response checks\n if (isToolCallResponse(response)) {\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n validateWebSearches(response, i, results);\n }\n\n // Error response checks\n if (isErrorResponse(response)) {\n if (!response.error.message) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"error.message is empty\",\n });\n }\n if (response.status !== undefined && (response.status < 100 || response.status > 599)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code`,\n });\n }\n }\n\n // Embedding response checks\n if (isEmbeddingResponse(response)) {\n if (response.embedding.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"embedding array is empty\",\n });\n }\n for (let j = 0; j < response.embedding.length; j++) {\n if (typeof response.embedding[j] !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `embedding[${j}] is not a number`,\n });\n break; // one error is enough\n }\n }\n }\n\n // Audio response checks — validate object-form audio\n if (isAudioResponse(response) && typeof response.audio === \"object\") {\n const audioObj = response.audio;\n if (typeof audioObj.b64Json !== \"string\" || audioObj.b64Json === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"audio.b64Json must be a non-empty string\",\n });\n }\n if (audioObj.contentType !== undefined && typeof audioObj.contentType !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `audio.contentType must be a string, got ${typeof audioObj.contentType}`,\n });\n }\n }\n\n // Validate ResponseOverrides fields\n if (\n isTextResponse(response) ||\n isToolCallResponse(response) ||\n isContentWithToolCallsResponse(response)\n ) {\n const r = response as ResponseOverrides;\n if (r.id !== undefined && typeof r.id !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"id\" must be a string, got ${typeof r.id}`,\n });\n }\n if (r.created !== undefined && (typeof r.created !== \"number\" || r.created < 0)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"created\" must be a non-negative number`,\n });\n }\n if (r.model !== undefined && typeof r.model !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"model\" must be a string, got ${typeof r.model}`,\n });\n }\n if (r.finishReason !== undefined && typeof r.finishReason !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"finishReason\" must be a string, got ${typeof r.finishReason}`,\n });\n }\n if (r.role !== undefined && typeof r.role !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"role\" must be a string, got ${typeof r.role}`,\n });\n }\n if (r.systemFingerprint !== undefined && typeof r.systemFingerprint !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"systemFingerprint\" must be a string, got ${typeof r.systemFingerprint}`,\n });\n }\n if (r.usage !== undefined) {\n if (typeof r.usage !== \"object\" || r.usage === null || Array.isArray(r.usage)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage\" must be an object`,\n });\n } else {\n // Check all known usage fields are numbers if present\n for (const key of Object.keys(r.usage)) {\n const val = (r.usage as Record<string, unknown>)[key];\n if (val !== undefined && typeof val !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}\" must be a number, got ${typeof val}`,\n });\n }\n }\n }\n }\n }\n } // end: skip response-shape validation for function responses\n\n // Numeric sanity checks\n if (f.latency !== undefined && f.latency < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"latency must be >= 0\",\n });\n }\n if (f.chunkSize !== undefined && f.chunkSize < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chunkSize must be >= 1\",\n });\n }\n if (f.truncateAfterChunks !== undefined && f.truncateAfterChunks < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"truncateAfterChunks must be >= 1\",\n });\n }\n if (f.disconnectAfterMs !== undefined && f.disconnectAfterMs < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"disconnectAfterMs must be >= 0\",\n });\n }\n if (f.streamingProfile !== undefined) {\n const sp = f.streamingProfile;\n if (sp.ttft !== undefined && sp.ttft < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.ttft must be >= 0\",\n });\n }\n if (sp.tps !== undefined && sp.tps <= 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.tps must be > 0\",\n });\n }\n if (sp.jitter !== undefined && (sp.jitter < 0 || sp.jitter > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.jitter must be between 0 and 1\",\n });\n }\n }\n if (f.chaos !== undefined) {\n const ch = f.chaos;\n if (ch.dropRate !== undefined && (ch.dropRate < 0 || ch.dropRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.dropRate must be between 0 and 1\",\n });\n }\n if (ch.malformedRate !== undefined && (ch.malformedRate < 0 || ch.malformedRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.malformedRate must be between 0 and 1\",\n });\n }\n if (ch.disconnectRate !== undefined && (ch.disconnectRate < 0 || ch.disconnectRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.disconnectRate must be between 0 and 1\",\n });\n }\n }\n\n // Match field type checks\n if (f.match.turnIndex !== undefined) {\n if (\n typeof f.match.turnIndex !== \"number\" ||\n f.match.turnIndex < 0 ||\n !Number.isInteger(f.match.turnIndex)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.turnIndex must be a non-negative integer\",\n });\n }\n }\n if (f.match.sequenceIndex !== undefined) {\n if (\n typeof f.match.sequenceIndex !== \"number\" ||\n f.match.sequenceIndex < 0 ||\n !Number.isInteger(f.match.sequenceIndex)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.sequenceIndex must be a non-negative integer\",\n });\n }\n }\n if (f.match.hasToolResult !== undefined && typeof f.match.hasToolResult !== \"boolean\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.hasToolResult must be a boolean, got ${typeof f.match.hasToolResult}`,\n });\n }\n if (f.match.systemMessage !== undefined) {\n const sm = f.match.systemMessage;\n if (typeof sm === \"string\") {\n // ok\n } else if (Array.isArray(sm)) {\n if (sm.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage array must contain at least one substring`,\n });\n } else {\n for (let j = 0; j < sm.length; j++) {\n if (typeof sm[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage[${j}] must be a string, got ${typeof sm[j]}`,\n });\n }\n }\n }\n } else {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage must be a string or string[], got ${typeof sm}`,\n });\n }\n }\n\n // --- Warning checks ---\n\n // Duplicate userMessage shadowing — include turnIndex, hasToolResult, and\n // sequenceIndex in the dedup key so that fixtures which share a userMessage\n // but differ on those fields are NOT considered duplicates.\n const um = f.match.userMessage;\n if (typeof um === \"string\" && um) {\n const dedupKey = `${um}|${f.match.turnIndex}|${f.match.hasToolResult}|${f.match.sequenceIndex}`;\n const prev = seenUserMessages.get(dedupKey);\n if (prev !== undefined) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `duplicate userMessage '${um}' — shadows fixture ${prev}`,\n });\n } else {\n seenUserMessages.set(dedupKey, i);\n }\n }\n\n // Catch-all not in last position\n const match = f.match;\n const hasDiscriminator =\n match.endpoint !== undefined ||\n match.userMessage !== undefined ||\n match.systemMessage !== undefined ||\n match.inputText !== undefined ||\n match.responseFormat !== undefined ||\n match.toolCallId !== undefined ||\n match.toolName !== undefined ||\n match.model !== undefined ||\n match.predicate !== undefined ||\n match.turnIndex !== undefined ||\n match.sequenceIndex !== undefined ||\n match.hasToolResult !== undefined;\n\n if (!hasDiscriminator && i < fixtures.length - 1) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fixture-loader.cjs","names":["isContentWithToolCallsResponse","isTextResponse","isToolCallResponse","isErrorResponse","isEmbeddingResponse","isImageResponse","isAudioResponse","isTranscriptionResponse","isVideoResponse","isJSONResponse"],"sources":["../src/fixture-loader.ts"],"sourcesContent":["import { readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type {\n Fixture,\n FixtureFile,\n FixtureFileEntry,\n FixtureFileResponse,\n FixtureResponse,\n ResponseOverrides,\n} from \"./types.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isEmbeddingResponse,\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n} from \"./helpers.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Auto-stringify object-valued `content` and `toolCalls[].arguments` fields.\n * This lets fixture authors write plain JSON objects instead of escaped strings.\n * All other fields (including ResponseOverrides) pass through unmodified.\n */\nexport function normalizeResponse(raw: FixtureFileResponse): FixtureResponse {\n // Shallow-clone so we don't mutate the parsed JSON input.\n const response = { ...raw } as Record<string, unknown>;\n\n // Auto-stringify object content (e.g. structured output)\n if (typeof response.content === \"object\" && response.content !== null) {\n response.content = JSON.stringify(response.content);\n }\n\n // Auto-stringify object arguments in toolCalls\n if (Array.isArray(response.toolCalls)) {\n response.toolCalls = (response.toolCalls as Array<Record<string, unknown>>).map((tc) => {\n if (typeof tc.arguments === \"object\" && tc.arguments !== null) {\n return { ...tc, arguments: JSON.stringify(tc.arguments) };\n }\n return tc;\n });\n }\n\n return response as unknown as FixtureResponse;\n}\n\nexport function entryToFixture(entry: FixtureFileEntry, logger?: Logger): Fixture {\n const fixture: Fixture = {\n match: {\n userMessage: entry.match.userMessage,\n systemMessage: entry.match.systemMessage,\n inputText: entry.match.inputText,\n toolCallId: entry.match.toolCallId,\n toolName: entry.match.toolName,\n model: entry.match.model,\n responseFormat: entry.match.responseFormat,\n endpoint: entry.match.endpoint,\n ...(entry.match.sequenceIndex !== undefined && { sequenceIndex: entry.match.sequenceIndex }),\n ...(entry.match.turnIndex !== undefined && {\n turnIndex: entry.match.turnIndex,\n }),\n ...(entry.match.hasToolResult !== undefined && {\n hasToolResult: entry.match.hasToolResult,\n }),\n ...(entry.match.context !== undefined && { context: entry.match.context }),\n },\n response: normalizeResponse(entry.response),\n ...(entry.latency !== undefined && { latency: entry.latency }),\n ...(entry.chunkSize !== undefined && { chunkSize: entry.chunkSize }),\n ...(entry.truncateAfterChunks !== undefined && {\n truncateAfterChunks: entry.truncateAfterChunks,\n }),\n ...(entry.disconnectAfterMs !== undefined && { disconnectAfterMs: entry.disconnectAfterMs }),\n ...(entry.streamingProfile !== undefined && { streamingProfile: entry.streamingProfile }),\n ...(entry.recordedTimings !== undefined && { recordedTimings: entry.recordedTimings }),\n ...(entry.replaySpeed != null && { replaySpeed: entry.replaySpeed }),\n ...(entry.chaos !== undefined && { chaos: entry.chaos }),\n ...(entry.metadata !== undefined && { metadata: entry.metadata }),\n };\n\n // Sanitize recordedTimings to guard against NaN or negative values that\n // would silently degrade replay timing calculations.\n if (fixture.recordedTimings) {\n const rt = fixture.recordedTimings;\n if (!Number.isFinite(rt.ttftMs) || rt.ttftMs < 0) rt.ttftMs = 0;\n rt.interChunkDelaysMs = rt.interChunkDelaysMs.filter((d) => Number.isFinite(d) && d >= 0);\n if (!Number.isFinite(rt.totalDurationMs) || rt.totalDurationMs < 0) rt.totalDurationMs = 0;\n }\n\n if (fixture.replaySpeed != null && fixture.replaySpeed <= 0) {\n logger?.warn(`Fixture replaySpeed must be positive, got ${fixture.replaySpeed}. Ignoring.`);\n delete fixture.replaySpeed;\n }\n\n return fixture;\n}\n\n// Logging helper — uses logger if provided, falls back to console.warn.\nfunction warn(logger: Logger | undefined, msg: string, ...rest: unknown[]): void {\n if (logger) {\n logger.warn(msg, ...rest);\n } else {\n console.warn(`[fixture-loader] ${msg}`, ...rest);\n }\n}\n\nexport function loadFixtureFile(filePath: string, logger?: Logger): Fixture[] {\n let raw: string;\n try {\n raw = readFileSync(filePath, \"utf-8\");\n } catch (err) {\n warn(logger, `Could not read file ${filePath}:`, err);\n return [];\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n warn(logger, `Invalid JSON in ${filePath}:`, err);\n return [];\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !Array.isArray((parsed as FixtureFile).fixtures)\n ) {\n warn(logger, `Missing or invalid \"fixtures\" array in ${filePath}`);\n return [];\n }\n\n return (parsed as FixtureFile).fixtures.map((e) => entryToFixture(e, logger));\n}\n\nexport function loadFixturesFromDir(dirPath: string, logger?: Logger): Fixture[] {\n let entries: string[];\n try {\n entries = readdirSync(dirPath);\n } catch (err) {\n warn(logger, `Could not read directory ${dirPath}:`, err);\n return [];\n }\n\n const jsonFiles: string[] = [];\n const subdirs: string[] = [];\n for (const name of entries) {\n const fullPath = join(dirPath, name);\n try {\n if (statSync(fullPath).isDirectory()) {\n subdirs.push(name);\n continue;\n }\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n warn(logger, `Could not stat ${fullPath}:`, err);\n }\n continue;\n }\n if (name.endsWith(\".json\")) {\n jsonFiles.push(name);\n }\n }\n jsonFiles.sort();\n\n const fixtures: Fixture[] = [];\n for (const name of jsonFiles) {\n const filePath = join(dirPath, name);\n fixtures.push(...loadFixtureFile(filePath, logger));\n }\n\n // Recurse one level into subdirectories to support snapshot-style layouts\n // where the recorder writes to <fixturePath>/<testId>/<provider>.json.\n subdirs.sort();\n for (const sub of subdirs) {\n const subPath = join(dirPath, sub);\n let subEntries: string[];\n try {\n subEntries = readdirSync(subPath);\n } catch (err) {\n warn(logger, `Could not read subdirectory ${subPath}:`, err);\n continue;\n }\n const subJsonFiles: string[] = [];\n for (const subName of subEntries) {\n const subFullPath = join(subPath, subName);\n try {\n if (statSync(subFullPath).isDirectory()) {\n // Only one level of recursion — skip deeper nesting\n continue;\n }\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n warn(logger, `Could not stat ${subFullPath}:`, err);\n }\n continue;\n }\n if (subName.endsWith(\".json\")) {\n subJsonFiles.push(subName);\n }\n }\n subJsonFiles.sort();\n for (const subName of subJsonFiles) {\n const filePath = join(subPath, subName);\n fixtures.push(...loadFixtureFile(filePath, logger));\n }\n }\n\n return fixtures;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture validation\n// ---------------------------------------------------------------------------\n\nexport interface ValidationResult {\n severity: \"error\" | \"warning\";\n fixtureIndex: number;\n message: string;\n}\n\nfunction validateReasoning(\n response: { reasoning?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.reasoning !== undefined) {\n if (typeof response.reasoning !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"reasoning must be a string\",\n });\n } else if (response.reasoning === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"reasoning is empty string — no reasoning events will be emitted\",\n });\n }\n }\n}\n\nfunction validateWebSearches(\n response: { webSearches?: unknown },\n fixtureIndex: number,\n results: ValidationResult[],\n): void {\n if (response.webSearches !== undefined) {\n if (!Array.isArray(response.webSearches)) {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: \"webSearches must be an array of strings\",\n });\n } else if (response.webSearches.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: \"webSearches is empty array — no web search events will be emitted\",\n });\n } else {\n for (let j = 0; j < response.webSearches.length; j++) {\n if (typeof response.webSearches[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex,\n message: `webSearches[${j}] is not a string`,\n });\n break;\n }\n if (response.webSearches[j] === \"\") {\n results.push({\n severity: \"warning\",\n fixtureIndex,\n message: `webSearches[${j}] is empty string`,\n });\n }\n }\n }\n }\n}\n\nexport function validateFixtures(fixtures: Fixture[]): ValidationResult[] {\n const results: ValidationResult[] = [];\n\n const seenUserMessages = new Map<string, number>();\n\n for (let i = 0; i < fixtures.length; i++) {\n const f = fixtures[i];\n const response = f.response;\n\n // Skip response-shape validation for function responses — they are\n // evaluated at runtime so we cannot statically inspect them.\n if (typeof response === \"function\") {\n // Still validate match fields and numeric options below.\n } else {\n // --- Error checks ---\n\n // Response type recognition\n // Note: isContentWithToolCallsResponse must be checked before isTextResponse\n // and isToolCallResponse since it is a structural superset of both.\n if (\n !isContentWithToolCallsResponse(response) &&\n !isTextResponse(response) &&\n !isToolCallResponse(response) &&\n !isErrorResponse(response) &&\n !isEmbeddingResponse(response) &&\n !isImageResponse(response) &&\n !isAudioResponse(response) &&\n !isTranscriptionResponse(response) &&\n !isVideoResponse(response) &&\n !isJSONResponse(response)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message:\n \"response is not a recognized type (must have content, toolCalls, error, embedding, image, audio, transcription, video, or json)\",\n });\n }\n\n // Text response checks\n if (isTextResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // ContentWithToolCalls response checks\n if (isContentWithToolCallsResponse(response)) {\n if (response.content === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"content is empty string\",\n });\n }\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n validateReasoning(response, i, results);\n validateWebSearches(response, i, results);\n }\n\n // Tool call response checks\n if (isToolCallResponse(response)) {\n if (response.toolCalls.length === 0) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: \"toolCalls array is empty — fixture will never produce tool calls\",\n });\n }\n for (let j = 0; j < response.toolCalls.length; j++) {\n const tc = response.toolCalls[j];\n if (!tc.name) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].name is empty`,\n });\n }\n try {\n JSON.parse(tc.arguments);\n } catch {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `toolCalls[${j}].arguments is not valid JSON: ${tc.arguments}`,\n });\n }\n }\n validateWebSearches(response, i, results);\n }\n\n // Error response checks\n if (isErrorResponse(response)) {\n if (!response.error.message) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"error.message is empty\",\n });\n }\n if (response.status !== undefined && (response.status < 100 || response.status > 599)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `error status ${response.status} is not a valid HTTP status code`,\n });\n }\n }\n\n // Embedding response checks\n if (isEmbeddingResponse(response)) {\n if (response.embedding.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"embedding array is empty\",\n });\n }\n for (let j = 0; j < response.embedding.length; j++) {\n if (typeof response.embedding[j] !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `embedding[${j}] is not a number`,\n });\n break; // one error is enough\n }\n }\n }\n\n // Audio response checks — validate object-form audio\n if (isAudioResponse(response) && typeof response.audio === \"object\") {\n const audioObj = response.audio;\n if (typeof audioObj.b64Json !== \"string\" || audioObj.b64Json === \"\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"audio.b64Json must be a non-empty string\",\n });\n }\n if (audioObj.contentType !== undefined && typeof audioObj.contentType !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `audio.contentType must be a string, got ${typeof audioObj.contentType}`,\n });\n }\n }\n\n // Validate ResponseOverrides fields\n if (\n isTextResponse(response) ||\n isToolCallResponse(response) ||\n isContentWithToolCallsResponse(response)\n ) {\n const r = response as ResponseOverrides;\n if (r.id !== undefined && typeof r.id !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"id\" must be a string, got ${typeof r.id}`,\n });\n }\n if (r.created !== undefined && (typeof r.created !== \"number\" || r.created < 0)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"created\" must be a non-negative number`,\n });\n }\n if (r.model !== undefined && typeof r.model !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"model\" must be a string, got ${typeof r.model}`,\n });\n }\n if (r.finishReason !== undefined && typeof r.finishReason !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"finishReason\" must be a string, got ${typeof r.finishReason}`,\n });\n }\n if (r.role !== undefined && typeof r.role !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"role\" must be a string, got ${typeof r.role}`,\n });\n }\n if (r.systemFingerprint !== undefined && typeof r.systemFingerprint !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"systemFingerprint\" must be a string, got ${typeof r.systemFingerprint}`,\n });\n }\n if (r.usage !== undefined) {\n if (typeof r.usage !== \"object\" || r.usage === null || Array.isArray(r.usage)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage\" must be an object`,\n });\n } else {\n // Check all known usage fields are numbers if present\n for (const key of Object.keys(r.usage)) {\n const val = (r.usage as Record<string, unknown>)[key];\n if (val !== undefined && typeof val !== \"number\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `override \"usage.${key}\" must be a number, got ${typeof val}`,\n });\n }\n }\n }\n }\n }\n } // end: skip response-shape validation for function responses\n\n // Numeric sanity checks\n if (f.latency !== undefined && f.latency < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"latency must be >= 0\",\n });\n }\n if (f.chunkSize !== undefined && f.chunkSize < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chunkSize must be >= 1\",\n });\n }\n if (f.truncateAfterChunks !== undefined && f.truncateAfterChunks < 1) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"truncateAfterChunks must be >= 1\",\n });\n }\n if (f.disconnectAfterMs !== undefined && f.disconnectAfterMs < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"disconnectAfterMs must be >= 0\",\n });\n }\n if (f.streamingProfile !== undefined) {\n const sp = f.streamingProfile;\n if (sp.ttft !== undefined && sp.ttft < 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.ttft must be >= 0\",\n });\n }\n if (sp.tps !== undefined && sp.tps <= 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.tps must be > 0\",\n });\n }\n if (sp.jitter !== undefined && (sp.jitter < 0 || sp.jitter > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"streamingProfile.jitter must be between 0 and 1\",\n });\n }\n }\n if (f.chaos !== undefined) {\n const ch = f.chaos;\n if (ch.dropRate !== undefined && (ch.dropRate < 0 || ch.dropRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.dropRate must be between 0 and 1\",\n });\n }\n if (ch.malformedRate !== undefined && (ch.malformedRate < 0 || ch.malformedRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.malformedRate must be between 0 and 1\",\n });\n }\n if (ch.disconnectRate !== undefined && (ch.disconnectRate < 0 || ch.disconnectRate > 1)) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"chaos.disconnectRate must be between 0 and 1\",\n });\n }\n }\n\n // Match field type checks\n if (f.match.turnIndex !== undefined) {\n if (\n typeof f.match.turnIndex !== \"number\" ||\n f.match.turnIndex < 0 ||\n !Number.isInteger(f.match.turnIndex)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.turnIndex must be a non-negative integer\",\n });\n }\n }\n if (f.match.sequenceIndex !== undefined) {\n if (\n typeof f.match.sequenceIndex !== \"number\" ||\n f.match.sequenceIndex < 0 ||\n !Number.isInteger(f.match.sequenceIndex)\n ) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: \"match.sequenceIndex must be a non-negative integer\",\n });\n }\n }\n if (f.match.hasToolResult !== undefined && typeof f.match.hasToolResult !== \"boolean\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.hasToolResult must be a boolean, got ${typeof f.match.hasToolResult}`,\n });\n }\n if (f.match.systemMessage !== undefined) {\n const sm = f.match.systemMessage;\n if (typeof sm === \"string\") {\n // ok\n } else if (Array.isArray(sm)) {\n if (sm.length === 0) {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage array must contain at least one substring`,\n });\n } else {\n for (let j = 0; j < sm.length; j++) {\n if (typeof sm[j] !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage[${j}] must be a string, got ${typeof sm[j]}`,\n });\n }\n }\n }\n } else {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.systemMessage must be a string or string[], got ${typeof sm}`,\n });\n }\n }\n if (f.match.context !== undefined && typeof f.match.context !== \"string\") {\n results.push({\n severity: \"error\",\n fixtureIndex: i,\n message: `match.context must be a string, got ${typeof f.match.context}`,\n });\n }\n\n // --- Warning checks ---\n\n // Duplicate userMessage shadowing — include turnIndex, hasToolResult, and\n // sequenceIndex in the dedup key so that fixtures which share a userMessage\n // but differ on those fields are NOT considered duplicates.\n const um = f.match.userMessage;\n if (typeof um === \"string\" && um) {\n const dedupKey = `${um}|${f.match.turnIndex}|${f.match.hasToolResult}|${f.match.sequenceIndex}|${f.match.context}`;\n const prev = seenUserMessages.get(dedupKey);\n if (prev !== undefined) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `duplicate userMessage '${um}' — shadows fixture ${prev}`,\n });\n } else {\n seenUserMessages.set(dedupKey, i);\n }\n }\n\n // Catch-all not in last position\n const match = f.match;\n const hasDiscriminator =\n match.endpoint !== undefined ||\n match.context !== undefined ||\n match.userMessage !== undefined ||\n match.systemMessage !== undefined ||\n match.inputText !== undefined ||\n match.responseFormat !== undefined ||\n match.toolCallId !== undefined ||\n match.toolName !== undefined ||\n match.model !== undefined ||\n match.predicate !== undefined ||\n match.turnIndex !== undefined ||\n match.sequenceIndex !== undefined ||\n match.hasToolResult !== undefined;\n\n if (!hasDiscriminator && i < fixtures.length - 1) {\n results.push({\n severity: \"warning\",\n fixtureIndex: i,\n message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`,\n });\n }\n }\n\n return results;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fixture-loader.d.cts","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;AA6BA;;;AAA6D,iBAA7C,iBAAA,CAA6C,GAAA,EAAtB,mBAAsB,CAAA,EAAA,eAAA;AAiF7C,iBAAA,eAAA,CAAe,QAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAA4B,MAA5B,CAAA,EAAqC,OAArC,EAAA;AAAA,iBA6Bf,mBAAA,CA7Be,OAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA6B+B,MA7B/B,CAAA,EA6BwC,OA7BxC,EAAA;AAA4B,UA+G1C,gBAAA,CA/G0C;UAAS,EAAA,OAAA,GAAA,SAAA;EAAO,YAAA,EAAA,MAAA;EA6B3D,OAAA,EAAA,MAAA;;AAA8C,iBAsJ9C,gBAAA,CAtJ8C,QAAA,EAsJnB,OAtJmB,EAAA,CAAA,EAsJP,gBAtJO,EAAA"}
1
+ {"version":3,"file":"fixture-loader.d.cts","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;AA6BA;;;AAA6D,iBAA7C,iBAAA,CAA6C,GAAA,EAAtB,mBAAsB,CAAA,EAAA,eAAA;AAkF7C,iBAAA,eAAA,CAAe,QAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAA4B,MAA5B,CAAA,EAAqC,OAArC,EAAA;AAAA,iBA6Bf,mBAAA,CA7Be,OAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA6B+B,MA7B/B,CAAA,EA6BwC,OA7BxC,EAAA;AAA4B,UA+G1C,gBAAA,CA/G0C;UAAS,EAAA,OAAA,GAAA,SAAA;EAAO,YAAA,EAAA,MAAA;EA6B3D,OAAA,EAAA,MAAA;;AAA8C,iBAsJ9C,gBAAA,CAtJ8C,QAAA,EAsJnB,OAtJmB,EAAA,CAAA,EAsJP,gBAtJO,EAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"fixture-loader.d.ts","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;AA6BA;;;AAA6D,iBAA7C,iBAAA,CAA6C,GAAA,EAAtB,mBAAsB,CAAA,EAAA,eAAA;AAiF7C,iBAAA,eAAA,CAAe,QAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAA4B,MAA5B,CAAA,EAAqC,OAArC,EAAA;AAAA,iBA6Bf,mBAAA,CA7Be,OAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA6B+B,MA7B/B,CAAA,EA6BwC,OA7BxC,EAAA;AAA4B,UA+G1C,gBAAA,CA/G0C;UAAS,EAAA,OAAA,GAAA,SAAA;EAAO,YAAA,EAAA,MAAA;EA6B3D,OAAA,EAAA,MAAA;;AAA8C,iBAsJ9C,gBAAA,CAtJ8C,QAAA,EAsJnB,OAtJmB,EAAA,CAAA,EAsJP,gBAtJO,EAAA"}
1
+ {"version":3,"file":"fixture-loader.d.ts","names":[],"sources":["../src/fixture-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;AA6BA;;;AAA6D,iBAA7C,iBAAA,CAA6C,GAAA,EAAtB,mBAAsB,CAAA,EAAA,eAAA;AAkF7C,iBAAA,eAAA,CAAe,QAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EAA4B,MAA5B,CAAA,EAAqC,OAArC,EAAA;AAAA,iBA6Bf,mBAAA,CA7Be,OAAA,EAAA,MAAA,EAAA,MAAA,CAAA,EA6B+B,MA7B/B,CAAA,EA6BwC,OA7BxC,EAAA;AAA4B,UA+G1C,gBAAA,CA/G0C;UAAS,EAAA,OAAA,GAAA,SAAA;EAAO,YAAA,EAAA,MAAA;EA6B3D,OAAA,EAAA,MAAA;;AAA8C,iBAsJ9C,gBAAA,CAtJ8C,QAAA,EAsJnB,OAtJmB,EAAA,CAAA,EAsJP,gBAtJO,EAAA"}
@@ -33,7 +33,8 @@ function entryToFixture(entry, logger) {
33
33
  endpoint: entry.match.endpoint,
34
34
  ...entry.match.sequenceIndex !== void 0 && { sequenceIndex: entry.match.sequenceIndex },
35
35
  ...entry.match.turnIndex !== void 0 && { turnIndex: entry.match.turnIndex },
36
- ...entry.match.hasToolResult !== void 0 && { hasToolResult: entry.match.hasToolResult }
36
+ ...entry.match.hasToolResult !== void 0 && { hasToolResult: entry.match.hasToolResult },
37
+ ...entry.match.context !== void 0 && { context: entry.match.context }
37
38
  },
38
39
  response: normalizeResponse(entry.response),
39
40
  ...entry.latency !== void 0 && { latency: entry.latency },
@@ -440,9 +441,14 @@ function validateFixtures(fixtures) {
440
441
  message: `match.systemMessage must be a string or string[], got ${typeof sm}`
441
442
  });
442
443
  }
444
+ if (f.match.context !== void 0 && typeof f.match.context !== "string") results.push({
445
+ severity: "error",
446
+ fixtureIndex: i,
447
+ message: `match.context must be a string, got ${typeof f.match.context}`
448
+ });
443
449
  const um = f.match.userMessage;
444
450
  if (typeof um === "string" && um) {
445
- const dedupKey = `${um}|${f.match.turnIndex}|${f.match.hasToolResult}|${f.match.sequenceIndex}`;
451
+ const dedupKey = `${um}|${f.match.turnIndex}|${f.match.hasToolResult}|${f.match.sequenceIndex}|${f.match.context}`;
446
452
  const prev = seenUserMessages.get(dedupKey);
447
453
  if (prev !== void 0) results.push({
448
454
  severity: "warning",
@@ -452,7 +458,7 @@ function validateFixtures(fixtures) {
452
458
  else seenUserMessages.set(dedupKey, i);
453
459
  }
454
460
  const match = f.match;
455
- if (!(match.endpoint !== void 0 || match.userMessage !== void 0 || match.systemMessage !== void 0 || match.inputText !== void 0 || match.responseFormat !== void 0 || match.toolCallId !== void 0 || match.toolName !== void 0 || match.model !== void 0 || match.predicate !== void 0 || match.turnIndex !== void 0 || match.sequenceIndex !== void 0 || match.hasToolResult !== void 0) && i < fixtures.length - 1) results.push({
461
+ if (!(match.endpoint !== void 0 || match.context !== void 0 || match.userMessage !== void 0 || match.systemMessage !== void 0 || match.inputText !== void 0 || match.responseFormat !== void 0 || match.toolCallId !== void 0 || match.toolName !== void 0 || match.model !== void 0 || match.predicate !== void 0 || match.turnIndex !== void 0 || match.sequenceIndex !== void 0 || match.hasToolResult !== void 0) && i < fixtures.length - 1) results.push({
456
462
  severity: "warning",
457
463
  fixtureIndex: i,
458
464
  message: `empty match acts as catch-all but is not the last fixture — shadows fixtures ${i + 1}+`