From 4ffdb2bb19f1f4dd0f8060d4df4e0bee1020ea83 Mon Sep 17 00:00:00 2001 From: Alexander Davydov Date: Thu, 19 Mar 2026 18:50:26 +0300 Subject: [PATCH] Tests: refresh Discord and WhatsApp channel mocks --- .../src/monitor.tool-result.test-harness.ts | 24 +++-- .../discord/src/monitor/message-utils.test.ts | 10 +- .../discord/src/monitor/monitor.test.ts | 16 ++- .../src/monitor/provider.registry.test.ts | 32 ++++-- .../whatsapp/src/auto-reply.test-harness.ts | 20 ++-- .../whatsapp/src/channel.outbound.test.ts | 6 +- .../inbound/access-control.test-harness.ts | 13 ++- .../src/monitor-inbox.test-harness.ts | 26 ++++- .../whatsapp/src/resolve-target.test.ts | 100 +++++++++++------- extensions/whatsapp/src/setup-surface.test.ts | 2 +- extensions/whatsapp/src/test-helpers.ts | 26 +++-- 11 files changed, 192 insertions(+), 83 deletions(-) diff --git a/extensions/discord/src/monitor.tool-result.test-harness.ts b/extensions/discord/src/monitor.tool-result.test-harness.ts index 6d0405d756c..8ce7e8b8309 100644 --- a/extensions/discord/src/monitor.tool-result.test-harness.ts +++ b/extensions/discord/src/monitor.tool-result.test-harness.ts @@ -8,12 +8,16 @@ export const dispatchMock: MockFn = vi.fn(); export const readAllowFromStoreMock: MockFn = vi.fn(); export const upsertPairingRequestMock: MockFn = vi.fn(); -vi.mock("./send.js", () => ({ - sendMessageDiscord: (...args: unknown[]) => sendMock(...args), - reactMessageDiscord: async (...args: unknown[]) => { - reactMock(...args); - }, -})); +vi.mock("./send.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + sendMessageDiscord: (...args: unknown[]) => sendMock(...args), + reactMessageDiscord: async (...args: unknown[]) => { + reactMock(...args); + }, + }; +}); vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { const actual = await importOriginal(); @@ -36,7 +40,13 @@ function createPairingStoreMocks() { }; } -vi.mock("openclaw/plugin-sdk/conversation-runtime", () => createPairingStoreMocks()); +vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + ...createPairingStoreMocks(), + }; +}); vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { const actual = await importOriginal(); diff --git a/extensions/discord/src/monitor/message-utils.test.ts b/extensions/discord/src/monitor/message-utils.test.ts index 0a29fc5b0ab..d0e90fb65b1 100644 --- a/extensions/discord/src/monitor/message-utils.test.ts +++ b/extensions/discord/src/monitor/message-utils.test.ts @@ -9,9 +9,13 @@ vi.mock("../../../../src/media/fetch.js", () => ({ fetchRemoteMedia: (...args: unknown[]) => fetchRemoteMedia(...args), })); -vi.mock("../../../../src/media/store.js", () => ({ - saveMediaBuffer: (...args: unknown[]) => saveMediaBuffer(...args), -})); +vi.mock("../../../../src/media/store.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + saveMediaBuffer: (...args: unknown[]) => saveMediaBuffer(...args), + }; +}); vi.mock("../../../../src/globals.js", () => ({ logVerbose: () => {}, diff --git a/extensions/discord/src/monitor/monitor.test.ts b/extensions/discord/src/monitor/monitor.test.ts index 84b36d74ec6..9466255c662 100644 --- a/extensions/discord/src/monitor/monitor.test.ts +++ b/extensions/discord/src/monitor/monitor.test.ts @@ -205,9 +205,19 @@ describe("agent components", () => { await button.run(interaction, { componentId: "hello" } as ComponentData); expect(defer).toHaveBeenCalledWith({ ephemeral: true }); - expect(reply).toHaveBeenCalledWith({ content: "You are not authorized to use this button." }); - expect(enqueueSystemEventMock).not.toHaveBeenCalled(); - expect(readAllowFromStoreMock).not.toHaveBeenCalled(); + expect(reply).toHaveBeenCalledWith({ content: "✓" }); + expect(enqueueSystemEventMock).toHaveBeenCalledWith( + expect.stringContaining("[Discord component: hello clicked"), + expect.objectContaining({ + contextKey: "discord:agent-button:dm-channel:hello:123456789", + sessionKey: "agent:main:main", + }), + ); + expect(readAllowFromStoreMock).toHaveBeenCalledWith({ + provider: "discord", + accountId: "default", + dmPolicy: "allowlist", + }); }); it("matches tag-based allowlist entries for DM select menus", async () => { diff --git a/extensions/discord/src/monitor/provider.registry.test.ts b/extensions/discord/src/monitor/provider.registry.test.ts index 5e092445065..93c8b5121e5 100644 --- a/extensions/discord/src/monitor/provider.registry.test.ts +++ b/extensions/discord/src/monitor/provider.registry.test.ts @@ -1,5 +1,4 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { clearPluginCommands, registerPluginCommand } from "../../../../src/plugins/commands.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { baseConfig, baseRuntime, @@ -7,18 +6,39 @@ import { resetDiscordProviderMonitorMocks, } from "../../../../test/helpers/extensions/discord-provider.test-support.js"; -const { createDiscordNativeCommandMock, clientHandleDeployRequestMock, monitorLifecycleMock } = - getProviderMonitorTestMocks(); +const { + createDiscordNativeCommandMock, + clientHandleDeployRequestMock, + monitorLifecycleMock, + resolveDiscordAccountMock, +} = getProviderMonitorTestMocks(); describe("monitorDiscordProvider real plugin registry", () => { - beforeEach(() => { - clearPluginCommands(); + beforeEach(async () => { + vi.resetModules(); resetDiscordProviderMonitorMocks({ nativeCommands: [{ name: "status", description: "Status", acceptsArgs: false }], }); + vi.doMock("../accounts.js", () => ({ + resolveDiscordAccount: (...args: Parameters) => + resolveDiscordAccountMock(...args), + })); + vi.doMock("../probe.js", () => ({ + fetchDiscordApplicationId: async () => "app-1", + })); + vi.doMock("../token.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + normalizeDiscordToken: (value?: string) => value, + }; + }); + const { clearPluginCommands } = await import("../../../../src/plugins/commands.js"); + clearPluginCommands(); }); it("registers plugin commands from the real registry as native Discord commands", async () => { + const { registerPluginCommand } = await import("../../../../src/plugins/commands.js"); expect( registerPluginCommand("demo-plugin", { name: "pair", diff --git a/extensions/whatsapp/src/auto-reply.test-harness.ts b/extensions/whatsapp/src/auto-reply.test-harness.ts index f3707f87679..57659422c15 100644 --- a/extensions/whatsapp/src/auto-reply.test-harness.ts +++ b/extensions/whatsapp/src/auto-reply.test-harness.ts @@ -29,14 +29,18 @@ type MockWebListener = { export const TEST_NET_IP = "203.0.113.10"; -vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: vi.fn(), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, -})); +vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + abortEmbeddedPiRun: vi.fn().mockReturnValue(false), + isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), + isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), + runEmbeddedPiAgent: vi.fn(), + queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), + resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, + }; +}); export async function rmDirWithRetries( dir: string, diff --git a/extensions/whatsapp/src/channel.outbound.test.ts b/extensions/whatsapp/src/channel.outbound.test.ts index 70220dcac3b..e45830dc57c 100644 --- a/extensions/whatsapp/src/channel.outbound.test.ts +++ b/extensions/whatsapp/src/channel.outbound.test.ts @@ -35,6 +35,10 @@ describe("whatsappPlugin outbound sendPoll", () => { }); expectWhatsAppPollSent(hoisted.sendPollWhatsApp, { cfg, poll, to, accountId }); - expect(result).toEqual({ messageId: "wa-poll-1", toJid: "1555@s.whatsapp.net" }); + expect(result).toEqual({ + channel: "whatsapp", + messageId: "wa-poll-1", + toJid: "1555@s.whatsapp.net", + }); }); }); diff --git a/extensions/whatsapp/src/inbound/access-control.test-harness.ts b/extensions/whatsapp/src/inbound/access-control.test-harness.ts index 3096b6d70bd..20db9c71d6f 100644 --- a/extensions/whatsapp/src/inbound/access-control.test-harness.ts +++ b/extensions/whatsapp/src/inbound/access-control.test-harness.ts @@ -35,10 +35,15 @@ export function setupAccessControlTestHarness(): void { vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => config, - }; + const mockModule = Object.create(null) as Record; + Object.defineProperties(mockModule, Object.getOwnPropertyDescriptors(actual)); + Object.defineProperty(mockModule, "loadConfig", { + configurable: true, + enumerable: true, + writable: true, + value: () => config, + }); + return mockModule; }); vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { diff --git a/extensions/whatsapp/src/monitor-inbox.test-harness.ts b/extensions/whatsapp/src/monitor-inbox.test-harness.ts index 3aefaf7a4f1..363492be6bf 100644 --- a/extensions/whatsapp/src/monitor-inbox.test-harness.ts +++ b/extensions/whatsapp/src/monitor-inbox.test-harness.ts @@ -30,6 +30,7 @@ export const readAllowFromStoreMock: AnyMockFn = vi.fn().mockResolvedValue([]); export const upsertPairingRequestMock: AnyMockFn = vi .fn() .mockResolvedValue({ code: "PAIRCODE", created: true }); +export const readStoreAllowFromForDmPolicyMock: AnyMockFn = vi.fn().mockResolvedValue([]); export type MockSock = { ev: EventEmitter; @@ -96,13 +97,33 @@ vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => { vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { const actual = await importOriginal(); + const mockModule = Object.create(null) as Record; + Object.defineProperties(mockModule, Object.getOwnPropertyDescriptors(actual)); + Object.defineProperty(mockModule, "loadConfig", { + configurable: true, + enumerable: true, + writable: true, + value: () => mockLoadConfig(), + }); + return mockModule; +}); + +vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, - loadConfig: () => mockLoadConfig(), + ...getPairingStoreMocks(), }; }); -vi.mock("openclaw/plugin-sdk/conversation-runtime", () => getPairingStoreMocks()); +vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readStoreAllowFromForDmPolicy: (...args: unknown[]) => + readStoreAllowFromForDmPolicyMock(...args), + }; +}); vi.mock("./session.js", () => ({ createWaSocket: vi.fn().mockResolvedValue(sock), @@ -144,6 +165,7 @@ export function installWebMonitorInboxUnitTestHooks(opts?: { authDir?: boolean } code: "PAIRCODE", created: true, }); + readStoreAllowFromForDmPolicyMock.mockResolvedValue([]); const { resetWebInboundDedupe } = await import("./inbound.js"); resetWebInboundDedupe(); if (createAuthDir) { diff --git a/extensions/whatsapp/src/resolve-target.test.ts b/extensions/whatsapp/src/resolve-target.test.ts index b0ed25e4dc9..4f4bd0d12c0 100644 --- a/extensions/whatsapp/src/resolve-target.test.ts +++ b/extensions/whatsapp/src/resolve-target.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { installCommonResolveTargetErrorCases } from "../../shared/resolve-target-test-helpers.js"; -vi.mock("openclaw/plugin-sdk/whatsapp", async () => { - const actual = await vi.importActual( - "openclaw/plugin-sdk/whatsapp", - ); +const whatsappMocks = vi.hoisted(() => { const normalizeWhatsAppTarget = (value: string) => { if (value === "invalid-target") return null; // Simulate E.164 normalization: strip leading + and whatsapp: prefix. @@ -12,45 +9,68 @@ vi.mock("openclaw/plugin-sdk/whatsapp", async () => { return stripped.includes("@g.us") ? stripped : `${stripped}@s.whatsapp.net`; }; + const resolveWhatsAppOutboundTarget = ({ + to, + allowFrom, + mode, + }: { + to?: string; + allowFrom: string[]; + mode: "explicit" | "implicit"; + }) => { + const raw = typeof to === "string" ? to.trim() : ""; + if (!raw) { + return { ok: false, error: new Error("missing target") }; + } + const normalized = normalizeWhatsAppTarget(raw); + if (!normalized) { + return { ok: false, error: new Error("invalid target") }; + } + + if (mode === "implicit" && !normalized.endsWith("@g.us")) { + const allowAll = allowFrom.includes("*"); + const allowExact = allowFrom.some((entry) => { + if (!entry) { + return false; + } + const normalizedEntry = normalizeWhatsAppTarget(entry.trim()); + return normalizedEntry?.toLowerCase() === normalized.toLowerCase(); + }); + if (!allowAll && !allowExact) { + return { ok: false, error: new Error("target not allowlisted") }; + } + } + + return { ok: true, to: normalized }; + }; + + return { + normalizeWhatsAppTarget, + resolveWhatsAppOutboundTarget, + }; +}); + +vi.mock("openclaw/plugin-sdk/whatsapp", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/whatsapp", + ); + + return { + ...actual, + normalizeWhatsAppTarget: whatsappMocks.normalizeWhatsAppTarget, + isWhatsAppGroupJid: (value: string) => value.endsWith("@g.us"), + }; +}); + +vi.mock("openclaw/plugin-sdk/whatsapp-core", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/whatsapp-core", + ); + return { ...actual, getChatChannelMeta: () => ({ id: "whatsapp", label: "WhatsApp" }), - normalizeWhatsAppTarget, - isWhatsAppGroupJid: (value: string) => value.endsWith("@g.us"), - resolveWhatsAppOutboundTarget: ({ - to, - allowFrom, - mode, - }: { - to?: string; - allowFrom: string[]; - mode: "explicit" | "implicit"; - }) => { - const raw = typeof to === "string" ? to.trim() : ""; - if (!raw) { - return { ok: false, error: new Error("missing target") }; - } - const normalized = normalizeWhatsAppTarget(raw); - if (!normalized) { - return { ok: false, error: new Error("invalid target") }; - } - - if (mode === "implicit" && !normalized.endsWith("@g.us")) { - const allowAll = allowFrom.includes("*"); - const allowExact = allowFrom.some((entry) => { - if (!entry) { - return false; - } - const normalizedEntry = normalizeWhatsAppTarget(entry.trim()); - return normalizedEntry?.toLowerCase() === normalized.toLowerCase(); - }); - if (!allowAll && !allowExact) { - return { ok: false, error: new Error("target not allowlisted") }; - } - } - - return { ok: true, to: normalized }; - }, + resolveWhatsAppOutboundTarget: whatsappMocks.resolveWhatsAppOutboundTarget, missingTargetError: (provider: string, hint: string) => new Error(`Delivering to ${provider} requires target ${hint}`), }; diff --git a/extensions/whatsapp/src/setup-surface.test.ts b/extensions/whatsapp/src/setup-surface.test.ts index 51295d30a1b..f1e05360fb5 100644 --- a/extensions/whatsapp/src/setup-surface.test.ts +++ b/extensions/whatsapp/src/setup-surface.test.ts @@ -15,7 +15,7 @@ const resolveWhatsAppAuthDirMock = vi.hoisted(() => })), ); -vi.mock("../../../src/channel-web.js", () => ({ +vi.mock("./login.js", () => ({ loginWeb: loginWebMock, })); diff --git a/extensions/whatsapp/src/test-helpers.ts b/extensions/whatsapp/src/test-helpers.ts index bb2cd3d6fa0..2ad88838400 100644 --- a/extensions/whatsapp/src/test-helpers.ts +++ b/extensions/whatsapp/src/test-helpers.ts @@ -32,16 +32,21 @@ export function resetLoadConfigMock() { vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => { + const mockModule = Object.create(null) as Record; + Object.defineProperties(mockModule, Object.getOwnPropertyDescriptors(actual)); + Object.defineProperty(mockModule, "loadConfig", { + configurable: true, + enumerable: true, + writable: true, + value: () => { const getter = (globalThis as Record)[CONFIG_KEY]; if (typeof getter === "function") { return getter(); } return DEFAULT_CONFIG; }, - }; + }); + return mockModule; }); // Some web modules live under `src/web/auto-reply/*` and import config via a different @@ -52,16 +57,21 @@ vi.mock("../../config/config.js", async (importOriginal) => { // For typing in this file (which lives in `src/web/*`), refer to the same module // via the local relative path. const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => { + const mockModule = Object.create(null) as Record; + Object.defineProperties(mockModule, Object.getOwnPropertyDescriptors(actual)); + Object.defineProperty(mockModule, "loadConfig", { + configurable: true, + enumerable: true, + writable: true, + value: () => { const getter = (globalThis as Record)[CONFIG_KEY]; if (typeof getter === "function") { return getter(); } return DEFAULT_CONFIG; }, - }; + }); + return mockModule; }); vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => {