import { describe, it, expect } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { createToolOutputTruncatorHandler } from "./tool-output-truncator/handler";
import { createHashlineReadEnhancerHandler } from "./hashline-read-enhancer/handler";
import {
createHashlineDiffEnhancerBeforeHandler,
createHashlineDiffEnhancerAfterHandler,
} from "./hashline-diff-enhancer/handler";
const HASHLINE_PATTERN = /^[0-9]+#[ZPMQVRWSNKTXJBYH]{2}\|/;
describe("tool-output-truncator", () => {
describe("#given a grep tool with output exceeding 2000 lines", () => {
describe("#when the handler processes the output", () => {
it("#then truncates the output and appends a notice with file path", async () => {
const handler = createToolOutputTruncatorHandler();
const lines = Array.from({ length: 3000 }, (_, i) => `line ${i + 1}: some content here`);
const input = { tool: "grep", sessionID: "s1", callID: "c1" };
const output = {
title: "grep result",
output: lines.join("\n"),
metadata: {},
};
await handler(input, output);
const resultLines = output.output.split("\n");
expect(resultLines.length).toBeLessThan(3000);
expect(output.output).toContain("[Output truncated:");
expect(output.output).toContain("Full output written to");
expect(output.output).toContain("Use Read with offset/limit");
});
});
});
describe("#given a Read tool (not in truncatable list)", () => {
describe("#when the handler processes the output", () => {
it("#then does not truncate the output", async () => {
const handler = createToolOutputTruncatorHandler();
const lines = Array.from({ length: 3000 }, (_, i) => `line ${i + 1}: content`);
const original = lines.join("\n");
const input = { tool: "Read", sessionID: "s1", callID: "c1" };
const output = {
title: "file content",
output: original,
metadata: {},
};
await handler(input, output);
expect(output.output).toBe(original);
});
});
});
describe("#given a bash tool with output within limits", () => {
describe("#when the handler processes the output", () => {
it("#then leaves the output unchanged", async () => {
const handler = createToolOutputTruncatorHandler();
const original = "short output";
const input = { tool: "bash", sessionID: "s1", callID: "c1" };
const output = {
title: "bash result",
output: original,
metadata: {},
};
await handler(input, output);
expect(output.output).toBe(original);
});
});
});
});
describe("hashline-read-enhancer", () => {
describe("#given a Read tool output with content tags", () => {
describe("#when the handler processes the output", () => {
it("#then prepends LINE#ID hashes to each file line", async () => {
const handler = createHashlineReadEnhancerHandler();
const input = { tool: "read", sessionID: "s1", callID: "c1" };
const output = {
title: "demo.ts",
output: [
"/tmp/demo.ts",
"file",
"",
"1: const x = 1",
"2: const y = 2",
"",
].join("\n"),
metadata: {},
};
await handler(input, output);
const lines = output.output.split("\n");
expect(lines[3]).toMatch(HASHLINE_PATTERN);
expect(lines[3]).toContain("const x = 1");
expect(lines[4]).toMatch(HASHLINE_PATTERN);
expect(lines[4]).toContain("const y = 2");
});
});
});
describe("#given a Read tool output without content tags", () => {
describe("#when the handler processes the output", () => {
it("#then hashifies plain numbered lines", async () => {
const handler = createHashlineReadEnhancerHandler();
const input = { tool: "read", sessionID: "s1", callID: "c1" };
const output = {
title: "README.md",
output: ["1: # Title", "2: Some content", "3: More content"].join("\n"),
metadata: {},
};
await handler(input, output);
const lines = output.output.split("\n");
expect(lines[0]).toMatch(HASHLINE_PATTERN);
expect(lines[0]).toContain("# Title");
expect(lines[1]).toMatch(HASHLINE_PATTERN);
expect(lines[2]).toMatch(HASHLINE_PATTERN);
});
});
});
describe("#given a non-Read tool", () => {
describe("#when the handler processes the output", () => {
it("#then leaves the output unchanged", async () => {
const handler = createHashlineReadEnhancerHandler();
const input = { tool: "bash", sessionID: "s1", callID: "c1" };
const original = "1: some output";
const output = {
title: "bash",
output: original,
metadata: {},
};
await handler(input, output);
expect(output.output).toBe(original);
});
});
});
describe("#given a Write tool with a valid file", () => {
describe("#when the handler processes the output", () => {
it("#then replaces output with line count summary", async () => {
const handler = createHashlineReadEnhancerHandler();
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hashline-write-"));
const filePath = path.join(tempDir, "demo.ts");
fs.writeFileSync(filePath, "const x = 1\nconst y = 2");
const input = { tool: "write", sessionID: "s1", callID: "c1" };
const output = {
title: "write",
output: "Wrote file successfully.",
metadata: { filepath: filePath },
};
await handler(input, output);
expect(output.output).toContain("File written successfully.");
expect(output.output).toContain("2 lines written.");
fs.rmSync(tempDir, { recursive: true, force: true });
});
});
});
});
describe("hashline-diff-enhancer", () => {
describe("#given a Write tool that modifies an existing file", () => {
describe("#when before and after handlers both run", () => {
it("#then metadata contains diff and hashlined content", async () => {
const beforeHandler = createHashlineDiffEnhancerBeforeHandler();
const afterHandler = createHashlineDiffEnhancerAfterHandler();
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hashline-diff-"));
const filePath = path.join(tempDir, "test.ts");
fs.writeFileSync(filePath, "const a = 1\nconst b = 2\n");
const sessionID = "s1";
const callID = "c1";
const beforeInput = { tool: "write", sessionID, callID };
const beforeOutput = { args: { filePath } };
await beforeHandler(beforeInput, beforeOutput);
fs.writeFileSync(filePath, "const a = 1\nconst b = 2\nconst c = 3\n");
const afterInput = { tool: "write", sessionID, callID };
const afterOutput = {
title: "write",
output: "File written successfully.",
metadata: {} as Record,
};
await afterHandler(afterInput, afterOutput);
expect(afterOutput.metadata.diff).toBeDefined();
expect(typeof afterOutput.metadata.diff).toBe("string");
expect(afterOutput.metadata.hashlined).toBeDefined();
const hashlined = afterOutput.metadata.hashlined as string;
expect(hashlined).toMatch(HASHLINE_PATTERN);
expect(afterOutput.title).toBe(filePath);
fs.rmSync(tempDir, { recursive: true, force: true });
});
});
});
describe("#given a non-Write tool", () => {
describe("#when the before handler processes the input", () => {
it("#then does not capture any state", async () => {
const beforeHandler = createHashlineDiffEnhancerBeforeHandler();
const input = { tool: "read", sessionID: "s1", callID: "c1" };
const output = { args: { filePath: "/some/file.ts" } };
await beforeHandler(input, output);
const afterHandler = createHashlineDiffEnhancerAfterHandler();
const afterInput = { tool: "read", sessionID: "s1", callID: "c1" };
const afterOutput = {
title: "read",
output: "file content",
metadata: {} as Record,
};
await afterHandler(afterInput, afterOutput);
expect(afterOutput.metadata.diff).toBeUndefined();
});
});
});
});