spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: tests/tools/read.test.ts4 * Description: Unit tests for the read tool — line numbering, paging, guards, and FileRead events.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { rmSync, writeFileSync } from "node:fs";11import * as path from "node:path";12import { afterEach, beforeEach, describe, expect, it } from "vitest";13import { createReadTool } from "../../src/tools/index.js";14import type { TestHarness } from "./helpers.js";15import { fixture, makeHarness } from "./helpers.js";1617let h: TestHarness;18const read = createReadTool();1920beforeEach(() => {21 h = makeHarness();22});2324afterEach(() => {25 rmSync(h.dir, { recursive: true, force: true });26});2728describe("read tool", () => {29 it("returns numbered lines with the absolute path header", async () => {30 const abs = fixture(h, "src/a.ts", "const a = 1;\nconst b = 2;\n");31 const result = await read.execute({ file_path: "src/a.ts" }, h.ctx);32 expect(result.isError).toBeUndefined();33 const lines = result.content.split("\n");34 expect(lines[0]).toBe(abs);35 expect(lines[1]).toBe(" 1→const a = 1;");36 expect(lines[2]).toBe(" 2→const b = 2;");37 expect(result.metadata?.title).toBe("Read src/a.ts · lines 1–2 of 2");38 });3940 it("pages with offset/limit and reports how to continue", async () => {41 const body = Array.from({ length: 10 }, (_, i) => `line ${i + 1}`).join("\n");42 fixture(h, "big.txt", `${body}\n`);43 const result = await read.execute({ file_path: "big.txt", offset: 3, limit: 4 }, h.ctx);44 expect(result.content).toContain(" 3→line 3");45 expect(result.content).toContain(" 6→line 6");46 expect(result.content).not.toContain("line 7");47 expect(result.content).toContain("(Showing lines 3–6 of 10. Use offset=7 to continue.)");48 });4950 it("rejects an offset beyond the end of the file", async () => {51 fixture(h, "small.txt", "one\ntwo\n");52 const result = await read.execute({ file_path: "small.txt", offset: 50 }, h.ctx);53 expect(result.isError).toBe(true);54 expect(result.content).toBe(55 "Offset 50 is beyond the end of the file (2 lines). Use offset ≤ 2.",56 );57 });5859 it("refuses binary files with a description instead of bytes", async () => {60 const abs = path.join(h.dir, "blob.bin");61 writeFileSync(abs, Buffer.from([0x00, 0x01, 0x02, 0xff, 0x00, 0x33]));62 const result = await read.execute({ file_path: "blob.bin" }, h.ctx);63 expect(result.isError).toBe(true);64 expect(result.content).toContain("File appears to be binary");65 expect(result.content).toContain(abs);66 });6768 it("reports missing files with a concrete next step", async () => {69 const result = await read.execute({ file_path: "nope/missing.ts" }, h.ctx);70 expect(result.isError).toBe(true);71 expect(result.content).toContain("File not found:");72 expect(result.content).toContain("glob");73 });7475 it("lists directory entries instead of erroring", async () => {76 fixture(h, "pkg/src/a.ts", "x\n");77 fixture(h, "pkg/readme.md", "x\n");78 fixture(h, "pkg/.hidden", "x\n");79 const result = await read.execute({ file_path: "pkg" }, h.ctx);80 expect(result.isError).toBeUndefined();81 expect(result.content).toContain("is a directory. Entries (2 levels):");82 expect(result.content).toContain("src/");83 expect(result.content).toContain(" a.ts");84 expect(result.content).toContain("readme.md");85 expect(result.content).toContain("(1 hidden entries not shown)");86 });8788 it("truncates very long lines with a marker", async () => {89 fixture(h, "long.txt", `${"x".repeat(2500)}\nshort\n`);90 const result = await read.execute({ file_path: "long.txt" }, h.ctx);91 expect(result.content).toContain("… [line truncated]");92 expect(result.content).toContain(" 2→short");93 });9495 it("stamps the FileTimeRegistry and emits file.read", async () => {96 const abs = fixture(h, "tracked.ts", "a\nb\n");97 await read.execute({ file_path: "tracked.ts" }, h.ctx);98 expect(h.fileTimes.get(abs)).toBeDefined();99 expect(h.events).toHaveLength(1);100 const event = h.events[0]!;101 expect(event.type).toBe("file.read");102 if (event.type === "file.read") {103 expect(event.payload.path).toBe("tracked.ts");104 expect(event.payload.range).toEqual({ start: 1, end: 2 });105 expect(event.payload.toolUseId).toBe("toolu_test");106 }107 });108109 it("handles empty files", async () => {110 fixture(h, "empty.txt", "");111 const result = await read.execute({ file_path: "empty.txt" }, h.ctx);112 expect(result.isError).toBeUndefined();113 expect(result.content).toContain("(empty file — 0 lines)");114 });115});116