WhatsApp: stabilize inbound monitor and setup tests (#50007)

This commit is contained in:
Josh Avant 2026-03-18 17:08:57 -05:00 committed by GitHub
parent 91d37ccfc3
commit 859889aae9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 114 additions and 49 deletions

View File

@ -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

View File

@ -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<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
};
});
vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/security-runtime")>();
return {
...actual,
readStoreAllowFromForDmPolicy: async (
params: Parameters<typeof actual.readStoreAllowFromForDmPolicy>[0],
) =>
await actual.readStoreAllowFromForDmPolicy({
...params,
readStore: async (provider, accountId) =>
(await readAllowFromStoreMock(provider, accountId)) as string[],
}),
};
});

View File

@ -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<typeof vi.fn>, 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();

View File

@ -12,8 +12,17 @@ describe("append upsert handling (#20952)", () => {
installWebMonitorInboxUnitTestHooks();
type InboxOnMessage = NonNullable<Parameters<typeof monitorWebInbox>[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<typeof vi.fn>, 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);

View File

@ -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<string, unknown>) => ({

View File

@ -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 };
}

View File

@ -14,8 +14,13 @@ describe("web monitor inbox", () => {
installWebMonitorInboxUnitTestHooks();
type InboxOnMessage = NonNullable<Parameters<typeof monitorWebInbox>[0]["onMessage"]>;
async function tick() {
await new Promise((resolve) => setImmediate(resolve));
async function waitForMessageCalls(onMessage: ReturnType<typeof vi.fn>, 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);

View File

@ -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<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
};
});
vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/security-runtime")>();
return {
...actual,
readStoreAllowFromForDmPolicy: async (
params: Parameters<typeof actual.readStoreAllowFromForDmPolicy>[0],
) =>
await actual.readStoreAllowFromForDmPolicy({
...params,
readStore: async (provider, accountId) =>
(await readAllowFromStoreMock(provider, accountId)) as string[],
}),
};
});
vi.mock("./session.js", () => ({
createWaSocket: vi.fn().mockResolvedValue(sock),

View File

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