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/write.test.ts4 * Description: Unit tests for the write tool — overwrite protection, header reminders, diffs, events.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { readFileSync, rmSync, writeFileSync } from "node:fs";11import * as path from "node:path";12import { afterEach, beforeEach, describe, expect, it } from "vitest";13import { createWriteTool } from "../../src/tools/index.js";14import type { TestHarness } from "./helpers.js";15import { fixture, makeHarness, markRead } from "./helpers.js";1617let h: TestHarness;18const write = createWriteTool();1920const HEADERED = `/**21 * KHAELOR22 * File: src/x.ts23 * Description: Test file.24 *25 * Author: Simon-Pierre Boucher26 * Contact: contact@spboucher.ai27 */28export const x = 1;29`;3031beforeEach(() => {32 h = makeHarness();33});3435afterEach(() => {36 rmSync(h.dir, { recursive: true, force: true });37});3839describe("write tool", () => {40 it("writes a new file, creating parent directories", async () => {41 const abs = path.join(h.dir, "deep/nested/new.txt");42 const result = await write.execute({ file_path: "deep/nested/new.txt", content: "a\nb\n" }, h.ctx);43 expect(result.isError).toBeUndefined();44 expect(result.content).toBe(`Wrote ${abs} (2 lines).`);45 expect(readFileSync(abs, "utf8")).toBe("a\nb\n");46 expect(result.metadata?.title).toBe("Write deep/nested/new.txt · new file · 2 lines");47 expect(result.metadata?.extra?.["created"]).toBe(true);48 });4950 it("appends the header reminder for a headerless new source file", async () => {51 const result = await write.execute(52 { file_path: "src/naked.ts", content: "export const y = 2;\n" },53 h.ctx,54 );55 expect(result.isError).toBeUndefined();56 expect(result.content).toContain(57 "NOTE: This new source file is missing the mandatory KHAELOR author header",58 );59 expect(result.content).toContain("the header lint check fails the build without it");60 });6162 it("does not remind when the header is present", async () => {63 const result = await write.execute({ file_path: "src/x.ts", content: HEADERED }, h.ctx);64 expect(result.content).not.toContain("NOTE:");65 });6667 it("does not remind for non-source or vendored files", async () => {68 const json = await write.execute({ file_path: "data.json", content: "{}\n" }, h.ctx);69 expect(json.content).not.toContain("NOTE:");70 const vendored = await write.execute(71 { file_path: "node_modules/pkg/index.js", content: "module.exports = 1;\n" },72 h.ctx,73 );74 expect(vendored.content).not.toContain("NOTE:");75 });7677 it("refuses to overwrite a file that was never read this session", async () => {78 const abs = fixture(h, "existing.ts", "original\n");79 const result = await write.execute({ file_path: "existing.ts", content: "clobber\n" }, h.ctx);80 expect(result.isError).toBe(true);81 expect(result.content).toBe(82 `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.`,83 );84 expect(readFileSync(abs, "utf8")).toBe("original\n");85 });8687 it("overwrites after a read, reporting old and new line counts", async () => {88 const abs = fixture(h, "counted.ts", "one\ntwo\nthree\n");89 await markRead(h, abs);90 const result = await write.execute({ file_path: "counted.ts", content: "single\n" }, h.ctx);91 expect(result.isError).toBeUndefined();92 expect(result.content).toContain(`Replaced ${abs} (was 3 lines, now 1 lines).`);93 expect(result.metadata?.additions).toBe(1);94 expect(result.metadata?.deletions).toBe(3);95 });9697 it("refuses when the file changed on disk after the read", async () => {98 const abs = fixture(h, "raced.ts", "original\n");99 await markRead(h, abs);100 writeFileSync(abs, "changed by the user\n");101 const result = await write.execute({ file_path: "raced.ts", content: "agent version\n" }, h.ctx);102 expect(result.isError).toBe(true);103 expect(result.content).toBe(104 `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.`,105 );106 expect(readFileSync(abs, "utf8")).toBe("changed by the user\n");107 });108109 it("emits file.modified with diff stats and stamps the registry", async () => {110 const abs = fixture(h, "diffed.ts", "a\nb\n");111 await markRead(h, abs);112 await write.execute({ file_path: "diffed.ts", content: "a\nc\n" }, h.ctx);113 const event = h.events.find((e) => e.type === "file.modified");114 expect(event).toBeDefined();115 if (event?.type === "file.modified") {116 expect(event.payload.operation).toBe("write");117 expect(event.payload.diffStats).toEqual({ added: 1, removed: 1 });118 expect(event.payload.diff).toContain("-b");119 expect(event.payload.diff).toContain("+c");120 }121 expect(h.fileTimes.check(abs, "a\nc\n")).toBe("clean");122 });123124 it("rejects writing to a directory path", async () => {125 fixture(h, "adir/inner.txt", "x\n");126 const result = await write.execute({ file_path: "adir", content: "nope" }, h.ctx);127 expect(result.isError).toBe(true);128 expect(result.content).toContain("Path is a directory, not a file");129 });130});131