import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtempSync, readFileSync, existsSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { makePaths } from "../src/paths.js"; import { callTool, toolDefinitions } from "../src/tools.js"; import type { PingEvent } from "../src/inbox.js"; let dir: string; beforeEach(() => { dir = mkdtempSync(join(tmpdir(), "watcher-tools-")); }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); const setupDeps = (recent: Map) => ({ paths: makePaths(dir), agent: "bob", recentEvents: () => recent, }); const sampleEvent = (over: Partial = {}): PingEvent => ({ ts: "2026-05-06T10:00:00Z", id: "ping-orig", from: "foreman", to: "bob", type: "NEEDS-RESPONSE", payload: "do the thing", ...over, }); describe("toolDefinitions", () => { it("declares ack, respond, and mark_handled with required ping_id", () => { const tools = toolDefinitions(); const names = tools.map((t) => t.name).sort(); expect(names).toEqual(["ack", "mark_handled", "respond"]); for (const t of tools) { expect(t.inputSchema.required).toContain("ping_id"); } }); }); describe("callTool — ack", () => { it("appends to acks/log with ack_kind=delivered + original_type", () => { const recent = new Map([["ping-orig", sampleEvent()]]); const deps = setupDeps(recent); const r = callTool("ack", { ping_id: "ping-orig" }, deps); expect(r.ok).toBe(true); const log = readFileSync(deps.paths.acksLog, "utf8").trim(); const obj = JSON.parse(log); expect(obj.acked_by).toBe("bob"); expect(obj.ping_id).toBe("ping-orig"); expect(obj.original_sender).toBe("foreman"); expect(obj.ack_kind).toBe("delivered"); expect(obj.original_type).toBe("NEEDS-RESPONSE"); }); it("rejects missing ping_id", () => { const r = callTool("ack", {}, setupDeps(new Map())); expect(r.ok).toBe(false); }); }); describe("callTool — respond", () => { it("writes a JSONL line to .inbox + ack with ack_kind=responded", () => { const recent = new Map([["ping-orig", sampleEvent()]]); const deps = setupDeps(recent); const r = callTool( "respond", { ping_id: "ping-orig", payload: "ok done", type: "INFO" }, deps, ); expect(r.ok).toBe(true); const inboxPath = deps.paths.inbox("foreman"); const lines = readFileSync(inboxPath, "utf8").trim().split("\n"); expect(lines).toHaveLength(1); const ev = JSON.parse(lines[0]); expect(ev.from).toBe("bob"); expect(ev.to).toBe("foreman"); expect(ev.type).toBe("INFO"); expect(ev.payload).toBe("ok done"); expect(ev.id).toMatch(/^ping-[a-f0-9]{8}$/); const ack = JSON.parse(readFileSync(deps.paths.acksLog, "utf8").trim()); expect(ack.ack_kind).toBe("responded"); expect(ack.response_id).toBe(ev.id); }); it("defaults missing type to INFO + missing priority to normal", () => { const recent = new Map([["ping-orig", sampleEvent()]]); const deps = setupDeps(recent); callTool("respond", { ping_id: "ping-orig", payload: "x" }, deps); const lines = readFileSync(deps.paths.inbox("foreman"), "utf8").trim().split("\n"); const ev = JSON.parse(lines[0]); expect(ev.type).toBe("INFO"); expect(ev.priority).toBe("normal"); }); it("rejects unknown ping_id", () => { const r = callTool( "respond", { ping_id: "ping-unknown", payload: "x" }, setupDeps(new Map()), ); expect(r.ok).toBe(false); expect(r.text).toContain("not found"); }); it("rejects invalid type", () => { const recent = new Map([["ping-orig", sampleEvent()]]); const r = callTool( "respond", { ping_id: "ping-orig", payload: "x", type: "bogus" }, setupDeps(recent), ); expect(r.ok).toBe(false); }); }); describe("callTool — mark_handled", () => { it("writes an ack with explanatory message", () => { const recent = new Map([["ping-orig", sampleEvent()]]); const deps = setupDeps(recent); const r = callTool("mark_handled", { ping_id: "ping-orig" }, deps); expect(r.ok).toBe(true); const ack = JSON.parse(readFileSync(deps.paths.acksLog, "utf8").trim()); expect(ack.ack_kind).toBe("delivered"); expect(ack.message).toMatch(/handled/i); }); }); describe("callTool — unknown tool", () => { it("returns ok=false on unknown tool name", () => { const r = callTool("bogus", { ping_id: "x" }, setupDeps(new Map())); expect(r.ok).toBe(false); expect(r.text).toContain("unknown"); }); }); describe("ack does not crash on unknown ping_id", () => { it("still writes an ack with original_sender=null", () => { const deps = setupDeps(new Map()); const r = callTool("ack", { ping_id: "ping-unknown" }, deps); expect(r.ok).toBe(true); const ack = JSON.parse(readFileSync(deps.paths.acksLog, "utf8").trim()); expect(ack.original_sender).toBeNull(); }); }); describe("paths writability through tools", () => { it("creates the acks dir on first ack", () => { const deps = setupDeps(new Map([["p1", sampleEvent({ id: "p1" })]])); expect(existsSync(deps.paths.acksLog)).toBe(false); callTool("ack", { ping_id: "p1" }, deps); expect(existsSync(deps.paths.acksLog)).toBe(true); }); });