import { promises as fs } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { ToolRegistry } from "../src/tools.js";
import { registerFilesystemTools } from "../src/tools/filesystem.js";
import { ReadTracker } from "../src/tools/read-tracker.js";
describe("delete_range tool", () => {
let root: string;
let tools: ToolRegistry;
let readTracker: ReadTracker;
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "reasonix-delete-tools-"));
tools = new ToolRegistry();
registerFilesystemTools(tools, { rootDir: root });
readTracker = new ReadTracker();
});
afterEach(async () => {
await rm(root, { recursive: true, force: true });
});
it("delete_range refuses unread files, then deletes an anchored range after read_file", async () => {
await fs.writeFile(join(root, "demo.txt"), "before\nSTART\nremove\nEND\nafter\n");
const unread = await tools.dispatch(
"delete_range",
{ path: "demo.txt", start_anchor: "START\n", end_anchor: "END\n" },
{ readTracker },
);
expect(unread).toMatch(/read_file first/);
await tools.dispatch("read_file", { path: "demo.txt" }, { readTracker });
const out = await tools.dispatch(
"delete_range",
{ path: "demo.txt", start_anchor: "START\n", end_anchor: "END\n" },
{ readTracker },
);
expect(out).toMatch(/delete_range: deleted/);
await expect(fs.readFile(join(root, "demo.txt"), "utf8")).resolves.toBe("before\nafter\n");
});
it("delete_range is a no-op when anchors are duplicated", async () => {
await fs.writeFile(join(root, "demo.txt"), "A\nSTART\nx\nSTART\nEND\n");
await tools.dispatch("read_file", { path: "demo.txt" }, { readTracker });
const out = await tools.dispatch(
"delete_range",
{ path: "demo.txt", start_anchor: "START", end_anchor: "END" },
{ readTracker },
);
expect(out).toMatch(/no-op/);
await expect(fs.readFile(join(root, "demo.txt"), "utf8")).resolves.toContain("x");
});
});
describe("delete_symbol tool", () => {
let root: string;
let tools: ToolRegistry;
let readTracker: ReadTracker;
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), "reasonix-delete-tools-"));
tools = new ToolRegistry();
registerFilesystemTools(tools, { rootDir: root });
readTracker = new ReadTracker();
});
afterEach(async () => {
await rm(root, { recursive: true, force: true });
});
it("deletes a single TypeScript function by AST range", async () => {
await fs.writeFile(
join(root, "demo.ts"),
"export function keep() {\n return 1;\n}\n\nexport function removeMe() {\n return 2;\n}\n",
);
await tools.dispatch("read_file", { path: "demo.ts" }, { readTracker });
const out = await tools.dispatch(
"delete_symbol",
{ path: "demo.ts", name: "removeMe", kind: "function" },
{ readTracker },
);
expect(out).toMatch(/delete_symbol: deleted lines/);
const after = await fs.readFile(join(root, "demo.ts"), "utf8");
expect(after).toContain("keep");
expect(after).not.toContain("removeMe");
});
it("deletes leading decorators and JSDoc with a TypeScript symbol", async () => {
await fs.writeFile(
join(root, "decorated.ts"),
[
"export function keep() {",
" return 1;",
"}",
"",
"/** Remove this class. */",
"@sealed",
"export class RemoveMe {",
" value = 2;",
"}",
"",
].join("\n"),
);
await tools.dispatch("read_file", { path: "decorated.ts" }, { readTracker });
const out = await tools.dispatch(
"delete_symbol",
{ path: "decorated.ts", name: "RemoveMe", kind: "class" },
{ readTracker },
);
expect(out).toMatch(/delete_symbol: deleted lines/);
const after = await fs.readFile(join(root, "decorated.ts"), "utf8");
expect(after).toContain("keep");
expect(after).not.toContain("RemoveMe");
expect(after).not.toContain("@sealed");
expect(after).not.toContain("Remove this class");
});
it("deletes Python decorators with the symbol", async () => {
await fs.writeFile(
join(root, "decorated.py"),
"def keep():\n return 1\n\n@cached\n@traced\ndef remove_me():\n return 2\n",
);
await tools.dispatch("read_file", { path: "decorated.py" }, { readTracker });
const out = await tools.dispatch(
"delete_symbol",
{ path: "decorated.py", name: "remove_me", kind: "function" },
{ readTracker },
);
expect(out).toMatch(/delete_symbol: deleted lines/);
const after = await fs.readFile(join(root, "decorated.py"), "utf8");
expect(after).toContain("keep");
expect(after).not.toContain("remove_me");
expect(after).not.toContain("@cached");
expect(after).not.toContain("@traced");
});
});