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 } 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.
`;
const OLD_SCREENSHOT_DATA = "b64-old-screenshot-bytes";
const LATEST_SCREENSHOT_DATA = "b64-latest-screenshot-bytes";
const USER_ATTACHED_IMAGE_DATA = "b64-user-attached-bytes";
const userTextWithTurnContext = (text: string, timestamp: string): Message => ({
role: "user",
content: [
{
type: "text",
text: `\ncurrent_time: ${timestamp}\n\n${text}`,
},
],
});
const assistantToolUse = (id: string): Message => ({
role: "assistant",
content: [{ type: "tool_use", id, name: "screenshot", input: {} }],
});
const toolResultWithImage = (id: string, data: string): Message => ({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: id,
content: "captured",
contentBlocks: [
{
type: "image",
source: { type: "base64", media_type: "image/png", data },
},
],
},
],
});
const userWithAttachedImage = (): Message => ({
role: "user",
content: [
{ type: "text", text: "here is a mockup" },
{
type: "image",
source: {
type: "base64",
media_type: "image/png",
data: USER_ATTACHED_IMAGE_DATA,
},
},
],
});
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[] }): Provider {
return {
name: "mock-provider",
sendMessage: async (msgs: Message[]) => {
sink.sent = msgs;
return {
content: [{ type: "text", text: compactionResponse }],
model: "mock-model",
usage: { inputTokens: 100, outputTokens: 50 },
stopReason: "end_turn",
};
},
};
}
// The summary request must carry the same media-stripped projection the agent
// loop sends on its own model calls. An unsanitized history resends every
// screenshot in the conversation; enough of them cross Anthropic's many-image
// threshold, where a stricter per-image dimension cap rejects the whole
// summary call (and trips the compaction circuit breaker).
describe("compaction summary calls — old tool-result media stripping", () => {
test("runAssistantDrivenCompaction strips images from older tool results, keeps the latest turn's image and user-attached images, and leaves durable history untouched", async () => {
// GIVEN a screenshot-heavy history: older tool results with images, a
// user-attached image, and a most-recent tool result with an image
const messages: Message[] = [
userWithAttachedImage(),
assistantToolUse("tu_old_1"),
toolResultWithImage("tu_old_1", OLD_SCREENSHOT_DATA),
userTextWithTurnContext("tail anchor message", TAIL_TIMESTAMP),
assistantToolUse("tu_latest"),
toolResultWithImage("tu_latest", LATEST_SCREENSHOT_DATA),
];
// 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-media-strip",
messages,
provider: recordingProvider(sink),
systemPrompt: "system",
compaction: { enabled: true, autoThreshold: 0.7 },
// Large budget so the request is not front-truncated — this test
// exercises the media-strip sanitizer, not the truncation fallback.
maxInputTokens: 100000,
previousEstimatedInputTokens: 90000,
});
// THEN older tool-result screenshots are replaced with a text marker
const sentSerialized = serializeBlocks(sink.sent);
expect(sentSerialized).not.toContain(OLD_SCREENSHOT_DATA);
expect(sentSerialized).toContain("binary data removed to save context");
// AND the most recent tool-result turn keeps its image (the model may
// still need it), as do images the user attached directly
expect(sentSerialized).toContain(LATEST_SCREENSHOT_DATA);
expect(sentSerialized).toContain(USER_ATTACHED_IMAGE_DATA);
// 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);
});
});