import assert from "node:assert/strict"; import { join } from "node:path"; import { cloneDefaultConfig, mock, runTest } from "./test-helpers.test.ts"; const TEST_AGENT_DIR = "/tmp/.pi/agent"; mock.module("@earendil-works/pi-coding-agent", { namedExports: { getAgentDir: () => TEST_AGENT_DIR, }, }); const { compactToolResult } = await import("./output-compactor.ts"); function buildReadContent(lineCount: number): string { const lines: string[] = []; for (let index = 0; index < lineCount; index += 1) { if (index % 2 === 0) { lines.push(`// comment ${index}`); } else { lines.push(`const value${index} = ${index};`); } } return `${lines.join("\n")}\n`; } function setReadCompaction(config: ReturnType, enabled: boolean): void { config.outputCompaction.readCompaction = { enabled }; } function firstTextBlock(content: unknown[] | undefined): string { if (!Array.isArray(content) || content.length === 0) { return ""; } const first = content[0] as { type?: string; text?: string }; if (first?.type !== "text" || typeof first.text !== "string") { return ""; } return first.text; } const OUTPUT_EMOJI_MARKERS = ["✓", "✔", "❌", "⚠️", "⚠", "📋", "📄", "🔍", "✅", "⏭️", "📌", "📝", "❓", "•"]; function compactBashOutput(command: string, text: string): string { const result = compactToolResult( { toolName: "bash", input: { command }, content: [{ type: "text", text }], }, cloneDefaultConfig(), ); assert.equal(result.changed, true); return firstTextBlock(result.content); } function assertNoOutputEmoji(text: string): void { for (const marker of OUTPUT_EMOJI_MARKERS) { assert.equal(text.includes(marker), false, `Unexpected output emoji marker: ${marker}`); } } function assertNoPartialHashlineAnchors(text: string): void { for (const line of text.split(/\r?\n/)) { if (/^\s*\d+\s*#[A-Za-z0-9_-]{2,32}:/.test(line)) { assert.equal(line.endsWith("..."), false, `Anchor line was partially truncated: ${line}`); } } } runTest("ssh_bash output uses Bash compaction while remote search output remains untouched", () => { const config = cloneDefaultConfig(); const sshBash = compactToolResult( { toolName: "ssh_bash", input: { command: "npm test" }, content: [{ type: "text", text: "\x1B[32mTests: 2 passed\x1B[0m\n" }], }, config, ); assert.equal(sshBash.changed, true); assert.ok(sshBash.techniques.includes("ansi")); assert.equal(firstTextBlock(sshBash.content).includes("\x1B"), false); for (const toolName of ["ssh_find", "ssh_grep"]) { const search = compactToolResult( { toolName, input: { pattern: "TODO" }, content: [{ type: "text", text: "\x1B[31mremote result\x1B[0m\n" }], }, config, ); assert.equal(search.changed, false); assert.deepEqual(search.techniques, []); } }); runTest("precision read with offset keeps exact output (no source/smart/hard truncation)", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 500; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; const content = buildReadContent(220); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts", offset: 1 }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, false); assert.deepEqual(result.techniques, []); }); runTest("precision read with limit keeps exact output", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 500; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; const content = buildReadContent(220); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts", limit: 200 }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, false); assert.deepEqual(result.techniques, []); }); runTest("default read output stays exact when read compaction is disabled by default", () => { const config = cloneDefaultConfig(); config.outputCompaction.sourceCodeFilteringEnabled = true; config.outputCompaction.sourceCodeFiltering = "aggressive"; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 500; const content = buildReadContent(220); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, false); assert.deepEqual(result.techniques, []); }); runTest("normal read compacts and adds banner when read compaction is enabled", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.sourceCodeFilteringEnabled = true; config.outputCompaction.sourceCodeFiltering = "minimal"; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; const content = buildReadContent(220); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, true); assert.ok(result.techniques.includes("source:minimal")); const compacted = firstTextBlock(result.content); assert.ok(compacted.startsWith("[RTK compacted output:")); assert.ok(compacted.includes("source:minimal")); }); runTest("line-anchor read output compacts without corrupting LINE#HASH anchors", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.sourceCodeFilteringEnabled = true; config.outputCompaction.sourceCodeFiltering = "minimal"; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 5000; const content = Array.from({ length: 120 }, (_value, index) => { const lineNumber = index + 1; const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`; return `${String(lineNumber).padStart(3, " ")}#ZP:${sourceLine}`; }).join("\n"); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, true); assert.ok(result.techniques.includes("source:minimal")); const compacted = firstTextBlock(result.content); assert.ok(compacted.startsWith("[RTK compacted output:")); assert.ok(compacted.includes("source:minimal")); assert.match(compacted, /\n\s*2#ZP:const value2 = 2;/); assert.equal(compacted.includes("#ZP:// comment"), false); assertNoPartialHashlineAnchors(compacted); }); runTest("colon-pipe anchor read output compacts without requiring hashline extension", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.sourceCodeFilteringEnabled = true; config.outputCompaction.sourceCodeFiltering = "minimal"; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 5000; const content = [ "Read sample.ts: 120 lines", "", ...Array.from({ length: 120 }, (_value, index) => { const lineNumber = index + 1; const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`; return `${lineNumber}:${(lineNumber % 256).toString(16).padStart(2, "0")}|${sourceLine}`; }), ].join("\n"); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, true); assert.ok(result.techniques.includes("source:minimal")); const compacted = firstTextBlock(result.content); assert.ok(compacted.includes("Read sample.ts: 120 lines")); assert.match(compacted, /\n2:02\|const value2 = 2;/); assert.equal(compacted.includes("|// comment"), false); }); runTest("compact LINEHASH pipe anchors from oh-my-pi style reads", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.sourceCodeFilteringEnabled = true; config.outputCompaction.sourceCodeFiltering = "minimal"; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 5000; const content = Array.from({ length: 120 }, (_value, index) => { const lineNumber = index + 1; const hash = lineNumber % 2 === 0 ? "sr" : "ab"; const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`; return `${lineNumber}${hash}|${sourceLine}`; }).join("\n"); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, true); assert.ok(result.techniques.includes("source:minimal")); const compacted = firstTextBlock(result.content); assert.match(compacted, /\n2sr\|const value2 = 2;/); assert.equal(compacted.includes("|// comment"), false); }); runTest("compact hashline-tools file wrapper while preserving non-anchor wrapper lines", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.sourceCodeFilteringEnabled = true; config.outputCompaction.sourceCodeFiltering = "minimal"; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 5000; const content = [ "", ...Array.from({ length: 120 }, (_value, index) => { const lineNumber = index + 1; const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`; return `${lineNumber}#ZM:${sourceLine}`; }), "", "(End of file - 120 total lines)", "", ].join("\n"); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, true); assert.ok(result.techniques.includes("source:minimal")); const compacted = firstTextBlock(result.content); assert.ok(compacted.includes("")); assert.ok(compacted.includes("(End of file - 120 total lines)")); assert.ok(compacted.includes("")); assert.match(compacted, /\n2#ZM:const value2 = 2;/); assert.equal(compacted.includes("#ZM:// comment"), false); }); runTest("anchor-safe read hard truncation preserves whole hashline anchors", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.sourceCodeFilteringEnabled = false; config.outputCompaction.smartTruncate.enabled = false; config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 350; const content = Array.from({ length: 120 }, (_value, index) => { const lineNumber = index + 1; return `${lineNumber}#ZP:const value${lineNumber} = "${"x".repeat(40)}";`; }).join("\n"); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, true); assert.ok(result.techniques.includes("truncate")); const compacted = firstTextBlock(result.content); assert.ok(compacted.includes("anchor-safe truncate")); assertNoPartialHashlineAnchors(compacted); }); runTest("incidental single anchor-like line does not disable normal read compaction", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.sourceCodeFilteringEnabled = true; config.outputCompaction.sourceCodeFiltering = "minimal"; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; const content = [`1#ZP:not an anchored read`, buildReadContent(120)].join("\n"); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, true); assert.ok(result.techniques.includes("source:minimal") || result.techniques.includes("smart-truncate")); }); runTest("short read output stays exact below threshold", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); const content = buildReadContent(40); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, false); assert.deepEqual(result.techniques, []); }); runTest("read output stays exact at the 80-line boundary with trailing newline", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; const content = buildReadContent(80); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, false); assert.deepEqual(result.techniques, []); }); runTest("read output compacts once the content exceeds the 80-line exactness threshold when read compaction is enabled", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.sourceCodeFilteringEnabled = true; config.outputCompaction.sourceCodeFiltering = "minimal"; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; const content = buildReadContent(81); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, true); assert.ok(result.techniques.includes("source:minimal") || result.techniques.includes("smart-truncate")); }); runTest("source file reads skip lossy source filtering when truncation safeguards are not needed", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.sourceCodeFilteringEnabled = true; config.outputCompaction.sourceCodeFiltering = "minimal"; config.outputCompaction.smartTruncate.enabled = false; config.outputCompaction.truncate.enabled = false; const content = buildReadContent(120); const result = compactToolResult( { toolName: "read", input: { path: "sample.ts" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, false); assert.deepEqual(result.techniques, []); assert.equal(firstTextBlock(result.content), ""); }); runTest("skill reads stay exact when preserveExactSkillReads is enabled for user skills", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.preserveExactSkillReads = true; config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 500; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; const content = buildReadContent(220); const result = compactToolResult( { toolName: "read", input: { path: join(TEST_AGENT_DIR, "skills", "example", "SKILL.md") }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, false); assert.deepEqual(result.techniques, []); }); runTest("project .pi skill reads stay exact when preserveExactSkillReads is enabled", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.preserveExactSkillReads = true; config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 500; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; const content = buildReadContent(220); const result = compactToolResult( { toolName: "read", input: { path: ".pi/skills/example/SKILL.md" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, false); assert.deepEqual(result.techniques, []); }); runTest("ancestor .agents skill reads stay exact when preserveExactSkillReads is enabled", () => { const config = cloneDefaultConfig(); setReadCompaction(config, true); config.outputCompaction.preserveExactSkillReads = true; config.outputCompaction.truncate.enabled = true; config.outputCompaction.truncate.maxChars = 500; config.outputCompaction.smartTruncate.enabled = true; config.outputCompaction.smartTruncate.maxLines = 40; const content = buildReadContent(220); const result = compactToolResult( { toolName: "read", input: { path: "../.agents/skills/example/SKILL.md" }, content: [{ type: "text", text: content }], }, config, ); assert.equal(result.changed, false); assert.deepEqual(result.techniques, []); }); runTest("build output uses plain-text status markers", () => { const compacted = compactBashOutput("npm run build", "Compiling app v0.1.0\n"); assert.equal(compacted, "[OK] Build successful (1 units compiled)"); assertNoOutputEmoji(compacted); }); runTest("git status output uses plain-text labels", () => { const compacted = compactBashOutput( "git status --short --branch", "## main...origin/main\nM staged.ts\n M modified.ts\n?? new.ts\nUU conflict.ts\n", ); assert.ok(compacted.startsWith("Branch: main\n")); assert.ok(compacted.includes("Staged: 1 files\n staged.ts\n")); assert.ok(compacted.includes("Modified: 1 files\n modified.ts\n")); assert.ok(compacted.includes("Untracked: 1 files\n new.ts\n")); assert.ok(compacted.includes("Conflicts: 1 files")); assertNoOutputEmoji(compacted); }); runTest("git diff output uses plain-text file markers", () => { const compacted = compactBashOutput( "git diff", "diff --git a/src/example.ts b/src/example.ts\n@@ -1 +1 @@\n-oldValue\n+newValue\n", ); assert.ok(compacted.includes("\n> src/example.ts\n")); assertNoOutputEmoji(compacted); }); runTest("linter success output uses plain-text status markers", () => { const compacted = compactBashOutput("npx eslint .", ""); assert.equal(compacted, "[OK] ESLint: No issues found"); assertNoOutputEmoji(compacted); }); runTest("test output uses plain-text labels and bullets", () => { const compacted = compactBashOutput( "bun test", "3 passed, 1 failed, 2 skipped\nFAIL src/example.test.ts\n Expected: true\n Received: false\n\n\n", ); assert.ok(compacted.includes("Test Results:")); assert.ok(compacted.includes("PASS: 3 passed")); assert.ok(compacted.includes("FAIL: 1 failed")); assert.ok(compacted.includes("SKIP: 2 skipped")); assert.ok(compacted.includes(" - FAIL src/example.test.ts")); assertNoOutputEmoji(compacted); }); runTest("all grep output stays intact because FFF owns search", () => { const text = "src/a.ts [modified in git]\n 1: const match = true;\n\n[Continue with cursor=\"fff_c1\"]"; const result = compactToolResult( { toolName: "grep", input: { pattern: "match" }, content: [{ type: "text", text }], }, cloneDefaultConfig(), ); assert.equal(result.changed, false); assert.equal(result.content, undefined); assert.deepEqual(result.techniques, []); }); runTest("git diff compaction skips already-compacted RTK-shaped output", () => { const result = compactToolResult( { toolName: "bash", input: { command: "git diff -- agent/extensions/pi-mcp-adapter/package.json" }, content: [{ type: "text", text: "agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n> agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@earendil-works/pi-coding-agent\": \"^0.58.1\",\n" }], }, cloneDefaultConfig(), ); assert.equal(result.changed, false); assert.equal(result.content, undefined); assert.deepEqual(result.techniques, []); }); runTest("non-hook RTK warnings are preserved verbatim", () => { const result = compactToolResult( { toolName: "bash", input: { command: "FOO=1 rtk git status" }, content: [{ type: "text", text: "[rtk] warning: builtin filters: parse failure\n\nworking tree clean\n" }], }, cloneDefaultConfig(), ); assert.equal(result.changed, false); assert.equal(result.content, undefined); assert.deepEqual(result.techniques, []); }); runTest("quoted hook warning text is preserved as payload", () => { const quotedHookText = 'const warning = "No hook installed — run `rtk init -g` for automatic token savings";\n'; const result = compactToolResult( { toolName: "bash", input: { command: "echo probe" }, content: [{ type: "text", text: quotedHookText }], }, cloneDefaultConfig(), ); assert.equal(result.changed, false); assert.equal(result.content, undefined); assert.deepEqual(result.techniques, []); }); console.log("All output-compactor tests passed.");