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