SPB Git

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%
4.6 KB · 116 lines typescript
Raw Blame History
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