import { test } from "node:test"; import assert from "node:assert/strict"; import { buildDebugExport, redactKeys, type DebugExportInput } from "./askDebug"; import type { UiMessage } from "./askConversation"; import type { DebugCall } from "./askProvider"; function input(over: Partial = {}): DebugExportInput { const messages: UiMessage[] = [ { role: "user", content: "write a report" }, { role: "assistant", content: "Here is the report…", phase: "done", truncated: true, finishReason: "length", model: "gemini-2.5-flash", }, ]; const trace: DebugCall[] = [ { phase: "answer", provider: "gemini", model: "gemini-2.5-flash", request: { system: "answer prompt", messages: [], maxTokens: 8192 }, response: "Here is the report…", finishReason: "length", rawFinish: "MAX_TOKENS", ms: 1200, }, ]; return { provider: "gemini", model: "gemini-2.5-flash", searchMode: "auto", markdownOn: true, reportMode: false, strictGrounding: true, detached: false, maxAnswerTokens: 8192, messages, report: "", grounding: { label: "wife", videoCount: 12, truncated: true }, contextOverride: null, trace, now: 0, ...over, }; } test("buildDebugExport captures state, finishReason, and the call trace", () => { const json = buildDebugExport(input()); const obj = JSON.parse(json); assert.equal(obj.meta.provider, "gemini"); assert.equal(obj.meta.model, "gemini-2.5-flash"); assert.equal(obj.meta.maxAnswerTokens, 8192); assert.equal(obj.meta.exportedAt, "1970-01-01T00:00:00.000Z"); // now:0 assert.equal(obj.messages[1].finishReason, "length"); assert.equal(obj.messages[1].truncated, true); assert.equal(obj.grounding.videoCount, 12); assert.equal(obj.trace[0].finishReason, "length"); assert.equal(obj.trace[0].rawFinish, "MAX_TOKENS"); }); test("buildDebugExport redacts anything key-shaped (never leaks an API key)", () => { // Even if a key sneaks into a message or the trace, it must be scrubbed. const messages: UiMessage[] = [ { role: "user", content: "my key is sk-ant-abcdefghijklmnop1234567890" }, ]; const trace: DebugCall[] = [ { phase: "answer", provider: "openai", request: { note: "Bearer sk-proj-ABCDEFGHIJKLMNOP1234" }, response: "AIzaSyA-1234567890abcdefghijklmnopqrstuv", ms: 1, }, ]; const json = buildDebugExport(input({ messages, trace })); assert.doesNotMatch(json, /sk-ant-[A-Za-z0-9]/); assert.doesNotMatch(json, /sk-proj-[A-Za-z0-9]/); assert.doesNotMatch(json, /AIzaSy[A-Za-z0-9]/); assert.match(json, /\[REDACTED\]/); }); test("redactKeys scrubs the three provider key shapes", () => { assert.equal(redactKeys("k=sk-ant-0123456789abcdef end"), "k=[REDACTED] end"); assert.equal(redactKeys("k=sk-0123456789abcdef0123 end"), "k=[REDACTED] end"); assert.equal( redactKeys("k=AIzaSyABCDEFGHIJ0123456789 end"), "k=[REDACTED] end", ); assert.equal(redactKeys("no key here"), "no key here"); });