From d4db45e8a9f61595672fdd6cd2f4e7a2f8f73408 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 23 Jan 2026 06:21:44 +0000 Subject: [PATCH] test(agents): merge sessions_spawn group announce coverage --- ...unces-back-requester-group-channel.test.ts | 235 ------------------ ...n-normalizes-allowlisted-agent-ids.test.ts | 192 ++++++++++++++ 2 files changed, 192 insertions(+), 235 deletions(-) delete mode 100644 src/agents/clawdbot-tools.subagents.sessions-spawn-announces-back-requester-group-channel.test.ts diff --git a/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-back-requester-group-channel.test.ts b/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-back-requester-group-channel.test.ts deleted file mode 100644 index 6a81c9d07..000000000 --- a/src/agents/clawdbot-tools.subagents.sessions-spawn-announces-back-requester-group-channel.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const callGatewayMock = vi.fn(); -vi.mock("../gateway/call.js", () => ({ - callGateway: (opts: unknown) => callGatewayMock(opts), -})); - -let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { - session: { - mainKey: "main", - scope: "per-sender", - }, -}; - -vi.mock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => configOverride, - resolveGatewayPort: () => 18789, - }; -}); - -import { emitAgentEvent } from "../infra/agent-events.js"; -import { createClawdbotTools } from "./clawdbot-tools.js"; -import { resetSubagentRegistryForTests } from "./subagent-registry.js"; - -describe("clawdbot-tools: subagents", () => { - beforeEach(() => { - configOverride = { - session: { - mainKey: "main", - scope: "per-sender", - }, - }; - }); - - it("sessions_spawn runs cleanup via lifecycle events", async () => { - resetSubagentRegistryForTests(); - callGatewayMock.mockReset(); - const calls: Array<{ method?: string; params?: unknown }> = []; - let agentCallCount = 0; - let deletedKey: string | undefined; - let childRunId: string | undefined; - let childSessionKey: string | undefined; - const waitCalls: Array<{ runId?: string; timeoutMs?: number }> = []; - - callGatewayMock.mockImplementation(async (opts: unknown) => { - const request = opts as { method?: string; params?: unknown }; - calls.push(request); - if (request.method === "agent") { - agentCallCount += 1; - const runId = `run-${agentCallCount}`; - const params = request.params as { - message?: string; - sessionKey?: string; - channel?: string; - timeout?: number; - lane?: string; - }; - // Only capture the first agent call (subagent spawn, not main agent trigger) - if (params?.lane === "subagent") { - childRunId = runId; - childSessionKey = params?.sessionKey ?? ""; - expect(params?.channel).toBe("discord"); - expect(params?.timeout).toBe(1); - } - return { - runId, - status: "accepted", - acceptedAt: 1000 + agentCallCount, - }; - } - if (request.method === "agent.wait") { - const params = request.params as { runId?: string; timeoutMs?: number } | undefined; - waitCalls.push(params ?? {}); - // Return "ok" with timing info for the child run - return { runId: params?.runId ?? "run-1", status: "ok", startedAt: 1000, endedAt: 2000 }; - } - if (request.method === "sessions.delete") { - const params = request.params as { key?: string } | undefined; - deletedKey = params?.key; - return { ok: true }; - } - return {}; - }); - - const tool = createClawdbotTools({ - agentSessionKey: "discord:group:req", - agentChannel: "discord", - }).find((candidate) => candidate.name === "sessions_spawn"); - if (!tool) throw new Error("missing sessions_spawn tool"); - - const result = await tool.execute("call1", { - task: "do thing", - runTimeoutSeconds: 1, - cleanup: "delete", - }); - expect(result.details).toMatchObject({ - status: "accepted", - runId: "run-1", - }); - - if (!childRunId) throw new Error("missing child runId"); - emitAgentEvent({ - runId: childRunId, - stream: "lifecycle", - data: { - phase: "end", - startedAt: 1234, - endedAt: 2345, - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - await new Promise((resolve) => setTimeout(resolve, 0)); - await new Promise((resolve) => setTimeout(resolve, 0)); - - const childWait = waitCalls.find((call) => call.runId === childRunId); - expect(childWait?.timeoutMs).toBe(1000); - - // Two agent calls: subagent spawn + main agent trigger - const agentCalls = calls.filter((call) => call.method === "agent"); - expect(agentCalls).toHaveLength(2); - - // First call: subagent spawn - const first = agentCalls[0]?.params as - | { - lane?: string; - deliver?: boolean; - sessionKey?: string; - channel?: string; - } - | undefined; - expect(first?.lane).toBe("subagent"); - expect(first?.deliver).toBe(false); - expect(first?.channel).toBe("discord"); - expect(first?.sessionKey?.startsWith("agent:main:subagent:")).toBe(true); - expect(childSessionKey?.startsWith("agent:main:subagent:")).toBe(true); - - // Second call: main agent trigger with announce message - const second = agentCalls[1]?.params as - | { - sessionKey?: string; - message?: string; - deliver?: boolean; - } - | undefined; - expect(second?.sessionKey).toBe("discord:group:req"); - expect(second?.deliver).toBe(true); - expect(second?.message).toContain("background task"); - - // No direct send to external channel (main agent handles delivery) - const sendCalls = calls.filter((c) => c.method === "send"); - expect(sendCalls.length).toBe(0); - - // Session should be deleted since cleanup=delete - expect(deletedKey?.startsWith("agent:main:subagent:")).toBe(true); - }); - - it("sessions_spawn announces with requester accountId", async () => { - resetSubagentRegistryForTests(); - callGatewayMock.mockReset(); - const calls: Array<{ method?: string; params?: unknown }> = []; - let agentCallCount = 0; - let childRunId: string | undefined; - - callGatewayMock.mockImplementation(async (opts: unknown) => { - const request = opts as { method?: string; params?: unknown }; - calls.push(request); - if (request.method === "agent") { - agentCallCount += 1; - const runId = `run-${agentCallCount}`; - const params = request.params as { lane?: string; sessionKey?: string } | undefined; - if (params?.lane === "subagent") { - childRunId = runId; - } - return { - runId, - status: "accepted", - acceptedAt: 4000 + agentCallCount, - }; - } - if (request.method === "agent.wait") { - const params = request.params as { runId?: string; timeoutMs?: number } | undefined; - return { runId: params?.runId ?? "run-1", status: "ok", startedAt: 1000, endedAt: 2000 }; - } - if (request.method === "sessions.delete" || request.method === "sessions.patch") { - return { ok: true }; - } - return {}; - }); - - const tool = createClawdbotTools({ - agentSessionKey: "main", - agentChannel: "whatsapp", - agentAccountId: "kev", - }).find((candidate) => candidate.name === "sessions_spawn"); - if (!tool) throw new Error("missing sessions_spawn tool"); - - const result = await tool.execute("call2", { - task: "do thing", - runTimeoutSeconds: 1, - cleanup: "keep", - }); - expect(result.details).toMatchObject({ - status: "accepted", - runId: "run-1", - }); - - if (!childRunId) throw new Error("missing child runId"); - emitAgentEvent({ - runId: childRunId, - stream: "lifecycle", - data: { - phase: "end", - startedAt: 1000, - endedAt: 2000, - }, - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - await new Promise((resolve) => setTimeout(resolve, 0)); - await new Promise((resolve) => setTimeout(resolve, 0)); - - const agentCalls = calls.filter((call) => call.method === "agent"); - expect(agentCalls).toHaveLength(2); - const announceParams = agentCalls[1]?.params as - | { accountId?: string; channel?: string; deliver?: boolean } - | undefined; - expect(announceParams?.deliver).toBe(true); - expect(announceParams?.channel).toBe("whatsapp"); - expect(announceParams?.accountId).toBe("kev"); - }); -}); diff --git a/src/agents/clawdbot-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.test.ts b/src/agents/clawdbot-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.test.ts index 1ad7e4fd6..b1b5b413b 100644 --- a/src/agents/clawdbot-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.test.ts +++ b/src/agents/clawdbot-tools.subagents.sessions-spawn-normalizes-allowlisted-agent-ids.test.ts @@ -21,6 +21,7 @@ vi.mock("../config/config.js", async (importOriginal) => { }; }); +import { emitAgentEvent } from "../infra/agent-events.js"; import { createClawdbotTools } from "./clawdbot-tools.js"; import { resetSubagentRegistryForTests } from "./subagent-registry.js"; @@ -120,4 +121,195 @@ describe("clawdbot-tools: subagents", () => { }); expect(callGatewayMock).not.toHaveBeenCalled(); }); + + it("sessions_spawn runs cleanup via lifecycle events", async () => { + resetSubagentRegistryForTests(); + callGatewayMock.mockReset(); + const calls: Array<{ method?: string; params?: unknown }> = []; + let agentCallCount = 0; + let deletedKey: string | undefined; + let childRunId: string | undefined; + let childSessionKey: string | undefined; + const waitCalls: Array<{ runId?: string; timeoutMs?: number }> = []; + + callGatewayMock.mockImplementation(async (opts: unknown) => { + const request = opts as { method?: string; params?: unknown }; + calls.push(request); + if (request.method === "agent") { + agentCallCount += 1; + const runId = `run-${agentCallCount}`; + const params = request.params as { + message?: string; + sessionKey?: string; + channel?: string; + timeout?: number; + lane?: string; + }; + if (params?.lane === "subagent") { + childRunId = runId; + childSessionKey = params?.sessionKey ?? ""; + expect(params?.channel).toBe("discord"); + expect(params?.timeout).toBe(1); + } + return { + runId, + status: "accepted", + acceptedAt: 1000 + agentCallCount, + }; + } + if (request.method === "agent.wait") { + const params = request.params as { runId?: string; timeoutMs?: number } | undefined; + waitCalls.push(params ?? {}); + return { runId: params?.runId ?? "run-1", status: "ok", startedAt: 1000, endedAt: 2000 }; + } + if (request.method === "sessions.delete") { + const params = request.params as { key?: string } | undefined; + deletedKey = params?.key; + return { ok: true }; + } + return {}; + }); + + const tool = createClawdbotTools({ + agentSessionKey: "discord:group:req", + agentChannel: "discord", + }).find((candidate) => candidate.name === "sessions_spawn"); + if (!tool) throw new Error("missing sessions_spawn tool"); + + const result = await tool.execute("call1", { + task: "do thing", + runTimeoutSeconds: 1, + cleanup: "delete", + }); + expect(result.details).toMatchObject({ + status: "accepted", + runId: "run-1", + }); + + if (!childRunId) throw new Error("missing child runId"); + emitAgentEvent({ + runId: childRunId, + stream: "lifecycle", + data: { + phase: "end", + startedAt: 1234, + endedAt: 2345, + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const childWait = waitCalls.find((call) => call.runId === childRunId); + expect(childWait?.timeoutMs).toBe(1000); + + const agentCalls = calls.filter((call) => call.method === "agent"); + expect(agentCalls).toHaveLength(2); + + const first = agentCalls[0]?.params as + | { + lane?: string; + deliver?: boolean; + sessionKey?: string; + channel?: string; + } + | undefined; + expect(first?.lane).toBe("subagent"); + expect(first?.deliver).toBe(false); + expect(first?.channel).toBe("discord"); + expect(first?.sessionKey?.startsWith("agent:main:subagent:")).toBe(true); + expect(childSessionKey?.startsWith("agent:main:subagent:")).toBe(true); + + const second = agentCalls[1]?.params as + | { + sessionKey?: string; + message?: string; + deliver?: boolean; + } + | undefined; + expect(second?.sessionKey).toBe("discord:group:req"); + expect(second?.deliver).toBe(true); + expect(second?.message).toContain("background task"); + + const sendCalls = calls.filter((c) => c.method === "send"); + expect(sendCalls.length).toBe(0); + + expect(deletedKey?.startsWith("agent:main:subagent:")).toBe(true); + }); + + it("sessions_spawn announces with requester accountId", async () => { + resetSubagentRegistryForTests(); + callGatewayMock.mockReset(); + const calls: Array<{ method?: string; params?: unknown }> = []; + let agentCallCount = 0; + let childRunId: string | undefined; + + callGatewayMock.mockImplementation(async (opts: unknown) => { + const request = opts as { method?: string; params?: unknown }; + calls.push(request); + if (request.method === "agent") { + agentCallCount += 1; + const runId = `run-${agentCallCount}`; + const params = request.params as { lane?: string; sessionKey?: string } | undefined; + if (params?.lane === "subagent") { + childRunId = runId; + } + return { + runId, + status: "accepted", + acceptedAt: 4000 + agentCallCount, + }; + } + if (request.method === "agent.wait") { + const params = request.params as { runId?: string; timeoutMs?: number } | undefined; + return { runId: params?.runId ?? "run-1", status: "ok", startedAt: 1000, endedAt: 2000 }; + } + if (request.method === "sessions.delete" || request.method === "sessions.patch") { + return { ok: true }; + } + return {}; + }); + + const tool = createClawdbotTools({ + agentSessionKey: "main", + agentChannel: "whatsapp", + agentAccountId: "kev", + }).find((candidate) => candidate.name === "sessions_spawn"); + if (!tool) throw new Error("missing sessions_spawn tool"); + + const result = await tool.execute("call2", { + task: "do thing", + runTimeoutSeconds: 1, + cleanup: "keep", + }); + expect(result.details).toMatchObject({ + status: "accepted", + runId: "run-1", + }); + + if (!childRunId) throw new Error("missing child runId"); + emitAgentEvent({ + runId: childRunId, + stream: "lifecycle", + data: { + phase: "end", + startedAt: 1000, + endedAt: 2000, + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const agentCalls = calls.filter((call) => call.method === "agent"); + expect(agentCalls).toHaveLength(2); + const announceParams = agentCalls[1]?.params as + | { accountId?: string; channel?: string; deliver?: boolean } + | undefined; + expect(announceParams?.deliver).toBe(true); + expect(announceParams?.channel).toBe("whatsapp"); + expect(announceParams?.accountId).toBe("kev"); + }); });