diff options
| author | Adam Malczewski <[email protected]> | 2026-05-28 22:51:47 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-28 22:51:47 +0900 |
| commit | d6609efd4e14101e77fb35a98ce597a32816862d (patch) | |
| tree | 09ea404ce0a780ca6b8c380fdd93ad1ae9960986 /packages/core/tests/tools | |
| parent | 2eeabc95b78f6624c187e1e3892f9413266b4b9a (diff) | |
| download | dispatch-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.ts | 137 |
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"); + }); +}); |
