import { beforeEach, describe, expect, it } from "vitest"; import "./test-helpers/fast-core-tools.js"; import { getCallGatewayMock, resetSessionsSpawnConfigOverride, setSessionsSpawnConfigOverride, } from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; import { resetSubagentRegistryForTests } from "./subagent-registry.js"; const callGatewayMock = getCallGatewayMock(); type CreateOpenClawTools = (typeof import("./openclaw-tools.js"))["createOpenClawTools"]; type CreateOpenClawToolsOpts = Parameters[0]; async function getSessionsSpawnTool(opts: CreateOpenClawToolsOpts) { // Dynamic import: ensure harness mocks are installed before tool modules load. const { createOpenClawTools } = await import("./openclaw-tools.js"); const tool = createOpenClawTools(opts).find((candidate) => candidate.name === "sessions_spawn"); if (!tool) { throw new Error("missing sessions_spawn tool"); } return tool; } describe("openclaw-tools: subagents (sessions_spawn allowlist)", () => { beforeEach(() => { resetSessionsSpawnConfigOverride(); }); it("sessions_spawn only allows same-agent by default", async () => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); const tool = await getSessionsSpawnTool({ agentSessionKey: "main", agentChannel: "whatsapp", }); const result = await tool.execute("call6", { task: "do thing", agentId: "beta", }); expect(result.details).toMatchObject({ status: "forbidden", }); expect(callGatewayMock).not.toHaveBeenCalled(); }); it("sessions_spawn forbids cross-agent spawning when not allowed", async () => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender", }, agents: { list: [ { id: "main", subagents: { allowAgents: ["alpha"], }, }, ], }, }); const tool = await getSessionsSpawnTool({ agentSessionKey: "main", agentChannel: "whatsapp", }); const result = await tool.execute("call9", { task: "do thing", agentId: "beta", }); expect(result.details).toMatchObject({ status: "forbidden", }); expect(callGatewayMock).not.toHaveBeenCalled(); }); it("sessions_spawn allows cross-agent spawning when configured", async () => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender", }, agents: { list: [ { id: "main", subagents: { allowAgents: ["beta"], }, }, ], }, }); let childSessionKey: string | undefined; callGatewayMock.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string; params?: unknown }; if (request.method === "agent") { const params = request.params as { sessionKey?: string } | undefined; childSessionKey = params?.sessionKey; return { runId: "run-1", status: "accepted", acceptedAt: 5000 }; } if (request.method === "agent.wait") { return { status: "timeout" }; } return {}; }); const tool = await getSessionsSpawnTool({ agentSessionKey: "main", agentChannel: "whatsapp", }); const result = await tool.execute("call7", { task: "do thing", agentId: "beta", }); expect(result.details).toMatchObject({ status: "accepted", runId: "run-1", }); expect(childSessionKey?.startsWith("agent:beta:subagent:")).toBe(true); }); it("sessions_spawn allows any agent when allowlist is *", async () => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender", }, agents: { list: [ { id: "main", subagents: { allowAgents: ["*"], }, }, ], }, }); let childSessionKey: string | undefined; callGatewayMock.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string; params?: unknown }; if (request.method === "agent") { const params = request.params as { sessionKey?: string } | undefined; childSessionKey = params?.sessionKey; return { runId: "run-1", status: "accepted", acceptedAt: 5100 }; } if (request.method === "agent.wait") { return { status: "timeout" }; } return {}; }); const tool = await getSessionsSpawnTool({ agentSessionKey: "main", agentChannel: "whatsapp", }); const result = await tool.execute("call8", { task: "do thing", agentId: "beta", }); expect(result.details).toMatchObject({ status: "accepted", runId: "run-1", }); expect(childSessionKey?.startsWith("agent:beta:subagent:")).toBe(true); }); it("sessions_spawn normalizes allowlisted agent ids", async () => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); setSessionsSpawnConfigOverride({ session: { mainKey: "main", scope: "per-sender", }, agents: { list: [ { id: "main", subagents: { allowAgents: ["Research"], }, }, ], }, }); let childSessionKey: string | undefined; callGatewayMock.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string; params?: unknown }; if (request.method === "agent") { const params = request.params as { sessionKey?: string } | undefined; childSessionKey = params?.sessionKey; return { runId: "run-1", status: "accepted", acceptedAt: 5200 }; } if (request.method === "agent.wait") { return { status: "timeout" }; } return {}; }); const tool = await getSessionsSpawnTool({ agentSessionKey: "main", agentChannel: "whatsapp", }); const result = await tool.execute("call10", { task: "do thing", agentId: "research", }); expect(result.details).toMatchObject({ status: "accepted", runId: "run-1", }); expect(childSessionKey?.startsWith("agent:research:subagent:")).toBe(true); }); });