diff --git a/extensions/whatsapp/src/monitor-inbox.test-harness.ts b/extensions/whatsapp/src/monitor-inbox.test-harness.ts index 11733a5b54f1..5af334a8ed66 100644 --- a/extensions/whatsapp/src/monitor-inbox.test-harness.ts +++ b/extensions/whatsapp/src/monitor-inbox.test-harness.ts @@ -257,25 +257,6 @@ export type InboxMonitorOptions = Parameters[0]; let monitorWebInbox: MonitorWebInbox; let resetWebInboundDedupe: ResetWebInboundDedupe; -function expectInboxPairingReplyText( - text: string, - params: { - channel: string; - idLine: string; - code?: string; - }, -): string { - const code = text.match(/Pairing code:\s*```[\r\n]+([A-Z2-9]{6,})/)?.[1]; - expect(code).toBeDefined(); - const resolvedCode = params.code ?? code ?? ""; - expect(text).toContain("OpenClaw: access not configured."); - expect(text).toContain(params.idLine); - expect(text).toContain("Pairing code:"); - expect(text).toContain(`\n\`\`\`\n${resolvedCode}\n\`\`\`\n`); - expect(text).toContain(`pairing approve ${params.channel} ${resolvedCode}`); - return resolvedCode; -} - // Yields two macrotask ticks so already-scheduled inbound continuations run. // This deliberately does NOT wait for pending inbound work to finish — tests // observing intermediate states (held handlers, parked debounce batches) rely @@ -291,23 +272,7 @@ export async function settleInboundWork() { type InboundWorkTracker = { pending: number }; const inboundWorkTrackers = new Set(); -let inboundDrainWaiters: Array<() => void> = []; - -function releaseInboundDrainWaitersIfIdle() { - if (inboundDrainWaiters.length === 0) { - return; - } - for (const tracker of inboundWorkTrackers) { - if (tracker.pending > 0) { - return; - } - } - const waiters = inboundDrainWaiters; - inboundDrainWaiters = []; - for (const release of waiters) { - release(); - } -} +let inboundIdle: { promise: Promise; release: () => void } | undefined; function hasPendingInboundWork(): boolean { for (const tracker of inboundWorkTrackers) { @@ -318,6 +283,14 @@ function hasPendingInboundWork(): boolean { return false; } +function publishInboundPendingWork(tracker: InboundWorkTracker, pending: number) { + tracker.pending = pending; + if (inboundIdle && !hasPendingInboundWork()) { + inboundIdle.release(); + inboundIdle = undefined; + } +} + // Event-driven drain: resolves when every harness-started listener reports zero // pending inbound work. Unlike a vi.waitFor deadline, this cannot fail spuriously // when a saturated no-isolate worker stalls mid-flow (sync module fetches against @@ -329,9 +302,14 @@ export async function waitForInboundWorkDrained() { throw new Error("waitForInboundWorkDrained requires a listener started via startInboxMonitor"); } if (hasPendingInboundWork()) { - await new Promise((resolve) => { - inboundDrainWaiters.push(resolve); - }); + if (!inboundIdle) { + let release!: () => void; + const promise = new Promise((resolve) => { + release = resolve; + }); + inboundIdle = { promise, release }; + } + await inboundIdle.promise; } // One extra tick lets drained-callback continuations (delivery bookkeeping) run. await new Promise((resolve) => { @@ -377,8 +355,7 @@ export async function startInboxMonitor( const listener = await monitorWebInbox({ ...merged, onPendingWorkChanged: (pendingWorkCount: number, at?: number) => { - tracker.pending = pendingWorkCount; - releaseInboundDrainWaitersIfIdle(); + publishInboundPendingWork(tracker, pendingWorkCount); callerOnPendingWorkChanged?.(pendingWorkCount, at); }, }); @@ -411,15 +388,18 @@ export function buildNotifyMessageUpsert(params: { }; } +// The mocked pairing upsert always issues PAIRCODE, so the reply text can be +// asserted directly instead of re-extracting the code from the message. function expectPairingPromptSent(sock: MockSock, jid: string, senderE164: string) { expect(sock.sendMessage).toHaveBeenCalledTimes(1); const sendCall = sock.sendMessage.mock.calls.at(0); expect(sendCall?.[0]).toBe(jid); - expectInboxPairingReplyText((sendCall?.[1] as { text?: string } | undefined)?.text ?? "", { - channel: "whatsapp", - idLine: `Your WhatsApp phone number: ${senderE164}`, - code: "PAIRCODE", - }); + const text = (sendCall?.[1] as { text?: string } | undefined)?.text ?? ""; + expect(text).toContain("OpenClaw: access not configured."); + expect(text).toContain(`Your WhatsApp phone number: ${senderE164}`); + expect(text).toContain("Pairing code:"); + expect(text).toContain("\n```\nPAIRCODE\n```\n"); + expect(text).toContain("pairing approve whatsapp PAIRCODE"); } // The pairing reply is sent before the inbound handler completes, so a full @@ -446,7 +426,7 @@ export function installWebMonitorInboxUnitTestHooks(opts?: { authDir?: boolean } vi.useRealTimers(); vi.clearAllMocks(); inboundWorkTrackers.clear(); - inboundDrainWaiters = []; + inboundIdle = undefined; channelActivityMocks.recordChannelActivity.mockClear(); pluginRuntimeMocks.reset(); setWhatsAppRuntime({