From b7e5d9a96e0e7a1b51348b3d4b4784802bb8f5bb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 22:44:06 +0100 Subject: [PATCH] test: decouple outbound tests from bundled plugins --- .../src/outbound-adapter.sendpayload.test.ts | 11 + src/infra/outbound/deliver.test-helpers.ts | 239 ---------- src/infra/outbound/deliver.test.ts | 450 +++++++++--------- test/helpers/infra/deliver-test-outbounds.ts | 269 ----------- 4 files changed, 237 insertions(+), 732 deletions(-) delete mode 100644 src/infra/outbound/deliver.test-helpers.ts delete mode 100644 test/helpers/infra/deliver-test-outbounds.ts diff --git a/extensions/whatsapp/src/outbound-adapter.sendpayload.test.ts b/extensions/whatsapp/src/outbound-adapter.sendpayload.test.ts index 52b44de49e7..96a23a19f7d 100644 --- a/extensions/whatsapp/src/outbound-adapter.sendpayload.test.ts +++ b/extensions/whatsapp/src/outbound-adapter.sendpayload.test.ts @@ -89,4 +89,15 @@ describe("whatsappOutbound sendPayload", () => { expect(result).toEqual({ channel: "whatsapp", messageId: "" }); expect(sendWhatsApp).not.toHaveBeenCalled(); }); + + it("sanitizes HTML-only text to whitespace-only payload", () => { + expect( + whatsappOutbound + .sanitizeText?.({ + text: "

", + payload: { text: "

" }, + }) + ?.trim(), + ).toBe(""); + }); }); diff --git a/src/infra/outbound/deliver.test-helpers.ts b/src/infra/outbound/deliver.test-helpers.ts deleted file mode 100644 index f7431e1b802..00000000000 --- a/src/infra/outbound/deliver.test-helpers.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { vi } from "vitest"; -import { createIMessageTestPlugin } from "../../../test/helpers/channels/imessage-test-plugin.js"; -import { - imessageOutboundForTest, - signalOutbound, - whatsappOutbound, -} from "../../../test/helpers/infra/deliver-test-outbounds.js"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { - releasePinnedPluginChannelRegistry, - setActivePluginRegistry, -} from "../../plugins/runtime.js"; -import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; -import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js"; -import type { DeliverOutboundPayloadsParams, OutboundDeliveryResult } from "./deliver.js"; - -type DeliverMockState = { - sessions: { - appendAssistantMessageToSessionTranscript: (...args: unknown[]) => Promise<{ - ok: boolean; - sessionFile: string; - }>; - }; - hooks: { - runner: { - hasHooks: (...args: unknown[]) => boolean; - runMessageSent: (...args: unknown[]) => Promise; - }; - }; - internalHooks: { - createInternalHookEvent: typeof createInternalHookEventPayload; - triggerInternalHook: (...args: unknown[]) => Promise; - }; - queue: { - enqueueDelivery: (...args: unknown[]) => Promise; - ackDelivery: (...args: unknown[]) => Promise; - failDelivery: (...args: unknown[]) => Promise; - }; - log: { - warn: (...args: unknown[]) => void; - }; -}; - -export const deliverMocks: DeliverMockState = { - sessions: { - appendAssistantMessageToSessionTranscript: async () => ({ ok: true, sessionFile: "x" }), - }, - hooks: { - runner: { - hasHooks: () => false, - runMessageSent: async () => {}, - }, - }, - internalHooks: { - createInternalHookEvent: createInternalHookEventPayload, - triggerInternalHook: async () => {}, - }, - queue: { - enqueueDelivery: async () => "mock-queue-id", - ackDelivery: async () => {}, - failDelivery: async () => {}, - }, - log: { - warn: () => {}, - }, -}; - -const _mocks = vi.hoisted(() => ({ - appendAssistantMessageToSessionTranscript: vi.fn(async () => - deliverMocks.sessions.appendAssistantMessageToSessionTranscript(), - ), -})); -const _hookMocks = vi.hoisted(() => ({ - runner: { - hasHooks: vi.fn(() => deliverMocks.hooks.runner.hasHooks()), - runMessageSent: vi.fn( - async (...args: unknown[]) => await deliverMocks.hooks.runner.runMessageSent(...args), - ), - }, -})); -const _internalHookMocks = vi.hoisted(() => ({ - createInternalHookEvent: vi.fn((...args: Parameters) => - deliverMocks.internalHooks.createInternalHookEvent(...args), - ), - triggerInternalHook: vi.fn( - async (...args: unknown[]) => await deliverMocks.internalHooks.triggerInternalHook(...args), - ), -})); -const _queueMocks = vi.hoisted(() => ({ - enqueueDelivery: vi.fn( - async (...args: unknown[]) => await deliverMocks.queue.enqueueDelivery(...args), - ), - ackDelivery: vi.fn(async (...args: unknown[]) => await deliverMocks.queue.ackDelivery(...args)), - failDelivery: vi.fn(async (...args: unknown[]) => await deliverMocks.queue.failDelivery(...args)), -})); -const _logMocks = vi.hoisted(() => ({ - warn: vi.fn((...args: unknown[]) => deliverMocks.log.warn(...args)), -})); - -export const mocks = _mocks; -export const hookMocks = _hookMocks; -export const internalHookMocks = _internalHookMocks; -export const queueMocks = _queueMocks; -export const logMocks = _logMocks; - -vi.mock("../../config/sessions/transcript.runtime.js", async () => { - const actual = await vi.importActual< - typeof import("../../config/sessions/transcript.runtime.js") - >("../../config/sessions/transcript.runtime.js"); - return { - ...actual, - appendAssistantMessageToSessionTranscript: _mocks.appendAssistantMessageToSessionTranscript, - }; -}); -vi.mock("../../config/sessions/transcript.js", async () => { - const actual = await vi.importActual( - "../../config/sessions/transcript.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(); - }, -})); - -export const whatsappChunkConfig: OpenClawConfig = { - channels: { whatsapp: { textChunkLimit: 4000 } }, -}; - -export const defaultRegistry = createTestRegistry([ - { - pluginId: "signal", - source: "test", - plugin: createOutboundTestPlugin({ - id: "signal", - outbound: signalOutbound, - }), - }, - { - pluginId: "whatsapp", - source: "test", - plugin: createOutboundTestPlugin({ - id: "whatsapp", - outbound: whatsappOutbound, - }), - }, - { - pluginId: "imessage", - source: "test", - plugin: createIMessageTestPlugin({ outbound: imessageOutboundForTest }), - }, -]); - -export const emptyRegistry = createTestRegistry([]); - -export function resetDeliverTestState() { - releasePinnedPluginChannelRegistry(); - setActivePluginRegistry(defaultRegistry); - deliverMocks.hooks.runner.hasHooks = () => false; - deliverMocks.hooks.runner.runMessageSent = async () => {}; - deliverMocks.internalHooks.createInternalHookEvent = createInternalHookEventPayload; - deliverMocks.internalHooks.triggerInternalHook = async () => {}; - deliverMocks.queue.enqueueDelivery = async () => "mock-queue-id"; - deliverMocks.queue.ackDelivery = async () => {}; - deliverMocks.queue.failDelivery = async () => {}; - deliverMocks.log.warn = () => {}; - deliverMocks.sessions.appendAssistantMessageToSessionTranscript = async () => ({ - ok: true, - sessionFile: "x", - }); -} - -export function clearDeliverTestRegistry() { - releasePinnedPluginChannelRegistry(); - setActivePluginRegistry(emptyRegistry); -} - -export function resetDeliverTestMocks(params?: { includeSessionMocks?: boolean }) { - hookMocks.runner.hasHooks.mockClear(); - hookMocks.runner.runMessageSent.mockClear(); - internalHookMocks.createInternalHookEvent.mockClear(); - internalHookMocks.triggerInternalHook.mockClear(); - queueMocks.enqueueDelivery.mockClear(); - queueMocks.ackDelivery.mockClear(); - queueMocks.failDelivery.mockClear(); - logMocks.warn.mockClear(); - if (params?.includeSessionMocks) { - mocks.appendAssistantMessageToSessionTranscript.mockClear(); - } -} - -export async function runChunkedWhatsAppDelivery(params: { - deliverOutboundPayloads: ( - params: DeliverOutboundPayloadsParams, - ) => Promise; - mirror?: DeliverOutboundPayloadsParams["mirror"]; -}) { - const sendWhatsApp = vi - .fn< - (to: string, text: string, opts?: unknown) => Promise<{ messageId: string; toJid: string }> - >() - .mockResolvedValueOnce({ messageId: "w1", toJid: "jid" }) - .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" }); - const cfg: OpenClawConfig = { - channels: { whatsapp: { textChunkLimit: 2 } }, - }; - const results = await params.deliverOutboundPayloads({ - cfg, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "abcd" }], - deps: { whatsapp: sendWhatsApp }, - ...(params.mirror ? { mirror: params.mirror } : {}), - }); - return { sendWhatsApp, results }; -} diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index 4348626e356..d3eb0365e07 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -1,11 +1,7 @@ import path from "node:path"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createIMessageTestPlugin } from "../../../test/helpers/channels/imessage-test-plugin.js"; -import { - imessageOutboundForTest, - signalOutbound, - whatsappOutbound, -} from "../../../test/helpers/infra/deliver-test-outbounds.js"; +import { chunkText } from "../../auto-reply/chunk.js"; +import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import * as mediaCapabilityModule from "../../media/read-capability.js"; import { createHookRunner } from "../../plugins/hooks.js"; @@ -93,84 +89,139 @@ type DeliverModule = typeof import("./deliver.js"); let deliverOutboundPayloads: DeliverModule["deliverOutboundPayloads"]; let normalizeOutboundPayloads: DeliverModule["normalizeOutboundPayloads"]; -const whatsappChunkConfig: OpenClawConfig = { - channels: { whatsapp: { textChunkLimit: 4000 } }, +const matrixChunkConfig: OpenClawConfig = { + channels: { matrix: { textChunkLimit: 4000 } } as OpenClawConfig["channels"], }; const expectedPreferredTmpRoot = resolvePreferredOpenClawTmpDir(); type DeliverOutboundArgs = Parameters[0]; type DeliverOutboundPayload = DeliverOutboundArgs["payloads"][number]; +type MatrixSendFn = ( + to: string, + text: string, + options?: Record, +) => Promise<{ messageId: string } & Record>; -async function deliverWhatsAppPayload(params: { - sendWhatsApp: NonNullable< - NonNullable[0]["deps"]>["whatsapp"] - >; +function resolveMatrixSender(deps: DeliverOutboundArgs["deps"]): MatrixSendFn { + const sender = deps?.matrix; + if (typeof sender !== "function") { + throw new Error("missing matrix sender"); + } + return sender as MatrixSendFn; +} + +function withMatrixChannel(result: Awaited>) { + return { + channel: "matrix" as const, + ...result, + }; +} + +const matrixOutboundForTest: ChannelOutboundAdapter = { + deliveryMode: "direct", + chunker: chunkText, + chunkerMode: "text", + textChunkLimit: 4000, + sanitizeText: ({ text }) => (text === "
" || text === "

" ? "" : text), + sendText: async ({ cfg, to, text, accountId, deps, gifPlayback }) => + withMatrixChannel( + await resolveMatrixSender(deps)(to, text, { + cfg, + accountId: accountId ?? undefined, + gifPlayback, + }), + ), + sendMedia: async ({ + cfg, + to, + text, + mediaUrl, + mediaLocalRoots, + mediaReadFile, + accountId, + deps, + gifPlayback, + }) => + withMatrixChannel( + await resolveMatrixSender(deps)(to, text, { + cfg, + mediaUrl, + mediaLocalRoots, + mediaReadFile, + accountId: accountId ?? undefined, + gifPlayback, + }), + ), +}; + +async function deliverMatrixPayload(params: { + sendMatrix: MatrixSendFn; payload: DeliverOutboundPayload; cfg?: OpenClawConfig; }) { return deliverOutboundPayloads({ - cfg: params.cfg ?? whatsappChunkConfig, - channel: "whatsapp", - to: "+1555", + cfg: params.cfg ?? matrixChunkConfig, + channel: "matrix", + to: "!room:example", payloads: [params.payload], - deps: { whatsapp: params.sendWhatsApp }, + deps: { matrix: params.sendMatrix }, }); } -async function runChunkedWhatsAppDelivery(params?: { +async function runChunkedMatrixDelivery(params?: { mirror?: Parameters[0]["mirror"]; }) { - const sendWhatsApp = vi + const sendMatrix = vi .fn() - .mockResolvedValueOnce({ messageId: "w1", toJid: "jid" }) - .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" }); + .mockResolvedValueOnce({ messageId: "m1", roomId: "!room:example" }) + .mockResolvedValueOnce({ messageId: "m2", roomId: "!room:example" }); const cfg: OpenClawConfig = { - channels: { whatsapp: { textChunkLimit: 2 } }, + channels: { matrix: { textChunkLimit: 2 } } as OpenClawConfig["channels"], }; const results = await deliverOutboundPayloads({ cfg, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "abcd" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, ...(params?.mirror ? { mirror: params.mirror } : {}), }); - return { sendWhatsApp, results }; + return { sendMatrix, results }; } -async function deliverSingleWhatsAppForHookTest(params?: { sessionKey?: string }) { - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); +async function deliverSingleMatrixForHookTest(params?: { sessionKey?: string }) { + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" }); await deliverOutboundPayloads({ - cfg: whatsappChunkConfig, - channel: "whatsapp", - to: "+1555", + cfg: matrixChunkConfig, + channel: "matrix", + to: "!room:example", payloads: [{ text: "hello" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, ...(params?.sessionKey ? { session: { key: params.sessionKey } } : {}), }); } async function runBestEffortPartialFailureDelivery() { - const sendWhatsApp = vi + const sendMatrix = vi .fn() .mockRejectedValueOnce(new Error("fail")) - .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" }); + .mockResolvedValueOnce({ messageId: "m2", roomId: "!room:example" }); const onError = vi.fn(); const cfg: OpenClawConfig = {}; const results = await deliverOutboundPayloads({ cfg, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "a" }, { text: "b" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, bestEffort: true, onError, }); - return { sendWhatsApp, onError, results }; + return { sendMatrix, onError, results }; } -function expectSuccessfulWhatsAppInternalHookPayload( +function expectSuccessfulMatrixInternalHookPayload( expected: Partial<{ content: string; messageId: string; @@ -179,10 +230,10 @@ function expectSuccessfulWhatsAppInternalHookPayload( }>, ) { return expect.objectContaining({ - to: "+1555", + to: "!room:example", success: true, - channelId: "whatsapp", - conversationId: "+1555", + channelId: "matrix", + conversationId: "!room:example", ...expected, }); } @@ -224,23 +275,23 @@ describe("deliverOutboundPayloads", () => { mediaCapabilityModule, "resolveAgentScopedOutboundMediaAccess", ); - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" }); await deliverOutboundPayloads({ cfg: {}, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "hello", mediaUrl: "file:///tmp/policy.png" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, session: { - key: "agent:main:whatsapp:group:ops", + key: "agent:main:matrix:room:ops", requesterSenderId: "attacker", }, }); expect(resolveMediaAccessSpy).toHaveBeenCalledWith( expect.objectContaining({ - sessionKey: "agent:main:whatsapp:group:ops", + sessionKey: "agent:main:matrix:room:ops", messageProvider: undefined, requesterSenderId: "attacker", }), @@ -253,17 +304,17 @@ describe("deliverOutboundPayloads", () => { mediaCapabilityModule, "resolveAgentScopedOutboundMediaAccess", ); - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w2", toJid: "jid" }); + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m2", roomId: "!room:example" }); await deliverOutboundPayloads({ cfg: {}, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "hello", mediaUrl: "file:///tmp/policy.png" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, session: { - key: "agent:main:whatsapp:group:ops", - requesterSenderId: "id:whatsapp:123", + key: "agent:main:matrix:room:ops", + requesterSenderId: "id:matrix:123", requesterSenderName: "Alice", requesterSenderUsername: "alice_u", requesterSenderE164: "+15551234567", @@ -272,7 +323,7 @@ describe("deliverOutboundPayloads", () => { expect(resolveMediaAccessSpy).toHaveBeenCalledWith( expect.objectContaining({ - requesterSenderId: "id:whatsapp:123", + requesterSenderId: "id:matrix:123", requesterSenderName: "Alice", requesterSenderUsername: "alice_u", requesterSenderE164: "+15551234567", @@ -286,17 +337,17 @@ describe("deliverOutboundPayloads", () => { mediaCapabilityModule, "resolveAgentScopedOutboundMediaAccess", ); - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w3", toJid: "jid" }); + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m3", roomId: "!room:example" }); await deliverOutboundPayloads({ cfg: {}, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", accountId: "destination-account", payloads: [{ text: "hello", mediaUrl: "file:///tmp/policy.png" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, session: { - key: "agent:main:whatsapp:group:ops", + key: "agent:main:matrix:room:ops", requesterAccountId: "source-account", requesterSenderId: "attacker", }, @@ -304,7 +355,7 @@ describe("deliverOutboundPayloads", () => { expect(resolveMediaAccessSpy).toHaveBeenCalledWith( expect.objectContaining({ - sessionKey: "agent:main:whatsapp:group:ops", + sessionKey: "agent:main:matrix:room:ops", accountId: "source-account", requesterSenderId: "attacker", }), @@ -317,16 +368,16 @@ describe("deliverOutboundPayloads", () => { mediaCapabilityModule, "resolveAgentScopedOutboundMediaAccess", ); - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w4", toJid: "jid" }); + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m4", roomId: "!room:example" }); await deliverOutboundPayloads({ cfg: {}, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "hello" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, session: { - key: "agent:main:whatsapp:group:ops", + key: "agent:main:matrix:room:ops", requesterSenderId: "attacker", }, }); @@ -473,19 +524,19 @@ describe("deliverOutboundPayloads", () => { expect(sendMedia).not.toHaveBeenCalled(); }); - it("includes OpenClaw tmp root in signal mediaLocalRoots", async () => { - const sendSignal = vi.fn().mockResolvedValue({ messageId: "s1", timestamp: 123 }); + it("includes OpenClaw tmp root in plugin mediaLocalRoots", async () => { + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m-media", roomId: "!room" }); await deliverOutboundPayloads({ - cfg: { channels: { signal: {} } }, - channel: "signal", - to: "+1555", + cfg: { channels: { matrix: {} } } as OpenClawConfig, + channel: "matrix", + to: "!room:example", payloads: [{ text: "hi", mediaUrl: "https://example.com/x.png" }], - deps: { sendSignal }, + deps: { matrix: sendMatrix }, }); - expect(sendSignal).toHaveBeenCalledWith( - "+1555", + expect(sendMatrix).toHaveBeenCalledWith( + "!room:example", "hi", expect.objectContaining({ mediaLocalRoots: expect.arrayContaining([expectedPreferredTmpRoot]), @@ -493,18 +544,18 @@ describe("deliverOutboundPayloads", () => { ); }); - it("sends telegram media to an explicit target once instead of fanning out over allowFrom", async () => { - const sendMedia = vi.fn().mockResolvedValue({ channel: "telegram", messageId: "t1" }); + it("sends plugin media to an explicit target once instead of fanning out over allowFrom", async () => { + const sendMedia = vi.fn().mockResolvedValue({ channel: "matrix", messageId: "m1" }); setActivePluginRegistry( createTestRegistry([ { - pluginId: "telegram", + pluginId: "matrix", source: "test", plugin: createOutboundTestPlugin({ - id: "telegram", + id: "matrix", outbound: { deliveryMode: "direct", - sendText: vi.fn().mockResolvedValue({ channel: "telegram", messageId: "text-1" }), + sendText: vi.fn().mockResolvedValue({ channel: "matrix", messageId: "text-1" }), sendMedia, }, }), @@ -515,14 +566,13 @@ describe("deliverOutboundPayloads", () => { await deliverOutboundPayloads({ cfg: { channels: { - telegram: { - botToken: "tok", + matrix: { allowFrom: ["111", "222", "333"], }, - }, + } as OpenClawConfig["channels"], }, - channel: "telegram", - to: "123", + channel: "matrix", + to: "!explicit:example", payloads: [{ text: "HEARTBEAT_OK", mediaUrl: "https://example.com/img.png" }], skipQueue: true, }); @@ -530,7 +580,7 @@ describe("deliverOutboundPayloads", () => { expect(sendMedia).toHaveBeenCalledTimes(1); expect(sendMedia).toHaveBeenCalledWith( expect.objectContaining({ - to: "123", + to: "!explicit:example", text: "HEARTBEAT_OK", mediaUrl: "https://example.com/img.png", accountId: undefined, @@ -581,104 +631,66 @@ describe("deliverOutboundPayloads", () => { ); }); - it("includes OpenClaw tmp root in whatsapp mediaLocalRoots", async () => { - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); + it("chunks plugin text and returns all results", async () => { + const { sendMatrix, results } = await runChunkedMatrixDelivery(); - await deliverOutboundPayloads({ - cfg: whatsappChunkConfig, - channel: "whatsapp", - to: "+1555", - payloads: [{ text: "hi", mediaUrl: "https://example.com/x.png" }], - deps: { whatsapp: sendWhatsApp }, - }); - - expect(sendWhatsApp).toHaveBeenCalledWith( - "+1555", - "hi", - expect.objectContaining({ - mediaLocalRoots: expect.arrayContaining([expectedPreferredTmpRoot]), - }), - ); + expect(sendMatrix).toHaveBeenCalledTimes(2); + expect(results.map((r) => r.messageId)).toEqual(["m1", "m2"]); }); - it("includes OpenClaw tmp root in imessage mediaLocalRoots", async () => { - const sendIMessage = vi.fn().mockResolvedValue({ messageId: "i1", chatId: "chat-1" }); - - await deliverOutboundPayloads({ - cfg: {}, - channel: "imessage", - to: "imessage:+15551234567", - payloads: [{ text: "hi", mediaUrl: "https://example.com/x.png" }], - deps: { imessage: sendIMessage }, - }); - - expect(sendIMessage).toHaveBeenCalledWith( - "imessage:+15551234567", - "hi", - expect.objectContaining({ - mediaLocalRoots: expect.arrayContaining([expectedPreferredTmpRoot]), - }), - ); - }); - - it("chunks WhatsApp text and returns all results", async () => { - const { sendWhatsApp, results } = await runChunkedWhatsAppDelivery(); - - expect(sendWhatsApp).toHaveBeenCalledTimes(2); - expect(results.map((r) => r.messageId)).toEqual(["w1", "w2"]); - }); - - it("respects newline chunk mode for WhatsApp", async () => { - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); + it("respects newline chunk mode for plugin text", async () => { + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" }); const cfg: OpenClawConfig = { - channels: { whatsapp: { textChunkLimit: 4000, chunkMode: "newline" } }, + channels: { + matrix: { textChunkLimit: 4000, chunkMode: "newline" }, + } as OpenClawConfig["channels"], }; await deliverOutboundPayloads({ cfg, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "Line one\n\nLine two" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, }); - expect(sendWhatsApp).toHaveBeenCalledTimes(2); - expect(sendWhatsApp).toHaveBeenNthCalledWith( + expect(sendMatrix).toHaveBeenCalledTimes(2); + expect(sendMatrix).toHaveBeenNthCalledWith( 1, - "+1555", + "!room:example", "Line one", - expect.objectContaining({ verbose: false }), + expect.objectContaining({ cfg }), ); - expect(sendWhatsApp).toHaveBeenNthCalledWith( + expect(sendMatrix).toHaveBeenNthCalledWith( 2, - "+1555", + "!room:example", "Line two", - expect.objectContaining({ verbose: false }), + expect.objectContaining({ cfg }), ); }); - it("drops HTML-only WhatsApp text payloads after sanitization", async () => { - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); - const results = await deliverWhatsAppPayload({ - sendWhatsApp, + it("drops text payloads after adapter sanitization removes all content", async () => { + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" }); + const results = await deliverMatrixPayload({ + sendMatrix, payload: { text: "

" }, }); - expect(sendWhatsApp).not.toHaveBeenCalled(); + expect(sendMatrix).not.toHaveBeenCalled(); expect(results).toEqual([]); }); - it("drops non-WhatsApp HTML-only text payloads after sanitization", async () => { - const sendSignal = vi.fn().mockResolvedValue({ messageId: "s1", toJid: "jid" }); + it("drops plugin HTML-only text payloads after sanitization", async () => { + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" }); const results = await deliverOutboundPayloads({ cfg: {}, - channel: "signal", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "
" }], - deps: { sendSignal }, + deps: { matrix: sendMatrix }, }); - expect(sendSignal).not.toHaveBeenCalled(); + expect(sendMatrix).not.toHaveBeenCalled(); expect(results).toEqual([]); }); @@ -730,14 +742,14 @@ describe("deliverOutboundPayloads", () => { expect(chunker).toHaveBeenNthCalledWith(1, text, 4000); }); - it("passes config through for iMessage media sends so the channel runtime can resolve limits", async () => { - const sendIMessage = vi.fn().mockResolvedValue({ messageId: "i1" }); + it("passes config through for plugin media sends", async () => { + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m-media", roomId: "!room" }); setActivePluginRegistry( createTestRegistry([ { - pluginId: "imessage", + pluginId: "matrix", source: "test", - plugin: createIMessageTestPlugin(), + plugin: createOutboundTestPlugin({ id: "matrix", outbound: matrixOutboundForTest }), }, ]), ); @@ -747,17 +759,17 @@ describe("deliverOutboundPayloads", () => { await deliverOutboundPayloads({ cfg, - channel: "imessage", - to: "chat_id:42", + channel: "matrix", + to: "!room:example", payloads: [{ text: "hello", mediaUrls: ["https://example.com/a.png"] }], - deps: { imessage: sendIMessage }, + deps: { matrix: sendMatrix }, }); - expect(sendIMessage).toHaveBeenCalledWith( - "chat_id:42", + expect(sendMatrix).toHaveBeenCalledWith( + "!room:example", "hello", expect.objectContaining({ - config: cfg, + cfg, mediaUrl: "https://example.com/a.png", }), ); @@ -776,74 +788,74 @@ describe("deliverOutboundPayloads", () => { }); it("continues on errors when bestEffort is enabled", async () => { - const { sendWhatsApp, onError, results } = await runBestEffortPartialFailureDelivery(); + const { sendMatrix, onError, results } = await runBestEffortPartialFailureDelivery(); - expect(sendWhatsApp).toHaveBeenCalledTimes(2); + expect(sendMatrix).toHaveBeenCalledTimes(2); expect(onError).toHaveBeenCalledTimes(1); - expect(results).toEqual([{ channel: "whatsapp", messageId: "w2", toJid: "jid" }]); + expect(results).toEqual([{ channel: "matrix", messageId: "m2", roomId: "!room:example" }]); }); it("emits internal message:sent hook with success=true for chunked payload delivery", async () => { - const { sendWhatsApp } = await runChunkedWhatsAppDelivery({ + const { sendMatrix } = await runChunkedMatrixDelivery({ mirror: { sessionKey: "agent:main:main", isGroup: true, - groupId: "whatsapp:group:123", + groupId: "matrix:room:123", }, }); - expect(sendWhatsApp).toHaveBeenCalledTimes(2); + expect(sendMatrix).toHaveBeenCalledTimes(2); expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledTimes(1); expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledWith( "message", "sent", "agent:main:main", - expectSuccessfulWhatsAppInternalHookPayload({ + expectSuccessfulMatrixInternalHookPayload({ content: "abcd", - messageId: "w2", + messageId: "m2", isGroup: true, - groupId: "whatsapp:group:123", + groupId: "matrix:room:123", }), ); expect(internalHookMocks.triggerInternalHook).toHaveBeenCalledTimes(1); }); it("does not emit internal message:sent hook when neither mirror nor sessionKey is provided", async () => { - await deliverSingleWhatsAppForHookTest(); + await deliverSingleMatrixForHookTest(); 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" }); + await deliverSingleMatrixForHookTest({ sessionKey: "agent:main:main" }); expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledTimes(1); expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledWith( "message", "sent", "agent:main:main", - expectSuccessfulWhatsAppInternalHookPayload({ content: "hello", messageId: "w1" }), + expectSuccessfulMatrixInternalHookPayload({ content: "hello", messageId: "m1" }), ); 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" }); + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" }); hookMocks.runner.hasHooks.mockReturnValue(true); await deliverOutboundPayloads({ - cfg: whatsappChunkConfig, - channel: "whatsapp", - to: "+1555", + cfg: matrixChunkConfig, + channel: "matrix", + to: "!room:example", payloads: [{ text: "hello" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, 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" }), + expect.objectContaining({ channel: "matrix", to: "!room:example", agentId: "agent-main" }), ); }); @@ -862,7 +874,7 @@ describe("deliverOutboundPayloads", () => { }); it("writes raw payloads to the queue before normalization", async () => { - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w-raw", toJid: "jid" }); + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m-raw", roomId: "!room:example" }); const rawPayloads: DeliverOutboundPayload[] = [ { text: "NO_REPLY" }, { text: '{"action":"NO_REPLY"}' }, @@ -871,11 +883,11 @@ describe("deliverOutboundPayloads", () => { ]; await deliverOutboundPayloads({ - cfg: whatsappChunkConfig, - channel: "whatsapp", - to: "+1555", + cfg: matrixChunkConfig, + channel: "matrix", + to: "!room:example", payloads: rawPayloads, - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, }); expect(queueMocks.enqueueDelivery).toHaveBeenCalledTimes(1); @@ -892,7 +904,7 @@ describe("deliverOutboundPayloads", () => { }); it("acks the queue entry when delivery is aborted", async () => { - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" }); const abortController = new AbortController(); abortController.abort(); const cfg: OpenClawConfig = {}; @@ -900,30 +912,30 @@ describe("deliverOutboundPayloads", () => { await expect( deliverOutboundPayloads({ cfg, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "a" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, abortSignal: abortController.signal, }), ).rejects.toThrow("Operation aborted"); expect(queueMocks.ackDelivery).toHaveBeenCalledWith("mock-queue-id"); expect(queueMocks.failDelivery).not.toHaveBeenCalled(); - expect(sendWhatsApp).not.toHaveBeenCalled(); + expect(sendMatrix).not.toHaveBeenCalled(); }); it("passes normalized payload to onError", async () => { - const sendWhatsApp = vi.fn().mockRejectedValue(new Error("boom")); + const sendMatrix = vi.fn().mockRejectedValue(new Error("boom")); const onError = vi.fn(); const cfg: OpenClawConfig = {}; await deliverOutboundPayloads({ cfg, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "hi", mediaUrl: "https://x.test/a.jpg" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, bestEffort: true, onError, }); @@ -977,19 +989,19 @@ describe("deliverOutboundPayloads", () => { it("emits message_sent success for text-only deliveries", async () => { hookMocks.runner.hasHooks.mockReturnValue(true); - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" }); await deliverOutboundPayloads({ cfg: {}, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "hello" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, }); expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith( - expect.objectContaining({ to: "+1555", content: "hello", success: true }), - expect.objectContaining({ channelId: "whatsapp" }), + expect.objectContaining({ to: "!room:example", content: "hello", success: true }), + expect.objectContaining({ channelId: "matrix" }), ); }); @@ -1019,19 +1031,19 @@ describe("deliverOutboundPayloads", () => { realRunner.runMessageSending(event as never, ctx as never), ); - const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" }); + const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" }); await deliverOutboundPayloads({ cfg: {}, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "hello" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, }); expect(hookMocks.runner.runMessageSending).toHaveBeenCalledTimes(1); expect(high).toHaveBeenCalledTimes(1); expect(low).not.toHaveBeenCalled(); - expect(sendWhatsApp).not.toHaveBeenCalled(); + expect(sendMatrix).not.toHaveBeenCalled(); expect(hookMocks.runner.runMessageSent).not.toHaveBeenCalled(); }); @@ -1066,7 +1078,7 @@ describe("deliverOutboundPayloads", () => { ); }); - it("preserves channelData-only payloads with empty text for non-WhatsApp sendPayload channels", async () => { + it("preserves channelData-only payloads with empty text for sendPayload channels", async () => { const sendPayload = vi.fn().mockResolvedValue({ channel: "line", messageId: "ln-1" }); const sendText = vi.fn(); const sendMedia = vi.fn(); @@ -1229,26 +1241,26 @@ describe("deliverOutboundPayloads", () => { it("emits message_sent failure when delivery errors", async () => { hookMocks.runner.hasHooks.mockReturnValue(true); - const sendWhatsApp = vi.fn().mockRejectedValue(new Error("downstream failed")); + const sendMatrix = vi.fn().mockRejectedValue(new Error("downstream failed")); await expect( deliverOutboundPayloads({ cfg: {}, - channel: "whatsapp", - to: "+1555", + channel: "matrix", + to: "!room:example", payloads: [{ text: "hi" }], - deps: { whatsapp: sendWhatsApp }, + deps: { matrix: sendMatrix }, }), ).rejects.toThrow("downstream failed"); expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith( expect.objectContaining({ - to: "+1555", + to: "!room:example", content: "hi", success: false, error: "downstream failed", }), - expect.objectContaining({ channelId: "whatsapp" }), + expect.objectContaining({ channelId: "matrix" }), ); }); }); @@ -1256,18 +1268,8 @@ describe("deliverOutboundPayloads", () => { const emptyRegistry = createTestRegistry([]); const defaultRegistry = createTestRegistry([ { - pluginId: "signal", - plugin: createOutboundTestPlugin({ id: "signal", outbound: signalOutbound }), - source: "test", - }, - { - pluginId: "whatsapp", - plugin: createOutboundTestPlugin({ id: "whatsapp", outbound: whatsappOutbound }), - source: "test", - }, - { - pluginId: "imessage", - plugin: createIMessageTestPlugin({ outbound: imessageOutboundForTest }), + pluginId: "matrix", + plugin: createOutboundTestPlugin({ id: "matrix", outbound: matrixOutboundForTest }), source: "test", }, ]); diff --git a/test/helpers/infra/deliver-test-outbounds.ts b/test/helpers/infra/deliver-test-outbounds.ts deleted file mode 100644 index f4b09fbada4..00000000000 --- a/test/helpers/infra/deliver-test-outbounds.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { chunkMarkdownTextWithMode, chunkText } from "../../../src/auto-reply/chunk.js"; -import { resolveChannelMediaMaxBytes } from "../../../src/channels/plugins/media-limits.js"; -import type { ChannelOutboundAdapter } from "../../../src/channels/plugins/types.js"; -import type { OpenClawConfig } from "../../../src/config/config.js"; -import { - resolveOutboundSendDep, - type OutboundSendDeps, -} from "../../../src/infra/outbound/send-deps.js"; -import { sanitizeForPlainText } from "../../../src/plugin-sdk/outbound-runtime.js"; - -type SignalSendFn = ( - to: string, - text: string, - options?: Record, -) => Promise<{ messageId: string } & Record>; - -const MB = 1024 * 1024; - -function resolveSignalMaxBytes(cfg: OpenClawConfig, accountId?: string): number | undefined { - const signalCfg = cfg.channels?.signal as - | { - mediaMaxMb?: number; - accounts?: Record; - } - | undefined; - const accountMb = accountId ? signalCfg?.accounts?.[accountId]?.mediaMaxMb : undefined; - const mediaMaxMb = accountMb ?? signalCfg?.mediaMaxMb; - return typeof mediaMaxMb === "number" ? mediaMaxMb * MB : undefined; -} - -function resolveSignalSender(deps: OutboundSendDeps | undefined): SignalSendFn { - const sender = resolveOutboundSendDep(deps, "signal"); - if (!sender) { - throw new Error("missing sendSignal dep"); - } - return sender; -} - -function resolveSignalTextChunkLimit(cfg: OpenClawConfig, accountId?: string | null): number { - const signalCfg = cfg.channels?.signal as - | { - textChunkLimit?: number; - accounts?: Record; - } - | undefined; - const accountLimit = accountId ? signalCfg?.accounts?.[accountId]?.textChunkLimit : undefined; - if (typeof accountLimit === "number") { - return accountLimit; - } - return typeof signalCfg?.textChunkLimit === "number" ? signalCfg.textChunkLimit : 4000; -} - -function withSignalChannel(result: Awaited>) { - return { - channel: "signal" as const, - ...result, - }; -} - -export const signalOutbound: ChannelOutboundAdapter = { - deliveryMode: "direct", - textChunkLimit: 4000, - sanitizeText: ({ text }) => sanitizeForPlainText(text), - sendFormattedText: async ({ cfg, to, text, accountId, deps, abortSignal }) => { - const send = resolveSignalSender(deps); - const maxBytes = resolveSignalMaxBytes(cfg, accountId ?? undefined); - const limit = resolveSignalTextChunkLimit(cfg, accountId); - const chunks = chunkMarkdownTextWithMode(text, limit, "length"); - const outputChunks = chunks.length === 0 && text ? [text] : chunks; - const results = []; - for (const chunk of outputChunks) { - abortSignal?.throwIfAborted(); - results.push( - withSignalChannel( - await send(to, chunk, { - cfg, - maxBytes, - accountId: accountId ?? undefined, - textMode: "plain", - textStyles: [], - }), - ), - ); - } - return results; - }, - sendFormattedMedia: async ({ - cfg, - to, - text, - mediaUrl, - mediaLocalRoots, - mediaReadFile, - accountId, - deps, - abortSignal, - }) => { - abortSignal?.throwIfAborted(); - const send = resolveSignalSender(deps); - const maxBytes = resolveSignalMaxBytes(cfg, accountId ?? undefined); - return withSignalChannel( - await send(to, text, { - cfg, - mediaUrl, - maxBytes, - accountId: accountId ?? undefined, - textMode: "plain", - textStyles: [], - mediaLocalRoots, - mediaReadFile, - }), - ); - }, - sendText: async ({ cfg, to, text, accountId, deps }) => { - const send = resolveSignalSender(deps); - const maxBytes = resolveSignalMaxBytes(cfg, accountId ?? undefined); - return withSignalChannel( - await send(to, text, { - cfg, - maxBytes, - accountId: accountId ?? undefined, - }), - ); - }, - sendMedia: async ({ - cfg, - to, - text, - mediaUrl, - mediaLocalRoots, - mediaReadFile, - accountId, - deps, - }) => { - const send = resolveSignalSender(deps); - const maxBytes = resolveSignalMaxBytes(cfg, accountId ?? undefined); - return withSignalChannel( - await send(to, text, { - cfg, - mediaUrl, - maxBytes, - accountId: accountId ?? undefined, - mediaLocalRoots, - mediaReadFile, - }), - ); - }, -}; - -type WhatsAppSendFn = ( - to: string, - text: string, - options?: Record, -) => Promise<{ messageId: string } & Record>; - -function resolveWhatsAppSender(deps: OutboundSendDeps | undefined): WhatsAppSendFn { - const sender = resolveOutboundSendDep(deps, "whatsapp"); - if (!sender) { - throw new Error("missing whatsapp dep"); - } - return sender; -} - -function withWhatsAppChannel(result: Awaited>) { - return { - channel: "whatsapp" as const, - ...result, - }; -} - -export const whatsappOutbound: ChannelOutboundAdapter = { - deliveryMode: "gateway", - chunker: chunkText, - chunkerMode: "text", - textChunkLimit: 4000, - sanitizeText: ({ text }) => sanitizeForPlainText(text), - sendText: async ({ cfg, to, text, accountId, deps, gifPlayback }) => { - const send = resolveWhatsAppSender(deps); - return withWhatsAppChannel( - await send(to, text, { - verbose: false, - cfg, - accountId: accountId ?? undefined, - gifPlayback, - }), - ); - }, - sendMedia: async ({ - cfg, - to, - text, - mediaUrl, - mediaLocalRoots, - mediaReadFile, - accountId, - deps, - gifPlayback, - }) => { - const send = resolveWhatsAppSender(deps); - return withWhatsAppChannel( - await send(to, text, { - verbose: false, - cfg, - mediaUrl, - mediaLocalRoots, - mediaReadFile, - accountId: accountId ?? undefined, - gifPlayback, - }), - ); - }, -}; - -function resolveIMessageSender(deps: OutboundSendDeps | undefined) { - const sender = resolveOutboundSendDep< - ( - to: string, - text: string, - options?: Record, - ) => Promise<{ messageId: string; chatId?: string }> - >(deps, "imessage"); - if (!sender) { - throw new Error("missing sendIMessage dep"); - } - return sender; -} - -function withIMessageChannel( - result: Awaited>>, -) { - return { - channel: "imessage" as const, - ...result, - }; -} - -function resolveIMessageMaxBytes( - cfg: OpenClawConfig, - accountId?: string | null, -): number | undefined { - return resolveChannelMediaMaxBytes({ - cfg, - resolveChannelLimitMb: ({ cfg, accountId }) => - cfg.channels?.imessage?.accounts?.[accountId]?.mediaMaxMb ?? - cfg.channels?.imessage?.mediaMaxMb, - accountId, - }); -} - -export const imessageOutboundForTest: ChannelOutboundAdapter = { - deliveryMode: "direct", - sanitizeText: ({ text }) => text, - sendText: async ({ to, text, accountId, deps }) => - withIMessageChannel( - await resolveIMessageSender(deps)(to, text, { - accountId: accountId ?? undefined, - }), - ), - sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, mediaReadFile, accountId, deps }) => - withIMessageChannel( - await resolveIMessageSender(deps)(to, text, { - mediaUrl, - mediaLocalRoots, - mediaReadFile, - maxBytes: resolveIMessageMaxBytes(cfg, accountId), - accountId: accountId ?? undefined, - }), - ), -};