import { describe, expect, mock, test } from "bun:test"; mock.module("../persistence/conversation-crud.js", () => ({ setConversationProcessingStartedAt: () => {}, isConversationProcessing: () => false, getMessages: () => [], reserveMessage: mock(async () => ({ id: "msg-reserve" })), })); mock.module("../persistence/attachments-store.js", () => ({ getAttachmentMetadataForMessage: () => [], getAttachmentContent: () => null, })); import { runAssistantDrivenCompaction, runEmergencyCompaction, } from "../context/compactor.js"; import type { Message, Provider } from "../providers/types.js"; const TAIL_TIMESTAMP = "2026-05-21 (Thursday) 10:00:00 -05:00 (America/Chicago)"; const compactionResponse = ` Earlier turns summarized here. - Nothing critical pending. `; // The emergency prompt asks for summary + key_state only — no tail_start — // so the emergency test's canned response mirrors a prompt-following model. const emergencyCompactionResponse = ` Earlier turns summarized here. - Nothing critical pending. `; const ENCRYPTED_TOKEN = "expired_encrypted_token_abc123"; const userText = (text: string): Message => ({ role: "user", content: [{ type: "text", text }], }); const userTextWithTurnContext = (text: string, timestamp: string): Message => ({ role: "user", content: [ { type: "text", text: `\ncurrent_time: ${timestamp}\n\n${text}`, }, ], }); const assistantWithWebSearch = (): Message => ({ role: "assistant", content: [ { type: "server_tool_use", id: "stu_1", name: "web_search", input: { query: "cats" }, }, { type: "web_search_tool_result", tool_use_id: "stu_1", content: [ { type: "web_search_result", url: "https://cats.example", title: "All About Cats", encrypted_content: ENCRYPTED_TOKEN, }, ], }, ], }); const assistantToolUse = (): Message => ({ role: "assistant", content: [ { type: "tool_use", id: "tu_1", name: "bash", input: { cmd: "ls" } }, ], }); const toolResult = (): Message => ({ role: "user", content: [{ type: "tool_result", tool_use_id: "tu_1", content: "ok" }], }); function serializeBlocks(messages: Message[]): string { return JSON.stringify(messages); } // Records the exact message list the provider is asked to summarize. function recordingProvider( sink: { sent: Message[] }, response: string, ): Provider { return { name: "mock-provider", sendMessage: async (msgs: Message[]) => { sink.sent = msgs; return { content: [{ type: "text", text: response }], model: "mock-model", usage: { inputTokens: 100, outputTokens: 50 }, stopReason: "end_turn", }; }, }; } // Expired Anthropic web-search tokens must never reach the summarization LLM: // both compaction provider calls funnel through the same request builder, so // each is guarded here. describe("compaction summary calls — historical web-search sanitization", () => { test("runAssistantDrivenCompaction strips expired encrypted_content from the request while leaving durable history untouched", async () => { // GIVEN a history whose older turn contains a native web_search_tool_result // block carrying an opaque, expired `encrypted_content` token const messages: Message[] = [ userText("old user turn"), assistantWithWebSearch(), userTextWithTurnContext("tail anchor message", TAIL_TIMESTAMP), assistantWithWebSearch(), ]; // AND a snapshot of the caller's array so we can assert it is not mutated const inputSnapshot = serializeBlocks(messages); const sink = { sent: [] as Message[] }; // WHEN compaction runs over that history const result = await runAssistantDrivenCompaction({ conversationId: "conv-web-search", messages, provider: recordingProvider(sink, compactionResponse), systemPrompt: "system", compaction: { enabled: true, autoThreshold: 0.7 }, maxInputTokens: 1000, previousEstimatedInputTokens: 900, }); // THEN the summary request the provider received carries no expired token // and no native web_search blocks, but keeps the search content as text const sentSerialized = serializeBlocks(sink.sent); expect(sentSerialized).not.toContain(ENCRYPTED_TOKEN); expect(sentSerialized).not.toContain("web_search_tool_result"); expect(sentSerialized).not.toContain("server_tool_use"); expect(sentSerialized).toContain("All About Cats"); expect(sentSerialized).toContain("https://cats.example"); // AND the caller's durable history array is left byte-for-byte untouched, // so persisted messages keep the original rich blocks expect(serializeBlocks(messages)).toBe(inputSnapshot); expect(result.compacted).toBe(true); }); test("runEmergencyCompaction strips expired encrypted_content from the prefix request", async () => { // GIVEN a mid-turn history whose prefix (before the last tool pair) holds a // web_search_tool_result block with an expired token const messages: Message[] = [ userText("old user turn"), assistantWithWebSearch(), userText("keep going"), assistantToolUse(), toolResult(), ]; const inputSnapshot = serializeBlocks(messages); const sink = { sent: [] as Message[] }; // WHEN emergency compaction splits at the last tool pair and summarizes the // prefix const result = await runEmergencyCompaction({ conversationId: "conv-web-search-emergency", messages, provider: recordingProvider(sink, emergencyCompactionResponse), systemPrompt: "system", compaction: { enabled: true, autoThreshold: 0.7 }, // Large budget so the prefix is not front-truncated — this test exercises // the web-search sanitizer, not the truncation fallback. maxInputTokens: 100000, previousEstimatedInputTokens: 90000, }); // THEN the prefix sent to the provider carries no expired token const sentSerialized = serializeBlocks(sink.sent); expect(sentSerialized).not.toContain(ENCRYPTED_TOKEN); expect(sentSerialized).not.toContain("web_search_tool_result"); expect(sentSerialized).not.toContain("server_tool_use"); expect(sentSerialized).toContain("All About Cats"); // AND the caller's durable history is untouched expect(serializeBlocks(messages)).toBe(inputSnapshot); expect(result.compacted).toBe(true); }); });