import type { ChatMessage, Logger, ProviderEvent, Span } from "@dispatch/kernel"; import type { HttpExchangeFixture } from "@dispatch/trace-replay"; import { loadFixture, recordFetch, replayFetch, serializeFixture } from "@dispatch/trace-replay"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { type StreamConfig, streamChat } from "./stream.js"; async function collectEvents(iter: AsyncIterable): Promise { const events: ProviderEvent[] = []; for await (const event of iter) { events.push(event); } return events; } function assertDefined(v: T, msg?: string): asserts v is NonNullable { if (v === undefined || v === null) { throw new Error(msg ?? "expected defined"); } } interface CapturedSpan { name: string; attrs: Record; body?: string | undefined; endOutcome?: | { err?: unknown; attrs?: Record } | undefined; } function createFakeLogger(): { logger: Logger; spans: CapturedSpan[] } { const spans: CapturedSpan[] = []; let spanAttrBuffer: Record = {}; let spanBodyBuffer: string | undefined; const fakeSpan: Span = { id: "fake-span-id", log: {} as Logger, setAttributes(attrs) { Object.assign(spanAttrBuffer, attrs); }, addLink() {}, child() { return fakeSpan; }, end(outcome?) { spans.push({ name: "provider.request", attrs: { ...spanAttrBuffer }, body: spanBodyBuffer, endOutcome: outcome as CapturedSpan["endOutcome"], }); }, }; const logger: Logger = { debug() {}, info() {}, warn() {}, error() {}, child() { return logger; }, span(_name, attrs, body) { spanAttrBuffer = attrs ? { ...attrs } : {}; spanBodyBuffer = body; return fakeSpan; }, }; return { logger, spans }; } function makeConfig(apiKey = "sk-test-1234567890abcdef"): StreamConfig { return { baseURL: "https://api.example.com/v1", apiKey, model: "test-model", }; } function mockFetch(handler: (url: string | URL | Request, init?: RequestInit) => unknown): void { globalThis.fetch = vi.fn(handler) as unknown as typeof globalThis.fetch; } function makeMessages(): readonly ChatMessage[] { return [ { role: "user", chunks: [{ type: "text", text: "Hello" }], }, ]; } function sseBody(...lines: string[]): ReadableStream { const encoder = new TextEncoder(); const chunks = lines.map((l) => encoder.encode(`${l}\n`)); let index = 0; return new ReadableStream({ pull(controller) { if (index < chunks.length) { const chunk = chunks[index]; assertDefined(chunk); controller.enqueue(chunk); index++; } else { controller.close(); } }, }); } describe("streamChat — provider.request AFTER capture", () => { let originalFetch: typeof globalThis.fetch; beforeEach(() => { originalFetch = globalThis.fetch; }); afterEach(() => { globalThis.fetch = originalFetch; }); it("opens a provider.request span with verbatim request body", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig(); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-1","choices":[{"delta":{"content":"Hi"},"index":0}]}', 'data: {"id":"cmpl-1","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); const events = await collectEvents(streamChat(config, makeMessages(), [], { logger })); expect(events.some((e) => e.type === "text-delta")).toBe(true); expect(spans).toHaveLength(1); assertDefined(spans[0]); const span = spans[0]; expect(span.name).toBe("provider.request"); expect(span.attrs["request.method"]).toBe("POST"); expect(span.attrs["request.body"]).toBeUndefined(); assertDefined(span.body); const capturedBody = JSON.parse(span.body); expect(capturedBody.model).toBe("test-model"); expect(capturedBody.stream).toBe(true); expect(capturedBody.messages).toEqual([{ role: "user", content: "Hello" }]); expect(span.endOutcome?.attrs?.status).toBe(200); }); it("redacts a long API key (≥13 chars → reveal 3 each side)", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig("sk-abcdefghijkmnop"); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-2","choices":[{"delta":{"content":"ok"},"index":0}]}', 'data: {"id":"cmpl-2","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); await collectEvents(streamChat(config, makeMessages(), [], { logger })); assertDefined(spans[0]); const span = spans[0]; const authHeader = span.attrs["request.headers.authorization"] as string; expect(authHeader).toBe("Bearer sk-…redacted…nop"); expect(authHeader).not.toContain("abcdefghijkm"); }); it("redacts a medium API key (8–10 chars → reveal 1 each side)", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig("sk-abcde"); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-3","choices":[{"delta":{"content":"ok"},"index":0}]}', 'data: {"id":"cmpl-3","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); await collectEvents(streamChat(config, makeMessages(), [], { logger })); assertDefined(spans[0]); const span = spans[0]; const authHeader = span.attrs["request.headers.authorization"] as string; expect(authHeader).toBe("Bearer s…redacted…e"); }); it("redacts a short API key (≤7 chars → full mask)", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig("secret!"); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-4","choices":[{"delta":{"content":"ok"},"index":0}]}', 'data: {"id":"cmpl-4","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); await collectEvents(streamChat(config, makeMessages(), [], { logger })); assertDefined(spans[0]); const span = spans[0]; const authHeader = span.attrs["request.headers.authorization"] as string; expect(authHeader).toBe("Bearer …redacted…"); }); it("captures cache tokens from the response", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig(); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-5","choices":[{"delta":{"content":"Hi"},"index":0}]}', 'data: {"id":"cmpl-5","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', 'data: {"id":"cmpl-5","usage":{"prompt_tokens":100,"completion_tokens":20,"cache_read_tokens":80,"cache_write_tokens":10}}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); await collectEvents(streamChat(config, makeMessages(), [], { logger })); assertDefined(spans[0]); const span = spans[0]; expect(span.endOutcome?.attrs?.["usage.inputTokens"]).toBe(100); expect(span.endOutcome?.attrs?.["usage.outputTokens"]).toBe(20); expect(span.endOutcome?.attrs?.["usage.cacheReadTokens"]).toBe(80); expect(span.endOutcome?.attrs?.["usage.cacheWriteTokens"]).toBe(10); }); it("captures cache_read_tokens alone", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig(); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-6","choices":[{"delta":{"content":"ok"},"index":0}]}', 'data: {"id":"cmpl-6","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', 'data: {"id":"cmpl-6","usage":{"prompt_tokens":50,"completion_tokens":5,"cache_read_tokens":45}}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); await collectEvents(streamChat(config, makeMessages(), [], { logger })); assertDefined(spans[0]); const span = spans[0]; expect(span.endOutcome?.attrs?.["usage.cacheReadTokens"]).toBe(45); expect(span.endOutcome?.attrs?.["usage.cacheWriteTokens"]).toBeUndefined(); }); it("records HTTP error status and error body without throwing", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig(); mockFetch( () => new Response("Invalid request body", { status: 400, headers: { "Content-Type": "text/plain" }, }), ); const events = await collectEvents(streamChat(config, makeMessages(), [], { logger })); expect(events).toHaveLength(1); expect(events[0]).toEqual({ type: "error", message: "HTTP 400: Invalid request body", code: "400", retryable: false, }); expect(spans).toHaveLength(1); assertDefined(spans[0]); const span = spans[0]; expect(span.endOutcome?.attrs?.status).toBe(400); expect(span.endOutcome?.attrs?.["response.error_body"]).toBe("Invalid request body"); expect(span.endOutcome?.err).toBeInstanceOf(Error); }); it("records network error without throwing", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig(); mockFetch(() => { throw new Error("connection refused"); }); const events = await collectEvents(streamChat(config, makeMessages(), [], { logger })); expect(events).toHaveLength(1); expect(events[0]).toEqual({ type: "error", message: "connection refused", retryable: true, }); expect(spans).toHaveLength(1); assertDefined(spans[0]); const span = spans[0]; expect(span.endOutcome?.err).toBeInstanceOf(Error); expect((span.endOutcome?.err as Error).message).toBe("connection refused"); }); it("detects cache_control breakpoint absence in a normal request body", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig(); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-7","choices":[{"delta":{"content":"ok"},"index":0}]}', 'data: {"id":"cmpl-7","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); await collectEvents(streamChat(config, makeMessages(), [], { logger })); assertDefined(spans[0]); const span = spans[0]; expect(span.attrs["request.cache_control_present"]).toBe(false); }); it("does not open a span when opts.logger is absent", async () => { const config = makeConfig(); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-8","choices":[{"delta":{"content":"ok"},"index":0}]}', 'data: {"id":"cmpl-8","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); const events = await collectEvents(streamChat(config, makeMessages(), [])); expect(events.some((e) => e.type === "text-delta")).toBe(true); }); it("fail-safe: logger throwing does not break stream()", async () => { const brokenLogger: Logger = { debug() {}, info() {}, warn() {}, error() {}, child() { return brokenLogger; }, span() { throw new Error("logger exploded"); }, }; const config = makeConfig(); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-9","choices":[{"delta":{"content":"ok"},"index":0}]}', 'data: {"id":"cmpl-9","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); const events = await collectEvents( streamChat(config, makeMessages(), [], { logger: brokenLogger }), ); expect(events.some((e) => e.type === "text-delta")).toBe(true); expect(events.some((e) => e.type === "finish")).toBe(true); }); it("redacts an 11-char API key (reveal 2 each side)", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig("sk-abcde1234"); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-10","choices":[{"delta":{"content":"ok"},"index":0}]}', 'data: {"id":"cmpl-10","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); await collectEvents(streamChat(config, makeMessages(), [], { logger })); assertDefined(spans[0]); const span = spans[0]; const authHeader = span.attrs["request.headers.authorization"] as string; expect(authHeader).toBe("Bearer sk…redacted…34"); }); it("records server error (500) as retryable", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig(); mockFetch( () => new Response("Internal Server Error", { status: 500, headers: { "Content-Type": "text/plain" }, }), ); const events = await collectEvents(streamChat(config, makeMessages(), [], { logger })); expect(events).toHaveLength(1); expect(events[0]).toEqual({ type: "error", message: "HTTP 500: Internal Server Error", code: "500", retryable: true, }); expect(spans).toHaveLength(1); assertDefined(spans[0]); expect(spans[0].endOutcome?.attrs?.status).toBe(500); }); it("captures model and url on the span", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig(); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-11","choices":[{"delta":{"content":"ok"},"index":0}]}', 'data: {"id":"cmpl-11","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); await collectEvents(streamChat(config, makeMessages(), [], { logger })); assertDefined(spans[0]); const span = spans[0]; expect(span.attrs.model).toBe("test-model"); expect(span.attrs.url).toBe("https://api.example.com/v1/chat/completions"); }); it("uses opts.model override in capture", async () => { const { logger, spans } = createFakeLogger(); const config = makeConfig(); mockFetch( () => new Response( sseBody( 'data: {"id":"cmpl-12","choices":[{"delta":{"content":"ok"},"index":0}]}', 'data: {"id":"cmpl-12","choices":[{"delta":{},"finish_reason":"stop","index":0}]}', "data: [DONE]", ), { status: 200, headers: { "Content-Type": "text/event-stream" } }, ), ); await collectEvents( streamChat(config, makeMessages(), [], { logger, model: "override-model" }), ); assertDefined(spans[0]); const span = spans[0]; expect(span.attrs.model).toBe("override-model"); assertDefined(span.body); const capturedBody = JSON.parse(span.body); expect(capturedBody.model).toBe("override-model"); }); }); describe("streamChat — hermetic replay (trace-replay)", () => { const testDir = new URL(".", import.meta.url).pathname; const fixturePath = `${testDir}__fixtures__/flash-text-turn.json`; const toolFixturePath = `${testDir}__fixtures__/tool-call-turn.json`; it("replays a text-turn fixture and produces correct ProviderEvents", async () => { const fixture = loadFixture(fixturePath); const { fetch: replayFetchFn, getCapturedRequest } = replayFetch(fixture, { chunkBytes: 64 }); const config: StreamConfig = { baseURL: "https://api.example.com/v1", apiKey: "sk-test-1234567890abcdef", model: "deepseek-v4-flash", fetchFn: replayFetchFn, }; const messages: ChatMessage[] = [ { role: "user", chunks: [{ type: "text", text: "Hello, how are you?" }] }, ]; const events = await collectEvents(streamChat(config, messages, [])); const textDeltas = events.filter( (e): e is Extract => e.type === "text-delta", ); const fullText = textDeltas.map((e) => e.delta).join(""); expect(fullText).toBe("Hello there friend"); const finishEvents = events.filter((e) => e.type === "finish"); expect(finishEvents).toHaveLength(1); expect(finishEvents[0]).toEqual({ type: "finish", reason: "stop" }); const usageEvents = events.filter( (e): e is Extract => e.type === "usage", ); expect(usageEvents).toHaveLength(1); expect(usageEvents[0]?.usage.inputTokens).toBe(665); expect(usageEvents[0]?.usage.outputTokens).toBe(90); expect(usageEvents[0]?.usage.cacheReadTokens).toBe(384); const captured = getCapturedRequest(); assertDefined(captured); expect(captured.method).toBe("POST"); expect(captured.url).toBe("https://api.example.com/v1/chat/completions"); expect(captured.headers["Content-Type"]).toBe("application/json"); expect(captured.headers.Authorization).toBe("Bearer sk-test-1234567890abcdef"); assertDefined(captured.body); const capturedBody = JSON.parse(captured.body); expect(capturedBody.model).toBe("deepseek-v4-flash"); expect(capturedBody.stream).toBe(true); expect(capturedBody.messages).toEqual([{ role: "user", content: "Hello, how are you?" }]); }); it("replays a tool-call-turn fixture and produces tool-call + finish events", async () => { const fixture = loadFixture(toolFixturePath); const { fetch: replayFetchFn, getCapturedRequest } = replayFetch(fixture, { chunkBytes: 48 }); const config: StreamConfig = { baseURL: "https://api.example.com/v1", apiKey: "sk-test-1234567890abcdef", model: "deepseek-v4-flash", fetchFn: replayFetchFn, }; const messages: ChatMessage[] = [ { role: "user", chunks: [{ type: "text", text: "What is the weather in Tokyo?" }] }, ]; const weatherTool = { name: "get_weather", description: "Get current weather for a location", parameters: { type: "object" as const, properties: { location: { type: "string" as const } }, required: ["location"], }, execute: async () => ({ content: "" }), }; const events = await collectEvents(streamChat(config, messages, [weatherTool])); const toolCalls = events.filter( (e): e is Extract => e.type === "tool-call", ); expect(toolCalls).toHaveLength(1); expect(toolCalls[0]?.toolCallId).toBe("call_abc123"); expect(toolCalls[0]?.toolName).toBe("get_weather"); expect(toolCalls[0]?.input).toEqual({ location: "Tokyo" }); const finishEvents = events.filter((e) => e.type === "finish"); expect(finishEvents).toHaveLength(1); expect(finishEvents[0]).toEqual({ type: "finish", reason: "tool_calls" }); const usageEvents = events.filter( (e): e is Extract => e.type === "usage", ); expect(usageEvents).toHaveLength(1); expect(usageEvents[0]?.usage.inputTokens).toBe(45); expect(usageEvents[0]?.usage.outputTokens).toBe(12); expect(usageEvents[0]?.usage.cacheReadTokens).toBe(30); expect(usageEvents[0]?.usage.cacheWriteTokens).toBe(5); const captured = getCapturedRequest(); assertDefined(captured); expect(captured.method).toBe("POST"); assertDefined(captured.body); const capturedBody = JSON.parse(captured.body); expect(capturedBody.tools).toHaveLength(1); expect(capturedBody.tools[0].function.name).toBe("get_weather"); }); }); describe("streamChat — record-mode redaction (trace-replay)", () => { /** * Graduated secret mask — §6 tiers. Duplicated locally (isolation-over-dry). * ≥13 → reveal 3 each side · 11–12 → 2 · 8–10 → 1 · ≤7 → full mask. */ function maskSecret(value: string): string { const len = value.length; if (len <= 7) return "…redacted…"; let reveal: number; if (len >= 13) { reveal = 3; } else if (len >= 11) { reveal = 2; } else { reveal = 1; } return `${value.slice(0, reveal)}…redacted…${value.slice(-reveal)}`; } it("self-redacts auth header in onExchange and produces a secret-free fixture", async () => { const apiKey = "sk-abcdefghijkmnop"; const responseBody = 'data: {"id":"cmpl-r","choices":[{"delta":{"content":"ok"},"index":0}],"usage":{"prompt_tokens":5,"completion_tokens":1,"cache_read_tokens":0,"cache_write_tokens":0}}\n\ndata: [DONE]\n'; let capturedFixture: HttpExchangeFixture | undefined; const wrappedFetch = recordFetch( async () => new Response(responseBody, { status: 200, headers: { "content-type": "text/event-stream" }, }), (fx) => { const redactedHeaders: Record = {}; for (const [key, value] of Object.entries(fx.request.headers)) { if (key.toLowerCase() === "authorization") { const token = value.replace(/^Bearer\s+/i, ""); redactedHeaders[key] = `Bearer ${maskSecret(token)}`; } else { redactedHeaders[key] = value; } } capturedFixture = { request: { ...fx.request, headers: redactedHeaders }, response: fx.response, ...(fx.meta !== undefined ? { meta: fx.meta } : {}), }; }, ); await wrappedFetch("https://api.example.com/v1/chat/completions", { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}`, }, body: '{"model":"test","messages":[{"role":"user","content":"hi"}],"stream":true}', }); assertDefined(capturedFixture); expect(capturedFixture.request.headers.authorization).toBe("Bearer sk-…redacted…nop"); expect(capturedFixture.request.headers.authorization).not.toContain("abcdefghijkm"); expect(capturedFixture.request.headers["content-type"]).toBe("application/json"); expect(capturedFixture.request.body).toContain('"model":"test"'); expect(capturedFixture.request.body).toContain('"content":"hi"'); expect(capturedFixture.response.status).toBe(200); expect(capturedFixture.response.body).toBe(responseBody); const serialized = serializeFixture(capturedFixture); expect(serialized).toContain("Bearer sk-…redacted…nop"); expect(serialized).not.toContain("abcdefghijkm"); expect(serialized).toContain("content"); expect(serialized).toContain("hi"); }); it("redacts capitalized Authorization header (the real leak casing)", async () => { const apiKey = "sk-LIVEKEY1234567890abcdef"; const responseBody = "data: [DONE]\n"; let capturedFixture: HttpExchangeFixture | undefined; const wrappedFetch = recordFetch( async () => new Response(responseBody, { status: 200, headers: { "content-type": "text/event-stream" }, }), (fx) => { const redactedHeaders: Record = {}; for (const [key, value] of Object.entries(fx.request.headers)) { if (key.toLowerCase() === "authorization") { const token = value.replace(/^Bearer\s+/i, ""); redactedHeaders[key] = `Bearer ${maskSecret(token)}`; } else { redactedHeaders[key] = value; } } capturedFixture = { request: { ...fx.request, headers: redactedHeaders }, response: fx.response, }; }, ); await wrappedFetch("https://api.example.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: '{"model":"test","messages":[],"stream":true}', }); assertDefined(capturedFixture); expect(capturedFixture.request.headers.Authorization).toBe("Bearer sk-…redacted…def"); expect(capturedFixture.request.headers.Authorization).not.toContain("LIVEKEY1234567890abc"); const serialized = serializeFixture(capturedFixture); expect(serialized).not.toContain("LIVEKEY1234567890abc"); expect(serialized).toContain("Bearer sk-…redacted…def"); }); it("redacts lowercase authorization header", async () => { const apiKey = "sk-abcdefghijkmnop"; let capturedFixture: HttpExchangeFixture | undefined; const wrappedFetch = recordFetch( async () => new Response("data: [DONE]\n", { status: 200 }), (fx) => { const redactedHeaders: Record = {}; for (const [key, value] of Object.entries(fx.request.headers)) { if (key.toLowerCase() === "authorization") { const token = value.replace(/^Bearer\s+/i, ""); redactedHeaders[key] = `Bearer ${maskSecret(token)}`; } else { redactedHeaders[key] = value; } } capturedFixture = { request: { ...fx.request, headers: redactedHeaders }, response: fx.response, }; }, ); await wrappedFetch("https://api.example.com/v1/chat/completions", { method: "POST", headers: { authorization: `Bearer ${apiKey}` }, body: null, }); assertDefined(capturedFixture); expect(capturedFixture.request.headers.authorization).toBe("Bearer sk-…redacted…nop"); expect(capturedFixture.request.headers.authorization).not.toContain("abcdefghijkm"); }); it("guard: no header named authorization (any case) survives with a raw sk- token", async () => { const apiKey = "sk-REALKEY_1234567890abcdef"; let capturedFixture: HttpExchangeFixture | undefined; const wrappedFetch = recordFetch( async () => new Response("data: [DONE]\n", { status: 200 }), (fx) => { const redactedHeaders: Record = {}; for (const [key, value] of Object.entries(fx.request.headers)) { if (key.toLowerCase() === "authorization") { const token = value.replace(/^Bearer\s+/i, ""); redactedHeaders[key] = `Bearer ${maskSecret(token)}`; } else { redactedHeaders[key] = value; } } capturedFixture = { request: { ...fx.request, headers: redactedHeaders }, response: fx.response, }; }, ); await wrappedFetch("https://api.example.com/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${apiKey}` }, body: null, }); assertDefined(capturedFixture); for (const [key, value] of Object.entries(capturedFixture.request.headers)) { if (key.toLowerCase() === "authorization") { expect(value).not.toContain(apiKey); expect(value).not.toMatch(/sk-[A-Za-z0-9]{10,}/); } } const serialized = serializeFixture(capturedFixture); expect(serialized).not.toContain(apiKey); expect(serialized).not.toMatch(/sk-[A-Za-z0-9]{10,}/); }); it("redacts a short API key (≤7 chars → full mask)", async () => { let capturedFixture: HttpExchangeFixture | undefined; const wrappedFetch = recordFetch( async () => new Response("data: [DONE]\n", { status: 200 }), (fx) => { const redactedHeaders: Record = {}; for (const [key, value] of Object.entries(fx.request.headers)) { if (key.toLowerCase() === "authorization") { const token = value.replace(/^Bearer\s+/i, ""); redactedHeaders[key] = `Bearer ${maskSecret(token)}`; } else { redactedHeaders[key] = value; } } capturedFixture = { request: { ...fx.request, headers: redactedHeaders }, response: fx.response, }; }, ); await wrappedFetch("https://api.example.com/v1/chat/completions", { method: "POST", headers: { authorization: "Bearer secret!" }, body: null, }); assertDefined(capturedFixture); expect(capturedFixture.request.headers.authorization).toBe("Bearer …redacted…"); }); });