summaryrefslogtreecommitdiffhomepage
path: root/packages/core/tests/tools
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-28 22:51:47 +0900
committerAdam Malczewski <[email protected]>2026-05-28 22:51:47 +0900
commitd6609efd4e14101e77fb35a98ce597a32816862d (patch)
tree09ea404ce0a780ca6b8c380fdd93ad1ae9960986 /packages/core/tests/tools
parent2eeabc95b78f6624c187e1e3892f9413266b4b9a (diff)
downloaddispatch-d6609efd4e14101e77fb35a98ce597a32816862d.tar.gz
dispatch-d6609efd4e14101e77fb35a98ce597a32816862d.zip
fix(core): normalize tool schemas for Anthropic, add toolChoice=auto; feat(summon): agent definition support; docs: cc/ research findings
- registry.ts: add normalizeForAnthropic() to strip , additionalProperties, default, nullable from zodToJsonSchema output so Anthropic doesn't silently reject tool definitions - agent.ts: add toolChoice=auto for Claude OAuth to prevent Opus thinking forever without calling tools - summon.ts: add agentSlug parameter, build agents catalog in description, add toAvailableAgents helper - agent-manager.ts: wire agent definition loading into spawnChildAgent, agent model fallback - loader.ts: export loadAgent, expandAgentToolNames, getAgentDirPaths; add getAgentDirPaths for permission gate - agent.ts: auto-allow read-only tools in agent definition directories - packaging/PKGBUILD: exclude ARM64 prebuilds from x86_64 package - cc/: research findings on Claude Opus tool calling issues - tests: loader tests, summon tool tests
Diffstat (limited to 'packages/core/tests/tools')
-rw-r--r--packages/core/tests/tools/summon.test.ts137
1 files changed, 137 insertions, 0 deletions
diff --git a/packages/core/tests/tools/summon.test.ts b/packages/core/tests/tools/summon.test.ts
new file mode 100644
index 0000000..3909e48
--- /dev/null
+++ b/packages/core/tests/tools/summon.test.ts
@@ -0,0 +1,137 @@
+import { describe, expect, it, vi } from "vitest";
+import {
+ type AvailableAgent,
+ createSummonTool,
+ type SummonCallbacks,
+} from "../../src/tools/summon.js";
+
+const noopCallbacks: SummonCallbacks = {
+ spawn: async () => "agent-id-stub",
+ getResult: async () => ({ status: "done", result: "" }),
+};
+
+describe("createSummonTool — description content", () => {
+ it("lists the agent directories so the LLM knows where to look", () => {
+ const tool = createSummonTool(
+ "/tmp/work",
+ noopCallbacks,
+ [],
+ ["/home/u/.config/dispatch/agents", "/tmp/work/.dispatch/agents"],
+ );
+ expect(tool.description).toContain("/home/u/.config/dispatch/agents");
+ expect(tool.description).toContain("/tmp/work/.dispatch/agents");
+ expect(tool.description).toContain("read_file");
+ });
+
+ it("includes available agent slugs+names in the description", () => {
+ const agents: AvailableAgent[] = [
+ {
+ slug: "programmer",
+ name: "Programmer",
+ description: "Implements code from a plan.",
+ path: "/home/u/.config/dispatch/agents/programmer.toml",
+ },
+ {
+ slug: "researcher",
+ name: "Researcher",
+ description: "Investigates topics.",
+ path: "/home/u/.config/dispatch/agents/researcher.toml",
+ },
+ ];
+ const tool = createSummonTool("/tmp/work", noopCallbacks, agents, [
+ "/home/u/.config/dispatch/agents",
+ ]);
+ expect(tool.description).toContain("programmer");
+ expect(tool.description).toContain("Programmer");
+ expect(tool.description).toContain("Implements code from a plan");
+ expect(tool.description).toContain("researcher");
+ expect(tool.description).toContain("Investigates topics");
+ });
+
+ it("emits a 'no agents defined' notice when the catalog is empty", () => {
+ const tool = createSummonTool(
+ "/tmp/work",
+ noopCallbacks,
+ [],
+ ["/home/u/.config/dispatch/agents"],
+ );
+ expect(tool.description).toContain("No agent definitions are currently defined");
+ });
+});
+
+describe("createSummonTool — execute() argument forwarding", () => {
+ it("forwards agent slug through to callbacks.spawn", async () => {
+ const spawn = vi.fn(async () => "tab-xyz");
+ const tool = createSummonTool(
+ "/tmp/work",
+ { spawn, getResult: async () => ({ status: "done", result: "ok" }) },
+ [],
+ [],
+ );
+ await tool.execute({
+ task: "do thing",
+ agent: "programmer",
+ background: true,
+ });
+ expect(spawn).toHaveBeenCalledTimes(1);
+ const callArg = spawn.mock.calls[0]?.[0];
+ expect(callArg).toMatchObject({
+ task: "do thing",
+ agentSlug: "programmer",
+ });
+ });
+
+ it("omits agentSlug from the spawn payload when no agent param is given", async () => {
+ const spawn = vi.fn(async () => "tab-xyz");
+ const tool = createSummonTool(
+ "/tmp/work",
+ { spawn, getResult: async () => ({ status: "done", result: "ok" }) },
+ [],
+ [],
+ );
+ await tool.execute({
+ task: "do thing",
+ background: true,
+ });
+ expect(spawn).toHaveBeenCalledTimes(1);
+ const callArg = spawn.mock.calls[0]?.[0];
+ expect(callArg).not.toHaveProperty("agentSlug");
+ });
+
+ it("returns spawned agent_id when background=true (no blocking on result)", async () => {
+ const getResult = vi.fn(async () => ({ status: "done" as const, result: "should-not-see" }));
+ const tool = createSummonTool("/tmp/work", { spawn: async () => "id-42", getResult }, [], []);
+ const out = await tool.execute({ task: "x", background: true });
+ expect(out).toContain("id-42");
+ // Background mode must not block on getResult
+ expect(getResult).not.toHaveBeenCalled();
+ });
+
+ it("blocks on result and returns it when background=false (default)", async () => {
+ const tool = createSummonTool(
+ "/tmp/work",
+ {
+ spawn: async () => "id-1",
+ getResult: async () => ({ status: "done", result: "child-output" }),
+ },
+ [],
+ [],
+ );
+ const out = await tool.execute({ task: "x" });
+ expect(out).toBe("child-output");
+ });
+
+ it("surfaces child errors when blocking", async () => {
+ const tool = createSummonTool(
+ "/tmp/work",
+ {
+ spawn: async () => "id-1",
+ getResult: async () => ({ status: "error", error: "boom" }),
+ },
+ [],
+ [],
+ );
+ const out = await tool.execute({ task: "x" });
+ expect(out).toContain("boom");
+ });
+});