Skip to content

Commit 9efbae7

Browse files
committed
fix(whatsapp): route login qr through runtime
1 parent 03ad3c0 commit 9efbae7

3 files changed

Lines changed: 52 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ Docs: https://docs.openclaw.ai
213213
- Gateway/update: avoid `launchctl kickstart -k` immediately after fresh macOS update bootstraps, and unlink dangling global plugin-runtime symlinks during packaged postinstall and `doctor --fix` so upgrades no longer SIGTERM the newly booted Gateway or leave bundled plugin imports pointed at pruned `plugin-runtime-deps` trees. Completes #76261 and fixes #76466. (#76929)
214214
- Google Chat: normalize custom Google auth transport headers before google-auth/gaxios interceptors run, restoring webhook token verification when certificate retrieval expects Fetch `Headers`. Fixes #76742. Thanks @donbowman.
215215
- Google Chat: normalize Google auth certificate response headers before google-auth-library reads cache-control, so inbound webhook auth no longer rejects with `res?.headers.get is not a function`. Fixes #76880. Thanks @donbowman.
216+
- WhatsApp: route terminal login QR output through the active runtime for initial and restart sockets, so `openclaw channels login --channel whatsapp` does not lose the QR behind direct stdout writes. Fixes #76213. Thanks @dougvk.
216217
- Doctor/plugins: reset stale `plugins.slots.memory` and `plugins.slots.contextEngine` references during `doctor --fix`, so cleanup of missing plugin config does not leave unrecoverable slot owners behind. Fixes #76550 and #76551. Thanks @vincentkoc.
217218
- Docs/WhatsApp: merge the duplicate top-level `web` objects in the gateway channel config example so copy-pasted WhatsApp config keeps both `web.whatsapp` and reconnect settings. Fixes #76619. Thanks @WadydX.
218219
- Plugins/Anthropic: expose Claude thinking profiles from the bundled provider-policy artifact so non-runtime callers keep Opus 4.7 `adaptive`, `xhigh`, and `max` instead of downgrading to `high`. Fixes #76779. Thanks @tomascupr and @iAbhi001.

extensions/whatsapp/src/login.coverage.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { rmSync } from "node:fs";
22
import fs from "node:fs/promises";
3+
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
34
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
45
import { loginWeb } from "./login.js";
6+
import { renderQrTerminal } from "./qr-terminal.js";
57
import { createWaSocket, formatError, waitForWaConnection } from "./session.js";
68

79
const rmMock = vi.spyOn(fs, "rm");
@@ -63,9 +65,14 @@ vi.mock("./session.js", async () => {
6365
};
6466
});
6567

68+
vi.mock("./qr-terminal.js", () => ({
69+
renderQrTerminal: vi.fn(async (qr: string) => `terminal:${qr}\n`),
70+
}));
71+
6672
const createWaSocketMock = vi.mocked(createWaSocket);
6773
const waitForWaConnectionMock = vi.mocked(waitForWaConnection);
6874
const formatErrorMock = vi.mocked(formatError);
75+
const renderQrTerminalMock = vi.mocked(renderQrTerminal);
6976

7077
async function flushTasks() {
7178
await Promise.resolve();
@@ -94,7 +101,7 @@ describe("loginWeb coverage", () => {
94101
.mockRejectedValueOnce({ error: { output: { statusCode: 515 } } })
95102
.mockResolvedValueOnce(undefined);
96103

97-
const runtime = { log: vi.fn(), error: vi.fn() } as never;
104+
const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
98105
const pendingLogin = loginWeb(false, waitForWaConnectionMock as never, runtime);
99106
await flushTasks();
100107

@@ -109,6 +116,35 @@ describe("loginWeb coverage", () => {
109116
expect(secondSock.ws.close).toHaveBeenCalled();
110117
});
111118

119+
it("routes QR output through runtime for initial and restart sockets", async () => {
120+
waitForWaConnectionMock
121+
.mockRejectedValueOnce({ error: { output: { statusCode: 515 } } })
122+
.mockResolvedValueOnce(undefined);
123+
124+
const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
125+
await loginWeb(false, waitForWaConnectionMock as never, runtime);
126+
127+
expect(createWaSocketMock).toHaveBeenCalledTimes(2);
128+
expect(createWaSocketMock.mock.calls[0]?.[0]).toBe(false);
129+
const initialOpts = createWaSocketMock.mock.calls[0]?.[2] as
130+
| { onQr?: (qr: string) => void }
131+
| undefined;
132+
const restartOpts = createWaSocketMock.mock.calls[1]?.[2] as
133+
| { onQr?: (qr: string) => void }
134+
| undefined;
135+
expect(initialOpts?.onQr).toBe(restartOpts?.onQr);
136+
137+
initialOpts?.onQr?.("initial-qr");
138+
restartOpts?.onQr?.("restart-qr");
139+
await flushTasks();
140+
141+
expect(runtime.log).toHaveBeenCalledWith("Scan this QR in WhatsApp (Linked Devices):");
142+
expect(runtime.log).toHaveBeenCalledWith("terminal:initial-qr");
143+
expect(runtime.log).toHaveBeenCalledWith("terminal:restart-qr");
144+
expect(renderQrTerminalMock).toHaveBeenCalledWith("initial-qr", { small: true });
145+
expect(renderQrTerminalMock).toHaveBeenCalledWith("restart-qr", { small: true });
146+
});
147+
112148
it("clears creds and throws when logged out", async () => {
113149
waitForWaConnectionMock.mockRejectedValueOnce({
114150
output: { statusCode: 401 },

extensions/whatsapp/src/login.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { logInfo } from "openclaw/plugin-sdk/text-runtime";
66
import { resolveWhatsAppAccount } from "./accounts.js";
77
import { restoreCredsFromBackupIfNeeded } from "./auth-store.js";
88
import { closeWaSocketSoon, waitForWhatsAppLoginResult } from "./connection-controller.js";
9+
import { renderQrTerminal } from "./qr-terminal.js";
910
import { createWaSocket, waitForWaConnection } from "./session.js";
1011
import { resolveWhatsAppSocketTiming } from "./socket-timing.js";
1112

@@ -19,9 +20,20 @@ export async function loginWeb(
1920
const account = resolveWhatsAppAccount({ cfg, accountId });
2021
const socketTiming = resolveWhatsAppSocketTiming(cfg);
2122
const restoredFromBackup = await restoreCredsFromBackupIfNeeded(account.authDir);
22-
let sock = await createWaSocket(true, verbose, {
23+
const onQr = (qr: string) => {
24+
runtime.log("Scan this QR in WhatsApp (Linked Devices):");
25+
void renderQrTerminal(qr, { small: true })
26+
.then((output) => {
27+
runtime.log(output.endsWith("\n") ? output.slice(0, -1) : output);
28+
})
29+
.catch((err) => {
30+
runtime.error(`failed rendering WhatsApp QR: ${String(err)}`);
31+
});
32+
};
33+
let sock = await createWaSocket(false, verbose, {
2334
authDir: account.authDir,
2435
...socketTiming,
36+
onQr,
2537
});
2638
logInfo("Waiting for WhatsApp connection...", runtime);
2739
try {
@@ -33,6 +45,7 @@ export async function loginWeb(
3345
runtime,
3446
waitForConnection,
3547
socketTiming,
48+
onQr,
3649
onSocketReplaced: (replacementSock) => {
3750
sock = replacementSock;
3851
},

0 commit comments

Comments
 (0)