import { describe, it, expect } from "vitest"; import { runImmediatePipeline, runDeferredPipeline, type PipelineConfig, type Message, } from "../extensions/compress/pipeline.js"; import { compilePathEntries } from "../extensions/compress/stages/paths.js"; const WS = "/Users/testuser/projects/my-long-project"; function makeConfig(opts: Partial = {}): PipelineConfig { const entries = [{ path: WS, sigil: "$WS" }]; return { pathEntries: entries, compiledPaths: compilePathEntries(entries), blockCache: new WeakMap(), ...opts, }; } function makeToolResult(text: string): Message { return { role: "toolResult", content: [{ type: "text", text }] }; } // ─── runImmediatePipeline ────────────────────────────────────────────────── describe("runImmediatePipeline", () => { it("returns original text when nothing needs changing", () => { const text = "clean output\nno issues here"; const result = runImmediatePipeline(text, makeConfig()); expect(result.text).toBe(text); expect(result.stagesApplied).toEqual([]); }); it("strips ANSI codes and records stage", () => { const text = "\x1b[31merror\x1b[0m: something failed"; const result = runImmediatePipeline(text, makeConfig()); expect(result.text).toBe("error: something failed"); expect(result.stagesApplied).toContain("ansi"); }); it("normalizes whitespace and records stage", () => { const text = "line with trailing \nother line"; const result = runImmediatePipeline(text, makeConfig()); expect(result.text).toBe("line with trailing\nother line"); expect(result.stagesApplied).toContain("whitespace"); }); it("applies both ansi and whitespace in order", () => { const text = "\x1b[32msuccess\x1b[0m: done \n\n\n\nextra"; const result = runImmediatePipeline(text, makeConfig()); expect(result.text).toBe("success: done\n\nextra"); expect(result.stagesApplied).toContain("ansi"); expect(result.stagesApplied).toContain("whitespace"); }); }); // ─── runDeferredPipeline ─────────────────────────────────────────────────── describe("runDeferredPipeline", () => { it("returns unchanged when no toolResult messages", () => { const messages: Message[] = [ { role: "user", content: [{ type: "text", text: "hello" }] }, { role: "assistant", content: [{ type: "text", text: "hi" }] }, ]; const result = runDeferredPipeline(messages, makeConfig()); expect(result.changed).toBe(false); // messages array has same content (map returns same message refs for non-toolResult) expect(result.messages).toStrictEqual(messages); }); it("returns unchanged for small toolResult blocks (< MIN_DEFERRED_SIZE)", () => { const messages: Message[] = [makeToolResult("short text")]; const result = runDeferredPipeline(messages, makeConfig()); expect(result.changed).toBe(false); }); it("compresses paths in toolResult messages", () => { // Use distinct filenames so dedup doesn't fold lines before paths stage runs const text = Array.from({ length: 4 }, (_, i) => `${WS}/src/file${i}.ts`).join("\n") + "\n" + "x".repeat(150); const messages: Message[] = [makeToolResult(text)]; const result = runDeferredPipeline(messages, makeConfig()); expect(result.changed).toBe(true); const compressed = (result.messages[0].content as Array<{type: string; text: string}>)[0].text; expect(compressed).toContain("$WS"); }); it("deduplicates consecutive identical lines", () => { const line = "ERROR: connection refused to host"; const text = Array(5).fill(line).join("\n") + "\n" + "x".repeat(100); const messages: Message[] = [makeToolResult(text)]; const result = runDeferredPipeline(messages, makeConfig()); expect(result.changed).toBe(true); const compressed = (result.messages[0].content as Array<{type: string; text: string}>)[0].text; expect(compressed).toContain("[5×]"); }); it("tracks charsBefore and charsAfter", () => { const text = Array(4).fill(`${WS}/src/index.ts`).join("\n") + "\n" + "x".repeat(200); const messages: Message[] = [makeToolResult(text)]; const result = runDeferredPipeline(messages, makeConfig()); if (result.changed) { expect(result.charsBefore).toBeGreaterThan(0); expect(result.charsAfter).toBeGreaterThan(0); expect(result.charsBefore).toBeGreaterThan(result.charsAfter); } }); it("uses block cache to serve the same result for the same block objects", () => { const config = makeConfig(); // Distinct filenames so dedup doesn't fold them const text = Array.from({ length: 4 }, (_, i) => `${WS}/src/file${i}.ts`).join("\n") + "\n" + "x".repeat(200); const messages: Message[] = [makeToolResult(text)]; // First pass populates the cache const r1 = runDeferredPipeline(messages, config); expect(r1.changed).toBe(true); const savedCharsBefore = r1.charsBefore; // Second pass with the SAME original messages — cache hits should replay // the same charsBefore/After values stored from the first compression const r2 = runDeferredPipeline(messages, config); expect(r2.changed).toBe(true); expect(r2.charsBefore).toBe(savedCharsBefore); }); it("does not modify non-text content blocks", () => { const msg: Message = { role: "toolResult", content: [ { type: "image", data: "base64data" }, { type: "text", text: "x".repeat(200) }, ], }; const result = runDeferredPipeline([msg], makeConfig()); const content = result.messages[0].content as Array<{type: string}>; // Image block should be unchanged expect(content[0].type).toBe("image"); }); });