import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, test, vi } from "vitest"; import { agentCommand, connectOk, installGatewayTestHooks, rpcReq, startServerWithClient, testState, } from "./test-helpers.js"; installGatewayTestHooks(); const BASE_IMAGE_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X3mIAAAAASUVORK5CYII="; function expectChannels(call: Record, channel: string) { expect(call.channel).toBe(channel); expect(call.messageChannel).toBe(channel); } describe("gateway server agent", () => { test("agent marks implicit delivery when lastTo is stale", async () => { testState.allowFrom = ["+436769770569"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-main-stale", updatedAt: Date.now(), lastChannel: "whatsapp", lastTo: "+1555", }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "main", channel: "last", deliver: true, idempotencyKey: "idem-agent-last-stale", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.to).toBe("+1555"); expect(call.deliveryTargetMode).toBe("implicit"); expect(call.sessionId).toBe("sess-main-stale"); ws.close(); await server.close(); testState.allowFrom = undefined; }); test("agent forwards sessionKey to agentCommand", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { "agent:main:subagent:abc": { sessionId: "sess-sub", updatedAt: Date.now(), }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "agent:main:subagent:abc", idempotencyKey: "idem-agent-subkey", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expect(call.sessionKey).toBe("agent:main:subagent:abc"); expect(call.sessionId).toBe("sess-sub"); expectChannels(call, "webchat"); expect(call.deliver).toBe(false); expect(call.to).toBeUndefined(); ws.close(); await server.close(); }); test("agent forwards accountId to agentCommand", async () => { testState.allowFrom = ["+1555"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-main-account", updatedAt: Date.now(), lastChannel: "whatsapp", lastTo: "+1555", lastAccountId: "default", }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "main", deliver: true, accountId: "kev", idempotencyKey: "idem-agent-account", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.to).toBe("+1555"); expect(call.accountId).toBe("kev"); ws.close(); await server.close(); testState.allowFrom = undefined; }); test("agent avoids lastAccountId when explicit to is provided", async () => { testState.allowFrom = ["+1555"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-main-explicit", updatedAt: Date.now(), lastChannel: "whatsapp", lastTo: "+1555", lastAccountId: "legacy", }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "main", deliver: true, to: "+1666", idempotencyKey: "idem-agent-explicit", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.to).toBe("+1666"); expect(call.accountId).toBeUndefined(); ws.close(); await server.close(); testState.allowFrom = undefined; }); test("agent falls back to lastAccountId for implicit delivery", async () => { testState.allowFrom = ["+1555"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-main-implicit", updatedAt: Date.now(), lastChannel: "whatsapp", lastTo: "+1555", lastAccountId: "kev", }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "main", deliver: true, idempotencyKey: "idem-agent-implicit-account", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.to).toBe("+1555"); expect(call.accountId).toBe("kev"); ws.close(); await server.close(); testState.allowFrom = undefined; }); test("agent forwards image attachments as images[]", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-main-images", updatedAt: Date.now(), }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "what is in the image?", sessionKey: "main", attachments: [ { mimeType: "image/png", fileName: "tiny.png", content: BASE_IMAGE_PNG, }, ], idempotencyKey: "idem-agent-attachments", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expect(call.sessionKey).toBe("main"); expectChannels(call, "webchat"); expect(call.message).toBe("what is in the image?"); const images = call.images as Array>; expect(Array.isArray(images)).toBe(true); expect(images.length).toBe(1); expect(images[0]?.type).toBe("image"); expect(images[0]?.mimeType).toBe("image/png"); expect(images[0]?.data).toBe(BASE_IMAGE_PNG); ws.close(); await server.close(); }); test("agent falls back to whatsapp when delivery requested and no last channel exists", async () => { testState.allowFrom = ["+1555"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-main-missing-provider", updatedAt: Date.now(), }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "main", deliver: true, idempotencyKey: "idem-agent-missing-provider", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.to).toBe("+1555"); expect(call.deliver).toBe(true); expect(call.sessionId).toBe("sess-main-missing-provider"); ws.close(); await server.close(); testState.allowFrom = undefined; }); test("agent routes main last-channel whatsapp", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-main-whatsapp", updatedAt: Date.now(), lastChannel: "whatsapp", lastTo: "+1555", }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "main", channel: "last", deliver: true, idempotencyKey: "idem-agent-last-whatsapp", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "whatsapp"); expect(call.messageChannel).toBe("whatsapp"); expect(call.to).toBe("+1555"); expect(call.deliver).toBe(true); expect(call.bestEffortDeliver).toBe(true); expect(call.sessionId).toBe("sess-main-whatsapp"); ws.close(); await server.close(); }); test("agent routes main last-channel telegram", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-main", updatedAt: Date.now(), lastChannel: "telegram", lastTo: "123", }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "main", channel: "last", deliver: true, idempotencyKey: "idem-agent-last", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "telegram"); expect(call.to).toBe("123"); expect(call.deliver).toBe(true); expect(call.bestEffortDeliver).toBe(true); expect(call.sessionId).toBe("sess-main"); ws.close(); await server.close(); }); test("agent routes main last-channel discord", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-discord", updatedAt: Date.now(), lastChannel: "discord", lastTo: "channel:discord-123", }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "main", channel: "last", deliver: true, idempotencyKey: "idem-agent-last-discord", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "discord"); expect(call.to).toBe("channel:discord-123"); expect(call.deliver).toBe(true); expect(call.bestEffortDeliver).toBe(true); expect(call.sessionId).toBe("sess-discord"); ws.close(); await server.close(); }); test("agent routes main last-channel slack", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-slack", updatedAt: Date.now(), lastChannel: "slack", lastTo: "channel:slack-123", }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "main", channel: "last", deliver: true, idempotencyKey: "idem-agent-last-slack", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "slack"); expect(call.to).toBe("channel:slack-123"); expect(call.deliver).toBe(true); expect(call.bestEffortDeliver).toBe(true); expect(call.sessionId).toBe("sess-slack"); ws.close(); await server.close(); }); test("agent routes main last-channel signal", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); await fs.writeFile( testState.sessionStorePath, JSON.stringify( { main: { sessionId: "sess-signal", updatedAt: Date.now(), lastChannel: "signal", lastTo: "+15551234567", }, }, null, 2, ), "utf-8", ); const { server, ws } = await startServerWithClient(); await connectOk(ws); const res = await rpcReq(ws, "agent", { message: "hi", sessionKey: "main", channel: "last", deliver: true, idempotencyKey: "idem-agent-last-signal", }); expect(res.ok).toBe(true); const spy = vi.mocked(agentCommand); const call = spy.mock.calls.at(-1)?.[0] as Record; expectChannels(call, "signal"); expect(call.to).toBe("+15551234567"); expect(call.deliver).toBe(true); expect(call.bestEffortDeliver).toBe(true); expect(call.sessionId).toBe("sess-signal"); ws.close(); await server.close(); }); });