/** * KHAELOR * File: tests/context/compaction.test.ts * Description: Compaction tests — prune selection, pairing-safe cuts, prune-then-summarize pipeline ordering. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { describe, expect, it } from "vitest"; import { DEFAULT_COMPACTION_OPTIONS, PRUNE_PLACEHOLDER, collectRunningProcesses, compressEvents, parseCheckpoint, selectCompactionCut, selectPruneCandidates, } from "../../src/context/index.js"; import type { CompactionOptions } from "../../src/context/index.js"; import { isPairingSafeCut } from "../../src/session/index.js"; import { FAKE_CHECKPOINT_TEXT, FakeModelClient, StreamBuilder, sampleSession } from "./fixtures.js"; const opts = (overrides: Partial): CompactionOptions => ({ ...DEFAULT_COMPACTION_OPTIONS, ...overrides, }); describe("selectPruneCandidates", () => { it("prunes old tool results while protecting the newest N tokens of tool output", () => { const big = "x".repeat(2_000); // ~500 estimated tokens each const session = sampleSession({ bigToolOutput: big }); // Make toolu_2's result big too, then add a third big recent result. const b = new StreamBuilder(); for (const event of session.events) { if (event.type === "tool.completed" && event.payload.toolUseId === "toolu_2") { b.add({ type: "tool.completed", payload: { ...event.payload, modelText: big } }); } else { b.add({ type: event.type, payload: event.payload } as never); } } b.add({ type: "tool.requested", payload: { requestId: "req_4", blockIndex: 0, toolUseId: "toolu_3", toolName: "read", input: {} }, }); b.add({ type: "tool.completed", payload: { toolUseId: "toolu_3", modelText: big, durationMs: 1, ui: { kind: "read", summary: "r" } }, }); const decision = selectPruneCandidates(b.events, opts({ protectRecentToolTokens: 600 })); expect(decision.toolUseIds).toEqual(["toolu_1", "toolu_2"]); expect(decision.placeholder).toBe(PRUNE_PLACEHOLDER); expect(decision.tokensReclaimedEstimate).toBeGreaterThan(900); }); it("skips results already pruned and tiny results not worth a placeholder", () => { const big = "x".repeat(2_000); const session = sampleSession({ bigToolOutput: big }); session.add({ type: "context.pruned", payload: { toolUseIds: ["toolu_1"], placeholder: PRUNE_PLACEHOLDER, tokensReclaimedEstimate: 470 }, }); // toolu_2's result is tiny; toolu_1 already pruned → nothing left. const decision = selectPruneCandidates(session.events, opts({ protectRecentToolTokens: 0 })); expect(decision.toolUseIds).toEqual([]); expect(decision.tokensReclaimedEstimate).toBe(0); }); }); describe("selectCompactionCut", () => { it("protects the first user message (head) and the latest user message (tail)", () => { const session = sampleSession(); const cut = selectCompactionCut(session.events, opts({ protectTailTokens: 1 })); expect(cut).not.toBeNull(); expect(cut as object).toEqual({ fromSeq: 4, toSeq: 14 }); expect(isPairingSafeCut(session.events, cut as { fromSeq: number; toSeq: number })).toBe(true); }); it("never orphans a tool_use/tool_result pair — toSeq walks down to a safe boundary", () => { const b = sampleSession(); // A tool round-trip whose result lands inside the protected tail. b.add({ type: "tool.requested", payload: { requestId: "req_5", blockIndex: 0, toolUseId: "toolu_9", toolName: "bash", input: { cmd: "npm test" } }, }); // seq 17 b.add({ type: "tool.completed", payload: { toolUseId: "toolu_9", modelText: "148 passed — full suite green, nothing else to report here", durationMs: 5, ui: { kind: "exec", summary: "npm test" }, }, }); // seq 18 b.add({ type: "user.message-created", payload: { text: "Great, now document it", mentions: [] } }); // seq 19 // Tail protection reaches back into seq 18 (the tool_result) but not seq 17: // a naive toSeq of 17 would orphan toolu_9, so the cut must retreat to 16. const cut = selectCompactionCut(b.events, opts({ protectTailTokens: 8 })); expect(cut).not.toBeNull(); expect((cut as { toSeq: number }).toSeq).toBeLessThan(17); expect(isPairingSafeCut(b.events, cut as { fromSeq: number; toSeq: number })).toBe(true); }); it("returns null when there is no compactable middle", () => { const b = new StreamBuilder(); b.add({ type: "user.message-created", payload: { text: "hello", mentions: [] } }); expect(selectCompactionCut(b.events, opts({}))).toBeNull(); expect(selectCompactionCut([], opts({}))).toBeNull(); }); }); describe("compressEvents pipeline", () => { it("prunes first: the summarizer sees recorded placeholders, never the reclaimed output", async () => { const big = "GIANT_TOOL_OUTPUT ".repeat(500); const session = sampleSession({ bigToolOutput: big }); // Simulate the kernel: record the prune decision as a durable event first. const decision = selectPruneCandidates(session.events, opts({ protectRecentToolTokens: 10 })); expect(decision.toolUseIds).toContain("toolu_1"); session.add({ type: "context.pruned", payload: decision }); const cut = selectCompactionCut(session.events, opts({ protectTailTokens: 1 })); expect(cut).not.toBeNull(); const client = new FakeModelClient(FAKE_CHECKPOINT_TEXT); const result = await compressEvents( { modelClient: client, model: "aux-model", maxOutputTokens: 2_000 }, { events: session.events, cut: cut as { fromSeq: number; toSeq: number }, trigger: "proactive-token-budget", tokensBefore: 150_000, }, ); // Ordering: exactly one summarization call, made AFTER pruning was applied. expect(client.requests).toHaveLength(1); const sent = JSON.stringify(client.requests[0]); expect(sent).toContain(PRUNE_PLACEHOLDER); expect(sent).not.toContain("GIANT_TOOL_OUTPUT"); expect(result.summaryModel).toBe("aux-model"); expect(result.trigger).toBe("proactive-token-budget"); expect(result.tokensBefore).toBe(150_000); const parsed = parseCheckpoint(result.checkpointYaml); expect(parsed.ok).toBe(true); if (parsed.ok) expect(parsed.value.objective).toBe("Fix the bug in the session store"); }); it("uses a small output budget and passes the AbortSignal through (cancellable)", async () => { const session = sampleSession(); const cut = selectCompactionCut(session.events, opts({ protectTailTokens: 1 })); const client = new FakeModelClient(FAKE_CHECKPOINT_TEXT); const options = opts({ protectTailTokens: 1, summaryMaxOutputTokens: 777 }); const controller = new AbortController(); await compressEvents( { modelClient: client, model: "aux-model", maxOutputTokens: options.summaryMaxOutputTokens }, { events: session.events, cut: cut as { fromSeq: number; toSeq: number }, trigger: "user-command", tokensBefore: 0, signal: controller.signal, }, options, ); expect(client.requests[0]?.maxOutputTokens).toBe(777); expect(client.signals[0]).toBe(controller.signal); const aborted = new AbortController(); aborted.abort(); await expect( compressEvents( { modelClient: new FakeModelClient(FAKE_CHECKPOINT_TEXT), model: "aux-model", maxOutputTokens: 100 }, { events: session.events, cut: cut as { fromSeq: number; toSeq: number }, trigger: "user-command", tokensBefore: 0, signal: aborted.signal, }, options, ), ).rejects.toThrow(); }); it("falls back to current_state when the model reply is unstructured, and derives running_processes from events", async () => { const session = sampleSession(); session.add({ type: "process.started", payload: { processId: "proc_9", pid: 1, command: "npm run dev", cwd: "/p", toolUseId: "t" }, }); expect(collectRunningProcesses(session.events)).toEqual([ { id: "proc_9", command: "npm run dev", status: "running" }, ]); const cut = selectCompactionCut(session.events, opts({ protectTailTokens: 1 })); const client = new FakeModelClient("Sorry, here is a prose summary instead."); const result = await compressEvents( { modelClient: client, model: "aux-model", maxOutputTokens: 100 }, { events: session.events, cut: cut as { fromSeq: number; toSeq: number }, trigger: "reactive-overflow", tokensBefore: 10, }, ); const parsed = parseCheckpoint(result.checkpointYaml); expect(parsed.ok).toBe(true); if (parsed.ok) { expect(parsed.value.currentState).toBe("Sorry, here is a prose summary instead."); expect(parsed.value.runningProcesses).toEqual([ { id: "proc_9", command: "npm run dev", status: "running" }, ]); } }); it("refuses a pairing-unsafe cut", async () => { const session = sampleSession(); await expect( compressEvents( { modelClient: new FakeModelClient(FAKE_CHECKPOINT_TEXT), model: "aux-model", maxOutputTokens: 100 }, { events: session.events, cut: { fromSeq: 4, toSeq: 5 }, trigger: "user-command", tokensBefore: 0 }, ), ).rejects.toThrow(/pairing/); }); });