From d688188864b838259b79358ae4d16181f5f9cdda Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 16 Feb 2026 17:22:12 +0000 Subject: [PATCH] refactor(tests): share outbound runner and delivery helpers --- src/infra/outbound/deliver.test.ts | 81 ++++--- .../outbound/message-action-runner.test.ts | 198 ++++++++---------- 2 files changed, 126 insertions(+), 153 deletions(-) diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index aece7cd9a9f..95ca30cd516 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -45,6 +45,28 @@ vi.mock("./delivery-queue.js", () => ({ const { deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js"); +const telegramChunkConfig: OpenClawConfig = { + channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, +}; + +const whatsappChunkConfig: OpenClawConfig = { + channels: { whatsapp: { textChunkLimit: 4000 } }, +}; + +async function deliverWhatsAppPayload(params: { + sendWhatsApp: ReturnType; + payload: { text: string; mediaUrl?: string }; + cfg?: OpenClawConfig; +}) { + return deliverOutboundPayloads({ + cfg: params.cfg ?? whatsappChunkConfig, + channel: "whatsapp", + to: "+1555", + payloads: [params.payload], + deps: { sendWhatsApp: params.sendWhatsApp }, + }); +} + describe("deliverOutboundPayloads", () => { beforeEach(() => { setActivePluginRegistry(defaultRegistry); @@ -65,14 +87,11 @@ describe("deliverOutboundPayloads", () => { }); it("chunks telegram markdown and passes through accountId", async () => { const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" }); - const cfg: OpenClawConfig = { - channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, - }; const prevTelegramToken = process.env.TELEGRAM_BOT_TOKEN; process.env.TELEGRAM_BOT_TOKEN = ""; try { const results = await deliverOutboundPayloads({ - cfg, + cfg: telegramChunkConfig, channel: "telegram", to: "123", payloads: [{ text: "abcd" }], @@ -98,12 +117,9 @@ describe("deliverOutboundPayloads", () => { it("passes explicit accountId to sendTelegram", async () => { const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" }); - const cfg: OpenClawConfig = { - channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, - }; await deliverOutboundPayloads({ - cfg, + cfg: telegramChunkConfig, channel: "telegram", to: "123", accountId: "default", @@ -120,12 +136,9 @@ describe("deliverOutboundPayloads", () => { it("scopes media local roots to the active agent workspace when agentId is provided", async () => { const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" }); - const cfg: OpenClawConfig = { - channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, - }; await deliverOutboundPayloads({ - cfg, + cfg: telegramChunkConfig, channel: "telegram", to: "123", agentId: "work", @@ -251,16 +264,9 @@ describe("deliverOutboundPayloads", () => { it("strips leading blank lines for WhatsApp text payloads", async () => { const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); - const cfg: OpenClawConfig = { - channels: { whatsapp: { textChunkLimit: 4000 } }, - }; - - await deliverOutboundPayloads({ - cfg, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "\n\nHello from WhatsApp" }], - deps: { sendWhatsApp }, + await deliverWhatsAppPayload({ + sendWhatsApp, + payload: { text: "\n\nHello from WhatsApp" }, }); expect(sendWhatsApp).toHaveBeenCalledTimes(1); @@ -274,16 +280,9 @@ describe("deliverOutboundPayloads", () => { it("drops whitespace-only WhatsApp text payloads when no media is attached", async () => { const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); - const cfg: OpenClawConfig = { - channels: { whatsapp: { textChunkLimit: 4000 } }, - }; - - const results = await deliverOutboundPayloads({ - cfg, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: " \n\t " }], - deps: { sendWhatsApp }, + const results = await deliverWhatsAppPayload({ + sendWhatsApp, + payload: { text: " \n\t " }, }); expect(sendWhatsApp).not.toHaveBeenCalled(); @@ -292,16 +291,9 @@ describe("deliverOutboundPayloads", () => { it("keeps WhatsApp media payloads but clears whitespace-only captions", async () => { const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); - const cfg: OpenClawConfig = { - channels: { whatsapp: { textChunkLimit: 4000 } }, - }; - - await deliverOutboundPayloads({ - cfg, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: " \n\t ", mediaUrl: "https://example.com/photo.png" }], - deps: { sendWhatsApp }, + await deliverWhatsAppPayload({ + sendWhatsApp, + payload: { text: " \n\t ", mediaUrl: "https://example.com/photo.png" }, }); expect(sendWhatsApp).toHaveBeenCalledTimes(1); @@ -504,13 +496,10 @@ describe("deliverOutboundPayloads", () => { it("mirrors delivered output when mirror options are provided", async () => { const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" }); - const cfg: OpenClawConfig = { - channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, - }; mocks.appendAssistantMessageToSessionTranscript.mockClear(); await deliverOutboundPayloads({ - cfg, + cfg: telegramChunkConfig, channel: "telegram", to: "123", payloads: [{ text: "caption", mediaUrl: "https://example.com/files/report.pdf?sig=1" }], diff --git a/src/infra/outbound/message-action-runner.test.ts b/src/infra/outbound/message-action-runner.test.ts index 28013c5764d..c02b4009a8e 100644 --- a/src/infra/outbound/message-action-runner.test.ts +++ b/src/infra/outbound/message-action-runner.test.ts @@ -39,6 +39,45 @@ const whatsappConfig = { }, } as OpenClawConfig; +async function withSandbox(test: (sandboxDir: string) => Promise) { + const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-")); + try { + await test(sandboxDir); + } finally { + await fs.rm(sandboxDir, { recursive: true, force: true }); + } +} + +const runDryAction = (params: { + cfg: OpenClawConfig; + action: "send" | "thread-reply" | "broadcast"; + actionParams: Record; + toolContext?: Record; + abortSignal?: AbortSignal; + sandboxRoot?: string; +}) => + runMessageAction({ + cfg: params.cfg, + action: params.action, + params: params.actionParams as never, + toolContext: params.toolContext as never, + dryRun: true, + abortSignal: params.abortSignal, + sandboxRoot: params.sandboxRoot, + }); + +const runDrySend = (params: { + cfg: OpenClawConfig; + actionParams: Record; + toolContext?: Record; + abortSignal?: AbortSignal; + sandboxRoot?: string; +}) => + runDryAction({ + ...params, + action: "send", + }); + describe("runMessageAction context isolation", () => { beforeEach(async () => { const { createPluginRuntime } = await import("../../plugins/runtime/index.js"); @@ -80,62 +119,54 @@ describe("runMessageAction context isolation", () => { }); it("allows send when target matches current channel", async () => { - const result = await runMessageAction({ + const result = await runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", target: "#C12345678", message: "hi", }, toolContext: { currentChannelId: "C12345678" }, - dryRun: true, }); expect(result.kind).toBe("send"); }); it("accepts legacy to parameter for send", async () => { - const result = await runMessageAction({ + const result = await runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", to: "#C12345678", message: "hi", }, - dryRun: true, }); expect(result.kind).toBe("send"); }); it("defaults to current channel when target is omitted", async () => { - const result = await runMessageAction({ + const result = await runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", message: "hi", }, toolContext: { currentChannelId: "C12345678" }, - dryRun: true, }); expect(result.kind).toBe("send"); }); it("allows media-only send when target matches current channel", async () => { - const result = await runMessageAction({ + const result = await runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", target: "#C12345678", media: "https://example.com/note.ogg", }, toolContext: { currentChannelId: "C12345678" }, - dryRun: true, }); expect(result.kind).toBe("send"); @@ -143,104 +174,92 @@ describe("runMessageAction context isolation", () => { it("requires message when no media hint is provided", async () => { await expect( - runMessageAction({ + runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", target: "#C12345678", }, toolContext: { currentChannelId: "C12345678" }, - dryRun: true, }), ).rejects.toThrow(/message required/i); }); it("blocks send when target differs from current channel", async () => { - const result = await runMessageAction({ + const result = await runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", target: "channel:C99999999", message: "hi", }, toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" }, - dryRun: true, }); expect(result.kind).toBe("send"); }); it("blocks thread-reply when channelId differs from current channel", async () => { - const result = await runMessageAction({ + const result = await runDryAction({ cfg: slackConfig, action: "thread-reply", - params: { + actionParams: { channel: "slack", target: "C99999999", message: "hi", }, toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" }, - dryRun: true, }); expect(result.kind).toBe("action"); }); it("allows WhatsApp send when target matches current chat", async () => { - const result = await runMessageAction({ + const result = await runDrySend({ cfg: whatsappConfig, - action: "send", - params: { + actionParams: { channel: "whatsapp", target: "123@g.us", message: "hi", }, toolContext: { currentChannelId: "123@g.us" }, - dryRun: true, }); expect(result.kind).toBe("send"); }); it("blocks WhatsApp send when target differs from current chat", async () => { - const result = await runMessageAction({ + const result = await runDrySend({ cfg: whatsappConfig, - action: "send", - params: { + actionParams: { channel: "whatsapp", target: "456@g.us", message: "hi", }, toolContext: { currentChannelId: "123@g.us", currentChannelProvider: "whatsapp" }, - dryRun: true, }); expect(result.kind).toBe("send"); }); it("allows iMessage send when target matches current handle", async () => { - const result = await runMessageAction({ + const result = await runDrySend({ cfg: whatsappConfig, - action: "send", - params: { + actionParams: { channel: "imessage", target: "imessage:+15551234567", message: "hi", }, toolContext: { currentChannelId: "imessage:+15551234567" }, - dryRun: true, }); expect(result.kind).toBe("send"); }); it("blocks iMessage send when target differs from current handle", async () => { - const result = await runMessageAction({ + const result = await runDrySend({ cfg: whatsappConfig, - action: "send", - params: { + actionParams: { channel: "imessage", target: "imessage:+15551230000", message: "hi", @@ -249,7 +268,6 @@ describe("runMessageAction context isolation", () => { currentChannelId: "imessage:+15551234567", currentChannelProvider: "imessage", }, - dryRun: true, }); expect(result.kind).toBe("send"); @@ -268,14 +286,12 @@ describe("runMessageAction context isolation", () => { }, } as OpenClawConfig; - const result = await runMessageAction({ + const result = await runDrySend({ cfg: multiConfig, - action: "send", - params: { + actionParams: { message: "hi", }, toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" }, - dryRun: true, }); expect(result.kind).toBe("send"); @@ -284,16 +300,14 @@ describe("runMessageAction context isolation", () => { it("blocks cross-provider sends by default", async () => { await expect( - runMessageAction({ + runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "telegram", target: "telegram:@ops", message: "hi", }, toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" }, - dryRun: true, }), ).rejects.toThrow(/Cross-context messaging denied/); }); @@ -311,16 +325,14 @@ describe("runMessageAction context isolation", () => { } as OpenClawConfig; await expect( - runMessageAction({ + runDrySend({ cfg, - action: "send", - params: { + actionParams: { channel: "slack", target: "channel:C99999999", message: "hi", }, toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" }, - dryRun: true, }), ).rejects.toThrow(/Cross-context messaging denied/); }); @@ -330,15 +342,13 @@ describe("runMessageAction context isolation", () => { controller.abort(); await expect( - runMessageAction({ + runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", target: "#C12345678", message: "hi", }, - dryRun: true, abortSignal: controller.signal, }), ).rejects.toMatchObject({ name: "AbortError" }); @@ -349,15 +359,14 @@ describe("runMessageAction context isolation", () => { controller.abort(); await expect( - runMessageAction({ + runDryAction({ cfg: slackConfig, action: "broadcast", - params: { + actionParams: { targets: ["channel:C12345678"], channel: "slack", message: "hi", }, - dryRun: true, abortSignal: controller.signal, }), ).rejects.toMatchObject({ name: "AbortError" }); @@ -461,8 +470,7 @@ describe("runMessageAction sendAttachment hydration", () => { }, }, } as OpenClawConfig; - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-")); - try { + await withSandbox(async (sandboxDir) => { await runMessageAction({ cfg, action: "sendAttachment", @@ -477,9 +485,7 @@ describe("runMessageAction sendAttachment hydration", () => { const call = vi.mocked(loadWebMedia).mock.calls[0]; expect(call?.[0]).toBe(path.join(sandboxDir, "data", "pic.png")); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } + }); }); }); @@ -505,106 +511,84 @@ describe("runMessageAction sandboxed media validation", () => { }); it("rejects media outside the sandbox root", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-")); - try { + await withSandbox(async (sandboxDir) => { await expect( - runMessageAction({ + runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", target: "#C12345678", media: "/etc/passwd", message: "", }, sandboxRoot: sandboxDir, - dryRun: true, }), ).rejects.toThrow(/sandbox/i); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } + }); }); it("rejects file:// media outside the sandbox root", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-")); - try { + await withSandbox(async (sandboxDir) => { await expect( - runMessageAction({ + runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", target: "#C12345678", media: "file:///etc/passwd", message: "", }, sandboxRoot: sandboxDir, - dryRun: true, }), ).rejects.toThrow(/sandbox/i); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } + }); }); it("rewrites sandbox-relative media paths", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-")); - try { - const result = await runMessageAction({ + await withSandbox(async (sandboxDir) => { + const result = await runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", target: "#C12345678", media: "./data/file.txt", message: "", }, sandboxRoot: sandboxDir, - dryRun: true, }); expect(result.kind).toBe("send"); expect(result.sendResult?.mediaUrl).toBe(path.join(sandboxDir, "data", "file.txt")); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } + }); }); it("rewrites MEDIA directives under sandbox", async () => { - const sandboxDir = await fs.mkdtemp(path.join(os.tmpdir(), "msg-sandbox-")); - try { - const result = await runMessageAction({ + await withSandbox(async (sandboxDir) => { + const result = await runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", target: "#C12345678", message: "Hello\nMEDIA: ./data/note.ogg", }, sandboxRoot: sandboxDir, - dryRun: true, }); expect(result.kind).toBe("send"); expect(result.sendResult?.mediaUrl).toBe(path.join(sandboxDir, "data", "note.ogg")); - } finally { - await fs.rm(sandboxDir, { recursive: true, force: true }); - } + }); }); it("rejects data URLs in media params", async () => { await expect( - runMessageAction({ + runDrySend({ cfg: slackConfig, - action: "send", - params: { + actionParams: { channel: "slack", target: "#C12345678", media: "data:image/png;base64,abcd", message: "", }, - dryRun: true, }), ).rejects.toThrow(/data:/i); });