test / read.test.ts
import { execFileSync } from "child_process";
import { promises as fs } from "fs";
import * as path from "path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { OUT_OF_SCOPE } from "../src/paths";
import { runRead } from "../src/tools/read";
import { makeRoot } from "./helpers";
let root: string;
beforeEach(async () => {
root = await makeRoot();
});
afterEach(async () => {
await fs.rm(root, { recursive: true, force: true });
});
const put = (name: string, content: string | Buffer) => fs.writeFile(path.join(root, name), content);
const numbered = (n: number) => Array.from({ length: n }, (_, i) => `line ${i + 1}`).join("\n");
describe("runRead", () => {
it("returns the first 100 lines by default with a continue footer", async () => {
await put("f.txt", numbered(250));
const out = await runRead(root, { path: "f.txt" });
expect(out).toContain(" 1 line 1");
expect(out).toContain("100 line 100");
expect(out).not.toContain("line 101");
expect(out.endsWith("--- lines 1-100 of 250 | continue: offset=101")).toBe(true);
});
it("honours offset and limit", async () => {
await put("f.txt", numbered(250));
const out = await runRead(root, { path: "f.txt", offset: 101, limit: 5 });
expect(out).toContain("101 line 101");
expect(out).toContain("105 line 105");
expect(out).not.toContain("line 106");
expect(out.endsWith("--- lines 101-105 of 250 | continue: offset=106")).toBe(true);
});
it("omits the continue hint at end of file", async () => {
await put("f.txt", numbered(250));
const out = await runRead(root, { path: "f.txt", offset: 249 });
expect(out.endsWith("--- lines 249-250 of 250")).toBe(true);
});
it("errors when offset is past the end", async () => {
await put("f.txt", numbered(250));
expect(await runRead(root, { path: "f.txt", offset: 999 })).toBe(
"Error: offset 999 past end of file (250 lines)",
);
});
it("truncates at a line boundary once the token budget is exceeded", async () => {
const line = Array(100).fill("w").join(" ");
await put("f.txt", Array(20).fill(line).join("\n"));
const out = await runRead(root, { path: "f.txt" });
expect(out.endsWith("--- lines 1-7 of 20 | truncated at ~1000 tokens | continue: offset=8")).toBe(true);
});
it("always returns at least one line even if it alone exceeds the budget", async () => {
await put("f.txt", Array(2000).fill("w").join(" ") + "\nsecond");
const out = await runRead(root, { path: "f.txt" });
expect(out.endsWith("--- lines 1-1 of 2 | truncated at ~1000 tokens | continue: offset=2")).toBe(true);
});
it("returns (empty file) for an empty file", async () => {
await put("f.txt", "");
expect(await runRead(root, { path: "f.txt" })).toBe("(empty file)");
});
it("reads UTF-8 non-latin text as text, not binary", async () => {
await put("f.txt", "γγγ«γ‘γ―δΈη\nγγγ«γ‘γ―");
expect(await runRead(root, { path: "f.txt" })).toContain("γγγ«γ‘γ―δΈη");
});
it("rejects binary files", async () => {
await put("b.bin", Buffer.from([0x89, 0x50, 0x00, 0x01, 0x02]));
expect(await runRead(root, { path: "b.bin" })).toBe("Error: binary file, cannot read as text: b.bin");
});
it("rejects files over 5 MB", async () => {
await put("big.txt", Buffer.alloc(5 * 1024 * 1024 + 1, "a"));
const out = await runRead(root, { path: "big.txt" });
expect(out).toContain("Error: file is too large to read");
});
it("errors on a directory", async () => {
await fs.mkdir(path.join(root, "d"));
expect(await runRead(root, { path: "d" })).toBe("Error: is a directory, not a file: d");
});
it("errors on a missing file", async () => {
expect(await runRead(root, { path: "nope.txt" })).toBe("Error: no such file: nope.txt");
});
it("rejects out-of-scope paths", async () => {
expect(await runRead(root, { path: "../x" })).toBe(OUT_OF_SCOPE);
});
it("refuses FIFOs instead of blocking", async () => {
try {
execFileSync("mkfifo", [path.join(root, "pipe")]);
} catch {
return;
}
expect(await runRead(root, { path: "pipe" })).toBe("Error: not a regular file: pipe");
});
});
test / read.test.ts
import { execFileSync } from "child_process";
import { promises as fs } from "fs";
import * as path from "path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { OUT_OF_SCOPE } from "../src/paths";
import { runRead } from "../src/tools/read";
import { makeRoot } from "./helpers";
let root: string;
beforeEach(async () => {
root = await makeRoot();
});
afterEach(async () => {
await fs.rm(root, { recursive: true, force: true });
});
const put = (name: string, content: string | Buffer) => fs.writeFile(path.join(root, name), content);
const numbered = (n: number) => Array.from({ length: n }, (_, i) => `line ${i + 1}`).join("\n");
describe("runRead", () => {
it("returns the first 100 lines by default with a continue footer", async () => {
await put("f.txt", numbered(250));
const out = await runRead(root, { path: "f.txt" });
expect(out).toContain(" 1 line 1");
expect(out).toContain("100 line 100");
expect(out).not.toContain("line 101");
expect(out.endsWith("--- lines 1-100 of 250 | continue: offset=101")).toBe(true);
});
it("honours offset and limit", async () => {
await put("f.txt", numbered(250));
const out = await runRead(root, { path: "f.txt", offset: 101, limit: 5 });
expect(out).toContain("101 line 101");
expect(out).toContain("105 line 105");
expect(out).not.toContain("line 106");
expect(out.endsWith("--- lines 101-105 of 250 | continue: offset=106")).toBe(true);
});
it("omits the continue hint at end of file", async () => {
await put("f.txt", numbered(250));
const out = await runRead(root, { path: "f.txt", offset: 249 });
expect(out.endsWith("--- lines 249-250 of 250")).toBe(true);
});
it("errors when offset is past the end", async () => {
await put("f.txt", numbered(250));
expect(await runRead(root, { path: "f.txt", offset: 999 })).toBe(
"Error: offset 999 past end of file (250 lines)",
);
});
it("truncates at a line boundary once the token budget is exceeded", async () => {
const line = Array(100).fill("w").join(" ");
await put("f.txt", Array(20).fill(line).join("\n"));
const out = await runRead(root, { path: "f.txt" });
expect(out.endsWith("--- lines 1-7 of 20 | truncated at ~1000 tokens | continue: offset=8")).toBe(true);
});
it("always returns at least one line even if it alone exceeds the budget", async () => {
await put("f.txt", Array(2000).fill("w").join(" ") + "\nsecond");
const out = await runRead(root, { path: "f.txt" });
expect(out.endsWith("--- lines 1-1 of 2 | truncated at ~1000 tokens | continue: offset=2")).toBe(true);
});
it("returns (empty file) for an empty file", async () => {
await put("f.txt", "");
expect(await runRead(root, { path: "f.txt" })).toBe("(empty file)");
});
it("reads UTF-8 non-latin text as text, not binary", async () => {
await put("f.txt", "γγγ«γ‘γ―δΈη\nγγγ«γ‘γ―");
expect(await runRead(root, { path: "f.txt" })).toContain("γγγ«γ‘γ―δΈη");
});
it("rejects binary files", async () => {
await put("b.bin", Buffer.from([0x89, 0x50, 0x00, 0x01, 0x02]));
expect(await runRead(root, { path: "b.bin" })).toBe("Error: binary file, cannot read as text: b.bin");
});
it("rejects files over 5 MB", async () => {
await put("big.txt", Buffer.alloc(5 * 1024 * 1024 + 1, "a"));
const out = await runRead(root, { path: "big.txt" });
expect(out).toContain("Error: file is too large to read");
});
it("errors on a directory", async () => {
await fs.mkdir(path.join(root, "d"));
expect(await runRead(root, { path: "d" })).toBe("Error: is a directory, not a file: d");
});
it("errors on a missing file", async () => {
expect(await runRead(root, { path: "nope.txt" })).toBe("Error: no such file: nope.txt");
});
it("rejects out-of-scope paths", async () => {
expect(await runRead(root, { path: "../x" })).toBe(OUT_OF_SCOPE);
});
it("refuses FIFOs instead of blocking", async () => {
try {
execFileSync("mkfifo", [path.join(root, "pipe")]);
} catch {
return;
}
expect(await runRead(root, { path: "pipe" })).toBe("Error: not a regular file: pipe");
});
});