test: share outbound delivery helpers

This commit is contained in:
Peter Steinberger 2026-03-14 00:46:46 +00:00
parent 81ea997d40
commit 487e188112
3 changed files with 237 additions and 234 deletions

View File

@ -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<typeof import("../../config/sessions.js")>(
"../../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<typeof deliverOutboundPayloads>[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",
},
]);

View File

@ -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<typeof import("../../config/sessions.js")>(
"../../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<typeof vi.fn> };
mirror?: unknown;
}) => Promise<Array<{ messageId?: string; toJid?: string }>>;
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 };
}

View File

@ -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<typeof import("../../config/sessions.js")>(
"../../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<typeof deliverOutboundPayloads>[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",
},
]);