/** * KHAELOR * File: tests/tools/read.test.ts * Description: Unit tests for the read tool — line numbering, paging, guards, and FileRead events. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { rmSync, writeFileSync } from "node:fs"; import * as path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createReadTool } from "../../src/tools/index.js"; import type { TestHarness } from "./helpers.js"; import { fixture, makeHarness } from "./helpers.js"; let h: TestHarness; const read = createReadTool(); beforeEach(() => { h = makeHarness(); }); afterEach(() => { rmSync(h.dir, { recursive: true, force: true }); }); describe("read tool", () => { it("returns numbered lines with the absolute path header", async () => { const abs = fixture(h, "src/a.ts", "const a = 1;\nconst b = 2;\n"); const result = await read.execute({ file_path: "src/a.ts" }, h.ctx); expect(result.isError).toBeUndefined(); const lines = result.content.split("\n"); expect(lines[0]).toBe(abs); expect(lines[1]).toBe(" 1→const a = 1;"); expect(lines[2]).toBe(" 2→const b = 2;"); expect(result.metadata?.title).toBe("Read src/a.ts · lines 1–2 of 2"); }); it("pages with offset/limit and reports how to continue", async () => { const body = Array.from({ length: 10 }, (_, i) => `line ${i + 1}`).join("\n"); fixture(h, "big.txt", `${body}\n`); const result = await read.execute({ file_path: "big.txt", offset: 3, limit: 4 }, h.ctx); expect(result.content).toContain(" 3→line 3"); expect(result.content).toContain(" 6→line 6"); expect(result.content).not.toContain("line 7"); expect(result.content).toContain("(Showing lines 3–6 of 10. Use offset=7 to continue.)"); }); it("rejects an offset beyond the end of the file", async () => { fixture(h, "small.txt", "one\ntwo\n"); const result = await read.execute({ file_path: "small.txt", offset: 50 }, h.ctx); expect(result.isError).toBe(true); expect(result.content).toBe( "Offset 50 is beyond the end of the file (2 lines). Use offset ≤ 2.", ); }); it("refuses binary files with a description instead of bytes", async () => { const abs = path.join(h.dir, "blob.bin"); writeFileSync(abs, Buffer.from([0x00, 0x01, 0x02, 0xff, 0x00, 0x33])); const result = await read.execute({ file_path: "blob.bin" }, h.ctx); expect(result.isError).toBe(true); expect(result.content).toContain("File appears to be binary"); expect(result.content).toContain(abs); }); it("reports missing files with a concrete next step", async () => { const result = await read.execute({ file_path: "nope/missing.ts" }, h.ctx); expect(result.isError).toBe(true); expect(result.content).toContain("File not found:"); expect(result.content).toContain("glob"); }); it("lists directory entries instead of erroring", async () => { fixture(h, "pkg/src/a.ts", "x\n"); fixture(h, "pkg/readme.md", "x\n"); fixture(h, "pkg/.hidden", "x\n"); const result = await read.execute({ file_path: "pkg" }, h.ctx); expect(result.isError).toBeUndefined(); expect(result.content).toContain("is a directory. Entries (2 levels):"); expect(result.content).toContain("src/"); expect(result.content).toContain(" a.ts"); expect(result.content).toContain("readme.md"); expect(result.content).toContain("(1 hidden entries not shown)"); }); it("truncates very long lines with a marker", async () => { fixture(h, "long.txt", `${"x".repeat(2500)}\nshort\n`); const result = await read.execute({ file_path: "long.txt" }, h.ctx); expect(result.content).toContain("… [line truncated]"); expect(result.content).toContain(" 2→short"); }); it("stamps the FileTimeRegistry and emits file.read", async () => { const abs = fixture(h, "tracked.ts", "a\nb\n"); await read.execute({ file_path: "tracked.ts" }, h.ctx); expect(h.fileTimes.get(abs)).toBeDefined(); expect(h.events).toHaveLength(1); const event = h.events[0]!; expect(event.type).toBe("file.read"); if (event.type === "file.read") { expect(event.payload.path).toBe("tracked.ts"); expect(event.payload.range).toEqual({ start: 1, end: 2 }); expect(event.payload.toolUseId).toBe("toolu_test"); } }); it("handles empty files", async () => { fixture(h, "empty.txt", ""); const result = await read.execute({ file_path: "empty.txt" }, h.ctx); expect(result.isError).toBeUndefined(); expect(result.content).toContain("(empty file — 0 lines)"); }); });