@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.
- package/dist/access-control-BQ3tCkH8.js +288 -0
- package/dist/account-config-CNQ33oav.js +47 -0
- package/dist/account-ids-DG18xddX.js +5 -0
- package/dist/accounts-BDqgpFEB.js +116 -0
- package/dist/action-runtime-DTGNgG-i.js +74 -0
- package/dist/action-runtime-api.js +2 -0
- package/dist/action-runtime.runtime.js +2 -0
- package/dist/active-listener-N7GFKsuN.js +11 -0
- package/dist/agent-tools-login-DpRxLNRB.js +89 -0
- package/dist/api.js +148 -0
- package/dist/audio-preflight.runtime-Bcdsl_ri.js +7 -0
- package/dist/auth-presence.js +32 -0
- package/dist/auth-store-BjCb50Ry.js +381 -0
- package/dist/channel-DQ-5Xoj9.js +542 -0
- package/dist/channel-config-api.js +2 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel-react-action-Df20qAy9.js +42 -0
- package/dist/channel.runtime-DKXRnA8x.js +55 -0
- package/dist/channel.setup-DOHR2BGp.js +20 -0
- package/dist/command-policy-O8sRnqUV.js +8 -0
- package/dist/config-schema-Dmo66Ubb.js +27 -0
- package/dist/connection-controller-BiHAsTM8.js +596 -0
- package/dist/connection-controller-registry-D_iIGz5m.js +22 -0
- package/dist/constants-DLGCIbfl.js +4 -0
- package/dist/constants.js +2 -0
- package/dist/contract-api.js +19 -0
- package/dist/creds-files-_2V8jzQa.js +19 -0
- package/dist/directory-config-DVs4ffXu.js +37 -0
- package/dist/directory-contract-api.js +2 -0
- package/dist/doctor-A9tXm9Om.js +48 -0
- package/dist/doctor-contract-api.js +10 -0
- package/dist/group-session-contract-B8QBn0I2.js +15 -0
- package/dist/identity-QHAlV7Fu.js +75 -0
- package/dist/index.js +18 -0
- package/dist/legacy-session-surface-api.js +8 -0
- package/dist/legacy-state-migrations-api.js +2 -0
- package/dist/light-runtime-api.js +5 -0
- package/dist/login-DymdrKca.js +52 -0
- package/dist/login-qr-DIS0Iv-7.js +378 -0
- package/dist/login-qr-api.js +2 -0
- package/dist/login-qr-runtime.js +16 -0
- package/dist/monitor-C4Fhg_lD.js +3349 -0
- package/dist/normalize-CWYiGkXw.js +2 -0
- package/dist/normalize-target-BPVHNsM5.js +74 -0
- package/dist/outbound-adapter-BssVnZdz.js +29 -0
- package/dist/outbound-base-Bq1UYEK5.js +119 -0
- package/dist/outbound-media-contract-CGss_QHy.js +163 -0
- package/dist/outbound-payload-test-api.js +2 -0
- package/dist/quoted-message-TENQzyxV.js +108 -0
- package/dist/reaction-level-CRVEVAfi.js +16 -0
- package/dist/reply-resolver.runtime-BRCgoO4C.js +2 -0
- package/dist/resolve-outbound-target-CN-Wo6IY.js +39 -0
- package/dist/rolldown-runtime-RkAeH_Qm.js +27 -0
- package/dist/runtime-6fZ67abR.js +8 -0
- package/dist/runtime-api.js +15 -0
- package/dist/runtime-group-policy-Gp1lQ0at.js +11 -0
- package/dist/secret-contract-api.js +5 -0
- package/dist/security-contract-DqH8JDX6.js +26 -0
- package/dist/security-contract-api.js +2 -0
- package/dist/send-CIyLFxdZ.js +234 -0
- package/dist/session-contract-B0efQf9P.js +27 -0
- package/dist/session-errors-CSuKZPub.js +63 -0
- package/dist/session.runtime-C1eSE_KK.js +13 -0
- package/dist/setup-core-DZKvpzbH.js +46 -0
- package/dist/setup-entry.js +23 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/setup-surface-ieFnIW4g.js +344 -0
- package/dist/shared-D_wzSVar.js +307 -0
- package/dist/state-migrations-DnCdvoYD.js +39 -0
- package/dist/targets.js +2 -0
- package/dist/test-api.js +3 -0
- package/dist/text-runtime-C1zNiZeO.js +116 -0
- package/package.json +14 -6
- package/action-runtime-api.ts +0 -1
- package/action-runtime.runtime.ts +0 -1
- package/api.ts +0 -213
- package/auth-presence.ts +0 -80
- package/channel-config-api.ts +0 -1
- package/channel-plugin-api.ts +0 -3
- package/config-api.ts +0 -4
- package/constants.ts +0 -1
- package/contract-api.ts +0 -29
- package/directory-contract-api.ts +0 -4
- package/doctor-contract-api.ts +0 -8
- package/index.test.ts +0 -13
- package/index.ts +0 -16
- package/legacy-session-surface-api.ts +0 -6
- package/legacy-state-migrations-api.ts +0 -1
- package/light-runtime-api.ts +0 -12
- package/login-qr-api.ts +0 -1
- package/login-qr-runtime.ts +0 -23
- package/outbound-payload-test-api.ts +0 -1
- package/runtime-api.ts +0 -84
- package/secret-contract-api.ts +0 -4
- package/security-contract-api.ts +0 -4
- package/setup-entry.test.ts +0 -21
- package/setup-entry.ts +0 -21
- package/setup-plugin-api.ts +0 -3
- package/src/account-config.ts +0 -77
- package/src/account-ids.ts +0 -13
- package/src/account-types.ts +0 -5
- package/src/accounts.test.ts +0 -182
- package/src/accounts.ts +0 -176
- package/src/accounts.whatsapp-auth.test.ts +0 -59
- package/src/action-runtime-target-auth.ts +0 -27
- package/src/action-runtime.test.ts +0 -330
- package/src/action-runtime.ts +0 -76
- package/src/active-listener.test.ts +0 -65
- package/src/active-listener.ts +0 -17
- package/src/agent-tools-login.test.ts +0 -81
- package/src/agent-tools-login.ts +0 -113
- package/src/approval-auth.test.ts +0 -24
- package/src/approval-auth.ts +0 -27
- package/src/auth-store.runtime.ts +0 -1
- package/src/auth-store.test.ts +0 -311
- package/src/auth-store.ts +0 -502
- package/src/auto-reply/config.runtime.ts +0 -16
- package/src/auto-reply/constants.ts +0 -1
- package/src/auto-reply/deliver-reply.test.ts +0 -843
- package/src/auto-reply/deliver-reply.ts +0 -279
- package/src/auto-reply/loggers.ts +0 -6
- package/src/auto-reply/mentions.ts +0 -131
- package/src/auto-reply/monitor/ack-reaction.test.ts +0 -170
- package/src/auto-reply/monitor/ack-reaction.ts +0 -99
- package/src/auto-reply/monitor/audio-preflight.runtime.ts +0 -9
- package/src/auto-reply/monitor/broadcast.ts +0 -153
- package/src/auto-reply/monitor/commands.ts +0 -19
- package/src/auto-reply/monitor/echo.ts +0 -64
- package/src/auto-reply/monitor/group-activation.runtime.ts +0 -1
- package/src/auto-reply/monitor/group-activation.test.ts +0 -189
- package/src/auto-reply/monitor/group-activation.ts +0 -73
- package/src/auto-reply/monitor/group-gating.audio-preflight.test.ts +0 -103
- package/src/auto-reply/monitor/group-gating.runtime.ts +0 -8
- package/src/auto-reply/monitor/group-gating.ts +0 -217
- package/src/auto-reply/monitor/group-members.test.ts +0 -56
- package/src/auto-reply/monitor/group-members.ts +0 -65
- package/src/auto-reply/monitor/inbound-context.test.ts +0 -97
- package/src/auto-reply/monitor/inbound-context.ts +0 -92
- package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +0 -21
- package/src/auto-reply/monitor/inbound-dispatch.test.ts +0 -895
- package/src/auto-reply/monitor/inbound-dispatch.ts +0 -422
- package/src/auto-reply/monitor/last-route.test.ts +0 -37
- package/src/auto-reply/monitor/last-route.ts +0 -61
- package/src/auto-reply/monitor/message-line.runtime.ts +0 -38
- package/src/auto-reply/monitor/message-line.ts +0 -54
- package/src/auto-reply/monitor/on-message.audio-preflight.test.ts +0 -354
- package/src/auto-reply/monitor/on-message.ts +0 -291
- package/src/auto-reply/monitor/peer.ts +0 -17
- package/src/auto-reply/monitor/process-message.audio-preflight.test.ts +0 -420
- package/src/auto-reply/monitor/process-message.test.ts +0 -355
- package/src/auto-reply/monitor/process-message.ts +0 -531
- package/src/auto-reply/monitor/runtime-api.ts +0 -30
- package/src/auto-reply/monitor-state.test.ts +0 -64
- package/src/auto-reply/monitor-state.ts +0 -106
- package/src/auto-reply/monitor.ts +0 -600
- package/src/auto-reply/reply-resolver.runtime.ts +0 -1
- package/src/auto-reply/types.ts +0 -48
- package/src/auto-reply/util.ts +0 -63
- package/src/auto-reply/web-auto-reply-monitor.test.ts +0 -687
- package/src/auto-reply/web-auto-reply-utils.test.ts +0 -392
- package/src/auto-reply.broadcast-groups.combined.test.ts +0 -246
- package/src/auto-reply.broadcast-groups.test-harness.ts +0 -47
- package/src/auto-reply.impl.ts +0 -6
- package/src/auto-reply.test-harness.ts +0 -421
- package/src/auto-reply.ts +0 -1
- package/src/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts +0 -427
- package/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts +0 -908
- package/src/auto-reply.web-auto-reply.last-route.test.ts +0 -184
- package/src/channel-actions.runtime.ts +0 -7
- package/src/channel-actions.test.ts +0 -216
- package/src/channel-actions.ts +0 -84
- package/src/channel-outbound.test.ts +0 -131
- package/src/channel-outbound.ts +0 -36
- package/src/channel-react-action.runtime.ts +0 -7
- package/src/channel-react-action.test.ts +0 -294
- package/src/channel-react-action.ts +0 -84
- package/src/channel.runtime.ts +0 -117
- package/src/channel.setup.test.ts +0 -461
- package/src/channel.setup.ts +0 -28
- package/src/channel.ts +0 -339
- package/src/command-policy.ts +0 -7
- package/src/config-accessors.test.ts +0 -34
- package/src/config-accessors.ts +0 -22
- package/src/config-schema.test.ts +0 -121
- package/src/config-schema.ts +0 -6
- package/src/config-ui-hints.ts +0 -24
- package/src/connection-controller-registry.test.ts +0 -27
- package/src/connection-controller-registry.ts +0 -49
- package/src/connection-controller.test.ts +0 -282
- package/src/connection-controller.ts +0 -679
- package/src/creds-files.ts +0 -19
- package/src/creds-persistence.ts +0 -102
- package/src/directory-config.test.ts +0 -60
- package/src/directory-config.ts +0 -40
- package/src/directory-contract.test.ts +0 -56
- package/src/doctor-contract.ts +0 -11
- package/src/doctor.test.ts +0 -74
- package/src/doctor.ts +0 -56
- package/src/group-intro.ts +0 -15
- package/src/group-policy.test.ts +0 -36
- package/src/group-policy.ts +0 -40
- package/src/group-session-contract.ts +0 -20
- package/src/group-session-key.test.ts +0 -53
- package/src/group-session-key.ts +0 -41
- package/src/heartbeat.ts +0 -34
- package/src/identity.ts +0 -164
- package/src/inbound/access-control.test-harness.ts +0 -37
- package/src/inbound/access-control.test.ts +0 -375
- package/src/inbound/access-control.ts +0 -237
- package/src/inbound/dedupe.ts +0 -132
- package/src/inbound/extract.test.ts +0 -282
- package/src/inbound/extract.ts +0 -488
- package/src/inbound/lifecycle.ts +0 -39
- package/src/inbound/media.node.test.ts +0 -83
- package/src/inbound/media.ts +0 -102
- package/src/inbound/monitor.ts +0 -890
- package/src/inbound/runtime-api.ts +0 -7
- package/src/inbound/save-media.runtime.ts +0 -1
- package/src/inbound/send-api.test.ts +0 -319
- package/src/inbound/send-api.ts +0 -145
- package/src/inbound/send-result.ts +0 -61
- package/src/inbound/types.ts +0 -106
- package/src/inbound-context.contract.test.ts +0 -32
- package/src/inbound-policy.ts +0 -231
- package/src/inbound.media.test.ts +0 -320
- package/src/inbound.test.ts +0 -290
- package/src/inbound.ts +0 -9
- package/src/login-qr.test.ts +0 -483
- package/src/login-qr.ts +0 -542
- package/src/login.coverage.test.ts +0 -133
- package/src/login.test.ts +0 -89
- package/src/login.ts +0 -70
- package/src/logout.test.ts +0 -162
- package/src/media.test.ts +0 -480
- package/src/media.ts +0 -10
- package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +0 -400
- package/src/monitor-inbox.append-upsert.test-support.ts +0 -133
- package/src/monitor-inbox.behavior.test.ts +0 -5
- package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +0 -362
- package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +0 -306
- package/src/monitor-inbox.streams-inbound-messages.test-support.ts +0 -729
- package/src/monitor-inbox.test-harness.ts +0 -304
- package/src/normalize-target.ts +0 -123
- package/src/normalize.ts +0 -7
- package/src/outbound-adapter.poll.test.ts +0 -62
- package/src/outbound-adapter.sendpayload.test.ts +0 -195
- package/src/outbound-adapter.ts +0 -36
- package/src/outbound-base.test.ts +0 -560
- package/src/outbound-base.ts +0 -244
- package/src/outbound-media-contract.ts +0 -278
- package/src/outbound-media.runtime.ts +0 -36
- package/src/outbound-payload.contract.test.ts +0 -61
- package/src/outbound-send-deps.ts +0 -1
- package/src/outbound-test-support.ts +0 -16
- package/src/pairing-security.test-harness.ts +0 -59
- package/src/qr-image.ts +0 -1
- package/src/qr-terminal.ts +0 -1
- package/src/quoted-message.test.ts +0 -83
- package/src/quoted-message.ts +0 -184
- package/src/reaction-level.test.ts +0 -94
- package/src/reaction-level.ts +0 -21
- package/src/reconnect.test.ts +0 -51
- package/src/reconnect.ts +0 -55
- package/src/resolve-outbound-target.test.ts +0 -288
- package/src/resolve-outbound-target.ts +0 -58
- package/src/resolve-target.test.ts +0 -120
- package/src/runtime-api.ts +0 -59
- package/src/runtime-group-policy.ts +0 -16
- package/src/runtime.ts +0 -9
- package/src/security-contract.ts +0 -47
- package/src/security-fix.ts +0 -71
- package/src/send.test.ts +0 -534
- package/src/send.ts +0 -292
- package/src/session-contract.test.ts +0 -31
- package/src/session-contract.ts +0 -43
- package/src/session-errors.ts +0 -125
- package/src/session-route.test.ts +0 -41
- package/src/session-route.ts +0 -32
- package/src/session.runtime.ts +0 -8
- package/src/session.test.ts +0 -559
- package/src/session.ts +0 -330
- package/src/setup-core.ts +0 -52
- package/src/setup-finalize.ts +0 -446
- package/src/setup-surface.test.ts +0 -367
- package/src/setup-surface.ts +0 -69
- package/src/setup-test-helpers.ts +0 -216
- package/src/shared.ts +0 -291
- package/src/socket-timing.test.ts +0 -49
- package/src/socket-timing.ts +0 -38
- package/src/state-migrations.ts +0 -54
- package/src/status-issues.test.ts +0 -137
- package/src/status-issues.ts +0 -185
- package/src/system-prompt.test.ts +0 -199
- package/src/system-prompt.ts +0 -31
- package/src/targets-runtime.ts +0 -180
- package/src/test-helpers.ts +0 -690
- package/src/text-runtime.test.ts +0 -162
- package/src/text-runtime.ts +0 -11
- package/src/vcard.ts +0 -84
- package/targets.ts +0 -5
- package/test-api.ts +0 -2
- 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
|
-
});
|