{"version":3,"file":"router.cjs","names":["isImageResponse","isAudioResponse","isJSONResponse","isErrorResponse","isTranscriptionResponse","isVideoResponse"],"sources":["../src/router.ts"],"sourcesContent":["import type {\n  ChatCompletionRequest,\n  ChatMessage,\n  ContentPart,\n  Fixture,\n  FixtureMatch,\n} from \"./types.js\";\nimport {\n  isImageResponse,\n  isAudioResponse,\n  isTranscriptionResponse,\n  isVideoResponse,\n  isJSONResponse,\n  isErrorResponse,\n} from \"./helpers.js\";\n\nexport function getLastMessageByRole(messages: ChatMessage[], role: string): ChatMessage | null {\n  for (let i = messages.length - 1; i >= 0; i--) {\n    if (messages[i].role === role) return messages[i];\n  }\n  return null;\n}\n\n/**\n * Concatenate the text content of every `system` role message in order.\n * Hosts that build a system context from multiple sources (persona, agent\n * context entries, tool guidance) often emit several system messages in one\n * request; this joins SEPARATE system messages with newlines so a substring\n * matcher sees the whole context as one body.\n *\n * Empty handling is symmetric with {@link getTextContent}: a system message\n * with no extractable text (`null`) contributes nothing, while a message that\n * extracts to an empty string is a present-but-empty body. We skip only the\n * `null` (no-text) case so a genuinely empty system message does not inject a\n * stray newline; this matches getTextContent treating \"no text\" and \"empty\n * text\" consistently.\n */\nexport function getSystemText(messages: ChatMessage[]): string {\n  const parts: string[] = [];\n  for (const m of messages) {\n    if (m.role !== \"system\") continue;\n    const text = getTextContent(m.content);\n    if (text === null) continue;\n    parts.push(text);\n  }\n  return parts.join(\"\\n\");\n}\n\n/**\n * Extract the text content from a message's content field.\n * Handles both plain string content and array-of-parts content\n * (e.g. `[{type: \"text\", text: \"...\"}]` as sent by some SDKs).\n *\n * Multi-part text is joined with `\"\"` (the parts form one logical body split\n * across segments). Empty handling is symmetric with the string path: a string\n * `\"\"` returns `\"\"`, and an array containing at least one text part whose\n * combined text is empty likewise returns `\"\"` (NOT `null`). `null` is reserved\n * for \"no text content at all\" — null content, or an array with no text parts —\n * so callers can distinguish \"absent\" from \"present but empty\" the same way for\n * both content shapes.\n */\nexport function getTextContent(content: string | ContentPart[] | null): string | null {\n  if (typeof content === \"string\") return content;\n  if (Array.isArray(content)) {\n    const texts = content\n      .filter((p) => p.type === \"text\" && typeof p.text === \"string\")\n      .map((p) => p.text as string);\n    return texts.length > 0 ? texts.join(\"\") : null;\n  }\n  return null;\n}\n\n/**\n * Result of {@link matchFixtureDiagnostic}: the matched fixture (or `null`) plus\n * the number of fixtures that matched the request SHAPE (every predicate above\n * the sequenceIndex/turnIndex gates) but were rejected ONLY by the\n * sequenceIndex/turnIndex count state.\n *\n * `skippedBySequenceOrTurn > 0` with `fixture === null` distinguishes a\n * \"sequence/turn exhausted\" miss (candidate fixtures existed but their count\n * gate had moved on) from a true \"no fixture had a matching shape\" miss — used\n * to disambiguate the strict-mode 503 message.\n */\nexport interface MatchFixtureDiagnostic {\n  fixture: Fixture | null;\n  skippedBySequenceOrTurn: number;\n  /**\n   * `true` when the served fixture was selected by relaxed content-anchored\n   * matching even though its `turnIndex` is defined and does NOT equal the\n   * request's assistant-message count — i.e. the legacy strict turnIndex gate\n   * (now opt-in via `AIMOCK_STRICT_TURN_INDEX`) WOULD HAVE rejected it. Absent /\n   * falsy on canonical-position matches, non-turnIndexed matches, and misses.\n   * Additive optional field — existing handler destructures are unaffected.\n   */\n  turnIndexRelaxed?: boolean;\n  /**\n   * How the served fixture was selected: `\"turnIndex\"` when its `turnIndex`\n   * sits exactly at the current assistant count (canonical position),\n   * `\"content\"` otherwise (a non-turnIndexed match or a relaxed off-by-N\n   * match). Absent on misses. Additive optional field.\n   */\n  matchedBy?: \"content\" | \"turnIndex\";\n}\n\n/**\n * Optional matcher tuning.\n *\n * `strictTurnIndex` restores the legacy behaviour where `turnIndex` must equal\n * the request's assistant-message count exactly (a hard reject gate). It is set\n * by the record path, where a miss proxies upstream to capture a fresh turn; an\n * earlier-turn fixture must not shadow a longer request or the new turn would\n * never be recorded. Replay (the default, `false`) treats `turnIndex` as a\n * non-fatal disambiguator instead — see {@link selectByTurnIndex}.\n */\nexport interface MatchOptions {\n  strictTurnIndex?: boolean;\n  /**\n   * Optional sink for the one-shot relaxed-turnIndex divergence warning. Handlers\n   * pass their `defaults.logger`; the structural `{ warn }` shape avoids an\n   * import cycle with logger.ts and keeps the matcher decoupled. When omitted no\n   * warning is emitted (the diagnostic fields are still populated). The Logger's\n   * own level gate keeps a passing programmatic run (silent default) quiet.\n   */\n  logger?: { warn(...args: unknown[]): void };\n}\n\n/**\n * Process-level opt-out: when `AIMOCK_STRICT_TURN_INDEX=1` (or `true`) is set,\n * REPLAY selection restores the legacy hard turnIndex gate — a content-matching\n * fixture whose `turnIndex` is defined and `!== assistantCount` is rejected,\n * reproducing origin/main semantics. Follows the `AIMOCK_ALLOW_PRIVATE_URLS`\n * precedent for parsing/precedence. Read per-call (not cached) so tests can flip\n * it. Does NOT affect the record path, which is already strict regardless.\n */\nfunction strictTurnIndexEnv(): boolean {\n  const v = process.env.AIMOCK_STRICT_TURN_INDEX;\n  return v === \"1\" || v === \"true\";\n}\n\n/**\n * Process-level set of fixtures for which the relaxed-turnIndex divergence\n * warning has already fired, so each divergent fixture warns at most ONCE per\n * process (throttle). Keyed by the selected fixture's OBJECT IDENTITY: the\n * `Fixture` references in the server's fixtures array are stable across replays\n * (the array is held by reference and only fully replaced on a fixtures reset),\n * so identity uniquely distinguishes divergent fixtures and warns each exactly\n * once. A `WeakSet` was chosen over the previous `JSON.stringify(match)` key for\n * two reasons: (1) stringifying the match DROPS `predicate` functions and\n * serialises any RegExp matcher to `{}`, so two distinct fixtures differing only\n * by a predicate/regex collided to one key and the second's warning was silently\n * suppressed; and (2) a string `Set` only grows, accumulating an entry per\n * divergent shape on a long-lived server, whereas a `WeakSet` auto-evicts when a\n * fixture object is released (e.g. after a fixtures reset drops the references).\n * `let` because `WeakSet` has no `.clear()`, so the test hook reassigns a fresh\n * one.\n */\nlet warnedRelaxedFixtures = new WeakSet<Fixture>();\n\n/**\n * Test-only hook to clear the throttle state between cases. Not part of the\n * public contract. `WeakSet` has no `.clear()`, so reassign a fresh instance.\n */\nexport function _resetTurnIndexRelaxWarnings(): void {\n  warnedRelaxedFixtures = new WeakSet<Fixture>();\n}\n\n/**\n * Build the {@link MatchOptions} a request handler must pass to\n * {@link matchFixtureDiagnostic} / {@link matchFixture}, derived from whether\n * the handler is about to record on a miss.\n *\n * EVERY record-capable handler (OpenAI chat, Anthropic messages, Responses,\n * Gemini, Bedrock, Bedrock-Converse, Cohere, Ollama, …) must build its match\n * options through THIS helper rather than hand-rolling `{ strictTurnIndex }` at\n * the call site. Recording proxies upstream on a miss to capture a fresh turn;\n * if `strictTurnIndex` is left false during recording, an earlier-turn fixture\n * can content-shadow a longer request, the `if (!fixture)` record branch never\n * fires, and the new turn is SILENTLY never recorded. Funnelling the decision\n * through one helper makes that wiring impossible for a future handler to miss:\n * pass `recording = true` whenever the handler's own record gate is satisfied\n * (i.e. it will call `proxyAndRecord` on a miss), `false` otherwise.\n */\nexport function recordMatchOptions(\n  recording: boolean,\n  logger?: { warn(...args: unknown[]): void },\n): MatchOptions {\n  return { strictTurnIndex: recording, logger };\n}\n\n/**\n * Match a fixture against a request, additionally reporting WHY a `null` result\n * occurred. Shares the exact predicate loop with {@link matchFixture}; a fixture\n * that passes every shape predicate but fails ONLY the sequenceIndex or\n * turnIndex gate increments `skippedBySequenceOrTurn`. {@link matchFixture} is a\n * thin wrapper that returns the `.fixture` field, so existing callers are\n * unaffected.\n */\nexport function matchFixtureDiagnostic(\n  fixtures: Fixture[],\n  req: ChatCompletionRequest,\n  matchCounts?: Map<Fixture, number>,\n  requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n  options?: MatchOptions,\n): MatchFixtureDiagnostic {\n  // Apply transform once before matching — used for stripping dynamic data\n  const effective = requestTransform ? requestTransform(req) : req;\n  const useExactMatch = !!requestTransform;\n  // In record mode the server proxies to the upstream on a miss, so a fixture\n  // already captured for an EARLIER turn must NOT shadow a longer (later-turn)\n  // request — otherwise the new turn would never be proxied and recorded.\n  // There turnIndex stays a strict hard gate. Replay (the default) instead\n  // treats turnIndex as a non-fatal disambiguator so a canonical multi-bubble\n  // run isn't falsely rejected for an off-by-N assistant count.\n  // Strict turnIndex is in force when the record path requests it OR the\n  // process-level AIMOCK_STRICT_TURN_INDEX opt-out is set (which restores the\n  // legacy hard gate for replay too). Record mode passes `true` explicitly; the\n  // env only matters when the caller left it `false`/unset (replay).\n  const strictTurnIndex = (options?.strictTurnIndex ?? false) || strictTurnIndexEnv();\n\n  let skippedBySequenceOrTurn = 0;\n  // Every fixture whose content / shape predicates (and sequenceIndex gate)\n  // pass. turnIndex is applied afterwards as a non-fatal disambiguator.\n  const contentMatches: Fixture[] = [];\n\n  for (const fixture of fixtures) {\n    const { match } = fixture;\n\n    // predicate — if present, must return true (receives original request)\n    if (match.predicate !== undefined) {\n      if (!match.predicate(req)) continue;\n    }\n\n    // endpoint — bidirectional filtering:\n    // 1. If fixture has endpoint set, only match requests of that type\n    // 2. If request has _endpointType but fixture doesn't, skip fixtures\n    //    whose response type is incompatible (prevents generic chat fixtures\n    //    from matching image/speech/video requests and causing 500s)\n    const reqEndpoint = effective._endpointType as string | undefined;\n    if (match.endpoint !== undefined) {\n      if (match.endpoint !== reqEndpoint) continue;\n    } else if (\n      reqEndpoint &&\n      reqEndpoint !== \"chat\" &&\n      reqEndpoint !== \"embedding\" &&\n      !reqEndpoint.startsWith(\"realtime\")\n    ) {\n      // Fixture has no endpoint restriction but request is multimedia —\n      // only match if the response type is compatible.\n      // Function responses cannot be checked statically, so treat them as compatible.\n      const r = fixture.response;\n      if (typeof r !== \"function\") {\n        const compatible =\n          (reqEndpoint === \"image\" && isImageResponse(r)) ||\n          (reqEndpoint === \"speech\" && isAudioResponse(r)) ||\n          (reqEndpoint === \"elevenlabs-tts\" && isAudioResponse(r)) ||\n          (reqEndpoint === \"audio-gen\" && isAudioResponse(r)) ||\n          (reqEndpoint === \"fal-audio\" && isAudioResponse(r)) ||\n          (reqEndpoint === \"fal\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n          (reqEndpoint === \"transcription\" && isTranscriptionResponse(r)) ||\n          (reqEndpoint === \"translation\" && isTranscriptionResponse(r)) ||\n          (reqEndpoint === \"video\" && isVideoResponse(r));\n        if (!compatible) continue;\n      }\n    }\n\n    // context — opt-in exact match against the request's _context field.\n    // If fixture specifies a context, only match requests with that exact context.\n    // If fixture omits context, match any request regardless of _context.\n    if (match.context !== undefined) {\n      if (effective._context !== match.context) continue;\n    }\n\n    // userMessage — case-sensitive match against the last user message content.\n    // String matching is intentionally case-sensitive so fixture authors can\n    // rely on exact string values. This differs from the case-insensitive\n    // matchesPattern() in helpers.ts, which is used for search/rerank/moderation\n    // where exact casing rarely matters.\n    if (match.userMessage !== undefined) {\n      const msg = getLastMessageByRole(effective.messages, \"user\");\n      const text = msg ? getTextContent(msg.content) : null;\n      if (!text) continue;\n      if (typeof match.userMessage === \"string\") {\n        if (useExactMatch) {\n          if (text !== match.userMessage) continue;\n        } else {\n          if (!text.includes(match.userMessage)) continue;\n        }\n      } else {\n        match.userMessage.lastIndex = 0;\n        if (!match.userMessage.test(text)) continue;\n      }\n    }\n\n    // systemMessage — case-sensitive substring, regexp, or array-of-substrings\n    // match against the joined text of every system message in the request.\n    // Use to gate a fixture on host-supplied context (e.g. agent-context\n    // entries) so that when the calling app changes that context the fixture\n    // stops matching and the request falls through to the next fixture or\n    // upstream proxy.\n    //\n    // Array form (string[]) requires ALL substrings to be present — useful\n    // when the gate must combine multiple non-adjacent tokens (e.g. a default\n    // name AND a default activity list whose positions in the serialised\n    // context JSON aren't stable).\n    if (match.systemMessage !== undefined) {\n      const sm = match.systemMessage;\n      // Empty array is treated as \"no constraint\" → matches unconditionally,\n      // INCLUDING requests with no system text at all. This is the documented\n      // contract (same permissive behaviour as not setting systemMessage), so\n      // it must be honored BEFORE the no-system-text guard below — otherwise a\n      // request without a system message would be wrongly skipped. Validation\n      // rejects [] at load time for JSON fixtures; programmatic callers that\n      // pass [] get this permissive behaviour.\n      if (Array.isArray(sm) && sm.length === 0) {\n        // no constraint — fall through to the next predicate\n      } else {\n        const text = getSystemText(effective.messages);\n        if (!text) continue;\n        if (Array.isArray(sm)) {\n          let allPresent = true;\n          for (const needle of sm) {\n            if (!text.includes(needle)) {\n              allPresent = false;\n              break;\n            }\n          }\n          if (!allPresent) continue;\n        } else if (typeof sm === \"string\") {\n          if (useExactMatch) {\n            if (text !== sm) continue;\n          } else {\n            if (!text.includes(sm)) continue;\n          }\n        } else {\n          sm.lastIndex = 0;\n          if (!sm.test(text)) continue;\n        }\n      }\n    }\n\n    // toolCallId — a toolCallId fixture answers the model's response to a tool\n    // result, which by API contract only happens when the conversation's LAST\n    // message is a tool result. If a newer user (or other) turn follows the\n    // tool message, the stale tool_call_id must not shadow userMessage matchers.\n    if (match.toolCallId !== undefined) {\n      const last = effective.messages[effective.messages.length - 1];\n      if (!last || last.role !== \"tool\" || last.tool_call_id !== match.toolCallId) continue;\n    }\n\n    // toolName — match against any tool definition by function.name\n    if (match.toolName !== undefined) {\n      const tools = effective.tools ?? [];\n      const found = tools.some((t) => t.function.name === match.toolName);\n      if (!found) continue;\n    }\n\n    // inputText — case-sensitive match against the embedding input text.\n    // Same rationale as userMessage above: fixture authors specify exact strings.\n    if (match.inputText !== undefined) {\n      const embeddingInput = effective.embeddingInput;\n      if (!embeddingInput) continue;\n      if (typeof match.inputText === \"string\") {\n        if (useExactMatch) {\n          if (embeddingInput !== match.inputText) continue;\n        } else {\n          if (!embeddingInput.includes(match.inputText)) continue;\n        }\n      } else {\n        match.inputText.lastIndex = 0;\n        if (!match.inputText.test(embeddingInput)) continue;\n      }\n    }\n\n    // responseFormat — exact string match against request response_format.type\n    if (match.responseFormat !== undefined) {\n      const reqType = effective.response_format?.type;\n      if (reqType !== match.responseFormat) continue;\n    }\n\n    // model — exact match or prefix + dash-digit boundary for strings (so that\n    // \"claude-opus-4\" matches \"claude-opus-4-20250514\" but \"gpt-4\" does NOT\n    // match \"gpt-4o\" and \"gpt-4o\" does NOT match \"gpt-4o-mini\"), regexp unchanged\n    if (match.model !== undefined) {\n      if (typeof match.model === \"string\") {\n        if (effective.model !== match.model) {\n          if (!effective.model?.startsWith(match.model)) continue;\n          const rest = effective.model.slice(match.model.length);\n          if (!/^-\\d/.test(rest)) continue;\n        }\n      } else {\n        match.model.lastIndex = 0;\n        if (!match.model.test(effective.model ?? \"\")) continue;\n      }\n    }\n\n    // hasToolResult — request-SHAPE predicate: does the conversation contain a\n    // tool result message? Must be evaluated with the other shape predicates\n    // ABOVE the sequence/turn state gates so that a fixture whose shape never\n    // matched is not miscounted as \"skipped by sequence/turn state\".\n    if (match.hasToolResult !== undefined) {\n      const hasTool = effective.messages.some((m) => m.role === \"tool\");\n      if (hasTool !== match.hasToolResult) continue;\n    }\n\n    // At this point every SHAPE / CONTENT predicate above has passed, so this\n    // fixture is a genuine CONTENT match for the request. The sequenceIndex and\n    // turnIndex constraints below are POSITION state, not request shape.\n    //\n    // sequenceIndex remains a hard, stateful gate: it consumes sequenced\n    // siblings one call at a time (and an exhausted index intentionally falls\n    // through to a later fixture). A fixture that matched the shape but fails\n    // ONLY the sequenceIndex gate is a \"candidate skipped by sequence/turn\n    // state\", counted separately so callers can disambiguate the strict-mode\n    // 503 message.\n    if (match.sequenceIndex !== undefined && matchCounts !== undefined) {\n      const count = matchCounts.get(fixture) ?? 0;\n      if (count !== match.sequenceIndex) {\n        skippedBySequenceOrTurn++;\n        continue;\n      }\n    }\n\n    // turnIndex is normally NOT a hard gate (replay). Multi-step agents emit\n    // several assistant bubbles per logical turn, so a canonical run's assistant\n    // count routinely differs from a fixture's hardcoded turnIndex even when the\n    // request content matches exactly. Rejecting a uniquely content-matching\n    // fixture on absolute position produced false \"empty assistant response\"\n    // misses. Instead we collect every content match and use turnIndex only as a\n    // non-fatal DISAMBIGUATOR to choose AMONG several content-matching fixtures\n    // (see selectByTurnIndex below). Content that does not match any fixture\n    // still matches nothing — only the position gate is relaxed.\n    //\n    // Under strictTurnIndex (record mode) turnIndex stays a hard, exact gate so\n    // an earlier-turn capture can't shadow a longer request; the miss then\n    // proxies upstream and records the new turn.\n    if (strictTurnIndex && match.turnIndex !== undefined) {\n      const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n      if (assistantCount !== match.turnIndex) {\n        skippedBySequenceOrTurn++;\n        continue;\n      }\n    }\n\n    contentMatches.push(fixture);\n  }\n\n  if (contentMatches.length === 0) {\n    return { fixture: null, skippedBySequenceOrTurn };\n  }\n\n  const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n  const { fixture: selected, byUniquePosition } = selectByTurnIndex(contentMatches, assistantCount);\n\n  // Divergence predicate: the served fixture carries a turnIndex that does NOT\n  // sit at the current assistant position. Under strict matching this fixture\n  // would have been rejected at the gate above, so serving it here is the (rare,\n  // off-by-N) relaxed behaviour change PR #276 introduced. Computed from values\n  // already in hand — no second matching pass.\n  const selectedTurn = selected.match.turnIndex;\n  const turnIndexRelaxed = selectedTurn !== undefined && selectedTurn !== assistantCount;\n  // `matchedBy` reports \"turnIndex\" ONLY when the selection was genuinely decided\n  // by a UNIQUE positional criterion (a single candidate whose turnIndex sits\n  // exactly at the current assistant count). A canonical-position fixture that\n  // tied with another at-position candidate, or that lost the exact-turn\n  // tie-break to an earlier fallback, was decided by REGISTRATION ORDER, not by\n  // position — those are \"content\". `selectByTurnIndex` reports which it was.\n  const matchedBy: \"content\" | \"turnIndex\" = byUniquePosition ? \"turnIndex\" : \"content\";\n\n  if (turnIndexRelaxed && options?.logger) {\n    // Throttle: warn at most once per divergent fixture per process. Keyed by\n    // the fixture's OBJECT IDENTITY so distinct fixtures whose match serialises\n    // identically (predicate/regex collisions) each warn, and entries auto-evict\n    // when the fixture is released (see warnedRelaxedFixtures above).\n    if (!warnedRelaxedFixtures.has(selected)) {\n      warnedRelaxedFixtures.add(selected);\n      // Human-readable description for the message only (NOT the throttle key,\n      // which is the fixture's object identity). `JSON.stringify(match)` is\n      // unfit here: it DROPS `predicate` functions entirely and collapses any\n      // RegExp matcher to `{}`, so a predicate/regex fixture's warning read\n      // \"served fixture {}\" / \"{\"userMessage\":{}}\". `describeMatch` instead\n      // summarizes the present matcher KEYS (annotating predicate/regex values)\n      // so the warned fixture is identifiable.\n      const idx = fixtures.indexOf(selected);\n      const desc = describeMatch(selected.match, idx);\n      options.logger.warn(\n        `turnIndex relaxed: served fixture ${desc} at assistantCount=${assistantCount} ` +\n          `(scripted turnIndex=${selectedTurn}); set AIMOCK_STRICT_TURN_INDEX=1 to restore strict matching`,\n      );\n    }\n  }\n\n  return { fixture: selected, skippedBySequenceOrTurn, turnIndexRelaxed, matchedBy };\n}\n\n/**\n * Build a stable, human-readable identifier for a fixture's match shape for the\n * relaxed-turnIndex warning. The previous `JSON.stringify(match)` was unfit: it\n * DROPS `predicate` functions (non-serialisable) and serialises any RegExp\n * matcher to `{}`, so a predicate- or regex-gated fixture's warning collapsed to\n * an uninformative \"served fixture {}\" / `{\"userMessage\":{}}` blob.\n *\n * Instead we list the PRESENT matcher keys in declaration order, annotating each\n * by VALUE KIND so predicates and regexes survive: `predicate(fn)`,\n * `userMessage(regex)`, `userMessage(\"hello\")`, `turnIndex=0`, etc. The\n * fixture's array `index` (when known, i.e. `>= 0`) is prefixed as the stable\n * positional identifier — the `Fixture` type carries no `id`/`name`, so its\n * registration index is the only stable handle. String/number values are shown\n * inline (truncated) so a content match remains recognisable; the whole string\n * is capped to keep the log line bounded.\n */\nfunction describeMatch(match: FixtureMatch, index: number): string {\n  const parts: string[] = [];\n  for (const [key, value] of Object.entries(match)) {\n    if (value === undefined) continue;\n    if (typeof value === \"function\") {\n      parts.push(`${key}(fn)`);\n    } else if (value instanceof RegExp) {\n      parts.push(`${key}(${value})`);\n    } else if (typeof value === \"string\") {\n      const v = value.length > 40 ? `${value.slice(0, 40)}…` : value;\n      parts.push(`${key}(${JSON.stringify(v)})`);\n    } else if (Array.isArray(value)) {\n      parts.push(`${key}(${value.length} item${value.length === 1 ? \"\" : \"s\"})`);\n    } else {\n      parts.push(`${key}=${String(value)}`);\n    }\n  }\n  const keys = parts.length > 0 ? parts.join(\", \") : \"no matchers\";\n  const prefix = index >= 0 ? `#${index} ` : \"\";\n  return `${prefix}{ ${keys} }`.slice(0, 160);\n}\n\n/**\n * Choose one fixture from a set that all CONTENT-matched the same request,\n * using `turnIndex` purely as a position disambiguator (never as a reject\n * gate).\n *\n * The selection rule is applied UNIFORMLY regardless of candidate count (a\n * single candidate is NOT special-cased), so the same request never flips its\n * answer just because an unrelated content-matching fixture was registered.\n * Within every tier ties are broken by REGISTRATION ORDER — the\n * earliest-registered eligible candidate wins — preserving the historical\n * greedy \"first matching fixture wins\" contract.\n *\n *  1. Prefer the turnIndexed candidate whose `turnIndex` is closest to\n *     `assistantCount` WITHOUT exceeding it (the highest `turnIndex <=\n *     assistantCount`). A behind-the-count scripted turn (turnIndex <\n *     assistantCount) beats a plain fallback — an explicit position is a\n *     stronger signal than an unpositioned default. A negative `turnIndex` such\n *     as -1 is a valid at/behind position (the seed is `-Infinity`, never a `-1`\n *     sentinel that would mis-skip it). Earlier registration breaks ties among\n *     equal turnIndexes.\n *  2. EXACT-turn tie-break: when the best at/behind scripted turn sits at the\n *     EXACT current position (`turnIndex === assistantCount`) a plain fallback\n *     also answers \"right now\", so the two are equally eligible and REGISTRATION\n *     ORDER decides — a later-registered `turnIndex:0` does NOT override an\n *     earlier-registered fallback, and vice-versa.\n *  3. Otherwise every turnIndexed candidate is still AHEAD of the conversation.\n *     An explicit future turn must NOT answer an earlier point, so a plain\n *     fallback (eligible at every position) is the better answer — applied\n *     uniformly, INCLUDING when the fallback is the sole partner of a single\n *     future-turn fixture (the single/multi asymmetry this fixes).\n *  4. Otherwise (pure script, every candidate turnIndexed and all ahead) the\n *     script genuinely has no earlier answer, so serve the lowest `turnIndex`\n *     candidate — the false-red-kill for a lone scripted turn whose run has\n *     FEWER assistant bubbles than its `turnIndex`; registration order breaks\n *     ties.\n *\n * A future-turn fixture therefore NEVER answers an earlier-point request when an\n * eligible alternative (a fallback, or an at/behind scripted turn) exists — the\n * future-turn guard is enforced uniformly for single and multiple candidates.\n *\n * Returns the selected fixture alongside `byUniquePosition`: `true` ONLY when the\n * choice was decided by a UNIQUE positional criterion — the served fixture's\n * `turnIndex` sits EXACTLY at `assistantCount`, no earlier fallback overrode it\n * (tier 2), and no other candidate shared that exact position (so registration\n * order did not break a tie). `matchFixtureDiagnostic` maps this to\n * `matchedBy === \"turnIndex\"`; every other selection path (tie-break,\n * registration order, behind/ahead scripted turn, fallback) is `\"content\"`.\n */\nfunction selectByTurnIndex(\n  candidates: Fixture[],\n  assistantCount: number,\n): { fixture: Fixture; byUniquePosition: boolean } {\n  // The first non-turnIndexed candidate is the registration-order-first plain\n  // fallback (eligible at every position). Tracked by index so the exact-turn\n  // tie-break can compare registration order against the chosen scripted turn.\n  const fallbackIdx = candidates.findIndex((f) => f.match.turnIndex === undefined);\n\n  // Tier 1: closest scripted turn at/before the current count. Strict `>`\n  // preserves registration order on equal turnIndexes; `-Infinity` seed so a\n  // negative turnIndex is a legitimate at/behind candidate, not a sentinel skip.\n  let bestIdx = -1;\n  let bestTurn = -Infinity;\n  for (let i = 0; i < candidates.length; i++) {\n    const t = candidates[i].match.turnIndex;\n    if (t === undefined) continue;\n    if (t <= assistantCount && t > bestTurn) {\n      bestIdx = i;\n      bestTurn = t;\n    }\n  }\n\n  if (bestIdx !== -1) {\n    // Tier 2: exact-turn tie with a fallback → earlier registration wins. A\n    // fallback won the tie, so position did NOT uniquely decide → content.\n    if (bestTurn === assistantCount && fallbackIdx !== -1 && fallbackIdx < bestIdx) {\n      return { fixture: candidates[fallbackIdx], byUniquePosition: false };\n    }\n    // A UNIQUE positional decision requires the chosen turn to sit EXACTLY at\n    // the current count AND to be the only candidate at that exact position —\n    // otherwise registration order, not position, broke the tie.\n    const atExactPosition =\n      bestTurn === assistantCount &&\n      candidates.filter((f) => f.match.turnIndex === assistantCount).length === 1;\n    return { fixture: candidates[bestIdx], byUniquePosition: atExactPosition };\n  }\n\n  // Tier 3: every scripted turn is ahead. A plain fallback answers this earlier\n  // point; first-registered fallback wins.\n  if (fallbackIdx !== -1) return { fixture: candidates[fallbackIdx], byUniquePosition: false };\n\n  // Tier 4: pure script, all turnIndexed and all ahead. Serve the lowest\n  // scripted turn; registration order breaks ties (first of the lowest wins).\n  let lowest = candidates[0];\n  for (const f of candidates) {\n    if ((f.match.turnIndex as number) < (lowest.match.turnIndex as number)) lowest = f;\n  }\n  return { fixture: lowest, byUniquePosition: false };\n}\n\n/**\n * Match a fixture against a request, returning the fixture or `null`. Thin\n * wrapper over {@link matchFixtureDiagnostic} that discards the skip diagnostic\n * — preserves the historical signature for all existing callers.\n */\nexport function matchFixture(\n  fixtures: Fixture[],\n  req: ChatCompletionRequest,\n  matchCounts?: Map<Fixture, number>,\n  requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n  options?: MatchOptions,\n): Fixture | null {\n  return matchFixtureDiagnostic(fixtures, req, matchCounts, requestTransform, options).fixture;\n}\n"],"mappings":"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"}