@openclaw/whatsapp 2026.5.1-beta.1

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 (239) hide show
  1. package/action-runtime-api.ts +1 -0
  2. package/action-runtime.runtime.ts +1 -0
  3. package/api.ts +213 -0
  4. package/auth-presence.ts +80 -0
  5. package/channel-config-api.ts +1 -0
  6. package/channel-plugin-api.ts +3 -0
  7. package/config-api.ts +4 -0
  8. package/constants.ts +1 -0
  9. package/contract-api.ts +29 -0
  10. package/directory-contract-api.ts +4 -0
  11. package/dist/.boundary-tsc.stamp +1 -0
  12. package/dist/.boundary-tsc.tsbuildinfo +1 -0
  13. package/doctor-contract-api.ts +8 -0
  14. package/index.test.ts +13 -0
  15. package/index.ts +16 -0
  16. package/legacy-session-surface-api.ts +6 -0
  17. package/legacy-state-migrations-api.ts +1 -0
  18. package/light-runtime-api.ts +12 -0
  19. package/login-qr-api.ts +1 -0
  20. package/login-qr-runtime.ts +23 -0
  21. package/openclaw.plugin.json +742 -0
  22. package/outbound-payload-test-api.ts +1 -0
  23. package/package.json +74 -0
  24. package/runtime-api.ts +86 -0
  25. package/secret-contract-api.ts +4 -0
  26. package/security-contract-api.ts +4 -0
  27. package/setup-entry.test.ts +21 -0
  28. package/setup-entry.ts +21 -0
  29. package/setup-plugin-api.ts +3 -0
  30. package/src/account-config.ts +77 -0
  31. package/src/account-ids.ts +13 -0
  32. package/src/account-types.ts +5 -0
  33. package/src/accounts.test.ts +182 -0
  34. package/src/accounts.ts +176 -0
  35. package/src/accounts.whatsapp-auth.test.ts +59 -0
  36. package/src/action-runtime-target-auth.ts +27 -0
  37. package/src/action-runtime.test.ts +330 -0
  38. package/src/action-runtime.ts +76 -0
  39. package/src/active-listener.test.ts +65 -0
  40. package/src/active-listener.ts +17 -0
  41. package/src/agent-tools-login.test.ts +81 -0
  42. package/src/agent-tools-login.ts +113 -0
  43. package/src/approval-auth.test.ts +24 -0
  44. package/src/approval-auth.ts +27 -0
  45. package/src/auth-store.runtime.ts +1 -0
  46. package/src/auth-store.test.ts +311 -0
  47. package/src/auth-store.ts +502 -0
  48. package/src/auto-reply/config.runtime.ts +16 -0
  49. package/src/auto-reply/constants.ts +1 -0
  50. package/src/auto-reply/deliver-reply.test.ts +843 -0
  51. package/src/auto-reply/deliver-reply.ts +279 -0
  52. package/src/auto-reply/heartbeat-runner.runtime.ts +33 -0
  53. package/src/auto-reply/heartbeat-runner.test.ts +214 -0
  54. package/src/auto-reply/heartbeat-runner.ts +330 -0
  55. package/src/auto-reply/loggers.ts +6 -0
  56. package/src/auto-reply/mentions.ts +131 -0
  57. package/src/auto-reply/monitor/ack-reaction.test.ts +170 -0
  58. package/src/auto-reply/monitor/ack-reaction.ts +99 -0
  59. package/src/auto-reply/monitor/audio-preflight.runtime.ts +9 -0
  60. package/src/auto-reply/monitor/broadcast.ts +153 -0
  61. package/src/auto-reply/monitor/commands.ts +19 -0
  62. package/src/auto-reply/monitor/echo.ts +64 -0
  63. package/src/auto-reply/monitor/group-activation.runtime.ts +1 -0
  64. package/src/auto-reply/monitor/group-activation.test.ts +189 -0
  65. package/src/auto-reply/monitor/group-activation.ts +73 -0
  66. package/src/auto-reply/monitor/group-gating.audio-preflight.test.ts +103 -0
  67. package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
  68. package/src/auto-reply/monitor/group-gating.ts +217 -0
  69. package/src/auto-reply/monitor/group-members.test.ts +56 -0
  70. package/src/auto-reply/monitor/group-members.ts +65 -0
  71. package/src/auto-reply/monitor/inbound-context.test.ts +97 -0
  72. package/src/auto-reply/monitor/inbound-context.ts +92 -0
  73. package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +21 -0
  74. package/src/auto-reply/monitor/inbound-dispatch.test.ts +895 -0
  75. package/src/auto-reply/monitor/inbound-dispatch.ts +422 -0
  76. package/src/auto-reply/monitor/last-route.test.ts +37 -0
  77. package/src/auto-reply/monitor/last-route.ts +61 -0
  78. package/src/auto-reply/monitor/message-line.runtime.ts +38 -0
  79. package/src/auto-reply/monitor/message-line.ts +54 -0
  80. package/src/auto-reply/monitor/on-message.audio-preflight.test.ts +354 -0
  81. package/src/auto-reply/monitor/on-message.ts +291 -0
  82. package/src/auto-reply/monitor/peer.ts +17 -0
  83. package/src/auto-reply/monitor/process-message.audio-preflight.test.ts +420 -0
  84. package/src/auto-reply/monitor/process-message.test.ts +355 -0
  85. package/src/auto-reply/monitor/process-message.ts +531 -0
  86. package/src/auto-reply/monitor/runtime-api.ts +30 -0
  87. package/src/auto-reply/monitor-state.test.ts +64 -0
  88. package/src/auto-reply/monitor-state.ts +106 -0
  89. package/src/auto-reply/monitor.ts +600 -0
  90. package/src/auto-reply/reply-resolver.runtime.ts +1 -0
  91. package/src/auto-reply/session-snapshot.ts +69 -0
  92. package/src/auto-reply/types.ts +48 -0
  93. package/src/auto-reply/util.ts +63 -0
  94. package/src/auto-reply/web-auto-reply-monitor.test.ts +687 -0
  95. package/src/auto-reply/web-auto-reply-utils.test.ts +327 -0
  96. package/src/auto-reply.broadcast-groups.combined.test.ts +246 -0
  97. package/src/auto-reply.broadcast-groups.test-harness.ts +47 -0
  98. package/src/auto-reply.impl.ts +7 -0
  99. package/src/auto-reply.test-harness.ts +421 -0
  100. package/src/auto-reply.ts +1 -0
  101. package/src/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts +427 -0
  102. package/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts +908 -0
  103. package/src/auto-reply.web-auto-reply.last-route.test.ts +184 -0
  104. package/src/channel-actions.runtime.ts +7 -0
  105. package/src/channel-actions.test.ts +216 -0
  106. package/src/channel-actions.ts +84 -0
  107. package/src/channel-outbound.test.ts +131 -0
  108. package/src/channel-outbound.ts +36 -0
  109. package/src/channel-react-action.runtime.ts +7 -0
  110. package/src/channel-react-action.test.ts +294 -0
  111. package/src/channel-react-action.ts +84 -0
  112. package/src/channel.runtime.ts +117 -0
  113. package/src/channel.setup.test.ts +461 -0
  114. package/src/channel.setup.ts +28 -0
  115. package/src/channel.ts +336 -0
  116. package/src/command-policy.ts +7 -0
  117. package/src/config-accessors.test.ts +34 -0
  118. package/src/config-accessors.ts +22 -0
  119. package/src/config-schema.test.ts +121 -0
  120. package/src/config-schema.ts +6 -0
  121. package/src/config-ui-hints.ts +24 -0
  122. package/src/connection-controller-registry.test.ts +27 -0
  123. package/src/connection-controller-registry.ts +49 -0
  124. package/src/connection-controller.test.ts +282 -0
  125. package/src/connection-controller.ts +679 -0
  126. package/src/creds-files.ts +19 -0
  127. package/src/creds-persistence.ts +102 -0
  128. package/src/directory-config.test.ts +60 -0
  129. package/src/directory-config.ts +40 -0
  130. package/src/directory-contract.test.ts +56 -0
  131. package/src/doctor-contract.ts +11 -0
  132. package/src/doctor.test.ts +74 -0
  133. package/src/doctor.ts +56 -0
  134. package/src/group-intro.ts +15 -0
  135. package/src/group-policy.test.ts +36 -0
  136. package/src/group-policy.ts +40 -0
  137. package/src/group-session-contract.ts +20 -0
  138. package/src/group-session-key.test.ts +53 -0
  139. package/src/group-session-key.ts +41 -0
  140. package/src/heartbeat-recipients.runtime.ts +6 -0
  141. package/src/heartbeat-recipients.test.ts +203 -0
  142. package/src/heartbeat-recipients.ts +104 -0
  143. package/src/heartbeat.ts +34 -0
  144. package/src/identity.ts +164 -0
  145. package/src/inbound/access-control.test-harness.ts +37 -0
  146. package/src/inbound/access-control.test.ts +375 -0
  147. package/src/inbound/access-control.ts +237 -0
  148. package/src/inbound/dedupe.ts +132 -0
  149. package/src/inbound/extract.test.ts +282 -0
  150. package/src/inbound/extract.ts +488 -0
  151. package/src/inbound/lifecycle.ts +39 -0
  152. package/src/inbound/media.node.test.ts +83 -0
  153. package/src/inbound/media.ts +102 -0
  154. package/src/inbound/monitor.ts +890 -0
  155. package/src/inbound/runtime-api.ts +7 -0
  156. package/src/inbound/save-media.runtime.ts +1 -0
  157. package/src/inbound/send-api.test.ts +307 -0
  158. package/src/inbound/send-api.ts +141 -0
  159. package/src/inbound/send-result.ts +61 -0
  160. package/src/inbound/types.ts +106 -0
  161. package/src/inbound-context.contract.test.ts +32 -0
  162. package/src/inbound-policy.ts +231 -0
  163. package/src/inbound.media.test.ts +320 -0
  164. package/src/inbound.test.ts +290 -0
  165. package/src/inbound.ts +9 -0
  166. package/src/login-qr.test.ts +483 -0
  167. package/src/login-qr.ts +542 -0
  168. package/src/login.coverage.test.ts +133 -0
  169. package/src/login.test.ts +89 -0
  170. package/src/login.ts +70 -0
  171. package/src/logout.test.ts +162 -0
  172. package/src/media.test.ts +480 -0
  173. package/src/media.ts +10 -0
  174. package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +400 -0
  175. package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
  176. package/src/monitor-inbox.behavior.test.ts +5 -0
  177. package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +362 -0
  178. package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +306 -0
  179. package/src/monitor-inbox.streams-inbound-messages.test-support.ts +729 -0
  180. package/src/monitor-inbox.test-harness.ts +304 -0
  181. package/src/normalize-target.ts +112 -0
  182. package/src/normalize.ts +6 -0
  183. package/src/outbound-adapter.poll.test.ts +62 -0
  184. package/src/outbound-adapter.sendpayload.test.ts +195 -0
  185. package/src/outbound-adapter.ts +36 -0
  186. package/src/outbound-base.test.ts +560 -0
  187. package/src/outbound-base.ts +244 -0
  188. package/src/outbound-media-contract.ts +278 -0
  189. package/src/outbound-media.runtime.ts +36 -0
  190. package/src/outbound-payload.contract.test.ts +61 -0
  191. package/src/outbound-send-deps.ts +1 -0
  192. package/src/outbound-test-support.ts +16 -0
  193. package/src/pairing-security.test-harness.ts +59 -0
  194. package/src/qr-image.ts +1 -0
  195. package/src/qr-terminal.ts +1 -0
  196. package/src/quoted-message.test.ts +83 -0
  197. package/src/quoted-message.ts +184 -0
  198. package/src/reaction-level.test.ts +94 -0
  199. package/src/reaction-level.ts +21 -0
  200. package/src/reconnect.test.ts +51 -0
  201. package/src/reconnect.ts +55 -0
  202. package/src/resolve-outbound-target.test.ts +265 -0
  203. package/src/resolve-outbound-target.ts +54 -0
  204. package/src/resolve-target.test.ts +97 -0
  205. package/src/runtime-api.ts +60 -0
  206. package/src/runtime-group-policy.ts +16 -0
  207. package/src/runtime.ts +9 -0
  208. package/src/security-contract.ts +47 -0
  209. package/src/security-fix.ts +71 -0
  210. package/src/send.test.ts +515 -0
  211. package/src/send.ts +287 -0
  212. package/src/session-contract.test.ts +31 -0
  213. package/src/session-contract.ts +43 -0
  214. package/src/session-errors.ts +125 -0
  215. package/src/session-route.ts +26 -0
  216. package/src/session.runtime.ts +8 -0
  217. package/src/session.test.ts +559 -0
  218. package/src/session.ts +330 -0
  219. package/src/setup-core.ts +52 -0
  220. package/src/setup-finalize.ts +446 -0
  221. package/src/setup-surface.test.ts +367 -0
  222. package/src/setup-surface.ts +69 -0
  223. package/src/setup-test-helpers.ts +216 -0
  224. package/src/shared.ts +291 -0
  225. package/src/socket-timing.test.ts +49 -0
  226. package/src/socket-timing.ts +38 -0
  227. package/src/state-migrations.ts +54 -0
  228. package/src/status-issues.test.ts +137 -0
  229. package/src/status-issues.ts +185 -0
  230. package/src/system-prompt.test.ts +199 -0
  231. package/src/system-prompt.ts +31 -0
  232. package/src/targets-runtime.ts +180 -0
  233. package/src/test-helpers.ts +690 -0
  234. package/src/text-runtime.test.ts +162 -0
  235. package/src/text-runtime.ts +11 -0
  236. package/src/vcard.ts +84 -0
  237. package/targets.ts +5 -0
  238. package/test-api.ts +2 -0
  239. package/tsconfig.json +16 -0
@@ -0,0 +1,355 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { WhatsAppSendResult } from "../../inbound/send-result.js";
3
+
4
+ // Hoisted mocks used across tests so vi.mock factories can reference them.
5
+ const { resolvePolicyMock, buildContextMock, runMessageReceivedMock, trackBackgroundTaskMock } =
6
+ vi.hoisted(() => ({
7
+ resolvePolicyMock: vi.fn(),
8
+ buildContextMock: vi.fn(),
9
+ runMessageReceivedMock: vi.fn(async () => undefined),
10
+ trackBackgroundTaskMock: vi.fn(),
11
+ }));
12
+
13
+ function acceptedSendResult(kind: "media" | "text", id: string): WhatsAppSendResult {
14
+ return {
15
+ kind,
16
+ messageId: id,
17
+ messageIds: [id],
18
+ keys: [{ id }],
19
+ providerAccepted: true,
20
+ };
21
+ }
22
+
23
+ vi.mock("../../inbound-policy.js", async (importOriginal) => {
24
+ const actual = await importOriginal<typeof import("../../inbound-policy.js")>();
25
+ return {
26
+ ...actual,
27
+ resolveWhatsAppCommandAuthorized: async () => true,
28
+ resolveWhatsAppInboundPolicy: resolvePolicyMock,
29
+ };
30
+ });
31
+
32
+ vi.mock("./inbound-dispatch.js", async (importOriginal) => {
33
+ const actual = await importOriginal<typeof import("./inbound-dispatch.js")>();
34
+ return {
35
+ ...actual,
36
+ buildWhatsAppInboundContext: buildContextMock,
37
+ dispatchWhatsAppBufferedReply: async () => ({
38
+ queuedFinal: false,
39
+ counts: { tool: 0, block: 0, final: 0 },
40
+ }),
41
+ resolveWhatsAppDmRouteTarget: () => null,
42
+ resolveWhatsAppResponsePrefix: () => undefined,
43
+ updateWhatsAppMainLastRoute: () => {},
44
+ };
45
+ });
46
+
47
+ vi.mock("openclaw/plugin-sdk/plugin-runtime", () => ({
48
+ getGlobalHookRunner: () => ({
49
+ hasHooks: (hookName: string) => hookName === "message_received",
50
+ runMessageReceived: runMessageReceivedMock,
51
+ }),
52
+ }));
53
+
54
+ vi.mock("../../identity.js", async (importOriginal) => {
55
+ const actual = await importOriginal<typeof import("../../identity.js")>();
56
+ return {
57
+ ...actual,
58
+ getPrimaryIdentityId: () => null,
59
+ getSelfIdentity: () => ({ e164: "+15550001111" }),
60
+ getSenderIdentity: () => ({ name: "Alice", e164: "+15550002222" }),
61
+ };
62
+ });
63
+
64
+ vi.mock("../../reconnect.js", async (importOriginal) => {
65
+ const actual = await importOriginal<typeof import("../../reconnect.js")>();
66
+ return { ...actual, newConnectionId: () => "test-conn-id" };
67
+ });
68
+
69
+ vi.mock("../../session.js", async (importOriginal) => {
70
+ const actual = await importOriginal<typeof import("../../session.js")>();
71
+ return { ...actual, formatError: (e: unknown) => String(e) };
72
+ });
73
+
74
+ vi.mock("../deliver-reply.js", async (importOriginal) => {
75
+ const actual = await importOriginal<typeof import("../deliver-reply.js")>();
76
+ return { ...actual, deliverWebReply: async () => {} };
77
+ });
78
+
79
+ vi.mock("../loggers.js", async (importOriginal) => {
80
+ const actual = await importOriginal<typeof import("../loggers.js")>();
81
+ return {
82
+ ...actual,
83
+ whatsappInboundLog: { info: () => {}, debug: () => {} },
84
+ };
85
+ });
86
+
87
+ vi.mock("./ack-reaction.js", async (importOriginal) => {
88
+ const actual = await importOriginal<typeof import("./ack-reaction.js")>();
89
+ return { ...actual, maybeSendAckReaction: async () => {} };
90
+ });
91
+
92
+ vi.mock("./inbound-context.js", async (importOriginal) => {
93
+ const actual = await importOriginal<typeof import("./inbound-context.js")>();
94
+ return {
95
+ ...actual,
96
+ resolveVisibleWhatsAppGroupHistory: () => [],
97
+ resolveVisibleWhatsAppReplyContext: () => null,
98
+ };
99
+ });
100
+
101
+ vi.mock("./last-route.js", async (importOriginal) => {
102
+ const actual = await importOriginal<typeof import("./last-route.js")>();
103
+ return {
104
+ ...actual,
105
+ trackBackgroundTask: trackBackgroundTaskMock,
106
+ updateLastRouteInBackground: () => {},
107
+ };
108
+ });
109
+
110
+ vi.mock("./message-line.js", async (importOriginal) => {
111
+ const actual = await importOriginal<typeof import("./message-line.js")>();
112
+ return { ...actual, buildInboundLine: () => "hi" };
113
+ });
114
+
115
+ vi.mock("./runtime-api.js", async (importOriginal) => {
116
+ const actual = await importOriginal<typeof import("./runtime-api.js")>();
117
+ return {
118
+ ...actual,
119
+ buildHistoryContextFromEntries: () => "hi",
120
+ createChannelReplyPipeline: () => ({ onModelSelected: () => {}, responsePrefix: undefined }),
121
+ formatInboundEnvelope: () => "hi",
122
+ logVerbose: () => {},
123
+ normalizeE164: (v: string) => v,
124
+ recordSessionMetaFromInbound: async () => {},
125
+ resolveChannelContextVisibilityMode: () => "off",
126
+ resolveInboundSessionEnvelopeContext: () => ({
127
+ storePath: "/tmp",
128
+ envelopeOptions: {},
129
+ previousTimestamp: undefined,
130
+ }),
131
+ resolvePinnedMainDmOwnerFromAllowlist: () => null,
132
+ shouldComputeCommandAuthorized: () => false,
133
+ shouldLogVerbose: () => false,
134
+ };
135
+ });
136
+
137
+ import { clearInternalHooks, registerInternalHook } from "openclaw/plugin-sdk/hook-runtime";
138
+ import { processMessage } from "./process-message.js";
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Helpers
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function makeAccount(groups: Record<string, { systemPrompt?: string }> = {}): {
145
+ accountId: string;
146
+ authDir: string;
147
+ groups: Record<string, { systemPrompt?: string }>;
148
+ } {
149
+ return { accountId: "default", authDir: "/tmp/wa-test-auth", groups };
150
+ }
151
+
152
+ function makePolicy(account: ReturnType<typeof makeAccount>) {
153
+ return {
154
+ account,
155
+ dmPolicy: "pairing",
156
+ groupPolicy: "allowlist",
157
+ configuredAllowFrom: [],
158
+ dmAllowFrom: [],
159
+ groupAllowFrom: [],
160
+ isSelfChat: false,
161
+ providerMissingFallbackApplied: false,
162
+ shouldReadStorePairingApprovals: true,
163
+ isSamePhone: () => false,
164
+ isDmSenderAllowed: () => false,
165
+ isGroupSenderAllowed: () => false,
166
+ resolveConversationGroupPolicy: () => "allowlist",
167
+ resolveConversationRequireMention: () => false,
168
+ };
169
+ }
170
+
171
+ const GROUP_JID = "123@g.us";
172
+
173
+ const baseMsg = {
174
+ id: "msg1",
175
+ from: GROUP_JID,
176
+ to: "+15550001111",
177
+ conversationId: GROUP_JID,
178
+ accountId: "default",
179
+ chatId: GROUP_JID,
180
+ chatType: "group" as const,
181
+ body: "hi",
182
+ sendComposing: async () => {},
183
+ reply: async () => acceptedSendResult("text", "r1"),
184
+ sendMedia: async () => acceptedSendResult("media", "m1"),
185
+ };
186
+
187
+ const baseRoute = {
188
+ agentId: "main",
189
+ channel: "whatsapp",
190
+ accountId: "default",
191
+ sessionKey: "agent:main:whatsapp:group:123@g.us",
192
+ mainSessionKey: "agent:main:whatsapp:group:123@g.us",
193
+ lastRoutePolicy: "main",
194
+ matchedBy: "default",
195
+ };
196
+
197
+ function callProcessMessage(overrides: { cfg?: unknown } = {}) {
198
+ return processMessage({
199
+ cfg: (overrides.cfg ?? {}) as never,
200
+ msg: baseMsg as never,
201
+ route: baseRoute as never,
202
+ groupHistoryKey: "whatsapp:default:group:123@g.us",
203
+ groupHistories: new Map(),
204
+ groupMemberNames: new Map(),
205
+ connectionId: "conn-1",
206
+ verbose: false,
207
+ maxMediaBytes: 1024,
208
+ replyResolver: (async () => undefined) as never,
209
+ replyLogger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } as never,
210
+ backgroundTasks: new Set(),
211
+ rememberSentText: () => {},
212
+ echoHas: () => false,
213
+ echoForget: () => {},
214
+ buildCombinedEchoKey: ({ sessionKey }) => sessionKey,
215
+ });
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Tests
220
+ // ---------------------------------------------------------------------------
221
+
222
+ describe("processMessage group system prompt wiring", () => {
223
+ beforeEach(() => {
224
+ buildContextMock.mockReset();
225
+ resolvePolicyMock.mockReset();
226
+ runMessageReceivedMock.mockClear();
227
+ trackBackgroundTaskMock.mockClear();
228
+ clearInternalHooks();
229
+ buildContextMock.mockImplementation(
230
+ (params: { groupSystemPrompt?: string; combinedBody?: string }) => ({
231
+ GroupSystemPrompt: params.groupSystemPrompt,
232
+ Body: params.combinedBody ?? "",
233
+ }),
234
+ );
235
+ });
236
+
237
+ afterEach(() => {
238
+ clearInternalHooks();
239
+ });
240
+
241
+ it("resolves group systemPrompt from account config and passes it into buildWhatsAppInboundContext", async () => {
242
+ resolvePolicyMock.mockReturnValue(
243
+ makePolicy(makeAccount({ [GROUP_JID]: { systemPrompt: "from config" } })),
244
+ );
245
+
246
+ await callProcessMessage();
247
+
248
+ expect(buildContextMock.mock.calls[0][0].groupSystemPrompt).toBe("from config");
249
+ });
250
+
251
+ it("fires message_received hooks with canonical WhatsApp correlation fields", async () => {
252
+ const internalReceived = vi.fn();
253
+ registerInternalHook("message:received", internalReceived);
254
+ resolvePolicyMock.mockReturnValue(makePolicy(makeAccount()));
255
+ buildContextMock.mockImplementationOnce(() => ({
256
+ Body: "hi",
257
+ BodyForCommands: "hi",
258
+ RawBody: "hi",
259
+ CommandBody: "hi",
260
+ From: GROUP_JID,
261
+ To: "+15550001111",
262
+ SessionKey: baseRoute.sessionKey,
263
+ AccountId: "default",
264
+ MessageSid: "msg1",
265
+ SenderId: "+15550002222",
266
+ SenderName: "Alice",
267
+ SenderE164: "+15550002222",
268
+ Timestamp: 1710000000,
269
+ Provider: "whatsapp",
270
+ Surface: "whatsapp",
271
+ OriginatingChannel: "whatsapp",
272
+ OriginatingTo: GROUP_JID,
273
+ GroupSubject: "Test Group",
274
+ }));
275
+
276
+ await callProcessMessage({
277
+ cfg: {
278
+ channels: {
279
+ whatsapp: {
280
+ pluginHooks: {
281
+ messageReceived: true,
282
+ },
283
+ },
284
+ },
285
+ },
286
+ });
287
+ await Promise.resolve();
288
+ await Promise.resolve();
289
+
290
+ expect(runMessageReceivedMock).toHaveBeenCalledTimes(1);
291
+ expect(runMessageReceivedMock).toHaveBeenCalledWith(
292
+ expect.objectContaining({
293
+ from: GROUP_JID,
294
+ content: "hi",
295
+ timestamp: 1710000000,
296
+ messageId: "msg1",
297
+ senderId: "+15550002222",
298
+ sessionKey: baseRoute.sessionKey,
299
+ }),
300
+ expect.objectContaining({
301
+ channelId: "whatsapp",
302
+ accountId: "default",
303
+ conversationId: GROUP_JID,
304
+ sessionKey: baseRoute.sessionKey,
305
+ messageId: "msg1",
306
+ senderId: "+15550002222",
307
+ }),
308
+ );
309
+ expect(internalReceived).toHaveBeenCalledWith(
310
+ expect.objectContaining({
311
+ type: "message",
312
+ action: "received",
313
+ sessionKey: baseRoute.sessionKey,
314
+ context: expect.objectContaining({
315
+ from: GROUP_JID,
316
+ content: "hi",
317
+ timestamp: 1710000000,
318
+ channelId: "whatsapp",
319
+ accountId: "default",
320
+ conversationId: GROUP_JID,
321
+ messageId: "msg1",
322
+ }),
323
+ }),
324
+ );
325
+ });
326
+
327
+ it("does not fire WhatsApp message_received hooks without explicit opt-in", async () => {
328
+ const internalReceived = vi.fn();
329
+ registerInternalHook("message:received", internalReceived);
330
+ resolvePolicyMock.mockReturnValue(makePolicy(makeAccount()));
331
+
332
+ await callProcessMessage();
333
+
334
+ expect(runMessageReceivedMock).not.toHaveBeenCalled();
335
+ expect(internalReceived).not.toHaveBeenCalled();
336
+ });
337
+
338
+ it("tracks session metadata writes as connection background tasks", async () => {
339
+ resolvePolicyMock.mockReturnValue(makePolicy(makeAccount()));
340
+ buildContextMock.mockImplementationOnce(() => ({
341
+ Body: "hi",
342
+ RawBody: "hi",
343
+ CommandBody: "hi",
344
+ SessionKey: baseRoute.sessionKey,
345
+ Provider: "whatsapp",
346
+ Surface: "whatsapp",
347
+ }));
348
+
349
+ await callProcessMessage();
350
+
351
+ expect(trackBackgroundTaskMock).toHaveBeenCalledTimes(1);
352
+ expect(trackBackgroundTaskMock.mock.calls[0]?.[0]).toBeInstanceOf(Set);
353
+ expect(trackBackgroundTaskMock.mock.calls[0]?.[1]).toBeInstanceOf(Promise);
354
+ });
355
+ });