From c2a9c5699dca19e5f76346673b263bccafe59873 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 21:13:52 +0000 Subject: [PATCH] test: extract outbound delivery lifecycle coverage --- src/infra/outbound/deliver.lifecycle.test.ts | 415 +++++++++++++++++++ src/infra/outbound/deliver.test.ts | 314 +------------- 2 files changed, 429 insertions(+), 300 deletions(-) create mode 100644 src/infra/outbound/deliver.lifecycle.test.ts diff --git a/src/infra/outbound/deliver.lifecycle.test.ts b/src/infra/outbound/deliver.lifecycle.test.ts new file mode 100644 index 00000000000..00d696162d8 --- /dev/null +++ b/src/infra/outbound/deliver.lifecycle.test.ts @@ -0,0 +1,415 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { signalOutbound } from "../../channels/plugins/outbound/signal.js"; +import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js"; +import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import { setActivePluginRegistry } from "../../plugins/runtime.js"; +import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; +import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js"; +import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js"; + +const mocks = vi.hoisted(() => ({ + appendAssistantMessageToSessionTranscript: vi.fn(async () => ({ ok: true, sessionFile: "x" })), +})); +const hookMocks = vi.hoisted(() => ({ + runner: { + hasHooks: vi.fn(() => false), + runMessageSent: vi.fn(async () => {}), + }, +})); +const internalHookMocks = vi.hoisted(() => ({ + createInternalHookEvent: vi.fn(), + triggerInternalHook: vi.fn(async () => {}), +})); +const queueMocks = vi.hoisted(() => ({ + enqueueDelivery: vi.fn(async () => "mock-queue-id"), + ackDelivery: vi.fn(async () => {}), + failDelivery: vi.fn(async () => {}), +})); +const logMocks = vi.hoisted(() => ({ + warn: vi.fn(), +})); + +vi.mock("../../config/sessions.js", async () => { + const actual = await vi.importActual( + "../../config/sessions.js", + ); + return { + ...actual, + appendAssistantMessageToSessionTranscript: mocks.appendAssistantMessageToSessionTranscript, + }; +}); +vi.mock("../../plugins/hook-runner-global.js", () => ({ + getGlobalHookRunner: () => hookMocks.runner, +})); +vi.mock("../../hooks/internal-hooks.js", () => ({ + createInternalHookEvent: internalHookMocks.createInternalHookEvent, + triggerInternalHook: internalHookMocks.triggerInternalHook, +})); +vi.mock("./delivery-queue.js", () => ({ + enqueueDelivery: queueMocks.enqueueDelivery, + ackDelivery: queueMocks.ackDelivery, + failDelivery: queueMocks.failDelivery, +})); +vi.mock("../../logging/subsystem.js", () => ({ + createSubsystemLogger: () => { + const makeLogger = () => ({ + warn: logMocks.warn, + info: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + child: vi.fn(() => makeLogger()), + }); + return makeLogger(); + }, +})); + +const { deliverOutboundPayloads } = await import("./deliver.js"); + +const whatsappChunkConfig: OpenClawConfig = { + channels: { whatsapp: { textChunkLimit: 4000 } }, +}; + +async function runChunkedWhatsAppDelivery(params?: { + mirror?: Parameters[0]["mirror"]; +}) { + const sendWhatsApp = vi + .fn() + .mockResolvedValueOnce({ messageId: "w1", toJid: "jid" }) + .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" }); + const cfg: OpenClawConfig = { + channels: { whatsapp: { textChunkLimit: 2 } }, + }; + const results = await deliverOutboundPayloads({ + cfg, + channel: "whatsapp", + to: "+1555", + payloads: [{ text: "abcd" }], + deps: { sendWhatsApp }, + ...(params?.mirror ? { mirror: params.mirror } : {}), + }); + return { sendWhatsApp, results }; +} + +async function deliverSingleWhatsAppForHookTest(params?: { sessionKey?: string }) { + const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); + await deliverOutboundPayloads({ + cfg: whatsappChunkConfig, + channel: "whatsapp", + to: "+1555", + payloads: [{ text: "hello" }], + deps: { sendWhatsApp }, + ...(params?.sessionKey ? { session: { key: params.sessionKey } } : {}), + }); +} + +async function runBestEffortPartialFailureDelivery() { + const sendWhatsApp = vi + .fn() + .mockRejectedValueOnce(new Error("fail")) + .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" }); + const onError = vi.fn(); + const cfg: OpenClawConfig = {}; + const results = await deliverOutboundPayloads({ + cfg, + channel: "whatsapp", + to: "+1555", + payloads: [{ text: "a" }, { text: "b" }], + deps: { sendWhatsApp }, + bestEffort: true, + onError, + }); + return { sendWhatsApp, onError, results }; +} + +function expectSuccessfulWhatsAppInternalHookPayload( + expected: Partial<{ + content: string; + messageId: string; + isGroup: boolean; + groupId: string; + }>, +) { + return expect.objectContaining({ + to: "+1555", + success: true, + channelId: "whatsapp", + conversationId: "+1555", + ...expected, + }); +} + +describe("deliverOutboundPayloads lifecycle", () => { + beforeEach(() => { + setActivePluginRegistry(defaultRegistry); + hookMocks.runner.hasHooks.mockClear(); + hookMocks.runner.hasHooks.mockReturnValue(false); + hookMocks.runner.runMessageSent.mockClear(); + hookMocks.runner.runMessageSent.mockResolvedValue(undefined); + internalHookMocks.createInternalHookEvent.mockClear(); + internalHookMocks.createInternalHookEvent.mockImplementation(createInternalHookEventPayload); + internalHookMocks.triggerInternalHook.mockClear(); + queueMocks.enqueueDelivery.mockClear(); + queueMocks.enqueueDelivery.mockResolvedValue("mock-queue-id"); + queueMocks.ackDelivery.mockClear(); + queueMocks.ackDelivery.mockResolvedValue(undefined); + queueMocks.failDelivery.mockClear(); + queueMocks.failDelivery.mockResolvedValue(undefined); + logMocks.warn.mockClear(); + mocks.appendAssistantMessageToSessionTranscript.mockClear(); + }); + + afterEach(() => { + setActivePluginRegistry(emptyRegistry); + }); + + it("continues on errors when bestEffort is enabled", async () => { + const { sendWhatsApp, onError, results } = await runBestEffortPartialFailureDelivery(); + + expect(sendWhatsApp).toHaveBeenCalledTimes(2); + expect(onError).toHaveBeenCalledTimes(1); + expect(results).toEqual([{ channel: "whatsapp", messageId: "w2", toJid: "jid" }]); + }); + + it("calls failDelivery instead of ackDelivery on bestEffort partial failure", async () => { + const { onError } = await runBestEffortPartialFailureDelivery(); + + expect(onError).toHaveBeenCalledTimes(1); + expect(queueMocks.ackDelivery).not.toHaveBeenCalled(); + expect(queueMocks.failDelivery).toHaveBeenCalledWith( + "mock-queue-id", + "partial delivery failure (bestEffort)", + ); + }); + + it("passes normalized payload to onError", async () => { + const sendWhatsApp = vi.fn().mockRejectedValue(new Error("boom")); + const onError = vi.fn(); + + await deliverOutboundPayloads({ + cfg: {}, + channel: "whatsapp", + to: "+1555", + payloads: [{ text: "hi", mediaUrl: "https://x.test/a.jpg" }], + deps: { sendWhatsApp }, + bestEffort: true, + onError, + }); + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith( + expect.any(Error), + expect.objectContaining({ text: "hi", mediaUrls: ["https://x.test/a.jpg"] }), + ); + }); + + it("acks the queue entry when delivery is aborted", async () => { + const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); + const abortController = new AbortController(); + abortController.abort(); + + await expect( + deliverOutboundPayloads({ + cfg: {}, + channel: "whatsapp", + to: "+1555", + payloads: [{ text: "a" }], + deps: { sendWhatsApp }, + abortSignal: abortController.signal, + }), + ).rejects.toThrow("Operation aborted"); + + expect(queueMocks.ackDelivery).toHaveBeenCalledWith("mock-queue-id"); + expect(queueMocks.failDelivery).not.toHaveBeenCalled(); + expect(sendWhatsApp).not.toHaveBeenCalled(); + }); + + it("emits internal message:sent hook with success=true for chunked payload delivery", async () => { + const { sendWhatsApp } = await runChunkedWhatsAppDelivery({ + mirror: { + sessionKey: "agent:main:main", + isGroup: true, + groupId: "whatsapp:group:123", + }, + }); + expect(sendWhatsApp).toHaveBeenCalledTimes(2); + + expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledTimes(1); + expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledWith( + "message", + "sent", + "agent:main:main", + expectSuccessfulWhatsAppInternalHookPayload({ + content: "abcd", + messageId: "w2", + isGroup: true, + groupId: "whatsapp:group:123", + }), + ); + expect(internalHookMocks.triggerInternalHook).toHaveBeenCalledTimes(1); + }); + + it("does not emit internal message:sent hook when neither mirror nor sessionKey is provided", async () => { + await deliverSingleWhatsAppForHookTest(); + + expect(internalHookMocks.createInternalHookEvent).not.toHaveBeenCalled(); + expect(internalHookMocks.triggerInternalHook).not.toHaveBeenCalled(); + }); + + it("emits internal message:sent hook when sessionKey is provided without mirror", async () => { + await deliverSingleWhatsAppForHookTest({ sessionKey: "agent:main:main" }); + + expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledTimes(1); + expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledWith( + "message", + "sent", + "agent:main:main", + expectSuccessfulWhatsAppInternalHookPayload({ content: "hello", messageId: "w1" }), + ); + expect(internalHookMocks.triggerInternalHook).toHaveBeenCalledTimes(1); + }); + + it("warns when session.agentId is set without a session key", async () => { + const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); + hookMocks.runner.hasHooks.mockReturnValue(true); + + await deliverOutboundPayloads({ + cfg: whatsappChunkConfig, + channel: "whatsapp", + to: "+1555", + payloads: [{ text: "hello" }], + deps: { sendWhatsApp }, + session: { agentId: "agent-main" }, + }); + + expect(logMocks.warn).toHaveBeenCalledWith( + "deliverOutboundPayloads: session.agentId present without session key; internal message:sent hook will be skipped", + expect.objectContaining({ channel: "whatsapp", to: "+1555", agentId: "agent-main" }), + ); + }); + + it("mirrors delivered output when mirror options are provided", async () => { + const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" }); + + await deliverOutboundPayloads({ + cfg: { + channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, + }, + channel: "telegram", + to: "123", + payloads: [{ text: "caption", mediaUrl: "https://example.com/files/report.pdf?sig=1" }], + deps: { sendTelegram }, + mirror: { + sessionKey: "agent:main:main", + text: "caption", + mediaUrls: ["https://example.com/files/report.pdf?sig=1"], + idempotencyKey: "idem-deliver-1", + }, + }); + + expect(mocks.appendAssistantMessageToSessionTranscript).toHaveBeenCalledWith( + expect.objectContaining({ + text: "report.pdf", + idempotencyKey: "idem-deliver-1", + }), + ); + }); + + it("emits message_sent success for text-only deliveries", async () => { + hookMocks.runner.hasHooks.mockReturnValue(true); + const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); + + await deliverOutboundPayloads({ + cfg: {}, + channel: "whatsapp", + to: "+1555", + payloads: [{ text: "hello" }], + deps: { sendWhatsApp }, + }); + + expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith( + expect.objectContaining({ to: "+1555", content: "hello", success: true }), + expect.objectContaining({ channelId: "whatsapp" }), + ); + }); + + it("emits message_sent success for sendPayload deliveries", async () => { + hookMocks.runner.hasHooks.mockReturnValue(true); + const sendPayload = vi.fn().mockResolvedValue({ channel: "matrix", messageId: "mx-1" }); + const sendText = vi.fn(); + const sendMedia = vi.fn(); + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "matrix", + source: "test", + plugin: createOutboundTestPlugin({ + id: "matrix", + outbound: { deliveryMode: "direct", sendPayload, sendText, sendMedia }, + }), + }, + ]), + ); + + await deliverOutboundPayloads({ + cfg: {}, + channel: "matrix", + to: "!room:1", + payloads: [{ text: "payload text", channelData: { mode: "custom" } }], + }); + + expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith( + expect.objectContaining({ to: "!room:1", content: "payload text", success: true }), + expect.objectContaining({ channelId: "matrix" }), + ); + }); + + it("emits message_sent failure when delivery errors", async () => { + hookMocks.runner.hasHooks.mockReturnValue(true); + const sendWhatsApp = vi.fn().mockRejectedValue(new Error("downstream failed")); + + await expect( + deliverOutboundPayloads({ + cfg: {}, + channel: "whatsapp", + to: "+1555", + payloads: [{ text: "hi" }], + deps: { sendWhatsApp }, + }), + ).rejects.toThrow("downstream failed"); + + expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith( + expect.objectContaining({ + to: "+1555", + content: "hi", + success: false, + error: "downstream failed", + }), + expect.objectContaining({ channelId: "whatsapp" }), + ); + }); +}); + +const emptyRegistry = createTestRegistry([]); +const defaultRegistry = createTestRegistry([ + { + pluginId: "telegram", + plugin: createOutboundTestPlugin({ id: "telegram", outbound: telegramOutbound }), + source: "test", + }, + { + pluginId: "signal", + plugin: createOutboundTestPlugin({ id: "signal", outbound: signalOutbound }), + source: "test", + }, + { + pluginId: "whatsapp", + plugin: createOutboundTestPlugin({ id: "whatsapp", outbound: whatsappOutbound }), + source: "test", + }, + { + pluginId: "imessage", + plugin: createIMessageTestPlugin(), + source: "test", + }, +]); diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index 8e5383ea055..223b984382b 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -117,75 +117,6 @@ async function deliverTelegramPayload(params: { }); } -async function runChunkedWhatsAppDelivery(params?: { - mirror?: Parameters[0]["mirror"]; -}) { - const sendWhatsApp = vi - .fn() - .mockResolvedValueOnce({ messageId: "w1", toJid: "jid" }) - .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" }); - const cfg: OpenClawConfig = { - channels: { whatsapp: { textChunkLimit: 2 } }, - }; - const results = await deliverOutboundPayloads({ - cfg, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "abcd" }], - deps: { sendWhatsApp }, - ...(params?.mirror ? { mirror: params.mirror } : {}), - }); - return { sendWhatsApp, results }; -} - -async function deliverSingleWhatsAppForHookTest(params?: { sessionKey?: string }) { - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); - await deliverOutboundPayloads({ - cfg: whatsappChunkConfig, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "hello" }], - deps: { sendWhatsApp }, - ...(params?.sessionKey ? { session: { key: params.sessionKey } } : {}), - }); -} - -async function runBestEffortPartialFailureDelivery() { - const sendWhatsApp = vi - .fn() - .mockRejectedValueOnce(new Error("fail")) - .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" }); - const onError = vi.fn(); - const cfg: OpenClawConfig = {}; - const results = await deliverOutboundPayloads({ - cfg, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "a" }, { text: "b" }], - deps: { sendWhatsApp }, - bestEffort: true, - onError, - }); - return { sendWhatsApp, onError, results }; -} - -function expectSuccessfulWhatsAppInternalHookPayload( - expected: Partial<{ - content: string; - messageId: string; - isGroup: boolean; - groupId: string; - }>, -) { - return expect.objectContaining({ - to: "+1555", - success: true, - channelId: "whatsapp", - conversationId: "+1555", - ...expected, - }); -} - describe("deliverOutboundPayloads", () => { beforeEach(() => { setActivePluginRegistry(defaultRegistry); @@ -529,7 +460,20 @@ describe("deliverOutboundPayloads", () => { }); it("chunks WhatsApp text and returns all results", async () => { - const { sendWhatsApp, results } = await runChunkedWhatsAppDelivery(); + const sendWhatsApp = vi + .fn() + .mockResolvedValueOnce({ messageId: "w1", toJid: "jid" }) + .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" }); + const cfg: OpenClawConfig = { + channels: { whatsapp: { textChunkLimit: 2 } }, + }; + const results = await deliverOutboundPayloads({ + cfg, + channel: "whatsapp", + to: "+1555", + payloads: [{ text: "abcd" }], + deps: { sendWhatsApp }, + }); expect(sendWhatsApp).toHaveBeenCalledTimes(2); expect(results.map((r) => r.messageId)).toEqual(["w1", "w2"]); @@ -725,211 +669,6 @@ describe("deliverOutboundPayloads", () => { ]); }); - it("continues on errors when bestEffort is enabled", async () => { - const { sendWhatsApp, onError, results } = await runBestEffortPartialFailureDelivery(); - - expect(sendWhatsApp).toHaveBeenCalledTimes(2); - expect(onError).toHaveBeenCalledTimes(1); - expect(results).toEqual([{ channel: "whatsapp", messageId: "w2", toJid: "jid" }]); - }); - - it("emits internal message:sent hook with success=true for chunked payload delivery", async () => { - const { sendWhatsApp } = await runChunkedWhatsAppDelivery({ - mirror: { - sessionKey: "agent:main:main", - isGroup: true, - groupId: "whatsapp:group:123", - }, - }); - expect(sendWhatsApp).toHaveBeenCalledTimes(2); - - expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledTimes(1); - expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledWith( - "message", - "sent", - "agent:main:main", - expectSuccessfulWhatsAppInternalHookPayload({ - content: "abcd", - messageId: "w2", - isGroup: true, - groupId: "whatsapp:group:123", - }), - ); - expect(internalHookMocks.triggerInternalHook).toHaveBeenCalledTimes(1); - }); - - it("does not emit internal message:sent hook when neither mirror nor sessionKey is provided", async () => { - await deliverSingleWhatsAppForHookTest(); - - expect(internalHookMocks.createInternalHookEvent).not.toHaveBeenCalled(); - expect(internalHookMocks.triggerInternalHook).not.toHaveBeenCalled(); - }); - - it("emits internal message:sent hook when sessionKey is provided without mirror", async () => { - await deliverSingleWhatsAppForHookTest({ sessionKey: "agent:main:main" }); - - expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledTimes(1); - expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledWith( - "message", - "sent", - "agent:main:main", - expectSuccessfulWhatsAppInternalHookPayload({ content: "hello", messageId: "w1" }), - ); - expect(internalHookMocks.triggerInternalHook).toHaveBeenCalledTimes(1); - }); - - it("warns when session.agentId is set without a session key", async () => { - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); - hookMocks.runner.hasHooks.mockReturnValue(true); - - await deliverOutboundPayloads({ - cfg: whatsappChunkConfig, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "hello" }], - deps: { sendWhatsApp }, - session: { agentId: "agent-main" }, - }); - - expect(logMocks.warn).toHaveBeenCalledWith( - "deliverOutboundPayloads: session.agentId present without session key; internal message:sent hook will be skipped", - expect.objectContaining({ channel: "whatsapp", to: "+1555", agentId: "agent-main" }), - ); - }); - - it("calls failDelivery instead of ackDelivery on bestEffort partial failure", async () => { - const { onError } = await runBestEffortPartialFailureDelivery(); - - // onError was called for the first payload's failure. - expect(onError).toHaveBeenCalledTimes(1); - - // Queue entry should NOT be acked — failDelivery should be called instead. - expect(queueMocks.ackDelivery).not.toHaveBeenCalled(); - expect(queueMocks.failDelivery).toHaveBeenCalledWith( - "mock-queue-id", - "partial delivery failure (bestEffort)", - ); - }); - - it("acks the queue entry when delivery is aborted", async () => { - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); - const abortController = new AbortController(); - abortController.abort(); - const cfg: OpenClawConfig = {}; - - await expect( - deliverOutboundPayloads({ - cfg, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "a" }], - deps: { sendWhatsApp }, - abortSignal: abortController.signal, - }), - ).rejects.toThrow("Operation aborted"); - - expect(queueMocks.ackDelivery).toHaveBeenCalledWith("mock-queue-id"); - expect(queueMocks.failDelivery).not.toHaveBeenCalled(); - expect(sendWhatsApp).not.toHaveBeenCalled(); - }); - - it("passes normalized payload to onError", async () => { - const sendWhatsApp = vi.fn().mockRejectedValue(new Error("boom")); - const onError = vi.fn(); - const cfg: OpenClawConfig = {}; - - await deliverOutboundPayloads({ - cfg, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "hi", mediaUrl: "https://x.test/a.jpg" }], - deps: { sendWhatsApp }, - bestEffort: true, - onError, - }); - - expect(onError).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenCalledWith( - expect.any(Error), - expect.objectContaining({ text: "hi", mediaUrls: ["https://x.test/a.jpg"] }), - ); - }); - - it("mirrors delivered output when mirror options are provided", async () => { - const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" }); - mocks.appendAssistantMessageToSessionTranscript.mockClear(); - - await deliverOutboundPayloads({ - cfg: telegramChunkConfig, - channel: "telegram", - to: "123", - payloads: [{ text: "caption", mediaUrl: "https://example.com/files/report.pdf?sig=1" }], - deps: { sendTelegram }, - mirror: { - sessionKey: "agent:main:main", - text: "caption", - mediaUrls: ["https://example.com/files/report.pdf?sig=1"], - idempotencyKey: "idem-deliver-1", - }, - }); - - expect(mocks.appendAssistantMessageToSessionTranscript).toHaveBeenCalledWith( - expect.objectContaining({ - text: "report.pdf", - idempotencyKey: "idem-deliver-1", - }), - ); - }); - - it("emits message_sent success for text-only deliveries", async () => { - hookMocks.runner.hasHooks.mockReturnValue(true); - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); - - await deliverOutboundPayloads({ - cfg: {}, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "hello" }], - deps: { sendWhatsApp }, - }); - - expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith( - expect.objectContaining({ to: "+1555", content: "hello", success: true }), - expect.objectContaining({ channelId: "whatsapp" }), - ); - }); - - it("emits message_sent success for sendPayload deliveries", async () => { - hookMocks.runner.hasHooks.mockReturnValue(true); - const sendPayload = vi.fn().mockResolvedValue({ channel: "matrix", messageId: "mx-1" }); - const sendText = vi.fn(); - const sendMedia = vi.fn(); - setActivePluginRegistry( - createTestRegistry([ - { - pluginId: "matrix", - source: "test", - plugin: createOutboundTestPlugin({ - id: "matrix", - outbound: { deliveryMode: "direct", sendPayload, sendText, sendMedia }, - }), - }, - ]), - ); - - await deliverOutboundPayloads({ - cfg: {}, - channel: "matrix", - to: "!room:1", - payloads: [{ text: "payload text", channelData: { mode: "custom" } }], - }); - - expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith( - expect.objectContaining({ to: "!room:1", content: "payload text", success: true }), - expect.objectContaining({ channelId: "matrix" }), - ); - }); - it("preserves channelData-only payloads with empty text for non-WhatsApp sendPayload channels", async () => { const sendPayload = vi.fn().mockResolvedValue({ channel: "line", messageId: "ln-1" }); const sendText = vi.fn(); @@ -1090,31 +829,6 @@ describe("deliverOutboundPayloads", () => { expect.objectContaining({ channelId: "matrix" }), ); }); - - it("emits message_sent failure when delivery errors", async () => { - hookMocks.runner.hasHooks.mockReturnValue(true); - const sendWhatsApp = vi.fn().mockRejectedValue(new Error("downstream failed")); - - await expect( - deliverOutboundPayloads({ - cfg: {}, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "hi" }], - deps: { sendWhatsApp }, - }), - ).rejects.toThrow("downstream failed"); - - expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith( - expect.objectContaining({ - to: "+1555", - content: "hi", - success: false, - error: "downstream failed", - }), - expect.objectContaining({ channelId: "whatsapp" }), - ); - }); }); const emptyRegistry = createTestRegistry([]);