/** * KHAELOR * File: tests/tools/write.test.ts * Description: Unit tests for the write tool — overwrite protection, header reminders, diffs, events. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { readFileSync, rmSync, writeFileSync } from "node:fs"; import * as path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createWriteTool } from "../../src/tools/index.js"; import type { TestHarness } from "./helpers.js"; import { fixture, makeHarness, markRead } from "./helpers.js"; let h: TestHarness; const write = createWriteTool(); const HEADERED = `/** * KHAELOR * File: src/x.ts * Description: Test file. * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ export const x = 1; `; beforeEach(() => { h = makeHarness(); }); afterEach(() => { rmSync(h.dir, { recursive: true, force: true }); }); describe("write tool", () => { it("writes a new file, creating parent directories", async () => { const abs = path.join(h.dir, "deep/nested/new.txt"); const result = await write.execute({ file_path: "deep/nested/new.txt", content: "a\nb\n" }, h.ctx); expect(result.isError).toBeUndefined(); expect(result.content).toBe(`Wrote ${abs} (2 lines).`); expect(readFileSync(abs, "utf8")).toBe("a\nb\n"); expect(result.metadata?.title).toBe("Write deep/nested/new.txt · new file · 2 lines"); expect(result.metadata?.extra?.["created"]).toBe(true); }); it("appends the header reminder for a headerless new source file", async () => { const result = await write.execute( { file_path: "src/naked.ts", content: "export const y = 2;\n" }, h.ctx, ); expect(result.isError).toBeUndefined(); expect(result.content).toContain( "NOTE: This new source file is missing the mandatory KHAELOR author header", ); expect(result.content).toContain("the header lint check fails the build without it"); }); it("does not remind when the header is present", async () => { const result = await write.execute({ file_path: "src/x.ts", content: HEADERED }, h.ctx); expect(result.content).not.toContain("NOTE:"); }); it("does not remind for non-source or vendored files", async () => { const json = await write.execute({ file_path: "data.json", content: "{}\n" }, h.ctx); expect(json.content).not.toContain("NOTE:"); const vendored = await write.execute( { file_path: "node_modules/pkg/index.js", content: "module.exports = 1;\n" }, h.ctx, ); expect(vendored.content).not.toContain("NOTE:"); }); it("refuses to overwrite a file that was never read this session", async () => { const abs = fixture(h, "existing.ts", "original\n"); const result = await write.execute({ file_path: "existing.ts", content: "clobber\n" }, h.ctx); expect(result.isError).toBe(true); expect(result.content).toBe( `Refusing to overwrite ${abs}: you have not read this file in this session. Read it first so you do not destroy existing content, then write or edit it.`, ); expect(readFileSync(abs, "utf8")).toBe("original\n"); }); it("overwrites after a read, reporting old and new line counts", async () => { const abs = fixture(h, "counted.ts", "one\ntwo\nthree\n"); await markRead(h, abs); const result = await write.execute({ file_path: "counted.ts", content: "single\n" }, h.ctx); expect(result.isError).toBeUndefined(); expect(result.content).toContain(`Replaced ${abs} (was 3 lines, now 1 lines).`); expect(result.metadata?.additions).toBe(1); expect(result.metadata?.deletions).toBe(3); }); it("refuses when the file changed on disk after the read", async () => { const abs = fixture(h, "raced.ts", "original\n"); await markRead(h, abs); writeFileSync(abs, "changed by the user\n"); const result = await write.execute({ file_path: "raced.ts", content: "agent version\n" }, h.ctx); expect(result.isError).toBe(true); expect(result.content).toBe( `Refusing to overwrite ${abs}: the file changed on disk after you last read it (content hash mismatch). Someone else may be editing it. Re-read the file and reapply your change.`, ); expect(readFileSync(abs, "utf8")).toBe("changed by the user\n"); }); it("emits file.modified with diff stats and stamps the registry", async () => { const abs = fixture(h, "diffed.ts", "a\nb\n"); await markRead(h, abs); await write.execute({ file_path: "diffed.ts", content: "a\nc\n" }, h.ctx); const event = h.events.find((e) => e.type === "file.modified"); expect(event).toBeDefined(); if (event?.type === "file.modified") { expect(event.payload.operation).toBe("write"); expect(event.payload.diffStats).toEqual({ added: 1, removed: 1 }); expect(event.payload.diff).toContain("-b"); expect(event.payload.diff).toContain("+c"); } expect(h.fileTimes.check(abs, "a\nc\n")).toBe("clean"); }); it("rejects writing to a directory path", async () => { fixture(h, "adir/inner.txt", "x\n"); const result = await write.execute({ file_path: "adir", content: "nope" }, h.ctx); expect(result.isError).toBe(true); expect(result.content).toContain("Path is a directory, not a file"); }); });