import assert from "node:assert/strict"; import test from "node:test"; import type { AssistantBlock, ChatMessage } from "../src/features/agent/messages/types"; import { TRANSCRIPT_CACHE_PREFIX, boundMessagesForCache, purgeLegacyTranscriptCache, readTranscriptSnapshot, writeTranscriptSnapshot, } from "../src/features/agent/workspace/transcript-cache"; type FakeStorage = Pick; const MAX_CHARS = 512 * 1024; const BLOCK_LIMIT = 16 * 1024; const LEGACY_KEY = "local-studio.agent.transcripts.v1"; function fakeStorage(opts: { failSetTimes?: number } = {}): { storage: FakeStorage; map: Map; } { const map = new Map(); let fails = opts.failSetTimes ?? 0; const storage: FakeStorage = { getItem: (key) => map.get(key) ?? null, setItem: (key, value) => { if (fails > 0) { fails -= 1; throw new Error("QuotaExceededError"); } map.set(key, value); }, removeItem: (key) => { map.delete(key); }, get length() { return map.size; }, key: (index) => Array.from(map.keys())[index] ?? null, }; return { storage, map }; } function transcriptKeyCount(map: Map): number { let count = 0; for (const key of map.keys()) { if (key.startsWith(TRANSCRIPT_CACHE_PREFIX)) count += 1; } return count; } function userMessage(id: string, text: string): ChatMessage { return { id, role: "user", text }; } function blockHeavyMessage(id: string, blockCount: number, blockLength: number): ChatMessage { const blocks: AssistantBlock[] = []; for (let index = 0; index < blockCount; index += 1) { blocks.push({ kind: "text", id: `${id}-b${index}`, text: "y".repeat(blockLength) }); } return { id, role: "assistant", text: "", blocks }; } test("writes and reads a session transcript through its per-session key", () => { const { storage, map } = fakeStorage(); const messages: ChatMessage[] = [ userMessage("u1", "hello"), { id: "a1", role: "assistant", text: "hi" }, ]; writeTranscriptSnapshot("pi-1", messages, "Greeting", storage, 1000); assert.equal(map.has(`${TRANSCRIPT_CACHE_PREFIX}pi-1`), true); assert.equal(transcriptKeyCount(map), 1); const restored = readTranscriptSnapshot("pi-1", storage); assert.equal(restored?.length, 2); assert.equal(restored?.[0].text, "hello"); assert.equal(restored?.[1].text, "hi"); }); test("write ignores empty and idless sessions; read returns null for unknown, null, and empty", () => { const { storage, map } = fakeStorage(); writeTranscriptSnapshot("pi-1", [], "t", storage, 1000); assert.equal(map.has(`${TRANSCRIPT_CACHE_PREFIX}pi-1`), false); writeTranscriptSnapshot(null, [userMessage("u", "x")], "t", storage, 1000); assert.equal(transcriptKeyCount(map), 0); assert.equal(readTranscriptSnapshot(null, storage), null); assert.equal(readTranscriptSnapshot(undefined, storage), null); assert.equal(readTranscriptSnapshot("pi-unknown", storage), null); map.set( `${TRANSCRIPT_CACHE_PREFIX}pi-empty`, JSON.stringify({ version: 2, updatedAt: 1, messages: [] }), ); assert.equal(readTranscriptSnapshot("pi-empty", storage), null); }); test("sanitization drops transient streaming state and strips attachment bodies", () => { const { storage } = fakeStorage(); const message: ChatMessage = { id: "a1", role: "assistant", text: "answer", blocks: [{ kind: "text", id: "b1", text: "answer" }], streamCalls: [[{ type: "text" }]], pending: true, attachments: [ { id: "att1", name: "big.png", type: "image/png", size: 999, mode: "data-url", content: "data:image/png;base64,AAAAHUGE", previewKind: "image", previewUrl: "blob:huge", path: "/tmp/big.png", }, ], }; writeTranscriptSnapshot("pi-1", [message], undefined, storage, 1000); const cached = readTranscriptSnapshot("pi-1", storage)?.[0]; assert.equal(cached?.streamCalls, undefined); assert.equal(cached?.pending, undefined); const attachment = cached?.attachments?.[0]; assert.equal(attachment?.content, ""); assert.equal(attachment?.name, "big.png"); assert.equal(attachment?.size, 999); assert.equal(attachment?.mode, "data-url"); assert.equal(attachment?.previewKind, "image"); assert.equal(attachment?.path, "/tmp/big.png"); assert.equal("previewUrl" in (attachment ?? {}), false); }); test("long block text is truncated with the marker while short text is preserved", () => { const { storage } = fakeStorage(); const longText = "y".repeat(BLOCK_LIMIT + 500); const marker = "\n…[truncated]"; const message: ChatMessage = { id: "a1", role: "assistant", text: "answer", blocks: [ { kind: "text", id: "long", text: longText }, { kind: "tool", id: "tool", name: "run", status: "done", text: "", resultText: longText }, { kind: "text", id: "short", text: "still short" }, ], }; writeTranscriptSnapshot("pi-1", [message], undefined, storage, 1000); const blocks = readTranscriptSnapshot("pi-1", storage)?.[0].blocks; const truncated = blocks?.[0]; assert.equal(truncated?.text.endsWith(marker), true); assert.equal(truncated?.text.length, BLOCK_LIMIT + marker.length); assert.equal(truncated?.text.startsWith("yyyy"), true); const toolBlock = blocks?.[1]; assert.equal(toolBlock?.kind === "tool" ? toolBlock.resultText?.endsWith(marker) : false, true); assert.equal(blocks?.[2].text, "still short"); }); test("boundMessagesForCache drops the oldest messages until the payload fits 512KB", () => { const messages = Array.from({ length: 5 }, (_, index) => blockHeavyMessage(`m${index}`, 10, BLOCK_LIMIT), ); const bounded = boundMessagesForCache(messages); assert.equal(JSON.stringify(bounded).length <= MAX_CHARS, true); assert.equal(bounded.length < messages.length, true); assert.equal(bounded[bounded.length - 1].id, "m4"); assert.equal(bounded[0].id === "m0", false); }); test("boundMessagesForCache keeps the last message even when it alone exceeds 512KB", () => { const bounded = boundMessagesForCache([blockHeavyMessage("big", 40, BLOCK_LIMIT)]); assert.equal(bounded.length, 1); assert.equal(bounded[0].id, "big"); assert.equal(JSON.stringify(bounded).length > MAX_CHARS, true); }); test("boundMessagesForCache keeps only the most recent 200 messages", () => { const messages = Array.from({ length: 250 }, (_, index) => userMessage(`m${index}`, `line ${index}`), ); const bounded = boundMessagesForCache(messages); assert.equal(bounded.length, 200); assert.equal(bounded[0].id, "m50"); assert.equal(bounded[bounded.length - 1].id, "m249"); }); test("writing a 25th session evicts the single oldest session by updatedAt", () => { const { storage, map } = fakeStorage(); for (let index = 0; index < 24; index += 1) { writeTranscriptSnapshot( `pi-${index}`, [userMessage(`u${index}`, "hi")], undefined, storage, index, ); } assert.equal(transcriptKeyCount(map), 24); writeTranscriptSnapshot("pi-24", [userMessage("u24", "hi")], undefined, storage, 24); assert.equal(transcriptKeyCount(map), 24); assert.equal(readTranscriptSnapshot("pi-0", storage), null); assert.equal(readTranscriptSnapshot("pi-1", storage)?.[0].text, "hi"); assert.equal(readTranscriptSnapshot("pi-24", storage)?.[0].text, "hi"); }); test("eviction never removes the just-written session even if it is the oldest", () => { const { storage, map } = fakeStorage(); for (let index = 1; index <= 24; index += 1) { writeTranscriptSnapshot( `pi-${index}`, [userMessage(`u${index}`, "hi")], undefined, storage, index, ); } writeTranscriptSnapshot("pi-new", [userMessage("un", "fresh")], undefined, storage, 0); assert.equal(transcriptKeyCount(map), 24); assert.equal(readTranscriptSnapshot("pi-new", storage)?.[0].text, "fresh"); assert.equal(readTranscriptSnapshot("pi-1", storage), null); }); test("a quota failure clears other transcript keys and persists the fresh entry", () => { const { storage, map } = fakeStorage({ failSetTimes: 1 }); const stale = JSON.stringify({ version: 2, updatedAt: 1, messages: [userMessage("o", "old")] }); map.set(`${TRANSCRIPT_CACHE_PREFIX}pi-a`, stale); map.set(`${TRANSCRIPT_CACHE_PREFIX}pi-b`, stale); writeTranscriptSnapshot("pi-new", [userMessage("u1", "keep me")], "t", storage, 1000); assert.equal(map.has(`${TRANSCRIPT_CACHE_PREFIX}pi-a`), false); assert.equal(map.has(`${TRANSCRIPT_CACHE_PREFIX}pi-b`), false); assert.equal(map.has(`${TRANSCRIPT_CACHE_PREFIX}pi-new`), true); assert.equal(readTranscriptSnapshot("pi-new", storage)?.[0].text, "keep me"); }); test("purgeLegacyTranscriptCache removes the v1 key and leaves v2 sessions intact", () => { const { storage, map } = fakeStorage(); map.set(LEGACY_KEY, "legacy-blob"); map.set( `${TRANSCRIPT_CACHE_PREFIX}pi-1`, JSON.stringify({ version: 2, updatedAt: 1, messages: [userMessage("u", "hi")] }), ); purgeLegacyTranscriptCache(storage); assert.equal(map.has(LEGACY_KEY), false); assert.equal(map.has(`${TRANSCRIPT_CACHE_PREFIX}pi-1`), true); });