@openclaw/whatsapp 2026.5.2 → 2026.5.3-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/dist/access-control-BQ3tCkH8.js +288 -0
  2. package/dist/account-config-CNQ33oav.js +47 -0
  3. package/dist/account-ids-DG18xddX.js +5 -0
  4. package/dist/accounts-BDqgpFEB.js +116 -0
  5. package/dist/action-runtime-DTGNgG-i.js +74 -0
  6. package/dist/action-runtime-api.js +2 -0
  7. package/dist/action-runtime.runtime.js +2 -0
  8. package/dist/active-listener-N7GFKsuN.js +11 -0
  9. package/dist/agent-tools-login-DpRxLNRB.js +89 -0
  10. package/dist/api.js +148 -0
  11. package/dist/audio-preflight.runtime-Bcdsl_ri.js +7 -0
  12. package/dist/auth-presence.js +32 -0
  13. package/dist/auth-store-BjCb50Ry.js +381 -0
  14. package/dist/channel-DQ-5Xoj9.js +542 -0
  15. package/dist/channel-config-api.js +2 -0
  16. package/dist/channel-plugin-api.js +2 -0
  17. package/dist/channel-react-action-Df20qAy9.js +42 -0
  18. package/dist/channel.runtime-DKXRnA8x.js +55 -0
  19. package/dist/channel.setup-DOHR2BGp.js +20 -0
  20. package/dist/command-policy-O8sRnqUV.js +8 -0
  21. package/dist/config-schema-Dmo66Ubb.js +27 -0
  22. package/dist/connection-controller-BiHAsTM8.js +596 -0
  23. package/dist/connection-controller-registry-D_iIGz5m.js +22 -0
  24. package/dist/constants-DLGCIbfl.js +4 -0
  25. package/dist/constants.js +2 -0
  26. package/dist/contract-api.js +19 -0
  27. package/dist/creds-files-_2V8jzQa.js +19 -0
  28. package/dist/directory-config-DVs4ffXu.js +37 -0
  29. package/dist/directory-contract-api.js +2 -0
  30. package/dist/doctor-A9tXm9Om.js +48 -0
  31. package/dist/doctor-contract-api.js +10 -0
  32. package/dist/group-session-contract-B8QBn0I2.js +15 -0
  33. package/dist/identity-QHAlV7Fu.js +75 -0
  34. package/dist/index.js +18 -0
  35. package/dist/legacy-session-surface-api.js +8 -0
  36. package/dist/legacy-state-migrations-api.js +2 -0
  37. package/dist/light-runtime-api.js +5 -0
  38. package/dist/login-DymdrKca.js +52 -0
  39. package/dist/login-qr-DIS0Iv-7.js +378 -0
  40. package/dist/login-qr-api.js +2 -0
  41. package/dist/login-qr-runtime.js +16 -0
  42. package/dist/monitor-C4Fhg_lD.js +3349 -0
  43. package/dist/normalize-CWYiGkXw.js +2 -0
  44. package/dist/normalize-target-BPVHNsM5.js +74 -0
  45. package/dist/outbound-adapter-BssVnZdz.js +29 -0
  46. package/dist/outbound-base-Bq1UYEK5.js +119 -0
  47. package/dist/outbound-media-contract-CGss_QHy.js +163 -0
  48. package/dist/outbound-payload-test-api.js +2 -0
  49. package/dist/quoted-message-TENQzyxV.js +108 -0
  50. package/dist/reaction-level-CRVEVAfi.js +16 -0
  51. package/dist/reply-resolver.runtime-BRCgoO4C.js +2 -0
  52. package/dist/resolve-outbound-target-CN-Wo6IY.js +39 -0
  53. package/dist/rolldown-runtime-RkAeH_Qm.js +27 -0
  54. package/dist/runtime-6fZ67abR.js +8 -0
  55. package/dist/runtime-api.js +15 -0
  56. package/dist/runtime-group-policy-Gp1lQ0at.js +11 -0
  57. package/dist/secret-contract-api.js +5 -0
  58. package/dist/security-contract-DqH8JDX6.js +26 -0
  59. package/dist/security-contract-api.js +2 -0
  60. package/dist/send-CIyLFxdZ.js +234 -0
  61. package/dist/session-contract-B0efQf9P.js +27 -0
  62. package/dist/session-errors-CSuKZPub.js +63 -0
  63. package/dist/session.runtime-C1eSE_KK.js +13 -0
  64. package/dist/setup-core-DZKvpzbH.js +46 -0
  65. package/dist/setup-entry.js +23 -0
  66. package/dist/setup-plugin-api.js +2 -0
  67. package/dist/setup-surface-ieFnIW4g.js +344 -0
  68. package/dist/shared-D_wzSVar.js +307 -0
  69. package/dist/state-migrations-DnCdvoYD.js +39 -0
  70. package/dist/targets.js +2 -0
  71. package/dist/test-api.js +3 -0
  72. package/dist/text-runtime-C1zNiZeO.js +116 -0
  73. package/package.json +14 -6
  74. package/action-runtime-api.ts +0 -1
  75. package/action-runtime.runtime.ts +0 -1
  76. package/api.ts +0 -213
  77. package/auth-presence.ts +0 -80
  78. package/channel-config-api.ts +0 -1
  79. package/channel-plugin-api.ts +0 -3
  80. package/config-api.ts +0 -4
  81. package/constants.ts +0 -1
  82. package/contract-api.ts +0 -29
  83. package/directory-contract-api.ts +0 -4
  84. package/doctor-contract-api.ts +0 -8
  85. package/index.test.ts +0 -13
  86. package/index.ts +0 -16
  87. package/legacy-session-surface-api.ts +0 -6
  88. package/legacy-state-migrations-api.ts +0 -1
  89. package/light-runtime-api.ts +0 -12
  90. package/login-qr-api.ts +0 -1
  91. package/login-qr-runtime.ts +0 -23
  92. package/outbound-payload-test-api.ts +0 -1
  93. package/runtime-api.ts +0 -84
  94. package/secret-contract-api.ts +0 -4
  95. package/security-contract-api.ts +0 -4
  96. package/setup-entry.test.ts +0 -21
  97. package/setup-entry.ts +0 -21
  98. package/setup-plugin-api.ts +0 -3
  99. package/src/account-config.ts +0 -77
  100. package/src/account-ids.ts +0 -13
  101. package/src/account-types.ts +0 -5
  102. package/src/accounts.test.ts +0 -182
  103. package/src/accounts.ts +0 -176
  104. package/src/accounts.whatsapp-auth.test.ts +0 -59
  105. package/src/action-runtime-target-auth.ts +0 -27
  106. package/src/action-runtime.test.ts +0 -330
  107. package/src/action-runtime.ts +0 -76
  108. package/src/active-listener.test.ts +0 -65
  109. package/src/active-listener.ts +0 -17
  110. package/src/agent-tools-login.test.ts +0 -81
  111. package/src/agent-tools-login.ts +0 -113
  112. package/src/approval-auth.test.ts +0 -24
  113. package/src/approval-auth.ts +0 -27
  114. package/src/auth-store.runtime.ts +0 -1
  115. package/src/auth-store.test.ts +0 -311
  116. package/src/auth-store.ts +0 -502
  117. package/src/auto-reply/config.runtime.ts +0 -16
  118. package/src/auto-reply/constants.ts +0 -1
  119. package/src/auto-reply/deliver-reply.test.ts +0 -843
  120. package/src/auto-reply/deliver-reply.ts +0 -279
  121. package/src/auto-reply/loggers.ts +0 -6
  122. package/src/auto-reply/mentions.ts +0 -131
  123. package/src/auto-reply/monitor/ack-reaction.test.ts +0 -170
  124. package/src/auto-reply/monitor/ack-reaction.ts +0 -99
  125. package/src/auto-reply/monitor/audio-preflight.runtime.ts +0 -9
  126. package/src/auto-reply/monitor/broadcast.ts +0 -153
  127. package/src/auto-reply/monitor/commands.ts +0 -19
  128. package/src/auto-reply/monitor/echo.ts +0 -64
  129. package/src/auto-reply/monitor/group-activation.runtime.ts +0 -1
  130. package/src/auto-reply/monitor/group-activation.test.ts +0 -189
  131. package/src/auto-reply/monitor/group-activation.ts +0 -73
  132. package/src/auto-reply/monitor/group-gating.audio-preflight.test.ts +0 -103
  133. package/src/auto-reply/monitor/group-gating.runtime.ts +0 -8
  134. package/src/auto-reply/monitor/group-gating.ts +0 -217
  135. package/src/auto-reply/monitor/group-members.test.ts +0 -56
  136. package/src/auto-reply/monitor/group-members.ts +0 -65
  137. package/src/auto-reply/monitor/inbound-context.test.ts +0 -97
  138. package/src/auto-reply/monitor/inbound-context.ts +0 -92
  139. package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +0 -21
  140. package/src/auto-reply/monitor/inbound-dispatch.test.ts +0 -895
  141. package/src/auto-reply/monitor/inbound-dispatch.ts +0 -422
  142. package/src/auto-reply/monitor/last-route.test.ts +0 -37
  143. package/src/auto-reply/monitor/last-route.ts +0 -61
  144. package/src/auto-reply/monitor/message-line.runtime.ts +0 -38
  145. package/src/auto-reply/monitor/message-line.ts +0 -54
  146. package/src/auto-reply/monitor/on-message.audio-preflight.test.ts +0 -354
  147. package/src/auto-reply/monitor/on-message.ts +0 -291
  148. package/src/auto-reply/monitor/peer.ts +0 -17
  149. package/src/auto-reply/monitor/process-message.audio-preflight.test.ts +0 -420
  150. package/src/auto-reply/monitor/process-message.test.ts +0 -355
  151. package/src/auto-reply/monitor/process-message.ts +0 -531
  152. package/src/auto-reply/monitor/runtime-api.ts +0 -30
  153. package/src/auto-reply/monitor-state.test.ts +0 -64
  154. package/src/auto-reply/monitor-state.ts +0 -106
  155. package/src/auto-reply/monitor.ts +0 -600
  156. package/src/auto-reply/reply-resolver.runtime.ts +0 -1
  157. package/src/auto-reply/types.ts +0 -48
  158. package/src/auto-reply/util.ts +0 -63
  159. package/src/auto-reply/web-auto-reply-monitor.test.ts +0 -687
  160. package/src/auto-reply/web-auto-reply-utils.test.ts +0 -392
  161. package/src/auto-reply.broadcast-groups.combined.test.ts +0 -246
  162. package/src/auto-reply.broadcast-groups.test-harness.ts +0 -47
  163. package/src/auto-reply.impl.ts +0 -6
  164. package/src/auto-reply.test-harness.ts +0 -421
  165. package/src/auto-reply.ts +0 -1
  166. package/src/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts +0 -427
  167. package/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts +0 -908
  168. package/src/auto-reply.web-auto-reply.last-route.test.ts +0 -184
  169. package/src/channel-actions.runtime.ts +0 -7
  170. package/src/channel-actions.test.ts +0 -216
  171. package/src/channel-actions.ts +0 -84
  172. package/src/channel-outbound.test.ts +0 -131
  173. package/src/channel-outbound.ts +0 -36
  174. package/src/channel-react-action.runtime.ts +0 -7
  175. package/src/channel-react-action.test.ts +0 -294
  176. package/src/channel-react-action.ts +0 -84
  177. package/src/channel.runtime.ts +0 -117
  178. package/src/channel.setup.test.ts +0 -461
  179. package/src/channel.setup.ts +0 -28
  180. package/src/channel.ts +0 -339
  181. package/src/command-policy.ts +0 -7
  182. package/src/config-accessors.test.ts +0 -34
  183. package/src/config-accessors.ts +0 -22
  184. package/src/config-schema.test.ts +0 -121
  185. package/src/config-schema.ts +0 -6
  186. package/src/config-ui-hints.ts +0 -24
  187. package/src/connection-controller-registry.test.ts +0 -27
  188. package/src/connection-controller-registry.ts +0 -49
  189. package/src/connection-controller.test.ts +0 -282
  190. package/src/connection-controller.ts +0 -679
  191. package/src/creds-files.ts +0 -19
  192. package/src/creds-persistence.ts +0 -102
  193. package/src/directory-config.test.ts +0 -60
  194. package/src/directory-config.ts +0 -40
  195. package/src/directory-contract.test.ts +0 -56
  196. package/src/doctor-contract.ts +0 -11
  197. package/src/doctor.test.ts +0 -74
  198. package/src/doctor.ts +0 -56
  199. package/src/group-intro.ts +0 -15
  200. package/src/group-policy.test.ts +0 -36
  201. package/src/group-policy.ts +0 -40
  202. package/src/group-session-contract.ts +0 -20
  203. package/src/group-session-key.test.ts +0 -53
  204. package/src/group-session-key.ts +0 -41
  205. package/src/heartbeat.ts +0 -34
  206. package/src/identity.ts +0 -164
  207. package/src/inbound/access-control.test-harness.ts +0 -37
  208. package/src/inbound/access-control.test.ts +0 -375
  209. package/src/inbound/access-control.ts +0 -237
  210. package/src/inbound/dedupe.ts +0 -132
  211. package/src/inbound/extract.test.ts +0 -282
  212. package/src/inbound/extract.ts +0 -488
  213. package/src/inbound/lifecycle.ts +0 -39
  214. package/src/inbound/media.node.test.ts +0 -83
  215. package/src/inbound/media.ts +0 -102
  216. package/src/inbound/monitor.ts +0 -890
  217. package/src/inbound/runtime-api.ts +0 -7
  218. package/src/inbound/save-media.runtime.ts +0 -1
  219. package/src/inbound/send-api.test.ts +0 -319
  220. package/src/inbound/send-api.ts +0 -145
  221. package/src/inbound/send-result.ts +0 -61
  222. package/src/inbound/types.ts +0 -106
  223. package/src/inbound-context.contract.test.ts +0 -32
  224. package/src/inbound-policy.ts +0 -231
  225. package/src/inbound.media.test.ts +0 -320
  226. package/src/inbound.test.ts +0 -290
  227. package/src/inbound.ts +0 -9
  228. package/src/login-qr.test.ts +0 -483
  229. package/src/login-qr.ts +0 -542
  230. package/src/login.coverage.test.ts +0 -133
  231. package/src/login.test.ts +0 -89
  232. package/src/login.ts +0 -70
  233. package/src/logout.test.ts +0 -162
  234. package/src/media.test.ts +0 -480
  235. package/src/media.ts +0 -10
  236. package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +0 -400
  237. package/src/monitor-inbox.append-upsert.test-support.ts +0 -133
  238. package/src/monitor-inbox.behavior.test.ts +0 -5
  239. package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +0 -362
  240. package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +0 -306
  241. package/src/monitor-inbox.streams-inbound-messages.test-support.ts +0 -729
  242. package/src/monitor-inbox.test-harness.ts +0 -304
  243. package/src/normalize-target.ts +0 -123
  244. package/src/normalize.ts +0 -7
  245. package/src/outbound-adapter.poll.test.ts +0 -62
  246. package/src/outbound-adapter.sendpayload.test.ts +0 -195
  247. package/src/outbound-adapter.ts +0 -36
  248. package/src/outbound-base.test.ts +0 -560
  249. package/src/outbound-base.ts +0 -244
  250. package/src/outbound-media-contract.ts +0 -278
  251. package/src/outbound-media.runtime.ts +0 -36
  252. package/src/outbound-payload.contract.test.ts +0 -61
  253. package/src/outbound-send-deps.ts +0 -1
  254. package/src/outbound-test-support.ts +0 -16
  255. package/src/pairing-security.test-harness.ts +0 -59
  256. package/src/qr-image.ts +0 -1
  257. package/src/qr-terminal.ts +0 -1
  258. package/src/quoted-message.test.ts +0 -83
  259. package/src/quoted-message.ts +0 -184
  260. package/src/reaction-level.test.ts +0 -94
  261. package/src/reaction-level.ts +0 -21
  262. package/src/reconnect.test.ts +0 -51
  263. package/src/reconnect.ts +0 -55
  264. package/src/resolve-outbound-target.test.ts +0 -288
  265. package/src/resolve-outbound-target.ts +0 -58
  266. package/src/resolve-target.test.ts +0 -120
  267. package/src/runtime-api.ts +0 -59
  268. package/src/runtime-group-policy.ts +0 -16
  269. package/src/runtime.ts +0 -9
  270. package/src/security-contract.ts +0 -47
  271. package/src/security-fix.ts +0 -71
  272. package/src/send.test.ts +0 -534
  273. package/src/send.ts +0 -292
  274. package/src/session-contract.test.ts +0 -31
  275. package/src/session-contract.ts +0 -43
  276. package/src/session-errors.ts +0 -125
  277. package/src/session-route.test.ts +0 -41
  278. package/src/session-route.ts +0 -32
  279. package/src/session.runtime.ts +0 -8
  280. package/src/session.test.ts +0 -559
  281. package/src/session.ts +0 -330
  282. package/src/setup-core.ts +0 -52
  283. package/src/setup-finalize.ts +0 -446
  284. package/src/setup-surface.test.ts +0 -367
  285. package/src/setup-surface.ts +0 -69
  286. package/src/setup-test-helpers.ts +0 -216
  287. package/src/shared.ts +0 -291
  288. package/src/socket-timing.test.ts +0 -49
  289. package/src/socket-timing.ts +0 -38
  290. package/src/state-migrations.ts +0 -54
  291. package/src/status-issues.test.ts +0 -137
  292. package/src/status-issues.ts +0 -185
  293. package/src/system-prompt.test.ts +0 -199
  294. package/src/system-prompt.ts +0 -31
  295. package/src/targets-runtime.ts +0 -180
  296. package/src/test-helpers.ts +0 -690
  297. package/src/text-runtime.test.ts +0 -162
  298. package/src/text-runtime.ts +0 -11
  299. package/src/vcard.ts +0 -84
  300. package/targets.ts +0 -5
  301. package/test-api.ts +0 -2
  302. package/tsconfig.json +0 -16
@@ -1,483 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { startWebLoginWithQr, waitForWebLogin } from "./login-qr.js";
3
- import { renderQrPngDataUrl } from "./qr-image.js";
4
- import {
5
- createWaSocket,
6
- logoutWeb,
7
- readWebAuthExistsForDecision,
8
- readWebSelfId,
9
- WHATSAPP_AUTH_UNSTABLE_CODE,
10
- waitForWaConnection,
11
- } from "./session.js";
12
-
13
- vi.mock("./session.js", async () => {
14
- const actual = await vi.importActual<typeof import("./session.js")>("./session.js");
15
- const createWaSocket = vi.fn();
16
- const waitForWaConnection = vi.fn();
17
- const formatError = vi.fn((err: unknown) => `formatted:${String(err)}`);
18
- const getStatusCode = vi.fn(
19
- (err: unknown) =>
20
- (err as { output?: { statusCode?: number } })?.output?.statusCode ??
21
- (err as { status?: number })?.status ??
22
- (err as { error?: { output?: { statusCode?: number } } })?.error?.output?.statusCode,
23
- );
24
- const readWebAuthExistsForDecision = vi.fn(async () => ({
25
- outcome: "stable" as const,
26
- exists: false,
27
- }));
28
- const readWebSelfId = vi.fn(() => ({ e164: null, jid: null, lid: null }));
29
- const logoutWeb = vi.fn(async () => true);
30
- return {
31
- ...actual,
32
- createWaSocket,
33
- waitForWaConnection,
34
- formatError,
35
- getStatusCode,
36
- readWebAuthExistsForDecision,
37
- readWebSelfId,
38
- logoutWeb,
39
- };
40
- });
41
-
42
- vi.mock("./qr-image.js", () => ({
43
- renderQrPngBase64: vi.fn(async () => "base64"),
44
- renderQrPngDataUrl: vi.fn(async (input: string) => `data:image/png;base64,encoded:${input}`),
45
- }));
46
-
47
- const createWaSocketMock = vi.mocked(createWaSocket);
48
- const readWebAuthExistsForDecisionMock = vi.mocked(readWebAuthExistsForDecision);
49
- const readWebSelfIdMock = vi.mocked(readWebSelfId);
50
- const waitForWaConnectionMock = vi.mocked(waitForWaConnection);
51
- const logoutWebMock = vi.mocked(logoutWeb);
52
- const renderQrPngDataUrlMock = vi.mocked(renderQrPngDataUrl);
53
-
54
- async function flushTasks() {
55
- await Promise.resolve();
56
- await Promise.resolve();
57
- }
58
-
59
- async function waitMs(ms: number) {
60
- await new Promise((resolve) => setTimeout(resolve, ms));
61
- }
62
-
63
- async function waitForQrRenderCallCount(count: number) {
64
- const deadline = Date.now() + 1000;
65
- while (renderQrPngDataUrlMock.mock.calls.length < count && Date.now() < deadline) {
66
- await waitMs(0);
67
- await flushTasks();
68
- }
69
- }
70
-
71
- describe("login-qr", () => {
72
- const rotatingAccountId = "rotating-qr";
73
- const concurrentAccountId = "concurrent-qr";
74
-
75
- beforeEach(() => {
76
- vi.clearAllMocks();
77
- createWaSocketMock
78
- .mockReset()
79
- .mockImplementation(
80
- async (
81
- _printQr: boolean,
82
- _verbose: boolean,
83
- opts?: { authDir?: string; onQr?: (qr: string) => void },
84
- ) => {
85
- const sock = { ws: { close: vi.fn() } };
86
- if (opts?.onQr) {
87
- setImmediate(() => opts.onQr?.("qr-data"));
88
- }
89
- return sock as never;
90
- },
91
- );
92
- waitForWaConnectionMock.mockReset();
93
- readWebAuthExistsForDecisionMock.mockReset().mockResolvedValue({
94
- outcome: "stable",
95
- exists: false,
96
- });
97
- readWebSelfIdMock.mockReset().mockReturnValue({ e164: null, jid: null, lid: null });
98
- logoutWebMock.mockReset().mockResolvedValue(true);
99
- renderQrPngDataUrlMock
100
- .mockReset()
101
- .mockImplementation(async (input) => `data:image/png;base64,encoded:${input}`);
102
- });
103
-
104
- it("restarts login once on status 515 and completes", async () => {
105
- waitForWaConnectionMock
106
- // Baileys v7 wraps the error: { error: BoomError(515) }
107
- .mockRejectedValueOnce({ error: { output: { statusCode: 515 } } })
108
- .mockResolvedValueOnce(undefined);
109
-
110
- const start = await startWebLoginWithQr({
111
- timeoutMs: 5000,
112
- accountId: rotatingAccountId,
113
- });
114
- expect(start.qrDataUrl).toBe("data:image/png;base64,encoded:qr-data");
115
-
116
- const resultPromise = waitForWebLogin({
117
- timeoutMs: 5000,
118
- currentQrDataUrl: start.qrDataUrl,
119
- accountId: rotatingAccountId,
120
- });
121
- await flushTasks();
122
- await flushTasks();
123
-
124
- expect(createWaSocketMock).toHaveBeenCalledTimes(2);
125
- const result = await resultPromise;
126
-
127
- expect(result.connected).toBe(true);
128
- expect(createWaSocketMock).toHaveBeenCalledTimes(2);
129
- expect(logoutWebMock).not.toHaveBeenCalled();
130
- });
131
-
132
- it("clears auth and reports a relink message when WhatsApp is logged out", async () => {
133
- waitForWaConnectionMock.mockRejectedValueOnce({
134
- output: { statusCode: 401 },
135
- });
136
-
137
- const start = await startWebLoginWithQr({ timeoutMs: 5000 });
138
- expect(start.qrDataUrl).toBe("data:image/png;base64,encoded:qr-data");
139
-
140
- const result = await waitForWebLogin({
141
- timeoutMs: 5000,
142
- currentQrDataUrl: start.qrDataUrl,
143
- });
144
-
145
- expect(result).toEqual({
146
- connected: false,
147
- message:
148
- "WhatsApp reported the session is logged out. Cleared cached web session; please scan a new QR.",
149
- });
150
- expect(logoutWebMock).toHaveBeenCalledOnce();
151
- });
152
-
153
- it("turns unexpected login cleanup failures into a normal login error", async () => {
154
- waitForWaConnectionMock.mockRejectedValueOnce({
155
- output: { statusCode: 401 },
156
- });
157
- logoutWebMock.mockRejectedValueOnce(new Error("cleanup failed"));
158
-
159
- const start = await startWebLoginWithQr({ timeoutMs: 5000 });
160
- expect(start.qrDataUrl).toBe("data:image/png;base64,encoded:qr-data");
161
-
162
- const result = await waitForWebLogin({
163
- timeoutMs: 5000,
164
- currentQrDataUrl: start.qrDataUrl,
165
- });
166
-
167
- expect(result).toEqual({
168
- connected: false,
169
- message: "WhatsApp login failed: cleanup failed",
170
- });
171
- });
172
-
173
- it("returns an unstable-auth result when creds flush does not settle", async () => {
174
- readWebAuthExistsForDecisionMock.mockResolvedValueOnce({ outcome: "unstable" });
175
-
176
- const result = await startWebLoginWithQr({ timeoutMs: 5000 });
177
-
178
- expect(result).toEqual({
179
- code: WHATSAPP_AUTH_UNSTABLE_CODE,
180
- message: "WhatsApp auth state is still stabilizing. Retry login in a moment.",
181
- });
182
- expect(createWaSocketMock).not.toHaveBeenCalled();
183
- });
184
-
185
- it("reports a recovered linked session when socket bootstrap restores auth without a QR", async () => {
186
- createWaSocketMock.mockImplementationOnce(
187
- async (
188
- _printQr: boolean,
189
- _verbose: boolean,
190
- _opts?: { authDir?: string; onQr?: (qr: string) => void },
191
- ) =>
192
- ({
193
- ws: { close: vi.fn() },
194
- }) as never,
195
- );
196
- waitForWaConnectionMock.mockResolvedValueOnce(undefined);
197
- readWebSelfIdMock.mockReturnValueOnce({ e164: "+5511977000000", jid: null, lid: null });
198
-
199
- const result = await startWebLoginWithQr({ timeoutMs: 5000 });
200
-
201
- expect(result).toEqual({
202
- connected: true,
203
- message: "WhatsApp recovered the existing linked session (+5511977000000).",
204
- });
205
- expect(createWaSocketMock).toHaveBeenCalledOnce();
206
- await expect(waitForWebLogin({ timeoutMs: 1000 })).resolves.toEqual({
207
- connected: false,
208
- message: "No active WhatsApp login in progress.",
209
- });
210
- });
211
-
212
- it("surfaces the latest QR after the socket rotates it", async () => {
213
- createWaSocketMock.mockImplementationOnce(
214
- async (
215
- _printQr: boolean,
216
- _verbose: boolean,
217
- opts?: { authDir?: string; onQr?: (qr: string) => void },
218
- ) => {
219
- const sock = { ws: { close: vi.fn() } };
220
- setImmediate(() => opts?.onQr?.("qr-data"));
221
- setTimeout(() => opts?.onQr?.("qr-data-2"), 100);
222
- return sock as never;
223
- },
224
- );
225
- waitForWaConnectionMock.mockImplementation(() => new Promise(() => {}));
226
-
227
- const start = await startWebLoginWithQr({ timeoutMs: 5000 });
228
- expect(start.qrDataUrl).toBe("data:image/png;base64,encoded:qr-data");
229
-
230
- const resultPromise = waitForWebLogin({
231
- timeoutMs: 5000,
232
- currentQrDataUrl: start.qrDataUrl,
233
- });
234
- await flushTasks();
235
- await waitMs(140);
236
- await flushTasks();
237
-
238
- await expect(resultPromise).resolves.toEqual({
239
- connected: false,
240
- message: "QR refreshed. Scan the latest code in WhatsApp → Linked Devices.",
241
- qrDataUrl: "data:image/png;base64,encoded:qr-data-2",
242
- });
243
- });
244
-
245
- it("does not short-circuit on an existing QR when the waiter has no current QR image", async () => {
246
- const accountId = "wait-without-current-qr";
247
- waitForWaConnectionMock.mockImplementationOnce(
248
- () => new Promise((resolve) => setTimeout(() => resolve(undefined), 20)),
249
- );
250
-
251
- const start = await startWebLoginWithQr({
252
- timeoutMs: 5000,
253
- accountId,
254
- });
255
- expect(start.qrDataUrl).toBe("data:image/png;base64,encoded:qr-data");
256
-
257
- await expect(
258
- waitForWebLogin({
259
- timeoutMs: 5000,
260
- accountId,
261
- }),
262
- ).resolves.toEqual({
263
- connected: true,
264
- message: "✅ Linked! WhatsApp is ready.",
265
- });
266
- });
267
-
268
- it("returns a terminal login result before a stale QR refresh", async () => {
269
- const accountId = "connected-before-refresh";
270
- let resolveLogin: () => void = () => {
271
- throw new Error("Expected login wait to be pending");
272
- };
273
- createWaSocketMock.mockImplementationOnce(
274
- async (
275
- _printQr: boolean,
276
- _verbose: boolean,
277
- opts?: { authDir?: string; onQr?: (qr: string) => void },
278
- ) => {
279
- const sock = { ws: { close: vi.fn() } };
280
- setImmediate(() => opts?.onQr?.("qr-data"));
281
- setTimeout(() => opts?.onQr?.("qr-data-2"), 20);
282
- return sock as never;
283
- },
284
- );
285
- waitForWaConnectionMock.mockImplementationOnce(
286
- () =>
287
- new Promise<void>((resolve) => {
288
- resolveLogin = resolve;
289
- }),
290
- );
291
-
292
- const start = await startWebLoginWithQr({
293
- timeoutMs: 5000,
294
- accountId,
295
- });
296
- expect(start.qrDataUrl).toBe("data:image/png;base64,encoded:qr-data");
297
-
298
- await waitMs(50);
299
- await flushTasks();
300
- resolveLogin();
301
- await flushTasks();
302
-
303
- await expect(
304
- waitForWebLogin({
305
- timeoutMs: 5000,
306
- currentQrDataUrl: start.qrDataUrl,
307
- accountId,
308
- }),
309
- ).resolves.toEqual({
310
- connected: true,
311
- message: "✅ Linked! WhatsApp is ready.",
312
- });
313
- });
314
-
315
- it("returns a terminal result when an older replaced waiter resolves without state", async () => {
316
- const accountId = "replaced-login-waiter";
317
- let resolveFirstConnection: () => void = () => {
318
- throw new Error("Expected first login wait to be pending");
319
- };
320
- waitForWaConnectionMock
321
- .mockImplementationOnce(
322
- () =>
323
- new Promise<void>((resolve) => {
324
- resolveFirstConnection = resolve;
325
- }),
326
- )
327
- .mockImplementation(() => new Promise(() => {}));
328
-
329
- const start = await startWebLoginWithQr({
330
- timeoutMs: 5000,
331
- accountId,
332
- });
333
- expect(start.qrDataUrl).toBe("data:image/png;base64,encoded:qr-data");
334
-
335
- const waiter = waitForWebLogin({
336
- timeoutMs: 1000,
337
- currentQrDataUrl: start.qrDataUrl,
338
- accountId,
339
- });
340
- await flushTasks();
341
-
342
- const now = Date.now();
343
- const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now + 3 * 60_000 + 1000);
344
- try {
345
- const replacement = await startWebLoginWithQr({
346
- timeoutMs: 5000,
347
- accountId,
348
- });
349
- expect(replacement.qrDataUrl).toBe("data:image/png;base64,encoded:qr-data");
350
-
351
- resolveFirstConnection();
352
-
353
- await expect(waiter).resolves.toEqual({
354
- connected: false,
355
- message: "Login ended without a connection.",
356
- });
357
- } finally {
358
- dateNowSpy.mockRestore();
359
- }
360
- });
361
-
362
- it("keeps an active login reusable while a rotated QR image renders", async () => {
363
- const accountId = "reuse-during-qr-render";
364
- let onQr: (qr: string) => void = () => {
365
- throw new Error("Expected QR callback to be registered");
366
- };
367
- createWaSocketMock.mockImplementation(
368
- async (
369
- _printQr: boolean,
370
- _verbose: boolean,
371
- opts?: { authDir?: string; onQr?: (qr: string) => void },
372
- ) => {
373
- const sock = { ws: { close: vi.fn() } };
374
- onQr = (qr) => opts?.onQr?.(qr);
375
- setImmediate(() => onQr("qr-data"));
376
- return sock as never;
377
- },
378
- );
379
- waitForWaConnectionMock.mockImplementation(() => new Promise(() => {}));
380
- renderQrPngDataUrlMock.mockImplementation((qr) =>
381
- qr === "qr-data-2"
382
- ? new Promise<string>(() => {})
383
- : Promise.resolve(`data:image/png;base64,encoded:${qr}`),
384
- );
385
-
386
- const start = await startWebLoginWithQr({
387
- timeoutMs: 5000,
388
- accountId,
389
- });
390
- expect(start.qrDataUrl).toBe("data:image/png;base64,encoded:qr-data");
391
-
392
- onQr("qr-data-2");
393
- await flushTasks();
394
-
395
- const reused = await startWebLoginWithQr({
396
- timeoutMs: 5000,
397
- accountId,
398
- });
399
-
400
- expect(createWaSocketMock).toHaveBeenCalledTimes(1);
401
- expect(reused).toEqual({
402
- qrDataUrl: "data:image/png;base64,encoded:qr-data",
403
- message: "QR already active. Scan it in WhatsApp → Linked Devices.",
404
- });
405
- });
406
-
407
- it("deduplicates initial QR rendering while the start path awaits the same image", async () => {
408
- const accountId = "single-flight-qr";
409
- let resolveRender: (value: string) => void = () => {
410
- throw new Error("Expected QR render promise to be pending");
411
- };
412
- renderQrPngDataUrlMock.mockImplementationOnce(
413
- () =>
414
- new Promise<string>((resolve) => {
415
- resolveRender = resolve;
416
- }),
417
- );
418
- waitForWaConnectionMock.mockImplementation(() => new Promise(() => {}));
419
-
420
- const resultPromise = startWebLoginWithQr({
421
- timeoutMs: 5000,
422
- accountId,
423
- });
424
- await waitForQrRenderCallCount(1);
425
-
426
- expect(renderQrPngDataUrlMock).toHaveBeenCalledTimes(1);
427
-
428
- resolveRender("data:image/png;base64,encoded:qr-data");
429
- await expect(resultPromise).resolves.toEqual({
430
- qrDataUrl: "data:image/png;base64,encoded:qr-data",
431
- message: "Scan this QR in WhatsApp → Linked Devices.",
432
- });
433
- expect(renderQrPngDataUrlMock).toHaveBeenCalledTimes(1);
434
- });
435
-
436
- it("returns the same rotated QR to concurrent waiters that share the same current image", async () => {
437
- createWaSocketMock.mockImplementationOnce(
438
- async (
439
- _printQr: boolean,
440
- _verbose: boolean,
441
- opts?: { authDir?: string; onQr?: (qr: string) => void },
442
- ) => {
443
- const sock = { ws: { close: vi.fn() } };
444
- setImmediate(() => opts?.onQr?.("qr-data"));
445
- setTimeout(() => opts?.onQr?.("qr-data-2"), 100);
446
- return sock as never;
447
- },
448
- );
449
- waitForWaConnectionMock.mockImplementation(() => new Promise(() => {}));
450
-
451
- const start = await startWebLoginWithQr({
452
- timeoutMs: 5000,
453
- accountId: concurrentAccountId,
454
- });
455
- expect(start.qrDataUrl).toBe("data:image/png;base64,encoded:qr-data");
456
-
457
- const waiterA = waitForWebLogin({
458
- timeoutMs: 5000,
459
- currentQrDataUrl: start.qrDataUrl,
460
- accountId: concurrentAccountId,
461
- });
462
- const waiterB = waitForWebLogin({
463
- timeoutMs: 5000,
464
- currentQrDataUrl: start.qrDataUrl,
465
- accountId: concurrentAccountId,
466
- });
467
-
468
- await flushTasks();
469
- await waitMs(140);
470
- await flushTasks();
471
-
472
- await expect(waiterA).resolves.toEqual({
473
- connected: false,
474
- message: "QR refreshed. Scan the latest code in WhatsApp → Linked Devices.",
475
- qrDataUrl: "data:image/png;base64,encoded:qr-data-2",
476
- });
477
- await expect(waiterB).resolves.toEqual({
478
- connected: false,
479
- message: "QR refreshed. Scan the latest code in WhatsApp → Linked Devices.",
480
- qrDataUrl: "data:image/png;base64,encoded:qr-data-2",
481
- });
482
- });
483
- });