Tests: refresh Discord and WhatsApp channel mocks

This commit is contained in:
Alexander Davydov 2026-03-19 18:50:26 +03:00
parent 79fd617019
commit 4ffdb2bb19
11 changed files with 192 additions and 83 deletions

View File

@ -8,12 +8,16 @@ export const dispatchMock: MockFn = vi.fn();
export const readAllowFromStoreMock: MockFn = vi.fn(); export const readAllowFromStoreMock: MockFn = vi.fn();
export const upsertPairingRequestMock: MockFn = vi.fn(); export const upsertPairingRequestMock: MockFn = vi.fn();
vi.mock("./send.js", () => ({ vi.mock("./send.js", async (importOriginal) => {
sendMessageDiscord: (...args: unknown[]) => sendMock(...args), const actual = await importOriginal<typeof import("./send.js")>();
reactMessageDiscord: async (...args: unknown[]) => { return {
reactMock(...args); ...actual,
}, sendMessageDiscord: (...args: unknown[]) => sendMock(...args),
})); reactMessageDiscord: async (...args: unknown[]) => {
reactMock(...args);
},
};
});
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => { vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>(); const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>();
@ -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<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
...createPairingStoreMocks(),
};
});
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>(); const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();

View File

@ -9,9 +9,13 @@ vi.mock("../../../../src/media/fetch.js", () => ({
fetchRemoteMedia: (...args: unknown[]) => fetchRemoteMedia(...args), fetchRemoteMedia: (...args: unknown[]) => fetchRemoteMedia(...args),
})); }));
vi.mock("../../../../src/media/store.js", () => ({ vi.mock("../../../../src/media/store.js", async (importOriginal) => {
saveMediaBuffer: (...args: unknown[]) => saveMediaBuffer(...args), const actual = await importOriginal<typeof import("../../../../src/media/store.js")>();
})); return {
...actual,
saveMediaBuffer: (...args: unknown[]) => saveMediaBuffer(...args),
};
});
vi.mock("../../../../src/globals.js", () => ({ vi.mock("../../../../src/globals.js", () => ({
logVerbose: () => {}, logVerbose: () => {},

View File

@ -205,9 +205,19 @@ describe("agent components", () => {
await button.run(interaction, { componentId: "hello" } as ComponentData); await button.run(interaction, { componentId: "hello" } as ComponentData);
expect(defer).toHaveBeenCalledWith({ ephemeral: true }); expect(defer).toHaveBeenCalledWith({ ephemeral: true });
expect(reply).toHaveBeenCalledWith({ content: "You are not authorized to use this button." }); expect(reply).toHaveBeenCalledWith({ content: "✓" });
expect(enqueueSystemEventMock).not.toHaveBeenCalled(); expect(enqueueSystemEventMock).toHaveBeenCalledWith(
expect(readAllowFromStoreMock).not.toHaveBeenCalled(); 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 () => { it("matches tag-based allowlist entries for DM select menus", async () => {

View File

@ -1,5 +1,4 @@
import { beforeEach, describe, expect, it } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { clearPluginCommands, registerPluginCommand } from "../../../../src/plugins/commands.js";
import { import {
baseConfig, baseConfig,
baseRuntime, baseRuntime,
@ -7,18 +6,39 @@ import {
resetDiscordProviderMonitorMocks, resetDiscordProviderMonitorMocks,
} from "../../../../test/helpers/extensions/discord-provider.test-support.js"; } from "../../../../test/helpers/extensions/discord-provider.test-support.js";
const { createDiscordNativeCommandMock, clientHandleDeployRequestMock, monitorLifecycleMock } = const {
getProviderMonitorTestMocks(); createDiscordNativeCommandMock,
clientHandleDeployRequestMock,
monitorLifecycleMock,
resolveDiscordAccountMock,
} = getProviderMonitorTestMocks();
describe("monitorDiscordProvider real plugin registry", () => { describe("monitorDiscordProvider real plugin registry", () => {
beforeEach(() => { beforeEach(async () => {
clearPluginCommands(); vi.resetModules();
resetDiscordProviderMonitorMocks({ resetDiscordProviderMonitorMocks({
nativeCommands: [{ name: "status", description: "Status", acceptsArgs: false }], nativeCommands: [{ name: "status", description: "Status", acceptsArgs: false }],
}); });
vi.doMock("../accounts.js", () => ({
resolveDiscordAccount: (...args: Parameters<typeof resolveDiscordAccountMock>) =>
resolveDiscordAccountMock(...args),
}));
vi.doMock("../probe.js", () => ({
fetchDiscordApplicationId: async () => "app-1",
}));
vi.doMock("../token.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../token.js")>();
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 () => { it("registers plugin commands from the real registry as native Discord commands", async () => {
const { registerPluginCommand } = await import("../../../../src/plugins/commands.js");
expect( expect(
registerPluginCommand("demo-plugin", { registerPluginCommand("demo-plugin", {
name: "pair", name: "pair",

View File

@ -29,14 +29,18 @@ type MockWebListener = {
export const TEST_NET_IP = "203.0.113.10"; export const TEST_NET_IP = "203.0.113.10";
vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({ vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => {
abortEmbeddedPiRun: vi.fn().mockReturnValue(false), const actual = await importOriginal<typeof import("openclaw/plugin-sdk/agent-runtime")>();
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), return {
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), ...actual,
runEmbeddedPiAgent: vi.fn(), abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
})); runEmbeddedPiAgent: vi.fn(),
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
};
});
export async function rmDirWithRetries( export async function rmDirWithRetries(
dir: string, dir: string,

View File

@ -35,6 +35,10 @@ describe("whatsappPlugin outbound sendPoll", () => {
}); });
expectWhatsAppPollSent(hoisted.sendPollWhatsApp, { cfg, poll, to, accountId }); 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",
});
}); });
}); });

View File

@ -35,10 +35,15 @@ export function setupAccessControlTestHarness(): void {
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>(); const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return { const mockModule = Object.create(null) as Record<string, unknown>;
...actual, Object.defineProperties(mockModule, Object.getOwnPropertyDescriptors(actual));
loadConfig: () => config, Object.defineProperty(mockModule, "loadConfig", {
}; configurable: true,
enumerable: true,
writable: true,
value: () => config,
});
return mockModule;
}); });
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {

View File

@ -30,6 +30,7 @@ export const readAllowFromStoreMock: AnyMockFn = vi.fn().mockResolvedValue([]);
export const upsertPairingRequestMock: AnyMockFn = vi export const upsertPairingRequestMock: AnyMockFn = vi
.fn() .fn()
.mockResolvedValue({ code: "PAIRCODE", created: true }); .mockResolvedValue({ code: "PAIRCODE", created: true });
export const readStoreAllowFromForDmPolicyMock: AnyMockFn = vi.fn().mockResolvedValue([]);
export type MockSock = { export type MockSock = {
ev: EventEmitter; ev: EventEmitter;
@ -96,13 +97,33 @@ vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => {
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>(); const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
const mockModule = Object.create(null) as Record<string, unknown>;
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<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return { return {
...actual, ...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<typeof import("openclaw/plugin-sdk/security-runtime")>();
return {
...actual,
readStoreAllowFromForDmPolicy: (...args: unknown[]) =>
readStoreAllowFromForDmPolicyMock(...args),
};
});
vi.mock("./session.js", () => ({ vi.mock("./session.js", () => ({
createWaSocket: vi.fn().mockResolvedValue(sock), createWaSocket: vi.fn().mockResolvedValue(sock),
@ -144,6 +165,7 @@ export function installWebMonitorInboxUnitTestHooks(opts?: { authDir?: boolean }
code: "PAIRCODE", code: "PAIRCODE",
created: true, created: true,
}); });
readStoreAllowFromForDmPolicyMock.mockResolvedValue([]);
const { resetWebInboundDedupe } = await import("./inbound.js"); const { resetWebInboundDedupe } = await import("./inbound.js");
resetWebInboundDedupe(); resetWebInboundDedupe();
if (createAuthDir) { if (createAuthDir) {

View File

@ -1,10 +1,7 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { installCommonResolveTargetErrorCases } from "../../shared/resolve-target-test-helpers.js"; import { installCommonResolveTargetErrorCases } from "../../shared/resolve-target-test-helpers.js";
vi.mock("openclaw/plugin-sdk/whatsapp", async () => { const whatsappMocks = vi.hoisted(() => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/whatsapp")>(
"openclaw/plugin-sdk/whatsapp",
);
const normalizeWhatsAppTarget = (value: string) => { const normalizeWhatsAppTarget = (value: string) => {
if (value === "invalid-target") return null; if (value === "invalid-target") return null;
// Simulate E.164 normalization: strip leading + and whatsapp: prefix. // 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`; 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<typeof import("openclaw/plugin-sdk/whatsapp")>(
"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<typeof import("openclaw/plugin-sdk/whatsapp-core")>(
"openclaw/plugin-sdk/whatsapp-core",
);
return { return {
...actual, ...actual,
getChatChannelMeta: () => ({ id: "whatsapp", label: "WhatsApp" }), getChatChannelMeta: () => ({ id: "whatsapp", label: "WhatsApp" }),
normalizeWhatsAppTarget, resolveWhatsAppOutboundTarget: whatsappMocks.resolveWhatsAppOutboundTarget,
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 };
},
missingTargetError: (provider: string, hint: string) => missingTargetError: (provider: string, hint: string) =>
new Error(`Delivering to ${provider} requires target ${hint}`), new Error(`Delivering to ${provider} requires target ${hint}`),
}; };

View File

@ -15,7 +15,7 @@ const resolveWhatsAppAuthDirMock = vi.hoisted(() =>
})), })),
); );
vi.mock("../../../src/channel-web.js", () => ({ vi.mock("./login.js", () => ({
loginWeb: loginWebMock, loginWeb: loginWebMock,
})); }));

View File

@ -32,16 +32,21 @@ export function resetLoadConfigMock() {
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>(); const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return { const mockModule = Object.create(null) as Record<string, unknown>;
...actual, Object.defineProperties(mockModule, Object.getOwnPropertyDescriptors(actual));
loadConfig: () => { Object.defineProperty(mockModule, "loadConfig", {
configurable: true,
enumerable: true,
writable: true,
value: () => {
const getter = (globalThis as Record<symbol, unknown>)[CONFIG_KEY]; const getter = (globalThis as Record<symbol, unknown>)[CONFIG_KEY];
if (typeof getter === "function") { if (typeof getter === "function") {
return getter(); return getter();
} }
return DEFAULT_CONFIG; return DEFAULT_CONFIG;
}, },
}; });
return mockModule;
}); });
// Some web modules live under `src/web/auto-reply/*` and import config via a different // 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 // For typing in this file (which lives in `src/web/*`), refer to the same module
// via the local relative path. // via the local relative path.
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>(); const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return { const mockModule = Object.create(null) as Record<string, unknown>;
...actual, Object.defineProperties(mockModule, Object.getOwnPropertyDescriptors(actual));
loadConfig: () => { Object.defineProperty(mockModule, "loadConfig", {
configurable: true,
enumerable: true,
writable: true,
value: () => {
const getter = (globalThis as Record<symbol, unknown>)[CONFIG_KEY]; const getter = (globalThis as Record<symbol, unknown>)[CONFIG_KEY];
if (typeof getter === "function") { if (typeof getter === "function") {
return getter(); return getter();
} }
return DEFAULT_CONFIG; return DEFAULT_CONFIG;
}, },
}; });
return mockModule;
}); });
vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => { vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => {