import { vi } from "vitest"; import type { MockBaileysSocket } from "../../../test/mocks/baileys.js"; import { createMockBaileys } from "../../../test/mocks/baileys.js"; // Use globalThis to store the mock config so it survives vi.mock hoisting const CONFIG_KEY = Symbol.for("remoteclaw:testConfigMock"); const DEFAULT_CONFIG = { // Sole-agent fixture so web tests that predate #2309's silent-drop policy // continue to exercise their inbound paths. The web adapter still uses the // backward-compat resolveAgentRoute (tagged matchedBy=fallback.legacyRoute // on silent-drop fallback), but having at least one configured agent keeps // the fallback branch from throwing on zero-agents configs. agents: { list: [{ id: "main" }] }, channels: { whatsapp: { // Tests can override; default remains open to avoid surprising fixtures allowFrom: ["*"], }, }, messages: { messagePrefix: undefined, responsePrefix: undefined, }, }; // Initialize default if not set if (!(globalThis as Record)[CONFIG_KEY]) { (globalThis as Record)[CONFIG_KEY] = () => DEFAULT_CONFIG; } export function setLoadConfigMock(fn: unknown) { const raw = typeof fn === "function" ? (fn as () => unknown) : () => fn; (globalThis as Record)[CONFIG_KEY] = () => applyRoutingDefaults(raw()); } export function resetLoadConfigMock() { (globalThis as Record)[CONFIG_KEY] = () => DEFAULT_CONFIG; } function applyRoutingDefaults(cfg: unknown): unknown { // Inject a sole-agent fixture when tests provide a cfg without agents.list. // The web adapter uses backward-compat resolveAgentRoute which fires silent- // drop telemetry then falls back to the first configured agent. Without at // least one agent in the list, the fallback branch throws. if (cfg && typeof cfg === "object") { const typed = cfg as { agents?: { list?: unknown } }; if (!typed.agents || !Array.isArray(typed.agents.list) || typed.agents.list.length === 0) { return { ...typed, agents: { list: [{ id: "main" }] } }; } } return cfg; } vi.mock("../../../src/config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadConfig: () => { const getter = (globalThis as Record)[CONFIG_KEY]; if (typeof getter === "function") { return applyRoutingDefaults(getter()); } return DEFAULT_CONFIG; }, }; }); // Some web modules live under `extensions/whatsapp/src/auto-reply/*` and import config // via a different relative path (`../../../../src/config/config.js`). Mock both specifiers // so tests stay stable across refactors that move files between folders. vi.mock("../../../config/config.js", async (importOriginal) => { // `../../../../src/config/config.js` is correct for modules under // `extensions/whatsapp/src/auto-reply/*`. For typing in this file (which lives in // `extensions/whatsapp/src/*`), refer to the same module via the local relative path. const actual = await importOriginal(); return { ...actual, loadConfig: () => { const getter = (globalThis as Record)[CONFIG_KEY]; if (typeof getter === "function") { return applyRoutingDefaults(getter()); } return DEFAULT_CONFIG; }, }; }); vi.mock("../../../src/media/store.js", async (importOriginal) => { const actual = await importOriginal(); const mockModule = Object.create(null) as Record; Object.defineProperties(mockModule, Object.getOwnPropertyDescriptors(actual)); Object.defineProperty(mockModule, "saveMediaBuffer", { configurable: true, enumerable: true, writable: true, value: vi.fn().mockImplementation(async (_buf: Buffer, contentType?: string) => ({ id: "mid", path: "/tmp/mid", size: _buf.length, contentType, })), }); return mockModule; }); vi.mock("@whiskeysockets/baileys", () => { const created = createMockBaileys(); (globalThis as Record)[Symbol.for("remoteclaw:lastSocket")] = created.lastSocket; return created.mod; }); vi.mock("qrcode-terminal", () => ({ default: { generate: vi.fn() }, generate: vi.fn(), })); export const baileys = await import("@whiskeysockets/baileys"); export function resetBaileysMocks() { const recreated = createMockBaileys(); (globalThis as Record)[Symbol.for("remoteclaw:lastSocket")] = recreated.lastSocket; const makeWASocket = vi.mocked(baileys.makeWASocket); const makeWASocketImpl: typeof baileys.makeWASocket = (...args) => (recreated.mod.makeWASocket as unknown as typeof baileys.makeWASocket)(...args); makeWASocket.mockReset(); makeWASocket.mockImplementation(makeWASocketImpl); const useMultiFileAuthState = vi.mocked(baileys.useMultiFileAuthState); const useMultiFileAuthStateImpl: typeof baileys.useMultiFileAuthState = (...args) => (recreated.mod.useMultiFileAuthState as unknown as typeof baileys.useMultiFileAuthState)( ...args, ); useMultiFileAuthState.mockReset(); useMultiFileAuthState.mockImplementation(useMultiFileAuthStateImpl); const fetchLatestBaileysVersion = vi.mocked(baileys.fetchLatestBaileysVersion); const fetchLatestBaileysVersionImpl: typeof baileys.fetchLatestBaileysVersion = (...args) => ( recreated.mod.fetchLatestBaileysVersion as unknown as typeof baileys.fetchLatestBaileysVersion )(...args); fetchLatestBaileysVersion.mockReset(); fetchLatestBaileysVersion.mockImplementation(fetchLatestBaileysVersionImpl); const makeCacheableSignalKeyStore = vi.mocked(baileys.makeCacheableSignalKeyStore); const makeCacheableSignalKeyStoreImpl: typeof baileys.makeCacheableSignalKeyStore = (...args) => ( recreated.mod .makeCacheableSignalKeyStore as unknown as typeof baileys.makeCacheableSignalKeyStore )(...args); makeCacheableSignalKeyStore.mockReset(); makeCacheableSignalKeyStore.mockImplementation(makeCacheableSignalKeyStoreImpl); } export function getLastSocket(): MockBaileysSocket { const getter = (globalThis as Record)[Symbol.for("remoteclaw:lastSocket")]; if (typeof getter === "function") { return (getter as () => MockBaileysSocket)(); } if (!getter) { throw new Error("Baileys mock not initialized"); } throw new Error("Invalid Baileys socket getter"); }