import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { extractTextContent, replaceTextContent, type ChatMessageLike, } from "../../../open-sse/services/compression/messageContent.ts"; import { applyAging } from "../../../open-sse/services/compression/progressiveAging.ts"; import { compressAggressive } from "../../../open-sse/services/compression/aggressive.ts"; import type { AgingThresholds } from "../../../open-sse/services/compression/types.ts"; // ─── ISSUE 1 — B-AGG-TEXTDROP ──────────────────────────────────────────────── // `replaceTextContent` previously dropped every text block after the first via // flatMap → []. With the standard call pattern (newText = compressed JOIN of all // text blocks), the joined original content must remain recoverable: nothing the // model can no longer see may be silently lost. describe("replaceTextContent — multi-text-block fidelity (B-AGG-TEXTDROP)", () => { it("does not silently drop trailing text-block content absent from newText", () => { const msg: ChatMessageLike = { role: "user", content: [ { type: "text", text: "FIRST block alpha" }, { type: "image", source: { foo: 1 } }, { type: "text", text: "SECOND block bravo" }, { type: "text", text: "THIRD block charlie" }, ], }; // newText does NOT subsume the trailing blocks (worst case): a caller that // only summarized the first block. The trailing blocks' content must not // silently vanish. const replaced = replaceTextContent(msg, "NEWTEXT-only-first"); const out = extractTextContent(replaced.content); assert.ok(out.includes("NEWTEXT-only-first"), "replacement text missing"); assert.ok(out.includes("SECOND block bravo"), "second block silently dropped"); assert.ok(out.includes("THIRD block charlie"), "third block silently dropped"); // Non-text blocks (image) must survive unchanged. const blocks = replaced.content as Array<{ type?: string }>; assert.ok( blocks.some((b) => b.type === "image"), "non-text block must be preserved" ); }); it("collapses trailing blocks when newText already subsumes them (no duplication)", () => { const msg: ChatMessageLike = { role: "user", content: [ { type: "text", text: "alpha" }, { type: "text", text: "bravo" }, ], }; // Standard call pattern: newText = compressed JOIN of all text blocks. const joined = extractTextContent(msg.content); // "alpha\nbravo" const replaced = replaceTextContent(msg, joined); const blocks = replaced.content as Array<{ type?: string; text?: string }>; const textBlocks = blocks.filter((b) => b.type === "text" || b.text !== undefined); // Should collapse to a single text block — no duplicated "alpha"/"bravo". assert.equal(textBlocks.length, 1, "subsumed trailing blocks should be collapsed"); assert.equal(extractTextContent(replaced.content), joined); }); it("aging a multi-text-block message keeps all original text represented", () => { // Build a long conversation so the first (multi-block) message ages out. const first: ChatMessageLike = { role: "user", content: [ { type: "text", text: "alpha-marker request: fix login" }, { type: "text", text: "bravo-marker error: TypeError: boom" }, ], }; const msgs: ChatMessageLike[] = [first]; for (let i = 1; i < 8; i++) { msgs.push({ role: i % 2 ? "assistant" : "user", content: `filler ${i} ${"z".repeat(60)}` }); } const result = applyAging(msgs, { fullSummary: 10, moderate: 10, light: 3, verbatim: 1 }); const out = extractTextContent(result.messages[0].content as ChatMessageLike["content"]); // Light tier keeps content; both blocks' text must still be present (joined). assert.ok(out.includes("alpha-marker"), "first text block lost during aging"); assert.ok(out.includes("bravo-marker"), "second text block silently dropped during aging"); }); }); // ─── ISSUE 2 — B-AGG-ANTHROPIC-TR ──────────────────────────────────────────── // Anthropic-shape tool_result blocks (a {type:"tool_result"} content block inside // a user message) must be compressed too, preserving tool_use_id + block type. describe("aggressive — Anthropic tool_result compression (B-AGG-ANTHROPIC-TR)", () => { it("compresses the text inside an Anthropic tool_result block", () => { const bigJsonArray = JSON.stringify( Array.from({ length: 200 }, (_, i) => ({ id: i, name: `item${i}`, data: "x".repeat(40) })) ); const userMsg: ChatMessageLike = { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_ABC123", content: [{ type: "text", text: bigJsonArray }], }, ], }; const result = compressAggressive([userMsg]); const outMsg = result.messages[0]; const blocks = outMsg.content as Array>; const tr = blocks.find((b) => b.type === "tool_result"); assert.ok(tr, "tool_result block must survive"); assert.equal(tr!.type, "tool_result", "block type must be unchanged"); assert.equal(tr!.tool_use_id, "toolu_ABC123", "tool_use_id must be preserved"); // The inner text must be smaller than the original (it was compressed). const innerText = typeof tr!.content === "string" ? (tr!.content as string) : ((tr!.content as Array<{ type?: string; text?: string }>) ?? []) .filter((c) => c.type === "text") .map((c) => c.text ?? "") .join("\n"); assert.ok( innerText.length < bigJsonArray.length, `tool_result inner text was not compressed (orig ${bigJsonArray.length}, got ${innerText.length})` ); assert.ok(result.stats.aggressive!.toolResultSavings > 0, "toolResultSavings must be > 0"); }); it("compresses a string-form Anthropic tool_result block", () => { const errorOutput = "TypeError: Cannot read property 'x' of undefined\n" + Array.from({ length: 40 }, (_, i) => ` at fn${i} (file${i}.ts:${i + 1}:${i + 5})`).join( "\n" ); const userMsg: ChatMessageLike = { role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_ERR", content: errorOutput }], }; const result = compressAggressive([userMsg]); const tr = (result.messages[0].content as Array>).find( (b) => b.type === "tool_result" ); assert.ok(tr, "tool_result block must survive"); assert.equal(tr!.tool_use_id, "toolu_ERR"); assert.ok((tr!.content as string).length < errorOutput.length, "string tool_result not compressed"); }); }); // ─── ISSUE 3 — B-AGG-JSONTAG ───────────────────────────────────────────────── // Aging must not corrupt structured content (JSON / fenced code block) with a // literal [COMPRESSED:...] inline prefix. describe("progressiveAging — structured-content tag safety (B-AGG-JSONTAG)", () => { const thresholds: AgingThresholds = { fullSummary: 10, moderate: 10, light: 3, verbatim: 1 }; function agedFirstContent(content: string, t: AgingThresholds): string { const msgs: ChatMessageLike[] = [ { role: "user", content }, { role: "assistant", content: "a " + "z".repeat(60) }, { role: "user", content: "b " + "z".repeat(60) }, { role: "assistant", content: "c" }, ]; const result = applyAging(msgs, t); const c = result.messages[0].content; return typeof c === "string" ? c : extractTextContent(c as ChatMessageLike["content"]); } it("keeps pure-JSON content JSON.parse-able after aging", () => { const json = JSON.stringify({ status: "ok", items: Array.from({ length: 5 }, (_, i) => ({ id: i, name: `n${i}` })), meta: { a: 1, b: 2 }, }); const aged = agedFirstContent(json, thresholds); // Must remain valid JSON (no inline tag corruption). assert.doesNotThrow(() => JSON.parse(aged), `aged JSON not parseable: ${aged.slice(0, 80)}`); }); it("keeps a fenced code block valid after aging (tag outside the fence)", () => { const fenced = "```json\n{\n \"a\": 1,\n \"b\": [1, 2, 3]\n}\n```"; const aged = agedFirstContent(fenced, thresholds); // The fenced block must still be present and intact. assert.ok(aged.includes("```json"), "opening fence lost"); assert.ok(aged.trimEnd().endsWith("```"), "closing fence lost"); assert.ok(aged.includes('"a": 1'), "fenced payload corrupted"); }); it("does not re-compress structured content on a second aging pass (recursion guard)", () => { const json = JSON.stringify({ status: "ok", items: [{ id: 0 }, { id: 1 }], meta: { a: 1 } }); const msgs: ChatMessageLike[] = [ { role: "user", content: json }, { role: "assistant", content: "a " + "z".repeat(60) }, { role: "user", content: "b " + "z".repeat(60) }, { role: "assistant", content: "c" }, ]; const first = applyAging(msgs, thresholds); const second = applyAging(first.messages as ChatMessageLike[], thresholds); const firstContent = JSON.stringify( first.messages.map((m) => (m as ChatMessageLike).content) ); const secondContent = JSON.stringify( second.messages.map((m) => (m as ChatMessageLike).content) ); assert.equal(secondContent, firstContent, "second aging pass changed structured content"); // And it must still be parseable. const c0 = (second.messages[0] as ChatMessageLike).content; const text0 = typeof c0 === "string" ? c0 : extractTextContent(c0 as ChatMessageLike["content"]); assert.doesNotThrow(() => JSON.parse(text0), "JSON corrupted after two aging passes"); }); });