diff --git a/src/infra/outbound/deliver.lifecycle.test.ts b/src/infra/outbound/deliver.lifecycle.test.ts index 00d696162d8..22fa829812e 100644 --- a/src/infra/outbound/deliver.lifecycle.test.ts +++ b/src/infra/outbound/deliver.lifecycle.test.ts @@ -1,94 +1,29 @@ 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(); - }, -})); +import { + clearDeliverTestRegistry, + hookMocks, + internalHookMocks, + logMocks, + mocks, + queueMocks, + resetDeliverTestState, + resetDeliverTestMocks, + runChunkedWhatsAppDelivery as runChunkedWhatsAppDeliveryHelper, + whatsappChunkConfig, +} from "./deliver.test-helpers.js"; 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 }, + return await runChunkedWhatsAppDeliveryHelper({ + deliverOutboundPayloads, ...(params?.mirror ? { mirror: params.mirror } : {}), }); - return { sendWhatsApp, results }; } async function deliverSingleWhatsAppForHookTest(params?: { sessionKey?: string }) { @@ -141,26 +76,12 @@ function expectSuccessfulWhatsAppInternalHookPayload( 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(); + resetDeliverTestState(); + resetDeliverTestMocks({ includeSessionMocks: true }); }); afterEach(() => { - setActivePluginRegistry(emptyRegistry); + clearDeliverTestRegistry(); }); it("continues on errors when bestEffort is enabled", async () => { @@ -389,27 +310,3 @@ describe("deliverOutboundPayloads lifecycle", () => { ); }); }); - -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-helpers.ts b/src/infra/outbound/deliver.test-helpers.ts new file mode 100644 index 00000000000..aab6280b338 --- /dev/null +++ b/src/infra/outbound/deliver.test-helpers.ts @@ -0,0 +1,206 @@ +import { 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"; + +export const deliverMocks = { + 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: unknown[]) => + 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.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(); + }, +})); + +export const whatsappChunkConfig: OpenClawConfig = { + channels: { whatsapp: { textChunkLimit: 4000 } }, +}; + +export const defaultRegistry = createTestRegistry([ + { + pluginId: "signal", + source: "test", + plugin: createOutboundTestPlugin({ + id: "signal", + outbound: signalOutbound, + }), + }, + { + pluginId: "telegram", + source: "test", + plugin: createOutboundTestPlugin({ + id: "telegram", + outbound: telegramOutbound, + }), + }, + { + pluginId: "whatsapp", + source: "test", + plugin: createOutboundTestPlugin({ + id: "whatsapp", + outbound: whatsappOutbound, + }), + }, + { + pluginId: "imessage", + source: "test", + plugin: createIMessageTestPlugin(), + }, +]); + +export const emptyRegistry = createTestRegistry([]); + +export function resetDeliverTestState() { + 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() { + 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: { + cfg: OpenClawConfig; + channel: string; + to: string; + payloads: Array<{ text: string }>; + deps: { sendWhatsApp: ReturnType }; + mirror?: unknown; + }) => Promise>; + mirror?: unknown; +}) { + const sendWhatsApp = vi + .fn() + .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: { 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 572046cfcc1..c7c43e098c6 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -11,64 +11,16 @@ import { markdownToSignalTextChunks } from "../../signal/format.js"; import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; import { withEnvAsync } from "../../test-utils/env.js"; import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js"; -import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js"; import { resolvePreferredOpenClawTmpDir } from "../tmp-openclaw-dir.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(); - }, -})); +import { + clearDeliverTestRegistry, + hookMocks, + logMocks, + resetDeliverTestState, + resetDeliverTestMocks, + runChunkedWhatsAppDelivery as runChunkedWhatsAppDeliveryHelper, + whatsappChunkConfig, +} from "./deliver.test-helpers.js"; const { deliverOutboundPayloads, normalizeOutboundPayloads } = await import("./deliver.js"); @@ -76,10 +28,6 @@ const telegramChunkConfig: OpenClawConfig = { channels: { telegram: { botToken: "tok-1", textChunkLimit: 2 } }, }; -const whatsappChunkConfig: OpenClawConfig = { - channels: { whatsapp: { textChunkLimit: 4000 } }, -}; - type DeliverOutboundArgs = Parameters[0]; type DeliverOutboundPayload = DeliverOutboundArgs["payloads"][number]; type DeliverSession = DeliverOutboundArgs["session"]; @@ -154,25 +102,12 @@ async function deliverTelegramPayload(params: { describe("deliverOutboundPayloads", () => { 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(); + resetDeliverTestState(); + resetDeliverTestMocks(); }); afterEach(() => { - setActivePluginRegistry(emptyRegistry); + clearDeliverTestRegistry(); }); it("chunks telegram markdown and passes through accountId", async () => { const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" }); @@ -495,19 +430,8 @@ describe("deliverOutboundPayloads", () => { }); it("chunks WhatsApp text and returns all results", async () => { - 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 }, + const { sendWhatsApp, results } = await runChunkedWhatsAppDeliveryHelper({ + deliverOutboundPayloads, }); expect(sendWhatsApp).toHaveBeenCalledTimes(2); @@ -801,27 +725,3 @@ describe("deliverOutboundPayloads", () => { ); }); }); - -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", - }, -]);