@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,908 +0,0 @@
1
- import "./test-helpers.js";
2
- import crypto from "node:crypto";
3
- import fs from "node:fs/promises";
4
- import path from "node:path";
5
- import { escapeRegExp, formatEnvelopeTimestamp } from "openclaw/plugin-sdk/channel-test-helpers";
6
- import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
7
- import { setLoggerOverride } from "openclaw/plugin-sdk/runtime-env";
8
- import { withEnvAsync } from "openclaw/plugin-sdk/test-env";
9
- import { beforeAll, describe, expect, it, vi } from "vitest";
10
- import { getActiveWebListener } from "./active-listener.js";
11
- import { WhatsAppAuthUnstableError, resolveWebCredsPath } from "./auth-store.js";
12
- import { resolveOAuthDir } from "./auth-store.runtime.js";
13
- import {
14
- createWebInboundDeliverySpies,
15
- createAcceptedWhatsAppSendResult,
16
- createMockWebListener,
17
- createScriptedWebListenerFactory,
18
- createWebListenerFactoryCapture,
19
- getLastWebAutoReplySessionSocket,
20
- installWebAutoReplyTestHomeHooks,
21
- installWebAutoReplyUnitTestHooks,
22
- makeSessionStore,
23
- resetLoadConfigMock,
24
- sendWebDirectInboundMessage,
25
- setLoadConfigMock,
26
- setRuntimeConfigSourceSnapshotMock,
27
- startWebAutoReplyMonitor,
28
- } from "./auto-reply.test-harness.js";
29
-
30
- installWebAutoReplyTestHomeHooks();
31
-
32
- async function startWatchdogScenario(params: {
33
- monitorWebChannel: typeof import("./auto-reply/monitor.js").monitorWebChannel;
34
- }) {
35
- const sleep = vi.fn(async () => {});
36
- const scripted = createScriptedWebListenerFactory();
37
- const started = startWebAutoReplyMonitor({
38
- monitorWebChannelFn: params.monitorWebChannel as never,
39
- listenerFactory: scripted.listenerFactory,
40
- sleep,
41
- heartbeatSeconds: 60,
42
- messageTimeoutMs: 30,
43
- watchdogCheckMs: 5,
44
- });
45
-
46
- await vi.waitFor(
47
- () => {
48
- expect(scripted.getListenerCount()).toBe(1);
49
- },
50
- { timeout: 250, interval: 2 },
51
- );
52
- await vi.waitFor(
53
- () => {
54
- expect(scripted.getOnMessage()).toBeTypeOf("function");
55
- },
56
- { timeout: 250, interval: 2 },
57
- );
58
-
59
- const spies = createWebInboundDeliverySpies();
60
- await sendWebDirectInboundMessage({
61
- onMessage: scripted.getOnMessage()!,
62
- body: "hi",
63
- from: "+1",
64
- to: "+2",
65
- id: "m1",
66
- spies,
67
- });
68
-
69
- return { scripted, sleep, spies, ...started };
70
- }
71
-
72
- describe("web auto-reply connection", () => {
73
- installWebAutoReplyUnitTestHooks();
74
-
75
- let monitorWebChannel: typeof import("./auto-reply/monitor.js").monitorWebChannel;
76
- beforeAll(async () => {
77
- ({ monitorWebChannel } = await import("./auto-reply/monitor.js"));
78
- });
79
-
80
- it("handles helper envelope timestamps with trimmed timezones (regression)", () => {
81
- const d = new Date("2025-01-01T00:00:00.000Z");
82
- expect(() => formatEnvelopeTimestamp(d, " America/Los_Angeles ")).not.toThrow();
83
- });
84
-
85
- it("handles reconnect progress and max-attempt stop behavior", async () => {
86
- for (const scenario of [
87
- {
88
- reconnect: { initialMs: 10, maxMs: 10, maxAttempts: 3, factor: 1.1 },
89
- expectedCallsAfterFirstClose: 2,
90
- closeTwiceAndFinish: false,
91
- expectedError: "Retry 1",
92
- },
93
- {
94
- reconnect: { initialMs: 5, maxMs: 5, maxAttempts: 2, factor: 1.1 },
95
- expectedCallsAfterFirstClose: 2,
96
- closeTwiceAndFinish: true,
97
- expectedError: "max attempts reached",
98
- },
99
- ]) {
100
- const sleep = vi.fn(async () => {});
101
- const scripted = createScriptedWebListenerFactory();
102
- const { runtime, controller, run } = startWebAutoReplyMonitor({
103
- monitorWebChannelFn: monitorWebChannel as never,
104
- listenerFactory: scripted.listenerFactory,
105
- sleep,
106
- reconnect: scenario.reconnect,
107
- });
108
-
109
- await vi.waitFor(
110
- () => {
111
- expect(scripted.getListenerCount()).toBe(1);
112
- },
113
- { timeout: 250, interval: 2 },
114
- );
115
-
116
- scripted.resolveClose(0);
117
- await vi.waitFor(
118
- () => {
119
- expect(scripted.getListenerCount()).toBe(scenario.expectedCallsAfterFirstClose);
120
- },
121
- { timeout: 250, interval: 2 },
122
- );
123
-
124
- if (scenario.closeTwiceAndFinish) {
125
- scripted.resolveClose(1);
126
- await run;
127
- } else {
128
- controller.abort();
129
- scripted.resolveClose(1);
130
- await Promise.resolve();
131
- await run;
132
- }
133
-
134
- expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining(scenario.expectedError));
135
- }
136
- });
137
-
138
- it("treats status 440 as non-retryable and stops without retrying", async () => {
139
- const sleep = vi.fn(async () => {});
140
- const scripted = createScriptedWebListenerFactory();
141
- const { runtime, controller, run } = startWebAutoReplyMonitor({
142
- monitorWebChannelFn: monitorWebChannel as never,
143
- listenerFactory: scripted.listenerFactory,
144
- sleep,
145
- reconnect: { initialMs: 10, maxMs: 10, maxAttempts: 3, factor: 1.1 },
146
- });
147
-
148
- await vi.waitFor(
149
- () => {
150
- expect(scripted.getListenerCount()).toBe(1);
151
- },
152
- { timeout: 250, interval: 2 },
153
- );
154
- scripted.resolveClose(0, {
155
- status: 440,
156
- isLoggedOut: false,
157
- error: "Unknown Stream Errored (conflict)",
158
- });
159
-
160
- const completedQuickly = await Promise.race([
161
- run.then(() => true),
162
- new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 60)),
163
- ]);
164
-
165
- if (!completedQuickly) {
166
- await vi.waitFor(
167
- () => {
168
- expect(scripted.getListenerCount()).toBeGreaterThanOrEqual(2);
169
- },
170
- { timeout: 250, interval: 2 },
171
- );
172
- controller.abort();
173
- scripted.resolveClose(1, { status: 499, isLoggedOut: false, error: "aborted" });
174
- await run;
175
- }
176
-
177
- expect(completedQuickly).toBe(true);
178
- expect(scripted.getListenerCount()).toBe(1);
179
- expect(sleep).not.toHaveBeenCalled();
180
- expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("status 440"));
181
- expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("session conflict"));
182
- expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("Stopping web monitoring"));
183
- });
184
-
185
- it.each([
186
- {
187
- status: 440,
188
- isLoggedOut: false,
189
- healthState: "conflict",
190
- error: "Unknown Stream Errored (conflict)",
191
- },
192
- {
193
- status: 401,
194
- isLoggedOut: true,
195
- healthState: "logged-out",
196
- error: "Stream Errored (logged out)",
197
- },
198
- ] as const)(
199
- "clears stale auth and active listener after terminal status $status",
200
- async ({ status, isLoggedOut, healthState, error }) => {
201
- const accountId = `terminal-${status}`;
202
- const authDir = path.join(resolveOAuthDir(), "whatsapp", accountId);
203
- await fs.mkdir(authDir, { recursive: true });
204
- await fs.writeFile(
205
- resolveWebCredsPath(authDir),
206
- JSON.stringify({ me: { id: "123@s.whatsapp.net" } }),
207
- );
208
- setLoadConfigMock({
209
- channels: {
210
- whatsapp: {
211
- allowFrom: ["*"],
212
- accounts: {
213
- [accountId]: {
214
- authDir,
215
- },
216
- },
217
- },
218
- },
219
- messages: {
220
- messagePrefix: undefined,
221
- responsePrefix: undefined,
222
- },
223
- });
224
-
225
- const sleep = vi.fn(async () => {});
226
- const statuses: Array<{ healthState?: string; running?: boolean; connected?: boolean }> = [];
227
- const scripted = createScriptedWebListenerFactory();
228
- const { run } = startWebAutoReplyMonitor({
229
- monitorWebChannelFn: monitorWebChannel as never,
230
- listenerFactory: scripted.listenerFactory,
231
- sleep,
232
- accountId,
233
- statusSink: (next) => statuses.push(next),
234
- reconnect: { initialMs: 10, maxMs: 10, maxAttempts: 3, factor: 1.1 },
235
- });
236
-
237
- await vi.waitFor(
238
- () => {
239
- expect(scripted.getListenerCount()).toBe(1);
240
- expect(getActiveWebListener(accountId)).not.toBeNull();
241
- },
242
- { timeout: 250, interval: 2 },
243
- );
244
-
245
- scripted.resolveClose(0, { status, isLoggedOut, error });
246
- await run;
247
-
248
- expect(scripted.getListenerCount()).toBe(1);
249
- expect(sleep).not.toHaveBeenCalled();
250
- expect(getActiveWebListener(accountId)).toBeNull();
251
- await expect(fs.stat(authDir)).rejects.toMatchObject({ code: "ENOENT" });
252
- expect(statuses).toContainEqual(
253
- expect.objectContaining({
254
- connected: false,
255
- healthState,
256
- }),
257
- );
258
- expect(statuses.at(-1)).toEqual(
259
- expect.objectContaining({
260
- running: false,
261
- connected: false,
262
- healthState,
263
- }),
264
- );
265
- },
266
- );
267
-
268
- it("retries inbox attach when auth state is still stabilizing", async () => {
269
- const sleep = vi.fn(async () => {});
270
- const listenerFactory = vi.fn(async () => {
271
- if (listenerFactory.mock.calls.length === 1) {
272
- throw new WhatsAppAuthUnstableError(
273
- "WhatsApp auth state is still stabilizing; retrying inbox attach.",
274
- );
275
- }
276
- return createMockWebListener();
277
- });
278
- const { runtime, controller, run } = startWebAutoReplyMonitor({
279
- monitorWebChannelFn: monitorWebChannel as never,
280
- listenerFactory,
281
- sleep,
282
- reconnect: { initialMs: 5, maxMs: 5, maxAttempts: 3, factor: 1.1 },
283
- });
284
-
285
- await vi.waitFor(
286
- () => {
287
- expect(listenerFactory).toHaveBeenCalledTimes(2);
288
- },
289
- { timeout: 250, interval: 2 },
290
- );
291
-
292
- controller.abort();
293
- await run;
294
-
295
- expect(sleep).toHaveBeenCalledWith(expect.any(Number), expect.any(AbortSignal));
296
- expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("inbox attach"));
297
- });
298
-
299
- it("stops retrying inbox attach when auth stays unstable past max attempts", async () => {
300
- const sleep = vi.fn(async () => {});
301
- const listenerFactory = vi.fn(async () => {
302
- throw new WhatsAppAuthUnstableError(
303
- "WhatsApp auth state is still stabilizing; retrying inbox attach.",
304
- );
305
- });
306
- const { runtime, run } = startWebAutoReplyMonitor({
307
- monitorWebChannelFn: monitorWebChannel as never,
308
- listenerFactory,
309
- sleep,
310
- reconnect: { initialMs: 5, maxMs: 5, maxAttempts: 2, factor: 1.1 },
311
- });
312
-
313
- await run;
314
-
315
- expect(listenerFactory).toHaveBeenCalledTimes(2);
316
- expect(sleep).toHaveBeenCalledTimes(1);
317
- expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("Retry 1/2"));
318
- expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("Stopping web monitoring"));
319
- });
320
-
321
- it("forces reconnect when watchdog closes without onClose", async () => {
322
- vi.useFakeTimers();
323
- try {
324
- const { scripted, controller, run } = await startWatchdogScenario({
325
- monitorWebChannel,
326
- });
327
-
328
- await vi.advanceTimersByTimeAsync(200);
329
- await Promise.resolve();
330
- await vi.waitFor(
331
- () => {
332
- expect(scripted.getListenerCount()).toBeGreaterThanOrEqual(2);
333
- },
334
- { timeout: 250, interval: 2 },
335
- );
336
-
337
- controller.abort();
338
- scripted.resolveClose(1, { status: 499, isLoggedOut: false });
339
- await Promise.resolve();
340
- await run;
341
- } finally {
342
- vi.useRealTimers();
343
- }
344
- });
345
-
346
- it("keeps quiet linked-device sessions open when transport frames keep arriving", async () => {
347
- vi.useFakeTimers();
348
- try {
349
- const sleep = vi.fn(async () => {});
350
- const scripted = createScriptedWebListenerFactory();
351
- const { controller, run } = startWebAutoReplyMonitor({
352
- monitorWebChannelFn: monitorWebChannel as never,
353
- listenerFactory: scripted.listenerFactory,
354
- sleep,
355
- heartbeatSeconds: 60,
356
- messageTimeoutMs: 30,
357
- watchdogCheckMs: 5,
358
- });
359
-
360
- await vi.waitFor(
361
- () => {
362
- expect(scripted.getListenerCount()).toBe(1);
363
- },
364
- { timeout: 250, interval: 2 },
365
- );
366
-
367
- const socket = getLastWebAutoReplySessionSocket();
368
- await vi.advanceTimersByTimeAsync(20);
369
- socket.ws.emit("frame");
370
- await vi.advanceTimersByTimeAsync(20);
371
- socket.ws.emit("frame");
372
- await vi.advanceTimersByTimeAsync(20);
373
-
374
- expect(scripted.getListenerCount()).toBe(1);
375
-
376
- controller.abort();
377
- scripted.resolveClose(0, { status: 499, isLoggedOut: false });
378
- await Promise.resolve();
379
- await run;
380
- } finally {
381
- vi.useRealTimers();
382
- }
383
- });
384
-
385
- it("does not let transport frames mask application silence forever", async () => {
386
- vi.useFakeTimers();
387
- try {
388
- const sleep = vi.fn(async () => {});
389
- const scripted = createScriptedWebListenerFactory();
390
- const { controller, run } = startWebAutoReplyMonitor({
391
- monitorWebChannelFn: monitorWebChannel as never,
392
- listenerFactory: scripted.listenerFactory,
393
- sleep,
394
- heartbeatSeconds: 60,
395
- messageTimeoutMs: 30,
396
- watchdogCheckMs: 5,
397
- });
398
-
399
- await vi.waitFor(
400
- () => {
401
- expect(scripted.getListenerCount()).toBe(1);
402
- },
403
- { timeout: 250, interval: 2 },
404
- );
405
-
406
- const socket = getLastWebAutoReplySessionSocket();
407
- for (let elapsedMs = 0; elapsedMs < 140; elapsedMs += 20) {
408
- socket.ws.emit("frame");
409
- await vi.advanceTimersByTimeAsync(20);
410
- }
411
- await vi.waitFor(
412
- () => {
413
- expect(scripted.getListenerCount()).toBeGreaterThanOrEqual(2);
414
- },
415
- { timeout: 250, interval: 2 },
416
- );
417
-
418
- controller.abort();
419
- scripted.resolveClose(scripted.getListenerCount() - 1, {
420
- status: 499,
421
- isLoggedOut: false,
422
- error: "aborted",
423
- });
424
- await Promise.resolve();
425
- await run;
426
- } finally {
427
- vi.useRealTimers();
428
- }
429
- });
430
-
431
- it("publishes frame-driven transport activity for quiet sessions", async () => {
432
- vi.useFakeTimers();
433
- try {
434
- const sleep = vi.fn(async () => {});
435
- const statuses: Array<Record<string, unknown>> = [];
436
- const scripted = createScriptedWebListenerFactory();
437
- const { controller, run } = startWebAutoReplyMonitor({
438
- monitorWebChannelFn: monitorWebChannel as never,
439
- listenerFactory: scripted.listenerFactory,
440
- sleep,
441
- heartbeatSeconds: 1,
442
- transportTimeoutMs: 60_000,
443
- messageTimeoutMs: 60_000,
444
- watchdogCheckMs: 5,
445
- statusSink: (next) => statuses.push({ ...next }),
446
- });
447
-
448
- await vi.waitFor(
449
- () => {
450
- expect(scripted.getListenerCount()).toBe(1);
451
- },
452
- { timeout: 250, interval: 2 },
453
- );
454
-
455
- const initialTransportAt = Number(statuses.at(-1)?.lastTransportActivityAt ?? 0);
456
- const socket = getLastWebAutoReplySessionSocket();
457
- await vi.advanceTimersByTimeAsync(250);
458
- socket.ws.emit("frame");
459
- await vi.advanceTimersByTimeAsync(1_000);
460
-
461
- const lastTransportAt = Number(statuses.at(-1)?.lastTransportActivityAt ?? 0);
462
- expect(lastTransportAt).toBeGreaterThan(initialTransportAt);
463
-
464
- controller.abort();
465
- scripted.resolveClose(0, { status: 499, isLoggedOut: false, error: "aborted" });
466
- await Promise.resolve();
467
- await run;
468
- } finally {
469
- vi.useRealTimers();
470
- }
471
- });
472
-
473
- it("reconnects on transport stall before the long app-silence window", async () => {
474
- vi.useFakeTimers();
475
- try {
476
- const sleep = vi.fn(async () => {});
477
- const scripted = createScriptedWebListenerFactory();
478
- const { controller, run } = startWebAutoReplyMonitor({
479
- monitorWebChannelFn: monitorWebChannel as never,
480
- listenerFactory: scripted.listenerFactory,
481
- sleep,
482
- heartbeatSeconds: 1,
483
- transportTimeoutMs: 30,
484
- messageTimeoutMs: 3_000,
485
- watchdogCheckMs: 5,
486
- });
487
-
488
- await vi.waitFor(
489
- () => {
490
- expect(scripted.getListenerCount()).toBe(1);
491
- },
492
- { timeout: 250, interval: 2 },
493
- );
494
-
495
- await vi.advanceTimersByTimeAsync(36);
496
- await Promise.resolve();
497
- await vi.waitFor(
498
- () => {
499
- expect(scripted.getListenerCount()).toBeGreaterThanOrEqual(2);
500
- },
501
- { timeout: 250, interval: 2 },
502
- );
503
-
504
- controller.abort();
505
- scripted.resolveClose(scripted.getListenerCount() - 1, {
506
- status: 499,
507
- isLoggedOut: false,
508
- error: "aborted",
509
- });
510
- await Promise.resolve();
511
- await run;
512
- } finally {
513
- vi.useRealTimers();
514
- }
515
- });
516
-
517
- it("recovers a post-408 listener when transport frames continue but app delivery stays silent", async () => {
518
- vi.useFakeTimers();
519
- try {
520
- const { scripted, controller, run } = await startWatchdogScenario({
521
- monitorWebChannel,
522
- });
523
-
524
- scripted.resolveClose(0, {
525
- status: 408,
526
- isLoggedOut: false,
527
- error: "status=408 Request Time-out",
528
- });
529
- await vi.waitFor(
530
- () => {
531
- expect(scripted.getListenerCount()).toBe(2);
532
- },
533
- { timeout: 250, interval: 2 },
534
- );
535
-
536
- const reconnectedSocket = getLastWebAutoReplySessionSocket();
537
- for (let elapsedMs = 0; elapsedMs < 45; elapsedMs += 5) {
538
- reconnectedSocket.ws.emit("frame");
539
- await vi.advanceTimersByTimeAsync(5);
540
- }
541
-
542
- await vi.waitFor(
543
- () => {
544
- expect(scripted.getListenerCount()).toBeGreaterThanOrEqual(3);
545
- },
546
- { timeout: 250, interval: 2 },
547
- );
548
-
549
- controller.abort();
550
- scripted.resolveClose(scripted.getListenerCount() - 1, {
551
- status: 499,
552
- isLoggedOut: false,
553
- error: "aborted",
554
- });
555
- await Promise.resolve();
556
- await run;
557
- } finally {
558
- vi.useRealTimers();
559
- }
560
- });
561
-
562
- it("gives a reconnected listener a fresh watchdog window", async () => {
563
- vi.useFakeTimers();
564
- try {
565
- const { scripted, controller, run } = await startWatchdogScenario({
566
- monitorWebChannel,
567
- });
568
-
569
- scripted.resolveClose(0, { status: 499, isLoggedOut: false, error: "first-close" });
570
- await vi.waitFor(
571
- () => {
572
- expect(scripted.getListenerCount()).toBe(2);
573
- },
574
- { timeout: 250, interval: 2 },
575
- );
576
-
577
- await vi.advanceTimersByTimeAsync(20);
578
- await Promise.resolve();
579
- expect(scripted.getListenerCount()).toBe(2);
580
-
581
- await vi.advanceTimersByTimeAsync(20);
582
- await Promise.resolve();
583
- await vi.waitFor(
584
- () => {
585
- expect(scripted.getListenerCount()).toBeGreaterThanOrEqual(3);
586
- },
587
- { timeout: 250, interval: 2 },
588
- );
589
-
590
- controller.abort();
591
- scripted.resolveClose(scripted.getListenerCount() - 1, {
592
- status: 499,
593
- isLoggedOut: false,
594
- error: "aborted",
595
- });
596
- await Promise.resolve();
597
- await run;
598
- } finally {
599
- vi.useRealTimers();
600
- }
601
- });
602
-
603
- it("passes accounts.default debounceMs into the live listener for named accounts", async () => {
604
- const capture = createWebListenerFactoryCapture();
605
-
606
- setLoadConfigMock({
607
- channels: {
608
- whatsapp: {
609
- accounts: {
610
- default: {
611
- debounceMs: 250,
612
- },
613
- work: {
614
- authDir: "/tmp/work",
615
- },
616
- },
617
- },
618
- },
619
- } as OpenClawConfig);
620
-
621
- await monitorWebChannel(
622
- false,
623
- capture.listenerFactory as never,
624
- false,
625
- async () => ({ text: "ok" }),
626
- undefined,
627
- undefined,
628
- {
629
- accountId: "work",
630
- },
631
- );
632
-
633
- resetLoadConfigMock();
634
- expect(capture.getLastOptions()?.debounceMs).toBe(250);
635
- });
636
-
637
- it("matches per-account debounce overrides case-insensitively", async () => {
638
- const capture = createWebListenerFactoryCapture();
639
-
640
- setLoadConfigMock({
641
- channels: {
642
- whatsapp: {
643
- accounts: {
644
- work: {
645
- authDir: "/tmp/work",
646
- debounceMs: 250,
647
- },
648
- },
649
- },
650
- },
651
- } as OpenClawConfig);
652
-
653
- await monitorWebChannel(
654
- false,
655
- capture.listenerFactory as never,
656
- false,
657
- async () => ({ text: "ok" }),
658
- undefined,
659
- undefined,
660
- {
661
- accountId: "Work",
662
- },
663
- );
664
-
665
- resetLoadConfigMock();
666
- expect(capture.getLastOptions()?.debounceMs).toBe(250);
667
- });
668
-
669
- it("keeps the global inbound debounce fallback when WhatsApp debounceMs is only the schema default", async () => {
670
- const capture = createWebListenerFactoryCapture();
671
-
672
- setLoadConfigMock({
673
- messages: {
674
- inbound: {
675
- debounceMs: 250,
676
- },
677
- },
678
- channels: {
679
- whatsapp: {
680
- accounts: {
681
- work: {
682
- authDir: "/tmp/work",
683
- },
684
- },
685
- },
686
- },
687
- } as OpenClawConfig);
688
- setRuntimeConfigSourceSnapshotMock(null);
689
-
690
- await monitorWebChannel(
691
- false,
692
- capture.listenerFactory as never,
693
- false,
694
- async () => ({ text: "ok" }),
695
- undefined,
696
- undefined,
697
- {
698
- accountId: "work",
699
- },
700
- );
701
-
702
- resetLoadConfigMock();
703
- expect(capture.getLastOptions()?.debounceMs).toBe(250);
704
- });
705
-
706
- it("processes inbound messages without batching and preserves timestamps", async () => {
707
- await withEnvAsync({ TZ: "Europe/Vienna" }, async () => {
708
- const originalMax = process.getMaxListeners();
709
- process.setMaxListeners?.(1);
710
-
711
- const store = await makeSessionStore({
712
- main: { sessionId: "sid", updatedAt: Date.now() },
713
- });
714
-
715
- try {
716
- const sendMedia = vi.fn();
717
- const reply = vi.fn().mockResolvedValue(createAcceptedWhatsAppSendResult("text", "r1"));
718
- const sendComposing = vi.fn();
719
- const resolver = vi.fn().mockResolvedValue({ text: "ok" });
720
-
721
- const capture = createWebListenerFactoryCapture();
722
-
723
- setLoadConfigMock(() => ({
724
- agents: {
725
- defaults: {
726
- envelopeTimezone: "utc",
727
- },
728
- },
729
- session: { store: store.storePath },
730
- }));
731
-
732
- await monitorWebChannel(false, capture.listenerFactory as never, false, resolver);
733
- const capturedOnMessage = capture.getOnMessage();
734
- expect(capturedOnMessage).toBeDefined();
735
-
736
- const spies = { sendMedia, reply, sendComposing };
737
- await sendWebDirectInboundMessage({
738
- onMessage: capturedOnMessage!,
739
- body: "first",
740
- from: "+1",
741
- to: "+2",
742
- id: "m1",
743
- timestamp: 1735689600000,
744
- spies,
745
- });
746
- await sendWebDirectInboundMessage({
747
- onMessage: capturedOnMessage!,
748
- body: "second",
749
- from: "+1",
750
- to: "+2",
751
- id: "m2",
752
- timestamp: 1735693200000,
753
- spies,
754
- });
755
-
756
- expect(resolver).toHaveBeenCalledTimes(2);
757
- const firstArgs = resolver.mock.calls[0][0];
758
- const secondArgs = resolver.mock.calls[1][0];
759
- const firstTimestamp = formatEnvelopeTimestamp(new Date("2025-01-01T00:00:00Z"));
760
- const secondTimestamp = formatEnvelopeTimestamp(new Date("2025-01-01T01:00:00Z"));
761
- const firstPattern = escapeRegExp(firstTimestamp);
762
- const secondPattern = escapeRegExp(secondTimestamp);
763
- expect(firstArgs.Body).toMatch(
764
- new RegExp(`\\[WhatsApp \\+1 (\\+\\d+[smhd] )?${firstPattern}\\] \\[openclaw\\] first`),
765
- );
766
- expect(firstArgs.Body).not.toContain("second");
767
- expect(secondArgs.Body).toMatch(
768
- new RegExp(`\\[WhatsApp \\+1 (\\+\\d+[smhd] )?${secondPattern}\\] \\[openclaw\\] second`),
769
- );
770
- expect(secondArgs.Body).not.toContain("first");
771
- expect(process.getMaxListeners?.()).toBeGreaterThanOrEqual(50);
772
- } finally {
773
- process.setMaxListeners?.(originalMax);
774
- await store.cleanup();
775
- resetLoadConfigMock();
776
- }
777
- });
778
- });
779
-
780
- it("emits heartbeat logs with connection metadata", async () => {
781
- vi.useFakeTimers();
782
- const logPath = `/tmp/openclaw-heartbeat-${crypto.randomUUID()}.log`;
783
- setLoggerOverride({ level: "trace", file: logPath });
784
-
785
- const runtime = {
786
- log: vi.fn(),
787
- error: vi.fn(),
788
- exit: vi.fn(),
789
- };
790
-
791
- const controller = new AbortController();
792
- const listenerFactory = vi.fn(async () => {
793
- const onClose = new Promise<void>(() => {
794
- // never resolves; abort will short-circuit
795
- });
796
- return { close: vi.fn(), onClose };
797
- });
798
-
799
- const run = monitorWebChannel(
800
- false,
801
- listenerFactory as never,
802
- true,
803
- async () => ({ text: "ok" }),
804
- runtime as never,
805
- controller.signal,
806
- {
807
- heartbeatSeconds: 1,
808
- reconnect: { initialMs: 5, maxMs: 5, maxAttempts: 1, factor: 1.1 },
809
- },
810
- );
811
-
812
- await vi.advanceTimersByTimeAsync(1_000);
813
- controller.abort();
814
- await vi.runAllTimersAsync();
815
- await run.catch(() => {});
816
-
817
- const content = await fs.readFile(logPath, "utf-8");
818
- expect(content).toMatch(/web-heartbeat/);
819
- expect(content).toMatch(/connectionId/);
820
- expect(content).toMatch(/messagesHandled/);
821
- });
822
-
823
- it("logs outbound replies to file", async () => {
824
- const logPath = `/tmp/openclaw-log-test-${crypto.randomUUID()}.log`;
825
- setLoggerOverride({ level: "trace", file: logPath });
826
-
827
- const capture = createWebListenerFactoryCapture();
828
-
829
- const resolver = vi.fn().mockResolvedValue({ text: "auto" });
830
- await monitorWebChannel(false, capture.listenerFactory as never, false, resolver as never);
831
- const capturedOnMessage = capture.getOnMessage();
832
- expect(capturedOnMessage).toBeDefined();
833
-
834
- await capturedOnMessage?.({
835
- body: "hello",
836
- from: "+1",
837
- conversationId: "+1",
838
- to: "+2",
839
- accountId: "default",
840
- chatType: "direct",
841
- chatId: "+1",
842
- id: "msg1",
843
- sendComposing: vi.fn(),
844
- reply: vi.fn(),
845
- sendMedia: vi.fn(),
846
- });
847
-
848
- const content = await fs.readFile(logPath, "utf-8");
849
- expect(content).toMatch(/web-auto-reply/);
850
- expect(content).toMatch(/auto/);
851
- });
852
-
853
- it("marks dispatch idle after replies flush", async () => {
854
- const markDispatchIdle = vi.fn();
855
- const typingMock = {
856
- onReplyStart: vi.fn(async () => {}),
857
- startTypingLoop: vi.fn(async () => {}),
858
- startTypingOnText: vi.fn(async () => {}),
859
- refreshTypingTtl: vi.fn(),
860
- isActive: vi.fn(() => false),
861
- markRunComplete: vi.fn(),
862
- markDispatchIdle,
863
- cleanup: vi.fn(),
864
- };
865
- const reply = vi.fn().mockResolvedValue(createAcceptedWhatsAppSendResult("text", "r1"));
866
- const sendComposing = vi.fn().mockResolvedValue(undefined);
867
- const sendMedia = vi.fn().mockResolvedValue(createAcceptedWhatsAppSendResult("media", "m1"));
868
-
869
- const replyResolver = vi.fn().mockImplementation(async (ctx, opts) => {
870
- void ctx;
871
- opts?.onTypingController?.(typingMock);
872
- return { text: "final reply" };
873
- });
874
-
875
- const mockConfig: OpenClawConfig = {
876
- channels: { whatsapp: { allowFrom: ["*"] } },
877
- };
878
-
879
- setLoadConfigMock(mockConfig);
880
-
881
- await monitorWebChannel(
882
- false,
883
- async ({ onMessage }) => {
884
- await onMessage({
885
- id: "m1",
886
- from: "+1000",
887
- conversationId: "+1000",
888
- to: "+2000",
889
- body: "hello",
890
- timestamp: Date.now(),
891
- chatType: "direct",
892
- chatId: "direct:+1000",
893
- accountId: "default",
894
- sendComposing,
895
- reply,
896
- sendMedia,
897
- });
898
- return createMockWebListener();
899
- },
900
- false,
901
- replyResolver,
902
- );
903
-
904
- resetLoadConfigMock();
905
-
906
- expect(markDispatchIdle).toHaveBeenCalled();
907
- });
908
- });