diff --git a/CHANGELOG.md b/CHANGELOG.md index 57767511f8a..c20a8f58c53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai - Dashboard: constrain exec approval modal overflow on desktop so long command content no longer pushes action buttons out of view. (#67082) Thanks @Ziy1-Tan. - Agents/CLI transcripts: persist successful CLI-backed turns into the OpenClaw session transcript so google-gemini-cli replies appear in session history and the Control UI again. (#67490) Thanks @obviyus. - Discord/tool-call text: strip standalone Gemma-style `...` tool-call payloads from visible assistant text without truncating prose examples or trailing replies. (#67318) Thanks @joelnishanth. +- WhatsApp/web-session: drain the pending per-auth creds save queue before reopening sockets so reconnect-time auth bootstrap no longer races in-flight `creds.json` writes and falsely restores from backup. (#67464) Thanks @neeravmakwana. ## 2026.4.15-beta.1 diff --git a/extensions/whatsapp/src/connection-controller.test.ts b/extensions/whatsapp/src/connection-controller.test.ts index cf5c4c73767..d767dfecdcc 100644 --- a/extensions/whatsapp/src/connection-controller.test.ts +++ b/extensions/whatsapp/src/connection-controller.test.ts @@ -1,20 +1,35 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { getRegisteredWhatsAppConnectionController } from "./connection-controller-registry.js"; import { WhatsAppConnectionController } from "./connection-controller.js"; -import { createWaSocket, waitForWaConnection } from "./session.js"; +import { + createWaSocket, + waitForCredsSaveQueueWithTimeout, + waitForWaConnection, +} from "./session.js"; vi.mock("./session.js", async () => { const actual = await vi.importActual("./session.js"); return { ...actual, createWaSocket: vi.fn(), + waitForCredsSaveQueueWithTimeout: vi.fn(async () => {}), waitForWaConnection: vi.fn(), }; }); const createWaSocketMock = vi.mocked(createWaSocket); +const waitForCredsSaveQueueWithTimeoutMock = vi.mocked(waitForCredsSaveQueueWithTimeout); const waitForWaConnectionMock = vi.mocked(waitForWaConnection); +function createListenerStub(messageId = "ok") { + return { + sendMessage: vi.fn(async () => ({ messageId })), + sendPoll: vi.fn(async () => ({ messageId })), + sendReaction: vi.fn(async () => {}), + sendComposingTo: vi.fn(async () => {}), + }; +} + describe("WhatsAppConnectionController", () => { let controller: WhatsAppConnectionController; @@ -66,6 +81,26 @@ describe("WhatsAppConnectionController", () => { expect(controller.getActiveListener()).toBeNull(); }); + it("flushes pending creds saves before opening a socket", async () => { + const callOrder: string[] = []; + waitForCredsSaveQueueWithTimeoutMock.mockImplementationOnce(async () => { + callOrder.push("wait"); + }); + createWaSocketMock.mockImplementationOnce(async () => { + callOrder.push("create"); + return { ws: { close: vi.fn() } } as never; + }); + waitForWaConnectionMock.mockResolvedValueOnce(undefined); + + await controller.openConnection({ + connectionId: "conn-flush-first", + createListener: async () => createListenerStub() as never, + }); + + expect(waitForCredsSaveQueueWithTimeoutMock).toHaveBeenCalledWith("/tmp/wa-auth"); + expect(callOrder).toEqual(["wait", "create"]); + }); + it("keeps the previous registered controller until a replacement listener is ready", async () => { const liveController = new WhatsAppConnectionController({ accountId: "work", @@ -83,12 +118,7 @@ describe("WhatsAppConnectionController", () => { maxAttempts: 5, }, }); - const liveListener = { - sendMessage: vi.fn(async () => ({ messageId: "live-msg" })), - sendPoll: vi.fn(async () => ({ messageId: "live-poll" })), - sendReaction: vi.fn(async () => {}), - sendComposingTo: vi.fn(async () => {}), - }; + const liveListener = createListenerStub("live"); createWaSocketMock.mockResolvedValueOnce({ ws: { close: vi.fn() } } as never); waitForWaConnectionMock.mockResolvedValueOnce(undefined); await liveController.openConnection({ diff --git a/extensions/whatsapp/src/connection-controller.ts b/extensions/whatsapp/src/connection-controller.ts index fa37bbf7a95..4545cd797af 100644 --- a/extensions/whatsapp/src/connection-controller.ts +++ b/extensions/whatsapp/src/connection-controller.ts @@ -347,6 +347,7 @@ export class WhatsAppConnectionController { let sock: WaSocket | null = null; let connection: WhatsAppLiveConnection | null = null; try { + await waitForCredsSaveQueueWithTimeout(this.authDir); sock = await createWaSocket(false, this.verbose, { authDir: this.authDir, });