diff --git a/CHANGELOG.md b/CHANGELOG.md index 3828916b1c9..a23d025fd8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -151,6 +151,7 @@ Docs: https://docs.openclaw.ai - xAI/web search: add missing Grok credential metadata so the bundled provider registration type-checks again. (#49472) thanks @scoootscooob. - Signal/runtime API: re-export `SignalAccountConfig` so Signal account resolution type-checks again. (#49470) Thanks @scoootscooob. - Google Chat/runtime API: thin the private runtime barrel onto the curated public SDK surface while keeping public Google Chat exports intact. (#49504) Thanks @scoootscooob. +- WhatsApp: stabilize inbound monitor and setup tests (#50007) Thanks @joshavant. ### Breaking diff --git a/extensions/whatsapp/src/inbound/access-control.test-harness.ts b/extensions/whatsapp/src/inbound/access-control.test-harness.ts index 495615a3cbb..5bff5f06ff5 100644 --- a/extensions/whatsapp/src/inbound/access-control.test-harness.ts +++ b/extensions/whatsapp/src/inbound/access-control.test-harness.ts @@ -41,7 +41,25 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { }; }); -vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({ - readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args), - upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), -})); +vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), + }; +}); + +vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readStoreAllowFromForDmPolicy: async ( + params: Parameters[0], + ) => + await actual.readStoreAllowFromForDmPolicy({ + ...params, + readStore: async (provider, accountId) => + (await readAllowFromStoreMock(provider, accountId)) as string[], + }), + }; +}); diff --git a/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts b/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts index 101357a9de6..cefe06a19ee 100644 --- a/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts +++ b/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts @@ -38,6 +38,19 @@ async function openInboxMonitor(onMessage = vi.fn()) { return { onMessage, listener, sock: getSock() }; } +async function settleInboundWork() { + await new Promise((resolve) => setTimeout(resolve, 25)); +} + +async function waitForMessageCalls(onMessage: ReturnType, count: number) { + await vi.waitFor( + () => { + expect(onMessage).toHaveBeenCalledTimes(count); + }, + { timeout: 2_000, interval: 5 }, + ); +} + async function expectOutboundDmSkipsPairing(params: { selfChatMode: boolean; messageId: string; @@ -77,7 +90,7 @@ async function expectOutboundDmSkipsPairing(params: { }, ], }); - await new Promise((resolve) => setImmediate(resolve)); + await settleInboundWork(); expect(onMessage).not.toHaveBeenCalled(); expect(upsertPairingRequestMock).not.toHaveBeenCalled(); @@ -111,7 +124,7 @@ describe("web monitor inbox", () => { }; sock.ev.emit("messages.upsert", upsert); - await new Promise((resolve) => setImmediate(resolve)); + await waitForMessageCalls(onMessage, 1); // Should call onMessage for authorized senders expect(onMessage).toHaveBeenCalledWith( @@ -145,7 +158,7 @@ describe("web monitor inbox", () => { }; sock.ev.emit("messages.upsert", upsert); - await new Promise((resolve) => setImmediate(resolve)); + await waitForMessageCalls(onMessage, 1); // Should allow self-messages even if not in allowFrom expect(onMessage).toHaveBeenCalledWith( @@ -181,7 +194,12 @@ describe("web monitor inbox", () => { }; sock.ev.emit("messages.upsert", upsertBlocked); - await new Promise((resolve) => setImmediate(resolve)); + await vi.waitFor( + () => { + expect(sock.sendMessage).toHaveBeenCalledTimes(1); + }, + { timeout: 2_000, interval: 5 }, + ); expect(onMessage).not.toHaveBeenCalled(); expectPairingPromptSent(sock, "999@s.whatsapp.net", "+999"); @@ -201,7 +219,7 @@ describe("web monitor inbox", () => { }; sock.ev.emit("messages.upsert", upsertBlockedAgain); - await new Promise((resolve) => setImmediate(resolve)); + await settleInboundWork(); expect(onMessage).not.toHaveBeenCalled(); expect(sock.sendMessage).toHaveBeenCalledTimes(1); @@ -222,7 +240,7 @@ describe("web monitor inbox", () => { }; sock.ev.emit("messages.upsert", upsertSelf); - await new Promise((resolve) => setImmediate(resolve)); + await waitForMessageCalls(onMessage, 1); expect(onMessage).toHaveBeenCalledTimes(1); expect(onMessage).toHaveBeenCalledWith( @@ -273,17 +291,19 @@ describe("web monitor inbox", () => { }; sock.ev.emit("messages.upsert", upsert); - await new Promise((resolve) => setImmediate(resolve)); - - // Verify it WAS marked as read - expect(sock.readMessages).toHaveBeenCalledWith([ - { - remoteJid: "999@s.whatsapp.net", - id: "history1", - participant: undefined, - fromMe: false, + await vi.waitFor( + () => { + expect(sock.readMessages).toHaveBeenCalledWith([ + { + remoteJid: "999@s.whatsapp.net", + id: "history1", + participant: undefined, + fromMe: false, + }, + ]); }, - ]); + { timeout: 2_000, interval: 5 }, + ); // Verify it WAS NOT passed to onMessage expect(onMessage).not.toHaveBeenCalled(); diff --git a/extensions/whatsapp/src/monitor-inbox.append-upsert.test.ts b/extensions/whatsapp/src/monitor-inbox.append-upsert.test.ts index e5746455432..1ccdd3e77b2 100644 --- a/extensions/whatsapp/src/monitor-inbox.append-upsert.test.ts +++ b/extensions/whatsapp/src/monitor-inbox.append-upsert.test.ts @@ -12,8 +12,17 @@ describe("append upsert handling (#20952)", () => { installWebMonitorInboxUnitTestHooks(); type InboxOnMessage = NonNullable[0]["onMessage"]>; - async function tick() { - await new Promise((resolve) => setImmediate(resolve)); + async function settleInboundWork() { + await new Promise((resolve) => setTimeout(resolve, 25)); + } + + async function waitForMessageCalls(onMessage: ReturnType, count: number) { + await vi.waitFor( + () => { + expect(onMessage).toHaveBeenCalledTimes(count); + }, + { timeout: 2_000, interval: 5 }, + ); } async function startInboxMonitor(onMessage: InboxOnMessage) { @@ -43,7 +52,7 @@ describe("append upsert handling (#20952)", () => { }, ], }); - await tick(); + await waitForMessageCalls(onMessage, 1); expect(onMessage).toHaveBeenCalledTimes(1); @@ -67,7 +76,7 @@ describe("append upsert handling (#20952)", () => { }, ], }); - await tick(); + await settleInboundWork(); expect(onMessage).not.toHaveBeenCalled(); @@ -90,7 +99,7 @@ describe("append upsert handling (#20952)", () => { }, ], }); - await tick(); + await settleInboundWork(); expect(onMessage).not.toHaveBeenCalled(); @@ -116,7 +125,7 @@ describe("append upsert handling (#20952)", () => { }, ], }); - await tick(); + await waitForMessageCalls(onMessage, 1); expect(onMessage).toHaveBeenCalledTimes(1); @@ -140,7 +149,7 @@ describe("append upsert handling (#20952)", () => { }, ], }); - await tick(); + await waitForMessageCalls(onMessage, 1); expect(onMessage).toHaveBeenCalledTimes(1); diff --git a/extensions/whatsapp/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test.ts b/extensions/whatsapp/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test.ts index 586df46a527..b995b5543d5 100644 --- a/extensions/whatsapp/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test.ts +++ b/extensions/whatsapp/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test.ts @@ -21,7 +21,7 @@ const TIMESTAMP_OFF_MESSAGES_CFG = { } as const; async function flushInboundQueue() { - await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setTimeout(resolve, 25)); } const createNotifyUpsert = (message: Record) => ({ diff --git a/extensions/whatsapp/src/monitor-inbox.captures-media-path-image-messages.test.ts b/extensions/whatsapp/src/monitor-inbox.captures-media-path-image-messages.test.ts index d9d9593c49b..54a00c167d3 100644 --- a/extensions/whatsapp/src/monitor-inbox.captures-media-path-image-messages.test.ts +++ b/extensions/whatsapp/src/monitor-inbox.captures-media-path-image-messages.test.ts @@ -31,7 +31,7 @@ describe("web monitor inbox", () => { const listener = await openMonitor(onMessage); const sock = getSock(); sock.ev.emit("messages.upsert", upsert); - await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setTimeout(resolve, 25)); return { onMessage, listener, sock }; } diff --git a/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test.ts b/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test.ts index 7e8b5c26887..9274abd0135 100644 --- a/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test.ts +++ b/extensions/whatsapp/src/monitor-inbox.streams-inbound-messages.test.ts @@ -14,8 +14,13 @@ describe("web monitor inbox", () => { installWebMonitorInboxUnitTestHooks(); type InboxOnMessage = NonNullable[0]["onMessage"]>; - async function tick() { - await new Promise((resolve) => setImmediate(resolve)); + async function waitForMessageCalls(onMessage: ReturnType, count: number) { + await vi.waitFor( + () => { + expect(onMessage).toHaveBeenCalledTimes(count); + }, + { timeout: 2_000, interval: 5 }, + ); } async function startInboxMonitor(onMessage: InboxOnMessage) { @@ -82,7 +87,7 @@ describe("web monitor inbox", () => { }; sock.ev.emit("messages.upsert", upsert); - await tick(); + await waitForMessageCalls(onMessage, 1); expect(onMessage).toHaveBeenCalledWith( expect.objectContaining({ @@ -115,7 +120,7 @@ describe("web monitor inbox", () => { }); sock.ev.emit("messages.upsert", upsert); - await tick(); + await waitForMessageCalls(onMessage, 1); expect(onMessage).toHaveBeenCalledWith( expect.objectContaining({ body: "ping", from: "+999", to: "+123" }), @@ -153,7 +158,7 @@ describe("web monitor inbox", () => { sock.ev.emit("messages.upsert", upsert); sock.ev.emit("messages.upsert", upsert); - await tick(); + await waitForMessageCalls(onMessage, 1); expect(onMessage).toHaveBeenCalledTimes(1); @@ -177,7 +182,7 @@ describe("web monitor inbox", () => { }); sock.ev.emit("messages.upsert", upsert); - await tick(); + await waitForMessageCalls(onMessage, 1); expect(getPNForLID).toHaveBeenCalledWith("999@lid"); expect(onMessage).toHaveBeenCalledWith( @@ -207,7 +212,7 @@ describe("web monitor inbox", () => { }); sock.ev.emit("messages.upsert", upsert); - await tick(); + await waitForMessageCalls(onMessage, 1); expect(onMessage).toHaveBeenCalledWith( expect.objectContaining({ body: "ping", from: "+1555", to: "+123" }), @@ -234,7 +239,7 @@ describe("web monitor inbox", () => { }); sock.ev.emit("messages.upsert", upsert); - await tick(); + await waitForMessageCalls(onMessage, 1); expect(getPNForLID).toHaveBeenCalledWith("444@lid"); expect(onMessage).toHaveBeenCalledWith( @@ -277,7 +282,7 @@ describe("web monitor inbox", () => { }; sock.ev.emit("messages.upsert", upsert); - await tick(); + await waitForMessageCalls(onMessage, 2); expect(onMessage).toHaveBeenCalledTimes(2); diff --git a/extensions/whatsapp/src/monitor-inbox.test-harness.ts b/extensions/whatsapp/src/monitor-inbox.test-harness.ts index 3aefaf7a4f1..719602b57eb 100644 --- a/extensions/whatsapp/src/monitor-inbox.test-harness.ts +++ b/extensions/whatsapp/src/monitor-inbox.test-harness.ts @@ -70,15 +70,6 @@ function createMockSock(): MockSock { }; } -function getPairingStoreMocks() { - const readChannelAllowFromStore = (...args: unknown[]) => readAllowFromStoreMock(...args); - const upsertChannelPairingRequest = (...args: unknown[]) => upsertPairingRequestMock(...args); - return { - readChannelAllowFromStore, - upsertChannelPairingRequest, - }; -} - const sock: MockSock = createMockSock(); vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => { @@ -102,7 +93,28 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => { }; }); -vi.mock("openclaw/plugin-sdk/conversation-runtime", () => getPairingStoreMocks()); +vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), + }; +}); + +vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readStoreAllowFromForDmPolicy: async ( + params: Parameters[0], + ) => + await actual.readStoreAllowFromForDmPolicy({ + ...params, + readStore: async (provider, accountId) => + (await readAllowFromStoreMock(provider, accountId)) as string[], + }), + }; +}); vi.mock("./session.js", () => ({ createWaSocket: vi.fn().mockResolvedValue(sock), 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, }));