import { test } from "node:test"; import assert from "node:assert/strict"; import { callTool, typedTools, untypedActions } from "../src/tools.mjs"; import { fakeToolSocket, resolvedFor, scratch } from "./helpers.mjs"; const names = (tools) => tools.map((t) => t.name); test("the PM gets launch, its task verbs and the reads", (t) => { const pm = resolvedFor(t, "pm"); const tools = typedTools({ resolved: pm, arbiter: true, tracker: true }); assert.deepEqual(names(tools), [ "send_message", "launch", "task_create", "task_assign", "task_reassign", "task_schedule", "task_priority_change", "task_scope_change", "task_update_assigned", "task_close", "raise_decision", "resolve_decision", "list_agents", "inbox", "trail", "list_tasks", ]); const launch = tools.find((x) => x.name === "launch"); assert.equal(launch.verb, "launch"); assert.equal(launch.action, "role.launch"); assert.deepEqual(launch.parameters.properties.instance.enum, ["coder", "reviewer"]); for (const tool of tools) { assert.equal(tool.parameters.type, "object"); assert.equal(tool.parameters.additionalProperties, false); assert.ok(tool.description.length > 10); } }); test("a coder gets no launch, no resolve_decision, and no task tools without a tracker", (t) => { const coder = resolvedFor(t, "coder"); const tools = typedTools({ resolved: coder }); assert.deepEqual(names(tools), ["send_message", "raise_decision", "list_agents", "inbox", "trail"]); // Held actions without a tool are listed, so the manifest shows them. const untyped = untypedActions(coder, tools); assert.ok(untyped.includes("git.push.working")); assert.ok(!untyped.includes("message.send")); }); test("launch only when the business's launch block names the instance as launcher", (t) => { const pm = resolvedFor(t, "pm", (d) => delete d.launch); assert.equal(pm.launch, null); assert.ok(!names(typedTools({ resolved: pm })).includes("launch")); }); test("an action outside the instance's authority has no tool", (t) => { const coder = resolvedFor(t, "coder", (d) => (d.roles.coder.vars["limits.authority"] = ["review.request"])); assert.deepEqual(names(typedTools({ resolved: coder, tracker: true })), ["raise_decision", "list_agents", "inbox", "trail", "list_tasks"]); }); test("callTool: one JSON line out, the result back, a refusal rejects", async (t) => { const dir = scratch(t); const sock = await fakeToolSocket(t, dir, (tool, args) => { if (tool === "launch") throw new Error("capacity-full"); return { echoed: args }; }); assert.deepEqual(await callTool(sock.path, "list_agents", { a: 1 }), { echoed: { a: 1 } }); await assert.rejects(callTool(sock.path, "launch", { instance: "coder" }), /refused: capacity-full/); await assert.rejects(callTool(`${dir}/missing.sock`, "x"), /tool socket: ENOENT/); assert.deepEqual(sock.calls.map((c) => c.tool), ["list_agents", "launch"]); });