summaryrefslogtreecommitdiffhomepage
path: root/packages/core/tests
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-02 15:54:39 +0900
committerAdam Malczewski <[email protected]>2026-06-02 15:54:39 +0900
commit3ff2db698c2633023934d8477a9e995f78fa011e (patch)
tree1797ca96cb8fbc8df23a2667037e3a6ddeec2f35 /packages/core/tests
parentc0c08720cceb75b5e635e71190ae1f956f535133 (diff)
downloaddispatch-3ff2db698c2633023934d8477a9e995f78fa011e.tar.gz
dispatch-3ff2db698c2633023934d8477a9e995f78fa011e.zip
fix(perm): decouple perm_user_agent from perm_summon for spawning user agents
Granting only the user-agent (top-level) permission without the subagent-summon permission left the agent unable to summon user agents: the whole summon tool was gated behind perm_summon, so perm_user_agent alone produced no summon tool. Register summon when EITHER perm_summon OR perm_user_agent is granted. createSummonTool now takes an independent subagentEnabled flag (mirrors perm_summon) alongside userAgentEnabled (mirrors perm_user_agent): - subagent-only -> ordinary subagents, no top_level - user-agent-only -> spawns ONLY top-level user agents (top_level forced, background/top_level params dropped, user-agent catalog only) - both -> unchanged full behavior retrieve stays bundled with perm_summon (user agents are fire-and-forget). Adds core summon tests (user-agent-only mode + legacy-default regression) and an agent-manager summon/user_agent permission-split suite.
Diffstat (limited to 'packages/core/tests')
-rw-r--r--packages/core/tests/tools/summon.test.ts108
1 files changed, 108 insertions, 0 deletions
diff --git a/packages/core/tests/tools/summon.test.ts b/packages/core/tests/tools/summon.test.ts
index f59f345..4885a94 100644
--- a/packages/core/tests/tools/summon.test.ts
+++ b/packages/core/tests/tools/summon.test.ts
@@ -239,3 +239,111 @@ describe("createSummonTool — execute() argument forwarding", () => {
expect(getResult).toHaveBeenCalled();
});
});
+
+describe("createSummonTool — user-agent-only mode (perm_user_agent without perm_summon)", () => {
+ // userAgentEnabled=true, subagentEnabled=false → the tool spawns ONLY
+ // top-level user agents. `top_level` is implied (and forced), the
+ // subagent/parallel-work prose is dropped, and only the user-agent
+ // catalog group is shown.
+ const subagents: AvailableAgent[] = [
+ {
+ slug: "programmer",
+ name: "Programmer",
+ description: "Codes things",
+ path: "/agents/programmer.toml",
+ },
+ ];
+ const userAgents: AvailableAgent[] = [
+ {
+ slug: "default",
+ name: "Default",
+ description: "Default agent",
+ path: "/agents/default.toml",
+ },
+ ];
+
+ function userAgentOnlyTool(
+ spawn = vi.fn(async () => "ua-1"),
+ getResult = vi.fn(async () => ({ status: "done" as const, result: "nope" })),
+ ) {
+ return {
+ spawn,
+ getResult,
+ tool: createSummonTool(
+ "/tmp/work",
+ { spawn, getResult },
+ subagents,
+ userAgents,
+ ["/agents"],
+ true, // userAgentEnabled
+ false, // subagentEnabled
+ ),
+ };
+ }
+
+ it("describes spawning user agents and omits subagent/parallel-work prose", () => {
+ const { tool } = userAgentOnlyTool();
+ expect(tool.description).toContain("Spawn an independent top-level user agent");
+ expect(tool.description).toContain("fire-and-forget");
+ expect(tool.description).not.toContain("Pattern for parallel work");
+ expect(tool.description).not.toContain("Set background=true");
+ });
+
+ it("lists only the user-agent catalog group, not subagents", () => {
+ const { tool } = userAgentOnlyTool();
+ expect(tool.description).toContain("User agents (spawned as independent top-level tabs):");
+ expect(tool.description).toContain("default");
+ // Subagents must not be advertised in user-agent-only mode.
+ expect(tool.description).not.toContain("Subagents (spawned as child tabs):");
+ expect(tool.description).not.toContain("- programmer: Programmer");
+ });
+
+ it("only lists user-agent slugs in the 'agent' parameter description", () => {
+ const { tool } = userAgentOnlyTool();
+ const agentParam = (tool.parameters as unknown as { shape: { agent: { description: string } } })
+ .shape.agent;
+ expect(agentParam.description).toContain("default");
+ expect(agentParam.description).not.toContain("programmer");
+ });
+
+ it("omits the top_level parameter (it is implied)", () => {
+ const { tool } = userAgentOnlyTool();
+ const shape = (tool.parameters as unknown as { shape: Record<string, unknown> }).shape;
+ expect("top_level" in shape).toBe(false);
+ });
+
+ it("omits the background parameter (user agents are fire-and-forget)", () => {
+ const { tool } = userAgentOnlyTool();
+ const shape = (tool.parameters as unknown as { shape: Record<string, unknown> }).shape;
+ expect("background" in shape).toBe(false);
+ });
+
+ it("forces topLevel=true on spawn even when top_level is not passed", async () => {
+ const spawn = vi.fn(async () => "ua-99");
+ const getResult = vi.fn(async () => ({ status: "done" as const, result: "nope" }));
+ const { tool } = userAgentOnlyTool(spawn, getResult);
+ const out = await tool.execute({ task: "do stuff", agent: "default" });
+ expect(out).toContain("User agent spawned successfully");
+ expect(out).toContain("ua-99");
+ expect(out).toContain("fire-and-forget");
+ // Never blocks on a result for fire-and-forget user agents.
+ expect(getResult).not.toHaveBeenCalled();
+ const callArg = spawn.mock.calls[0]?.[0];
+ expect(callArg).toMatchObject({ topLevel: true, agentSlug: "default" });
+ });
+});
+
+describe("createSummonTool — subagentEnabled defaults preserve legacy behavior", () => {
+ it("defaults subagentEnabled=true so omitting it keeps subagent spawning", async () => {
+ const spawn = vi.fn(async () => "tab-1");
+ const getResult = vi.fn(async () => ({ status: "done" as const, result: "child" }));
+ // No userAgentEnabled/subagentEnabled args → legacy subagent-only mode.
+ const tool = createSummonTool("/tmp/work", { spawn, getResult }, [], []);
+ const out = await tool.execute({ task: "x", agent: "programmer" });
+ // Foreground subagent summon blocks and returns the child result.
+ expect(out).toBe("agent_id: tab-1\n\nchild");
+ expect(getResult).toHaveBeenCalled();
+ const callArg = spawn.mock.calls[0]?.[0];
+ expect(callArg).not.toHaveProperty("topLevel");
+ });
+});