From 6c43d0a08e1cd04ebc600b8c98ffc24ae9e3dc4e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 24 Feb 2026 01:16:36 +0000 Subject: [PATCH] test(gateway): move sessions_send error paths to unit tests --- src/agents/tools/sessions.test.ts | 41 ++++++++ src/gateway/server.sessions-send.test.ts | 123 ++++++++++------------- 2 files changed, 95 insertions(+), 69 deletions(-) diff --git a/src/agents/tools/sessions.test.ts b/src/agents/tools/sessions.test.ts index 7a08d335d..9796ac88a 100644 --- a/src/agents/tools/sessions.test.ts +++ b/src/agents/tools/sessions.test.ts @@ -204,6 +204,47 @@ describe("sessions_send gating", () => { callGatewayMock.mockClear(); }); + it("returns an error when neither sessionKey nor label is provided", async () => { + const tool = createSessionsSendTool({ + agentSessionKey: "agent:main:main", + agentChannel: "whatsapp", + }); + + const result = await tool.execute("call-missing-target", { + message: "hi", + timeoutSeconds: 5, + }); + + expect(result.details).toMatchObject({ + status: "error", + error: "Either sessionKey or label is required", + }); + expect(callGatewayMock).not.toHaveBeenCalled(); + }); + + it("returns an error when label resolution fails", async () => { + callGatewayMock.mockRejectedValueOnce(new Error("No session found with label: nope")); + const tool = createSessionsSendTool({ + agentSessionKey: "agent:main:main", + agentChannel: "whatsapp", + }); + + const result = await tool.execute("call-missing-label", { + label: "nope", + message: "hello", + timeoutSeconds: 5, + }); + + expect(result.details).toMatchObject({ + status: "error", + }); + expect((result.details as { error?: string } | undefined)?.error ?? "").toContain( + "No session found with label", + ); + expect(callGatewayMock).toHaveBeenCalledTimes(1); + expect(callGatewayMock.mock.calls[0]?.[0]).toMatchObject({ method: "sessions.resolve" }); + }); + it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => { const tool = createSessionsSendTool({ agentSessionKey: "agent:main:main", diff --git a/src/gateway/server.sessions-send.test.ts b/src/gateway/server.sessions-send.test.ts index 453af5a15..7f1e49e8f 100644 --- a/src/gateway/server.sessions-send.test.ts +++ b/src/gateway/server.sessions-send.test.ts @@ -22,13 +22,19 @@ const gatewayToken = "test-token"; let envSnapshot: ReturnType; type SessionSendTool = ReturnType[number]; +const SESSION_SEND_E2E_TIMEOUT_MS = 10_000; +let cachedSessionsSendTool: SessionSendTool | null = null; function getSessionsSendTool(): SessionSendTool { + if (cachedSessionsSendTool) { + return cachedSessionsSendTool; + } const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send"); if (!tool) { throw new Error("missing sessions_send tool"); } - return tool; + cachedSessionsSendTool = tool; + return cachedSessionsSendTool; } async function emitLifecycleAssistantReply(params: { @@ -145,76 +151,55 @@ describe("sessions_send gateway loopback", () => { }); describe("sessions_send label lookup", () => { - it("finds session by label and sends message", { timeout: 60_000 }, async () => { - // This is an operator feature; enable broader session tool targeting for this test. - const configPath = process.env.OPENCLAW_CONFIG_PATH; - if (!configPath) { - throw new Error("OPENCLAW_CONFIG_PATH missing in gateway test environment"); - } - await fs.mkdir(path.dirname(configPath), { recursive: true }); - await fs.writeFile( - configPath, - JSON.stringify({ tools: { sessions: { visibility: "all" } } }, null, 2) + "\n", - "utf-8", - ); + it( + "finds session by label and sends message", + { timeout: SESSION_SEND_E2E_TIMEOUT_MS }, + async () => { + // This is an operator feature; enable broader session tool targeting for this test. + const configPath = process.env.OPENCLAW_CONFIG_PATH; + if (!configPath) { + throw new Error("OPENCLAW_CONFIG_PATH missing in gateway test environment"); + } + await fs.mkdir(path.dirname(configPath), { recursive: true }); + await fs.writeFile( + configPath, + JSON.stringify({ tools: { sessions: { visibility: "all" } } }, null, 2) + "\n", + "utf-8", + ); - const spy = agentCommand as unknown as Mock<(opts: unknown) => Promise>; - spy.mockImplementation(async (opts: unknown) => - emitLifecycleAssistantReply({ - opts, - defaultSessionId: "test-labeled", - resolveText: () => "labeled response", - }), - ); + const spy = agentCommand as unknown as Mock<(opts: unknown) => Promise>; + spy.mockImplementation(async (opts: unknown) => + emitLifecycleAssistantReply({ + opts, + defaultSessionId: "test-labeled", + resolveText: () => "labeled response", + }), + ); - // First, create a session with a label via sessions.patch - const { callGateway } = await import("./call.js"); - await callGateway({ - method: "sessions.patch", - params: { key: "test-labeled-session", label: "my-test-worker" }, - timeoutMs: 5000, - }); + // First, create a session with a label via sessions.patch + const { callGateway } = await import("./call.js"); + await callGateway({ + method: "sessions.patch", + params: { key: "test-labeled-session", label: "my-test-worker" }, + timeoutMs: 5000, + }); - const tool = getSessionsSendTool(); + const tool = getSessionsSendTool(); - // Send using label instead of sessionKey - const result = await tool.execute("call-by-label", { - label: "my-test-worker", - message: "hello labeled session", - timeoutSeconds: 5, - }); - const details = result.details as { - status?: string; - reply?: string; - sessionKey?: string; - }; - expect(details.status).toBe("ok"); - expect(details.reply).toBe("labeled response"); - expect(details.sessionKey).toBe("agent:main:test-labeled-session"); - }); - - it("returns error when label not found", { timeout: 60_000 }, async () => { - const tool = getSessionsSendTool(); - - const result = await tool.execute("call-missing-label", { - label: "nonexistent-label", - message: "hello", - timeoutSeconds: 5, - }); - const details = result.details as { status?: string; error?: string }; - expect(details.status).toBe("error"); - expect(details.error).toContain("No session found with label"); - }); - - it("returns error when neither sessionKey nor label provided", { timeout: 60_000 }, async () => { - const tool = getSessionsSendTool(); - - const result = await tool.execute("call-no-key", { - message: "hello", - timeoutSeconds: 5, - }); - const details = result.details as { status?: string; error?: string }; - expect(details.status).toBe("error"); - expect(details.error).toContain("Either sessionKey or label is required"); - }); + // Send using label instead of sessionKey + const result = await tool.execute("call-by-label", { + label: "my-test-worker", + message: "hello labeled session", + timeoutSeconds: 5, + }); + const details = result.details as { + status?: string; + reply?: string; + sessionKey?: string; + }; + expect(details.status).toBe("ok"); + expect(details.reply).toBe("labeled response"); + expect(details.sessionKey).toBe("agent:main:test-labeled-session"); + }, + ); });