import { describe, expect, test } from "bun:test"; import { fakeLedger } from "./fakes.ts"; import { createDisabledBridge } from "../src/context/bridge.ts"; import type { ContextBridge, ContextResolveInput, TurnRecord } from "../src/context/types.ts"; import type { CatalogModel, CatalogSource } from "../src/catalog/types.ts"; import type { EscalationConfig, RouterConfig } from "../src/config/types.ts"; import { EMPTY_USAGE, type AsyncLedger, type LedgerEntry, type UsageCounts } from "../src/cost/types.ts"; import type { ConversationState, ConversationStore, Decision, Features, ProbePlan, Router, Tier, } from "../src/router/types.ts"; import { runTurn } from "../src/server/turn.ts"; import { BudgetExceededError } from "../src/router/select.ts"; import { parseMessagesRequest } from "../src/wire/anthropic/messages.ts"; import { parseChatRequest } from "../src/wire/openai/request.ts"; import { isMintedRequestId } from "../src/util/requestid.ts"; import { UpstreamError, type DispatchOptions, type UpstreamClient } from "../src/upstream/types.ts"; import type { FinishReason, NormRequest, ResponseSink, StreamEvent, TurnSummary, UpstreamChunk, WireError, } from "../src/wire/types.ts"; // ---------- fakes ---------- function mkConfig(escalation: Partial = {}): RouterConfig { return { server: { host: "127.0.0.1", port: 8787, maxConcurrentTurns: 24, subagentProfile: "auto-sub" }, openrouter: { baseUrl: "https://openrouter.ai/api/v1", apiKey: "", title: "test", timeoutMs: 30_000, catalogTtlMs: 3_600_000, catalogRefreshMs: 0, minCreditsUsd: 0, usagePollMs: 0 }, ollama: { enabled: false, baseUrl: "http://127.0.0.1:11434/v1", apiKey: "", timeoutMs: 30_000, catalogTtlMs: 300_000, includeLocal: false, prices: {}, twins: {}, costBias: 1, biasUntilUsage: 0.9, usagePollMs: 0, quotaCooldownMs: 0, rateLimitCooldownMs: 0, planCreditsUsd: 0 }, upstreams: [], benchmarks: { enabled: false, artificialAnalysisApiKey: "", benchlm: true, refreshMs: 86_400_000, timeoutMs: 30_000, useLocalScores: false }, tiers: { trivial: { minQuality: 0, maxInputPerMtok: 0.3, qualityExponent: 0, pin: [] }, simple: { minQuality: 40, maxInputPerMtok: 1.5, qualityExponent: 0, pin: [] }, moderate: { minQuality: 60, maxInputPerMtok: 4, qualityExponent: 1, pin: [] }, hard: { minQuality: 72, qualityExponent: 3, pin: [] }, }, tasks: { coding: { axis: "coding", minQuality: 40 }, vision: { axis: "intelligence", requireImage: true }, documentation: { axis: "intelligence", minQuality: 0 }, data: { axis: "intelligence", minQuality: 0 }, chat: { axis: "intelligence", minQuality: 0 }, }, filters: { allow: [], deny: [], providerLocks: {}, includeFree: false, requireToolSupport: true, agenticAxisForToolTurns: true, minAgenticForToolTurns: 0, minTrust: 0.6, feedbackWeight: 0, feedbackByTask: false, minTrustSamples: 5, trustScopedByHarness: false, trustWindowDays: 0, contextHeadroom: 1.2, reasoningCompletionFloor: 0, latencyWeight: 0, latencyReferenceMs: 5000, latencyReferenceTokensPerSec: 30, cacheReliabilityMinSamples: 10, latencyMinSamples: 20, escalationCostWeight: 0 }, classifier: { ambiguityThreshold: 0, model: "test/adjudicator", learnedModelPath: "", maxCostFraction: 0.1, maxCostUsd: 0.01, timeoutMs: 5000, cacheSize: 128, toolAxis: "coding", chatAxis: "intelligence", agenticLoopDepth: 3, mechanicalRetryFactor: 0.2, readOnlyToolWeight: 0, reasoningWeights: { medium: 0.14, high: 0.24, xhigh: 0.3, max: 0.34 }, }, escalation: { enabled: true, probeTokens: 24, maxHoldMs: 5000, maxAttempts: 3, probeTiers: ["trivial", "simple", "moderate"], triggers: ["malformed_tool_args", "refusal", "empty_completion", "repeat_tool_call", "missing_expected_tool_call"], escalateOnLengthStop: false, ...escalation, }, hysteresis: { holdTurns: 2, holdTurnsAfterEscalation: 4, switchMargin: 1.5, cacheWarmTtlMs: 600_000, maxDowngradePerTurn: 1, breakHoldOnMechanical: false, switchHorizonTurns: 1, confirmUpgradesBelowConfidence: 0.6 }, exploration: { enabled: false, rates: {}, stickyPolicy: "never", holdTurns: { enabled: false, values: [2, 3, 4] } }, cache: { injectBreakpoints: true, maxBreakpoints: 4, minPromptTokens: 1024, milestoneTokens: 20_000 }, context: { enabled: false, baseUrl: "", token: "", defaultScope: "", timeoutMs: 3_000, maxStalenessMs: 900_000, maxBlockChars: 24_000, memoryLimit: 8, docsLimit: 2, sessionLimit: 6, briefChars: 0, layers: true, recordTurns: false, injectWithoutTools: false, maxQueue: 64 }, compaction: { enabled: false, budgetTokens: 40_000, floorRatio: 1, fitToWindow: true, protectRecentTurns: 4, maxToolResultBytes: 4_096, keepHeadBytes: 512, keepTailBytes: 512, elideSupersededReads: true, collapseDuplicateResults: true, replanGrowthRatio: 1, digestToolResults: false, digestMaxPerTurn: 2 }, budget: { onExceeded: "downgrade" }, report: { baselines: [], dailySummary: false }, anthropic: { models: { "*haiku*": "auto-cheap", "claude-*": "auto" } }, harnessSwitch: { enabled: false, models: {}, minConfidence: 0.6 }, digest: { enabled: false, minBytes: 12_000, maxBytes: 400_000, tools: ["read"], fromTier: "moderate", tier: "simple", model: "", maxOutputTokens: 700, maxCostUsd: 0.02, timeoutMs: 25_000, toolAliases: {} }, profiles: [], ledger: { path: ":memory:", blendWindowDays: 7, blendMinSamples: 20, fallbackBlend: { inputPerMtok: 1, outputPerMtok: 4 }, conversationTtlMs: 86_400_000 , retentionDays: 0,}, redaction: { enabled: false, rules: [], scanTools: false }, adaptiveTierFloors: true, adaptivePriceCeilings: false, logLevel: "silent", }; } function mkReq(): NormRequest { return { protocol: "openai-chat", conversationKey: "conv-test", harnessId: "", ompSessionId: "", agentdoxScope: "", agentdoxGroup: "", agentdoxPersonal: "", agentdoxOrigin: "", isSubagent: false, requestedModel: "auto", messages: [{ role: "user", text: "hi", images: 0, textBytes: 2, toolCalls: [] }], tools: [], forcedToolChoice: false, stream: true, hasImages: false, promptBytes: 2, renderUpstreamBody: (m) => ({ model: m.slug, session_id: m.sessionId }), }; } // Neutral feature vector for fake decisions; runTurn never reads it, but the // amended contract requires it on every Decision. const FEATURES: Features = { promptTokens: 0, newContentTokens: 0, turnDepth: 0, toolCount: 0, toolSchemaBytes: 0, isToolResultContinuation: false, toolLoopDepth: 0, distinctToolsUsed: 0, lastToolFailed: false, repeatedToolCall: false, circularToolCall: false, hasImages: false, hasNewImage: false, codeBlocks: 0, codeBytes: 0, looksLikeDiff: false, complexityKeywords: [], trivialityKeywords: [], requestedReasoning: undefined, questionCount: 0, isTerseInstruction: false, }; function mkDecision(tier: Tier, slug: string, probe: Partial = {}): Decision { return { slug, fallbacks: [], tier, features: FEATURES, classification: { tier, task: "chat", confidence: 0.9, source: "heuristic", reasons: ["test"], score: 0.5 }, forecast: { slug, expectedUsd: 0.001, coldUsd: 0.002, breakdown: { freshPrompt: 0.001, cacheRead: 0, cacheWrite: 0, completion: 0.001, reasoning: 0, images: 0, request: 0, total: 0.002, tierAtPromptTokens: 0 }, assumedPromptTokens: 100, assumedCompletionTokens: 50, assumedCacheHitRate: 0, }, sessionId: "omp-conv-test", sticky: false, cacheBreakpointMessageIndices: [], compactionPlan: [], promptTokensSaved: 0, compactionSavedBytes: 0, compactionPlanTokens: 0, reasoning: undefined, maxTokens: undefined, stripAssistantReasoning: false, probe: { enabled: true, maxTokens: 24, maxHoldMs: 5000, escalateTo: null, ...probe }, considered: [], rejected: [], reasons: ["test decision"], explored: null, budgetDowngraded: false, upgradeDeferred: null, }; } function chunk(events: StreamEvent[]): UpstreamChunk { return { raw: {}, events }; } function startChunk(slug: string): UpstreamChunk { return chunk([{ type: "start", servedSlug: slug, generationId: "gen-1" }]); } function textChunk(delta: string): UpstreamChunk { return chunk([{ type: "text", delta }]); } function finishChunk(reason: FinishReason): UpstreamChunk { return chunk([{ type: "finish", reason }]); } function usageChunk(usage: Partial, cost: number | null): UpstreamChunk { return chunk([{ type: "usage", usage: { ...EMPTY_USAGE, ...usage }, reportedCostUsd: cost }]); } type FakePlan = | { kind: "chunks"; chunks: UpstreamChunk[] } | { kind: "fail"; error: UpstreamError } | { kind: "die"; chunks: UpstreamChunk[]; error: UpstreamError }; function mkUpstream(plans: FakePlan[]): { upstream: UpstreamClient; calls: DispatchOptions[] } { const calls: DispatchOptions[] = []; let i = 0; const upstream: UpstreamClient = { dispatch: (opts) => { calls.push(opts); const plan = plans[Math.min(i, plans.length - 1)]!; i++; if (plan.kind === "fail") return Promise.reject(plan.error); const error = plan.kind === "die" ? plan.error : null; return Promise.resolve({ generationId: () => Promise.resolve("gen-fake"), chunks: (async function* (): AsyncGenerator { for (const c of plan.chunks) yield c; if (error) throw error; })(), }); }, complete: () => Promise.reject(new Error("not used by runTurn")), fetchModels: () => Promise.resolve([]), fetchModelsForUser: () => Promise.resolve([]), }; return { upstream, calls }; } function mkRouter(decisions: Decision[]): { router: Router; calls: { attempt: number; escalateFrom?: Tier }[] } { const calls: { attempt: number; escalateFrom?: Tier }[] = []; let i = 0; const router: Router = { route: (_req, opts) => { calls.push(opts.escalateFrom !== undefined ? { attempt: opts.attempt, escalateFrom: opts.escalateFrom } : { attempt: opts.attempt }); const d = decisions[Math.min(i, decisions.length - 1)]; i++; if (!d) return Promise.reject(new Error("no decision queued")); return Promise.resolve(d); }, }; return { router, calls }; } function mkLedger(): { ledger: AsyncLedger; entries: LedgerEntry[] } { const entries: LedgerEntry[] = []; const ledger: AsyncLedger = fakeLedger({ record: async (e) => { entries.push(e); }, conversationSpend: async () => 0, spendSince: async () => 0, blendedRate: async () => null, latency: async () => null, trust: async () => null, allTrust: async () => [], tokenRatio: async () => null, recentEntries: async () => [], }); return { ledger, entries }; } function mkConversations(): { store: ConversationStore; map: Map; accrued: Map; } { const map = new Map(); // Mirrors the real store: money accumulates here, NOT through `save`. const accrued = new Map(); const store: ConversationStore = { get: async (k) => map.get(k) ?? null, load: async (k) => { const existing = map.get(k); if (existing) return existing; const fresh: ConversationState = { key: k, sessionId: `omp-${k}`, turn: 0, currentSlug: null, currentTier: null, stickyUntilTurn: 0, escalations: 0, spentUsd: 0, lastPromptTokens: 0, cacheWarmSlug: null, cacheWarmAtMs: 0, contextVersion: null, contextFetchedAtMs: 0, compactionPlan: null, updatedAtMs: 0, }; map.set(k, fresh); return fresh; }, save: async (s) => { map.set(s.key, s); }, accrue: async (k, d) => { const cur = accrued.get(k) ?? { spentUsd: 0, escalations: 0 }; cur.spentUsd += d.spentUsd ?? 0; cur.escalations += d.escalations ?? 0; accrued.set(k, cur); }, prune: async () => 0, }; return { store, map, accrued }; } function mkSink(): { sink: ResponseSink; chunks: UpstreamChunk[]; errors: WireError[]; finishes: TurnSummary[] } { const chunks: UpstreamChunk[] = []; const errors: WireError[] = []; const finishes: TurnSummary[] = []; const sink: ResponseSink = { chunk: (c) => { chunks.push(c); }, error: (e) => { errors.push(e); }, finish: (s) => { finishes.push(s); }, }; return { sink, chunks, errors, finishes }; } const catalog: CatalogSource = { get: () => Promise.resolve({ models: [], fetchedAtMs: 0 }), refresh: () => Promise.resolve({ models: [], fetchedAtMs: 0 }), peek: () => null, find: () => undefined, }; function textOut(chunks: UpstreamChunk[]): string { return chunks .flatMap((c) => c.events) .filter((e): e is Extract => e.type === "text") .map((e) => e.delta) .join(""); } // ---------- tests ---------- describe("runTurn", () => { test("a clean cheap-tier generation writes exactly one ledger entry, wasted: false", async () => { const { router } = mkRouter([mkDecision("trivial", "cheap/model", { escalateTo: "simple" })]); const { upstream } = mkUpstream([ { kind: "chunks", chunks: [ startChunk("cheap/model"), textChunk("hi"), finishChunk("stop"), usageChunk({ promptTokens: 120, cachedTokens: 100, completionTokens: 4 }, 0.0004), ], }, ]); const { ledger, entries } = mkLedger(); const { store, map } = mkConversations(); const { sink, chunks, errors, finishes } = mkSink(); await runTurn(mkReq(), sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); expect(errors).toHaveLength(0); expect(finishes).toHaveLength(1); expect(entries).toHaveLength(1); const entry = entries[0]!; expect(entry.wasted).toBe(false); expect(entry.escalationSignal).toBeNull(); expect(entry.attempt).toBe(0); expect(entry.slug).toBe("cheap/model"); expect(entry.servedSlug).toBe("cheap/model"); expect(entry.reportedUsd).toBe(0.0004); expect(entry.usage.promptTokens).toBe(120); expect(entry.finishReason).toBe("stop"); expect(entry.error).toBeNull(); expect(chunks).toHaveLength(4); expect(finishes[0]!.escalated).toBe(false); expect(finishes[0]!.attempts).toBe(1); const state = map.get("conv-test")!; expect(state.turn).toBe(1); expect(state.currentSlug).toBe("cheap/model"); expect(state.currentTier).toBe("trivial"); expect(state.lastPromptTokens).toBe(120); expect(state.spentUsd).toBeCloseTo(0.0004); // cachedTokens > 0 is direct evidence of an upstream cache. expect(state.cacheWarmSlug).toBe("cheap/model"); expect(state.cacheWarmAtMs).toBeGreaterThan(0); }); test("an escalated turn writes two entries; the client sees only the second generation", async () => { const { router, calls } = mkRouter([ mkDecision("trivial", "cheap/model", { escalateTo: "simple" }), mkDecision("simple", "better/model", { escalateTo: "moderate" }), ]); const { upstream } = mkUpstream([ { kind: "chunks", chunks: [startChunk("cheap/model"), textChunk("I'm sorry, but I can't help with that request."), finishChunk("stop")], }, { kind: "chunks", chunks: [startChunk("better/model"), textChunk("Here is the answer."), finishChunk("stop"), usageChunk({ promptTokens: 130, completionTokens: 6 }, 0.0009)], }, ]); const { ledger, entries } = mkLedger(); const { store } = mkConversations(); const { sink, chunks, errors, finishes } = mkSink(); await runTurn(mkReq(), sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); expect(errors).toHaveLength(0); expect(entries).toHaveLength(2); expect(entries[0]!.wasted).toBe(true); expect(entries[0]!.escalationSignal).toBe("refusal"); expect(entries[0]!.slug).toBe("cheap/model"); expect(entries[0]!.attempt).toBe(0); expect(entries[1]!.wasted).toBe(false); expect(entries[1]!.attempt).toBe(1); expect(entries[1]!.slug).toBe("better/model"); // A refusal indicts the provider, so a same-tier sibling is probed first; // this fake router only has the simple-tier decision left, which is the // wrong tier, so the turn then escalates one tier up for real. expect(calls).toHaveLength(3); expect(calls[0]).toEqual({ attempt: 0 }); expect(calls[1]).toEqual({ attempt: 1 }); expect(calls[2]).toEqual({ attempt: 1, escalateFrom: "trivial" }); // The held refusal text never reached the client. expect(textOut(chunks)).toBe("Here is the answer."); expect(finishes).toHaveLength(1); expect(finishes[0]!.escalated).toBe(true); expect(finishes[0]!.attempts).toBe(2); expect(finishes[0]!.servedSlug).toBe("better/model"); }); test("a committed stream is never retried, even when later chunks fail", async () => { const { router } = mkRouter([mkDecision("trivial", "cheap/model", { maxTokens: 1 })]); const { upstream, calls } = mkUpstream([ { kind: "die", chunks: [startChunk("cheap/model"), textChunk("lots of text here, plenty to commit on")], error: new UpstreamError("rate_limit", 429, "slow down", true), }, ]); const { ledger, entries } = mkLedger(); const { store } = mkConversations(); const { sink, chunks, errors, finishes } = mkSink(); await runTurn(mkReq(), sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); // Bytes reached the client, so the 429 mid-stream is surfaced, not retried. expect(calls).toHaveLength(1); expect(entries).toHaveLength(1); expect(entries[0]!.wasted).toBe(false); expect(entries[0]!.error).toContain("rate_limit"); expect(textOut(chunks)).toBe("lots of text here, plenty to commit on"); expect(errors).toHaveLength(1); expect(errors[0]!.code).toBe("rate_limit"); expect(finishes).toHaveLength(0); }); test("a non-retryable upstream error before commit reaches sink.error", async () => { const { router, calls } = mkRouter([mkDecision("trivial", "cheap/model", { escalateTo: "simple" })]); const { upstream } = mkUpstream([{ kind: "fail", error: new UpstreamError("auth", 401, "invalid key", false) }]); const { ledger, entries } = mkLedger(); const { store } = mkConversations(); const { sink, errors, finishes } = mkSink(); await runTurn(mkReq(), sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); expect(calls).toHaveLength(1); // no retry, no escalation on auth expect(finishes).toHaveLength(0); expect(errors).toHaveLength(1); expect(errors[0]).toEqual({ status: 401, code: "auth", message: "invalid key" }); expect(entries).toHaveLength(1); expect(entries[0]!.wasted).toBe(false); expect(entries[0]!.error).toContain("auth"); }); test("a budget refusal answers 402 budget_exceeded, not a 500", async () => { // `reject` mode is a refusal the router MEANS. Reported as a 500 it reads // as "the router broke" — and a team front door relaying it sends a // capped deployment looking for a crash. Anything else from `route()` is // still an internal failure. const refusing: Router = { route: () => Promise.reject(new BudgetExceededError("24h spend $0.10 > per-day budget $0.05")) }; const { upstream } = mkUpstream([{ kind: "chunks", chunks: [] }]); const { ledger, entries } = mkLedger(); const { store } = mkConversations(); const { sink, errors, finishes } = mkSink(); await runTurn(mkReq(), sink, { config: mkConfig(), router: refusing, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); expect(errors).toEqual([{ status: 402, code: "budget_exceeded", message: "24h spend $0.10 > per-day budget $0.05" }]); expect(finishes).toHaveLength(0); expect(entries).toHaveLength(0); // refused before dispatch: no turn to record }); test("an unexpected routing failure is still a 500", async () => { const broken: Router = { route: () => Promise.reject(new Error("catalog exhausted")) }; const { upstream } = mkUpstream([{ kind: "chunks", chunks: [] }]); const { ledger } = mkLedger(); const { store } = mkConversations(); const { sink, errors } = mkSink(); await runTurn(mkReq(), sink, { config: mkConfig(), router: broken, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); expect(errors).toEqual([{ status: 500, code: "router_error", message: "catalog exhausted" }]); }); test("a 429 before commit fails over to a different model in the same tier", async () => { const { router, calls } = mkRouter([ mkDecision("trivial", "cheap/model", { escalateTo: "simple" }), mkDecision("trivial", "spare/model", { escalateTo: "simple" }), ]); const { upstream } = mkUpstream([ { kind: "fail", error: new UpstreamError("rate_limit", 429, "slow down", true) }, { kind: "chunks", chunks: [startChunk("spare/model"), textChunk("done"), finishChunk("stop"), usageChunk({}, 0.001)] }, ]); const { ledger, entries } = mkLedger(); const { store } = mkConversations(); const { sink, errors, finishes } = mkSink(); await runTurn(mkReq(), sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); expect(errors).toHaveLength(0); expect(finishes).toHaveLength(1); expect(entries).toHaveLength(2); expect(entries[0]!.wasted).toBe(true); expect(entries[0]!.error).toContain("rate_limit"); expect(entries[0]!.escalationSignal).toBeNull(); // same-tier failover, not an escalation expect(entries[0]!.slug).toBe("cheap/model"); expect(entries[1]!.wasted).toBe(false); expect(entries[1]!.slug).toBe("spare/model"); expect(entries[1]!.tier).toBe("trivial"); expect(calls).toHaveLength(2); expect(calls[0]).toEqual({ attempt: 0 }); expect(calls[1]).toEqual({ attempt: 1 }); expect(finishes[0]!.escalated).toBe(false); expect(finishes[0]!.attempts).toBe(2); expect(finishes[0]!.servedSlug).toBe("spare/model"); }); test("a stable tier does not re-arm the hysteresis window (no permanent hard lock)", async () => { // Regression: the sticky window was re-armed on EVERY committed turn, so // once a conversation reached `hard` it stayed there forever — the // classifier kept saying trivial but the window kept getting pushed out. // A stable tier must NOT extend the window; only a tier change or an // escalation re-arms it. const { router } = mkRouter([ mkDecision("hard", "strong/model", { escalateTo: null }), mkDecision("hard", "strong/model", { escalateTo: null }), ]); const { upstream } = mkUpstream([ { kind: "chunks", chunks: [startChunk("strong/model"), textChunk("a"), finishChunk("stop"), usageChunk({}, 0.001)] }, { kind: "chunks", chunks: [startChunk("strong/model"), textChunk("b"), finishChunk("stop"), usageChunk({}, 0.001)] }, ]); const { ledger } = mkLedger(); const { store, map } = mkConversations(); const { sink, errors } = mkSink(); // Turn 1: first turn, no prior tier → re-arms (tierChanged true). await runTurn(mkReq(), sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); const afterFirst = map.get("conv-test")!; expect(afterFirst.currentTier).toBe("hard"); expect(afterFirst.stickyUntilTurn).toBe(1 + 2); // holdTurns=2 // Turn 2: same tier served again → must NOT re-arm. The window should // stay at its previous expiry (turn 3), not extend to turn 4. await runTurn(mkReq(), sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); const afterSecond = map.get("conv-test")!; expect(afterSecond.currentTier).toBe("hard"); expect(afterSecond.stickyUntilTurn).toBe(3); // unchanged, not 4 expect(errors).toHaveLength(0); }); }); describe("the context scope reaches the ledger", () => { const run = async (req: NormRequest, config: RouterConfig) => { const { router } = mkRouter([mkDecision("trivial", "cheap/model", { escalateTo: "simple" })]); const { upstream } = mkUpstream([{ kind: "chunks", chunks: [startChunk("cheap/model"), textChunk("hi"), finishChunk("stop"), usageChunk({ promptTokens: 10, completionTokens: 2 }, 0.0001)] }]); const { ledger, entries } = mkLedger(); const { store } = mkConversations(); const { sink } = mkSink(); await runTurn(req, sink, { config, router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); return entries; }; test("a turn's scope is recorded, header first and the configured default behind it", async () => { // The header the team front door sets: the row can be charged to that project. expect((await run({ ...mkReq(), agentdoxScope: "acme.api" }, mkConfig()))[0]!.scope).toBe("acme.api"); // No header: the resolved scope is the configured default, which is what the bridge would have used. const base = mkConfig(); const withDefault: RouterConfig = { ...base, context: { ...base.context, defaultScope: "solo" } }; expect((await run(mkReq(), withDefault))[0]!.scope).toBe("solo"); // Neither: no scope at all, which the ledger stores as NULL. expect((await run(mkReq(), mkConfig()))[0]!.scope).toBe(""); }); }); describe("the request id reaches the ledger", () => { const runOne = async (req: NormRequest) => { const { router } = mkRouter([mkDecision("trivial", "cheap/model", { escalateTo: "simple" })]); const { upstream } = mkUpstream([{ kind: "chunks", chunks: [startChunk("cheap/model"), textChunk("hi"), finishChunk("stop"), usageChunk({ promptTokens: 10, completionTokens: 2 }, 0.0001)] }]); const { ledger, entries } = mkLedger(); const { store } = mkConversations(); const { sink } = mkSink(); await runTurn(req, sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); return entries; }; test("the front door's id is recorded verbatim, so a quoted id names this exact turn", async () => { const req = parseChatRequest({ model: "auto", messages: [{ role: "user", content: "hi" }] }, new Headers({ "x-request-id": "abc123" })); expect((await runOne(req))[0]!.requestId).toBe("abc123"); }); test("a caller that sends none still gets an addressable row, marked as minted", async () => { const req = parseChatRequest({ model: "auto", messages: [{ role: "user", content: "hi" }] }, new Headers()); const id = (await runOne(req))[0]!.requestId ?? ""; expect(isMintedRequestId(id)).toBe(true); // And it is the id the caller was handed back, not a second one invented // for the row: the header the wire read is what the ledger stores. expect(id).toBe(req.requestId ?? ""); }); test("every attempt of an escalated turn files under the ONE id the caller holds", async () => { // The customer made one request; support must land on the whole turn, // including the attempt that was thrown away. const { router } = mkRouter([mkDecision("trivial", "cheap/model", { escalateTo: "simple" }), mkDecision("simple", "better/model", { escalateTo: "moderate" })]); const { upstream } = mkUpstream([ { kind: "chunks", chunks: [startChunk("cheap/model"), textChunk("I'm sorry, but I can't help with that request."), finishChunk("stop")] }, { kind: "chunks", chunks: [startChunk("better/model"), textChunk("Here is the answer."), finishChunk("stop"), usageChunk({ promptTokens: 130, completionTokens: 6 }, 0.0009)] }, ]); const { ledger, entries } = mkLedger(); const { store } = mkConversations(); const { sink } = mkSink(); const req = parseChatRequest({ model: "auto", messages: [{ role: "user", content: "hi" }] }, new Headers({ "x-request-id": "one-request" })); await runTurn(req, sink, { config: mkConfig(), router, upstream, ledger, conversations: store, catalog, context: createDisabledBridge() }, new AbortController().signal); expect(entries).toHaveLength(2); expect(entries.map((e) => e.requestId)).toEqual(["one-request", "one-request"]); }); test("a hostile header never reaches the row; the turn is recorded under a minted id instead", async () => { for (const hostile of ["x".repeat(129), "id with spaces", "amr-forged0000", '">