Files
oh-my-pi/packages/snapcompact/test/snapcompact.test.ts
T
can1357 6b1ca33bf7 refactor(snapcompact): dropped the snapcompact qualifier from every export
- Renamed all functions, types, and constants in @oh-my-pi/snapcompact to namespace-relative names (`snapcompactCompact` → `compact`, `renderSnapcompactFrames` → `renderMany`, `snapcompactFrameCount` → `frames`, `SnapcompactShape` → `Shape`, `SNAPCOMPACT_SHAPES` → `SHAPES`, …).
- Converted every consumer to `import * as snapcompact` member access: `agent/compaction.ts`, `coding-agent` `agent-session.ts`/`session-manager.ts`/`snapcompact-inline.ts`, and all affected tests.
- Renamed internal `geometry` locals to `geo` in `snapcompact.ts` to avoid TDZ collisions with the new `geometry` export.
- Updated `docs/compaction.md` prose and added a Breaking Changes entry to the snapcompact changelog documenting the full rename map.
2026-06-12 03:27:50 +02:00

509 lines
20 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import type { AssistantMessage, Message, Usage } from "@oh-my-pi/pi-ai";
import * as snapcompact from "../src";
// Small frames keep render time negligible. Legacy 5x8 shape: 320px → 64 cols
// x 40 rows = 2560 chars. Default (anthropic 8x8r-bw): 40 cols x 20 rows = 800.
const TEST_FRAME_SIZE = 320;
function createUserMessage(content: string): Message {
return { role: "user", content, timestamp: 0 };
}
const ZERO_USAGE: Usage = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
};
function createAssistantMessage(content: AssistantMessage["content"]): Message {
return {
role: "assistant",
content,
api: "mock",
provider: "mock",
model: "mock",
usage: ZERO_USAGE,
stopReason: "stop",
timestamp: 0,
};
}
function createToolResultMessage(text: string): Message {
return {
role: "toolResult",
toolCallId: "call-1",
toolName: "bash",
content: [{ type: "text", text }],
isError: false,
timestamp: 0,
};
}
function makePreparation(
overrides: Partial<snapcompact.CompactionPreparation<Message>> = {},
): snapcompact.CompactionPreparation<Message> {
return {
firstKeptEntryId: "kept-1",
messagesToSummarize: [
createUserMessage("Fix the login bug. The token expires too early!"),
createAssistantMessage([{ type: "text", text: "Fixed the TTL comparison in src/login.ts." }]),
],
turnPrefixMessages: [],
tokensBefore: 99000,
previousSummary: undefined,
previousPreserveData: undefined,
fileOps: snapcompact.createFileOps(),
...overrides,
};
}
interface DecodedPng {
width: number;
height: number;
colorType: number;
/** Palette indices, one byte per pixel (filter bytes stripped). */
pixels: Uint8Array;
}
/** Minimal PNG reader for the encoder's own output (indexed, filter None). */
function decodePng(png: Uint8Array): DecodedPng {
expect(Array.from(png.subarray(0, 8))).toEqual([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const view = new DataView(png.buffer, png.byteOffset, png.byteLength);
let pos = 8;
let width = 0;
let height = 0;
let colorType = -1;
let depth = 0;
const idatParts: Uint8Array[] = [];
while (pos < png.length) {
const length = view.getUint32(pos);
const type = String.fromCharCode(png[pos + 4], png[pos + 5], png[pos + 6], png[pos + 7]);
const data = png.subarray(pos + 8, pos + 8 + length);
if (type === "IHDR") {
width = view.getUint32(pos + 8);
height = view.getUint32(pos + 12);
depth = data[8];
colorType = data[9];
} else if (type === "IDAT") {
idatParts.push(data);
}
pos += 12 + length;
}
let idatLength = 0;
for (const part of idatParts) idatLength += part.length;
const idat = new Uint8Array(idatLength);
let offset = 0;
for (const part of idatParts) {
idat.set(part, offset);
offset += part.length;
}
// Strip the zlib envelope (2-byte header + trailing Adler-32).
const raw = Bun.inflateSync(idat.subarray(2, idat.length - 4));
const rowBytes = depth === 4 ? Math.ceil(width / 2) : width;
expect(raw.length).toBe(height * (rowBytes + 1));
const pixels = new Uint8Array(width * height);
for (let y = 0; y < height; y++) {
expect(raw[y * (rowBytes + 1)]).toBe(0); // filter byte: None
const row = raw.subarray(y * (rowBytes + 1) + 1, (y + 1) * (rowBytes + 1));
if (depth === 4) {
for (let x = 0; x < width; x++) {
const byte = row[x >> 1];
pixels[y * width + x] = x % 2 === 0 ? byte >> 4 : byte & 0xf;
}
} else {
pixels.set(row, y * width);
}
}
return { width, height, colorType, pixels };
}
describe("normalize", () => {
it("collapses whitespace runs and folds non-Latin-1 to ASCII", () => {
expect(snapcompact.normalize("a\n\n\tb c\r\nd")).toBe("a b c d");
expect(snapcompact.normalize("x → y ✓ “quoted” — em…")).toBe(`x -> y v "quoted" - em...`);
expect(snapcompact.normalize("café größe")).toBe("café größe"); // Latin-1 has glyphs
expect(snapcompact.normalize("box │─┌ emoji 🎞")).toBe("box |-+ emoji ?");
});
});
describe("shape resolution", () => {
it("maps provider APIs to their eval-winning shapes", () => {
expect(snapcompact.resolveShape("anthropic-messages")).toBe(snapcompact.SHAPES.anthropic);
expect(snapcompact.resolveShape("openai-responses")).toBe(snapcompact.SHAPES.openaiDense);
expect(snapcompact.resolveShape("azure-openai-responses")).toBe(snapcompact.SHAPES.openaiDense);
expect(snapcompact.resolveShape("google-generative-ai")).toBe(snapcompact.SHAPES.google);
// Unknown and absent APIs fall back to the refusal-robust plain shape.
expect(snapcompact.resolveShape("some-future-api")).toBe(snapcompact.SHAPES.anthropic);
expect(snapcompact.resolveShape(undefined)).toBe(snapcompact.SHAPES.anthropic);
});
it("recognizes complete shape overrides and rejects malformed ones", () => {
expect(snapcompact.isShape(snapcompact.SHAPES.openaiDense)).toBe(true);
expect(snapcompact.isShape({ ...snapcompact.SHAPES.openaiDense, cellWidth: 0 })).toBe(false);
expect(snapcompact.isShape({ ...snapcompact.SHAPES.openaiDense, variant: "color" })).toBe(false);
expect(snapcompact.isShape({ ...snapcompact.SHAPES.openaiDense, imageDetail: "original" })).toBe(true);
});
it("images forwards the per-frame detail hint", () => {
const archive: snapcompact.Archive = {
frames: [
{ data: "ZmFrZQ==", mimeType: "image/png", cols: 10, rows: 10, chars: 5, detail: "original" },
{ data: "ZmFrZTI=", mimeType: "image/png", cols: 10, rows: 10, chars: 5 },
],
totalChars: 10,
truncatedChars: 0,
};
const [withDetail, without] = snapcompact.images(archive);
expect(withDetail.detail).toBe("original");
expect("detail" in without).toBe(false);
});
});
describe("render", () => {
it("produces an indexed PNG of the declared geometry with sentence-cycled ink (legacy 5x8)", () => {
const geometry = snapcompact.geometry(snapcompact.SHAPES.legacy, TEST_FRAME_SIZE);
expect(geometry).toEqual({ cols: 64, rows: 40, capacity: 2560 });
const frame = snapcompact.render(
"First sentence here. Second one differs.",
snapcompact.SHAPES.legacy,
TEST_FRAME_SIZE,
);
expect(frame.cols).toBe(64);
expect(frame.rows).toBe(40);
expect(frame.chars).toBe(40);
const decoded = decodePng(Buffer.from(frame.data, "base64"));
expect(decoded.width).toBe(TEST_FRAME_SIZE);
expect(decoded.height).toBe(TEST_FRAME_SIZE);
expect(decoded.colorType).toBe(3); // indexed color
// Two sentences → glyphs printed in ink 1 then ink 2; background stays 0.
const used = new Set(decoded.pixels);
expect(used.has(1)).toBe(true);
expect(used.has(2)).toBe(true);
expect(used.has(3)).toBe(false);
});
it("renders the anthropic shape with doubled lines, black ink, and highlight bands", () => {
const geometry = snapcompact.geometry(snapcompact.SHAPES.anthropic, TEST_FRAME_SIZE);
expect(geometry).toEqual({ cols: 40, rows: 20, capacity: 800 });
const frame = snapcompact.render("Hello world. Again.", snapcompact.SHAPES.anthropic, TEST_FRAME_SIZE);
const decoded = decodePng(Buffer.from(frame.data, "base64"));
expect(decoded.colorType).toBe(3);
const used = new Set(decoded.pixels);
expect(used.has(7)).toBe(true); // black bw ink
expect(used.has(8)).toBe(true); // repeat highlight band
expect(used.has(1)).toBe(false); // no sentence hues in bw
});
it("renders the openai stretch shape as truecolor RGB", () => {
const frame = snapcompact.render("Hello world.", snapcompact.SHAPES.openaiDense, TEST_FRAME_SIZE);
// IHDR color type byte: 2 = truecolor RGB (anti-aliased stretch output).
expect(Buffer.from(frame.data, "base64")[25]).toBe(2);
expect(frame.cols).toBe(Math.floor(TEST_FRAME_SIZE / 6));
});
it("caps printed characters at frame capacity", () => {
const { capacity } = snapcompact.geometry(snapcompact.SHAPES.legacy, TEST_FRAME_SIZE);
const frame = snapcompact.render("x".repeat(capacity + 500), snapcompact.SHAPES.legacy, TEST_FRAME_SIZE);
expect(frame.chars).toBe(capacity);
});
});
describe("renderMany", () => {
it("returns no frames for empty or whitespace-only input", () => {
expect(snapcompact.renderMany("", { shape: snapcompact.SHAPES.anthropic, frameSize: TEST_FRAME_SIZE })).toEqual(
[],
);
expect(
snapcompact.renderMany(" \n\t ", { shape: snapcompact.SHAPES.anthropic, frameSize: TEST_FRAME_SIZE }),
).toEqual([]);
expect(snapcompact.frames("", { shape: snapcompact.SHAPES.anthropic, frameSize: TEST_FRAME_SIZE })).toBe(0);
});
it("pages text into image blocks matching the predicted frame count", () => {
const shape = snapcompact.SHAPES.anthropic;
const { capacity } = snapcompact.geometry(shape, TEST_FRAME_SIZE);
const short = snapcompact.renderMany("hello world", { shape, frameSize: TEST_FRAME_SIZE });
expect(short).toHaveLength(1);
expect(short[0].type).toBe("image");
expect(short[0].mimeType).toBe("image/png");
expect(short[0].data.length).toBeGreaterThan(0);
const text = "x".repeat(capacity * 2 + 10);
const frames = snapcompact.renderMany(text, { shape, frameSize: TEST_FRAME_SIZE });
expect(frames).toHaveLength(3);
expect(snapcompact.frames(text, { shape, frameSize: TEST_FRAME_SIZE })).toBe(3);
});
it("honors maxFrames and propagates the shape's detail hint", () => {
const shape = snapcompact.SHAPES.openaiDense;
const { capacity } = snapcompact.geometry(shape, TEST_FRAME_SIZE);
const frames = snapcompact.renderMany("x".repeat(capacity * 3), {
shape,
frameSize: TEST_FRAME_SIZE,
maxFrames: 2,
});
expect(frames).toHaveLength(2);
// openaiDense carries imageDetail: "original"; anthropic carries none.
expect(frames[0].detail).toBe("original");
const bw = snapcompact.renderMany("hi", { shape: snapcompact.SHAPES.anthropic, frameSize: TEST_FRAME_SIZE });
expect(bw[0].detail).toBeUndefined();
});
});
describe("serializeConversation", () => {
it("truncates oversized tool results keeping head and tail", () => {
const text = `HEAD-${"x".repeat(5000)}-TAIL`;
const out = snapcompact.serializeConversation([createToolResultMessage(text)]);
// Default cap 2000 at 0.6 head ratio: 1200 head + 800 tail survive.
expect(out).toContain("[Tool result]: ");
expect(out).toContain("HEAD-");
expect(out).toContain("[... 3010 chars elided ...]");
expect(out.endsWith(`-TAIL${snapcompact.DIM_OFF}`)).toBe(true);
});
it("honors configured budgets; Infinity disables a cap", () => {
const text = "a".repeat(100);
const tight = snapcompact.serializeConversation([createToolResultMessage(text)], {
toolResultMaxChars: 10,
truncateHeadRatio: 0.5,
});
expect(tight).toContain("[... 90 chars elided ...]");
const off = snapcompact.serializeConversation([createToolResultMessage(text)], {
toolResultMaxChars: Number.POSITIVE_INFINITY,
});
expect(off).toContain(text);
});
it("caps oversized tool-call argument values without touching small ones", () => {
const out = snapcompact.serializeConversation([
createAssistantMessage([
{ type: "toolCall", id: "c1", name: "write", arguments: { path: "a.ts", content: "y".repeat(3000) } },
]),
]);
// JSON-encoded content is 3002 chars; per-value cap 500 elides 2502.
expect(out).toContain('write(path="a.ts", content=');
expect(out).toContain("[... 2502 chars elided ...]");
});
it("caps the whole serialized argument list per call", () => {
const args: Record<string, unknown> = {};
for (let i = 0; i < 10; i++) args[`arg${i}`] = "z".repeat(400);
const out = snapcompact.serializeConversation([
createAssistantMessage([{ type: "toolCall", id: "c1", name: "tool", arguments: args }]),
]);
expect(out).toContain("arg0=");
expect(out).toContain("chars elided");
// 10 values x ~400 chars collapse to the 2000-char call budget plus markers.
expect(out.length).toBeLessThan(2200);
});
it("wraps tool results in dim toggles by default and strips stray toggles from content", () => {
const out = snapcompact.serializeConversation([
createUserMessage(`hello ${snapcompact.DIM_ON}world`),
createToolResultMessage("ok"),
]);
expect(out).toContain(`[Tool result]: ${snapcompact.DIM_ON}ok${snapcompact.DIM_OFF}`);
// A stray toggle in user content cannot forge a dim span.
expect(out).toContain("[User]: hello world");
});
it("omits dim toggles when dimToolResults is false", () => {
const out = snapcompact.serializeConversation([createToolResultMessage("ok")], { dimToolResults: false });
expect(out).toBe("[Tool result]: ok");
});
});
describe("compact", () => {
it("archives history onto frames with a self-describing summary", async () => {
const fileOps = snapcompact.createFileOps();
fileOps.read.add("src/auth.ts");
fileOps.edited.add("src/login.ts");
const result = await snapcompact.compact(makePreparation({ fileOps }), { frameSize: TEST_FRAME_SIZE });
expect(result.firstKeptEntryId).toBe("kept-1");
expect(result.tokensBefore).toBe(99000);
// Reading instructions reflect the default (anthropic 8x8r-bw) shape.
expect(result.summary).toContain("40 characters per row");
expect(result.summary).toContain("printed twice");
expect(result.summary).toContain("plain black ink");
expect(result.summary).toContain("snapcompact frame");
// File operations are upserted like every other compaction summary:
// one grouped <files> tree with per-file access markers.
expect(result.summary).toContain("<files>\n# src/\nauth.ts (Read)\nlogin.ts (Write)\n</files>");
expect(result.shortSummary).toContain("snapcompact frame");
const archive = snapcompact.getPreservedArchive(result.preserveData);
expect(archive).toBeDefined();
expect(archive?.frames.length).toBe(1);
expect(archive?.frames[0].mimeType).toBe("image/png");
expect(archive?.frames[0].chars).toBe(archive?.totalChars);
expect(archive?.frames[0].font).toBe("8x8");
expect(archive?.frames[0].variant).toBe("bw");
expect(archive?.frames[0].lineRepeat).toBe(2);
expect(archive?.truncatedChars).toBe(0);
// Frame data round-trips as a decodable PNG.
const decoded = decodePng(Buffer.from(archive?.frames[0].data ?? "", "base64"));
expect(decoded.width).toBe(TEST_FRAME_SIZE);
});
it("prints tool results in dim gray ink, persisting the span across frame boundaries", async () => {
// Anthropic shape at 320px holds 800 chars/frame; a 1650-char tool
// result spans three frames, so the reopened span must dim in each.
const result = await snapcompact.compact(
makePreparation({
messagesToSummarize: [createUserMessage("Run the suite."), createToolResultMessage("FAIL ".repeat(330))],
}),
{ frameSize: TEST_FRAME_SIZE },
);
const archive = snapcompact.getPreservedArchive(result.preserveData);
expect(archive?.frames.length).toBeGreaterThanOrEqual(2);
for (const frame of archive?.frames ?? []) {
const decoded = decodePng(Buffer.from(frame.data, "base64"));
// Palette index 9 is the dim tool-output ink.
expect(new Set(decoded.pixels).has(9)).toBe(true);
}
// Conversation text outside the span stays in black bw ink (frame 1).
const first = decodePng(Buffer.from(archive?.frames[0].data ?? "", "base64"));
expect(new Set(first.pixels).has(7)).toBe(true);
expect(result.summary).toContain("dim gray ink");
});
it("keeps frames free of dim ink when dimToolResults is false", async () => {
const result = await snapcompact.compact(
makePreparation({
messagesToSummarize: [createUserMessage("Run."), createToolResultMessage("all good")],
}),
{ frameSize: TEST_FRAME_SIZE, dimToolResults: false },
);
const archive = snapcompact.getPreservedArchive(result.preserveData);
const decoded = decodePng(Buffer.from(archive?.frames[0].data ?? "", "base64"));
expect(new Set(decoded.pixels).has(9)).toBe(false);
expect(result.summary).not.toContain("dim gray ink");
});
it("splits oversized history across frames and evicts beyond the budget", async () => {
const { capacity } = snapcompact.geometry(snapcompact.SHAPES.anthropic, TEST_FRAME_SIZE);
// Sentences avoid whitespace collapse shrinking the payload below 2.5 frames.
const longText = "Important fact number one. ".repeat(Math.ceil((capacity * 2.5) / 28));
const result = await snapcompact.compact(
makePreparation({ messagesToSummarize: [createUserMessage(longText)] }),
{
frameSize: TEST_FRAME_SIZE,
maxFrames: 2,
},
);
const archive = snapcompact.getPreservedArchive(result.preserveData);
expect(archive?.frames.length).toBe(2);
expect(archive?.truncatedChars).toBeGreaterThan(0);
expect(result.summary).toContain("dropped");
});
it("evicts the oldest unpinned frames, keeping the session-head frame alive", async () => {
let previous: snapcompact.CompactionResult | undefined;
let headFrameData = "";
let secondFrameData = "";
for (let pass = 1; pass <= 4; pass++) {
previous = await snapcompact.compact(
makePreparation({
messagesToSummarize: [createUserMessage(`Distinct turn number ${pass}.`)],
previousSummary: previous?.summary,
previousPreserveData: previous?.preserveData,
}),
{ frameSize: TEST_FRAME_SIZE, maxFrames: 3 },
);
const archive = snapcompact.getPreservedArchive(previous.preserveData);
if (pass === 1) headFrameData = archive?.frames[0].data ?? "";
if (pass === 2) secondFrameData = archive?.frames[1].data ?? "";
}
const final = snapcompact.getPreservedArchive(previous?.preserveData);
expect(final?.frames.length).toBe(3);
// The head frame (original request) is pinned through every eviction;
// the archive fades from the middle out.
expect(final?.frames[0].data).toBe(headFrameData);
expect(final?.frames.some(frame => frame.data === secondFrameData)).toBe(false);
expect(final?.truncatedChars).toBeGreaterThan(0);
});
it("includes the previous text summary when the prior compaction was not snapcompact", async () => {
const result = await snapcompact.compact(
makePreparation({ previousSummary: "Older context: project scaffolding done." }),
{ frameSize: TEST_FRAME_SIZE },
);
expect(result.summary).toContain("[Summary of earlier history]");
});
it("carries previous frames forward and strips the OpenAI remote payload", async () => {
const first = await snapcompact.compact(makePreparation(), { frameSize: TEST_FRAME_SIZE });
const firstArchive = snapcompact.getPreservedArchive(first.preserveData);
const second = await snapcompact.compact(
makePreparation({
messagesToSummarize: [createUserMessage("A new turn happened after the first compaction.")],
previousSummary: first.summary,
previousPreserveData: {
...first.preserveData,
openaiRemoteCompaction: { provider: "openai", replacementHistory: [] },
appKey: "kept",
},
}),
{ frameSize: TEST_FRAME_SIZE },
);
const archive = snapcompact.getPreservedArchive(second.preserveData);
expect(archive?.frames.length).toBe(2);
// Oldest frame rides along unchanged, new frame appended after it.
expect(archive?.frames[0].data).toBe(firstArchive?.frames[0].data ?? "");
// Previous archive present → previous summary is snapcompact boilerplate, not re-archived.
expect(second.summary).not.toContain("[Summary of earlier history]");
expect(second.preserveData?.openaiRemoteCompaction).toBeUndefined();
expect(second.preserveData?.appKey).toBe("kept");
});
it("flags mixed shapes when merged frames disagree with the active shape", async () => {
const first = await snapcompact.compact(makePreparation(), {
frameSize: TEST_FRAME_SIZE,
shape: snapcompact.SHAPES.legacy,
});
const second = await snapcompact.compact(
makePreparation({
messagesToSummarize: [createUserMessage("Another turn after a provider switch.")],
previousSummary: first.summary,
previousPreserveData: first.preserveData,
}),
{ frameSize: TEST_FRAME_SIZE, model: { api: "anthropic-messages" } },
);
expect(second.summary).toContain("Older frames may use a different font");
// Same-shape merges stay silent.
expect(first.summary).not.toContain("Older frames may use a different font");
});
});
describe("archive helpers", () => {
it("getPreservedArchive rejects malformed payloads", () => {
expect(snapcompact.getPreservedArchive(undefined)).toBeUndefined();
expect(snapcompact.getPreservedArchive({ [snapcompact.PRESERVE_KEY]: "nope" })).toBeUndefined();
expect(snapcompact.getPreservedArchive({ [snapcompact.PRESERVE_KEY]: { frames: [] } })).toBeUndefined();
const valid: snapcompact.Archive = {
frames: [{ data: "ZmFrZQ==", mimeType: "image/png", cols: 64, rows: 40, chars: 10 }],
totalChars: 10,
truncatedChars: 0,
};
expect(snapcompact.getPreservedArchive({ [snapcompact.PRESERVE_KEY]: valid })).toEqual(valid);
});
});