@openclaw/zalouser 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 (95) hide show
  1. package/dist/accounts-C00IMUgd.js +63 -0
  2. package/dist/accounts.runtime-uG7S8cXT.js +2 -0
  3. package/dist/api-BRwdUWuS.js +139 -0
  4. package/dist/api.js +7 -0
  5. package/dist/channel-ou_w_2j-.js +484 -0
  6. package/dist/channel-plugin-api.js +2 -0
  7. package/dist/channel.runtime-C9WxiAiR.js +25 -0
  8. package/dist/channel.setup-CiDeBFrn.js +10 -0
  9. package/dist/contract-api.js +3 -0
  10. package/dist/doctor-contract-DgqHp8E2.js +128 -0
  11. package/dist/doctor-contract-api.js +2 -0
  12. package/dist/index.js +27 -0
  13. package/dist/monitor-Cg7K_s_s.js +705 -0
  14. package/dist/runtime-QNU7vLgI.js +106 -0
  15. package/dist/runtime-api.js +22 -0
  16. package/dist/secret-contract-api.js +5 -0
  17. package/dist/security-audit-BZLhil-V.js +34 -0
  18. package/dist/send-BsmySxe3.js +534 -0
  19. package/dist/session-route-C0-Xr8bt.js +92 -0
  20. package/dist/setup-core-CqipqY98.js +40 -0
  21. package/dist/setup-entry.js +11 -0
  22. package/dist/setup-plugin-api.js +2 -0
  23. package/dist/setup-surface-NCOuKu-l.js +359 -0
  24. package/dist/shared-DSy8aIUx.js +120 -0
  25. package/dist/test-api.js +5 -0
  26. package/dist/zalo-js-CHCUlY3c.js +1279 -0
  27. package/package.json +15 -6
  28. package/api.ts +0 -9
  29. package/channel-plugin-api.ts +0 -3
  30. package/contract-api.ts +0 -2
  31. package/doctor-contract-api.ts +0 -1
  32. package/index.ts +0 -34
  33. package/runtime-api.ts +0 -67
  34. package/secret-contract-api.ts +0 -4
  35. package/setup-entry.ts +0 -9
  36. package/setup-plugin-api.ts +0 -2
  37. package/src/accounts.runtime.ts +0 -1
  38. package/src/accounts.test-mocks.ts +0 -14
  39. package/src/accounts.test.ts +0 -266
  40. package/src/accounts.ts +0 -131
  41. package/src/channel-api.ts +0 -20
  42. package/src/channel.adapters.ts +0 -391
  43. package/src/channel.directory.test.ts +0 -59
  44. package/src/channel.runtime.ts +0 -12
  45. package/src/channel.sendpayload.test.ts +0 -172
  46. package/src/channel.setup.test.ts +0 -33
  47. package/src/channel.setup.ts +0 -12
  48. package/src/channel.test.ts +0 -377
  49. package/src/channel.ts +0 -219
  50. package/src/config-schema.ts +0 -33
  51. package/src/directory.ts +0 -54
  52. package/src/doctor-contract.ts +0 -156
  53. package/src/doctor.test.ts +0 -77
  54. package/src/doctor.ts +0 -37
  55. package/src/group-policy.test.ts +0 -61
  56. package/src/group-policy.ts +0 -83
  57. package/src/message-sid.test.ts +0 -66
  58. package/src/message-sid.ts +0 -80
  59. package/src/monitor.account-scope.test.ts +0 -107
  60. package/src/monitor.group-gating.test.ts +0 -816
  61. package/src/monitor.send-mocks.ts +0 -20
  62. package/src/monitor.ts +0 -1044
  63. package/src/probe.test.ts +0 -60
  64. package/src/probe.ts +0 -35
  65. package/src/qr-temp-file.ts +0 -22
  66. package/src/reaction.test.ts +0 -19
  67. package/src/reaction.ts +0 -32
  68. package/src/runtime.ts +0 -9
  69. package/src/security-audit.test.ts +0 -80
  70. package/src/security-audit.ts +0 -71
  71. package/src/send.test.ts +0 -395
  72. package/src/send.ts +0 -272
  73. package/src/session-route.ts +0 -121
  74. package/src/setup-core.ts +0 -33
  75. package/src/setup-surface.test.ts +0 -363
  76. package/src/setup-surface.ts +0 -470
  77. package/src/setup-test-helpers.ts +0 -42
  78. package/src/shared.ts +0 -92
  79. package/src/status-issues.test.ts +0 -31
  80. package/src/status-issues.ts +0 -58
  81. package/src/test-helpers.ts +0 -26
  82. package/src/text-styles.test.ts +0 -203
  83. package/src/text-styles.ts +0 -540
  84. package/src/tool.test.ts +0 -212
  85. package/src/tool.ts +0 -210
  86. package/src/types.ts +0 -125
  87. package/src/zalo-js.credentials.test.ts +0 -465
  88. package/src/zalo-js.test-mocks.ts +0 -89
  89. package/src/zalo-js.ts +0 -1911
  90. package/src/zca-client.test.ts +0 -24
  91. package/src/zca-client.ts +0 -259
  92. package/src/zca-constants.ts +0 -55
  93. package/src/zca-js-exports.d.ts +0 -22
  94. package/test-api.ts +0 -21
  95. package/tsconfig.json +0 -16
@@ -1,816 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
3
- import "./monitor.send-mocks.js";
4
- import "./zalo-js.test-mocks.js";
5
- import { resolveZalouserAccountSync } from "./accounts.js";
6
- import { __testing } from "./monitor.js";
7
- import {
8
- sendDeliveredZalouserMock,
9
- sendMessageZalouserMock,
10
- sendSeenZalouserMock,
11
- sendTypingZalouserMock,
12
- } from "./monitor.send-mocks.js";
13
- import { setZalouserRuntime } from "./runtime.js";
14
- import { createZalouserRuntimeEnv } from "./test-helpers.js";
15
- import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
16
-
17
- function createAccount(): ResolvedZalouserAccount {
18
- return {
19
- accountId: "default",
20
- enabled: true,
21
- profile: "default",
22
- authenticated: true,
23
- config: {
24
- dmPolicy: "open",
25
- allowFrom: ["*"],
26
- groupPolicy: "open",
27
- groups: {
28
- "*": { requireMention: true },
29
- },
30
- },
31
- };
32
- }
33
-
34
- function createConfig(): OpenClawConfig {
35
- return {
36
- channels: {
37
- zalouser: {
38
- enabled: true,
39
- dmPolicy: "open",
40
- allowFrom: ["*"],
41
- groups: {
42
- "*": { requireMention: true },
43
- },
44
- },
45
- },
46
- };
47
- }
48
-
49
- const createRuntimeEnv = () => createZalouserRuntimeEnv();
50
-
51
- function installRuntime(params: {
52
- commandAuthorized?: boolean;
53
- replyPayload?: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
54
- resolveCommandAuthorizedFromAuthorizers?: (params: {
55
- useAccessGroups: boolean;
56
- authorizers: Array<{ configured: boolean; allowed: boolean }>;
57
- }) => boolean;
58
- }) {
59
- const dispatchReplyWithBufferedBlockDispatcher = vi.fn(async ({ dispatcherOptions, ctx }) => {
60
- await dispatcherOptions.typingCallbacks?.onReplyStart?.();
61
- if (params.replyPayload) {
62
- await dispatcherOptions.deliver(params.replyPayload);
63
- }
64
- return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 }, ctx };
65
- });
66
- const resolveCommandAuthorizedFromAuthorizers = vi.fn(
67
- (input: {
68
- useAccessGroups: boolean;
69
- authorizers: Array<{ configured: boolean; allowed: boolean }>;
70
- }) => {
71
- if (params.resolveCommandAuthorizedFromAuthorizers) {
72
- return params.resolveCommandAuthorizedFromAuthorizers(input);
73
- }
74
- return params.commandAuthorized ?? false;
75
- },
76
- );
77
- const resolveAgentRoute = vi.fn((input: { peer?: { kind?: string; id?: string } }) => {
78
- const peerKind = input.peer?.kind === "direct" ? "direct" : "group";
79
- const peerId = input.peer?.id ?? "1";
80
- return {
81
- agentId: "main",
82
- sessionKey:
83
- peerKind === "direct" ? "agent:main:main" : `agent:main:zalouser:${peerKind}:${peerId}`,
84
- accountId: "default",
85
- mainSessionKey: "agent:main:main",
86
- };
87
- });
88
- const readAllowFromStore = vi.fn(async () => []);
89
- const readSessionUpdatedAt = vi.fn(
90
- (_params?: { storePath: string; sessionKey: string }): number | undefined => undefined,
91
- );
92
- type ResolvedTurn = Awaited<
93
- ReturnType<Parameters<PluginRuntime["channel"]["turn"]["run"]>[0]["adapter"]["resolveTurn"]>
94
- >;
95
- const dispatchAssembled = vi.fn(async (turn: ResolvedTurn) => {
96
- await turn.recordInboundSession({
97
- storePath: turn.storePath,
98
- sessionKey: turn.ctxPayload.SessionKey ?? turn.routeSessionKey,
99
- ctx: turn.ctxPayload,
100
- groupResolution: turn.record?.groupResolution,
101
- createIfMissing: turn.record?.createIfMissing,
102
- updateLastRoute: turn.record?.updateLastRoute,
103
- onRecordError: turn.record?.onRecordError ?? (() => undefined),
104
- });
105
- if ("runDispatch" in turn) {
106
- const dispatchResult = await turn.runDispatch();
107
- return {
108
- admission: { kind: "dispatch" as const },
109
- dispatched: true,
110
- ctxPayload: turn.ctxPayload,
111
- routeSessionKey: turn.routeSessionKey,
112
- dispatchResult,
113
- };
114
- }
115
- const dispatchResult = await turn.dispatchReplyWithBufferedBlockDispatcher({
116
- ctx: turn.ctxPayload,
117
- cfg: turn.cfg,
118
- dispatcherOptions: {
119
- ...turn.dispatcherOptions,
120
- deliver: async (...args: Parameters<typeof turn.delivery.deliver>) => {
121
- await turn.delivery.deliver(...args);
122
- },
123
- onError: turn.delivery.onError,
124
- },
125
- replyOptions: turn.replyOptions,
126
- replyResolver: turn.replyResolver,
127
- });
128
- return {
129
- admission: { kind: "dispatch" as const },
130
- dispatched: true,
131
- ctxPayload: turn.ctxPayload,
132
- routeSessionKey: turn.routeSessionKey,
133
- dispatchResult,
134
- };
135
- });
136
- const runTurn = vi.fn(async (params: Parameters<PluginRuntime["channel"]["turn"]["run"]>[0]) => {
137
- const input = await params.adapter.ingest(params.raw);
138
- if (!input) {
139
- return { admission: { kind: "drop" as const, reason: "ingest-null" }, dispatched: false };
140
- }
141
- const resolved = await params.adapter.resolveTurn(
142
- input,
143
- {
144
- kind: "message",
145
- canStartAgentTurn: true,
146
- },
147
- {},
148
- );
149
- return await dispatchAssembled(resolved);
150
- });
151
- const buildContext = vi.fn(
152
- (params: Parameters<PluginRuntime["channel"]["turn"]["buildContext"]>[0]) =>
153
- ({
154
- Body: params.message.body ?? params.message.rawBody,
155
- BodyForAgent: params.message.bodyForAgent ?? params.message.rawBody,
156
- InboundHistory: params.message.inboundHistory,
157
- RawBody: params.message.rawBody,
158
- CommandBody: params.message.commandBody ?? params.message.rawBody,
159
- BodyForCommands: params.message.commandBody ?? params.message.rawBody,
160
- From: params.from,
161
- To: params.reply.to,
162
- SessionKey: params.route.dispatchSessionKey ?? params.route.routeSessionKey,
163
- AccountId: params.route.accountId ?? params.accountId,
164
- ChatType: params.conversation.kind,
165
- ConversationLabel: params.conversation.label,
166
- SenderName: params.sender.name,
167
- SenderId: params.sender.id,
168
- Provider: params.provider ?? params.channel,
169
- Surface: params.surface ?? params.provider ?? params.channel,
170
- MessageSid: params.messageId,
171
- MessageSidFull: params.messageIdFull,
172
- OriginatingChannel: params.channel,
173
- OriginatingTo: params.reply.originatingTo,
174
- ...params.extra,
175
- }) as ReturnType<PluginRuntime["channel"]["turn"]["buildContext"]>,
176
- );
177
- const buildAgentSessionKey = vi.fn(
178
- (input: {
179
- agentId: string;
180
- channel: string;
181
- accountId?: string;
182
- peer?: { kind?: string; id?: string };
183
- dmScope?: string;
184
- }) => {
185
- const peerKind = input.peer?.kind === "direct" ? "direct" : "group";
186
- const peerId = input.peer?.id ?? "1";
187
- if (peerKind === "direct") {
188
- if (input.dmScope === "per-account-channel-peer") {
189
- return `agent:${input.agentId}:${input.channel}:${input.accountId ?? "default"}:direct:${peerId}`;
190
- }
191
- if (input.dmScope === "per-peer") {
192
- return `agent:${input.agentId}:direct:${peerId}`;
193
- }
194
- if (input.dmScope === "main" || !input.dmScope) {
195
- return "agent:main:main";
196
- }
197
- }
198
- return `agent:${input.agentId}:${input.channel}:${peerKind}:${peerId}`;
199
- },
200
- );
201
-
202
- setZalouserRuntime({
203
- logging: {
204
- shouldLogVerbose: () => false,
205
- },
206
- channel: {
207
- pairing: {
208
- readAllowFromStore,
209
- upsertPairingRequest: vi.fn(async () => ({ code: "PAIR", created: true })),
210
- buildPairingReply: vi.fn(() => "pair"),
211
- },
212
- commands: {
213
- shouldComputeCommandAuthorized: vi.fn((body: string) => body.trim().startsWith("/")),
214
- resolveCommandAuthorizedFromAuthorizers,
215
- isControlCommandMessage: vi.fn((body: string) => body.trim().startsWith("/")),
216
- shouldHandleTextCommands: vi.fn(() => true),
217
- },
218
- mentions: {
219
- buildMentionRegexes: vi.fn(() => []),
220
- matchesMentionWithExplicit: vi.fn(
221
- (input) => input.explicit?.isExplicitlyMentioned === true,
222
- ),
223
- },
224
- groups: {
225
- resolveRequireMention: vi.fn((input) => {
226
- const cfg = input.cfg as OpenClawConfig;
227
- const groupCfg = cfg.channels?.zalouser?.groups ?? {};
228
- const typedGroupCfg = groupCfg as Record<string, { requireMention?: boolean }>;
229
- const groupEntry = input.groupId ? typedGroupCfg[input.groupId] : undefined;
230
- const defaultEntry = typedGroupCfg["*"];
231
- if (typeof groupEntry?.requireMention === "boolean") {
232
- return groupEntry.requireMention;
233
- }
234
- if (typeof defaultEntry?.requireMention === "boolean") {
235
- return defaultEntry.requireMention;
236
- }
237
- return true;
238
- }),
239
- },
240
- routing: {
241
- buildAgentSessionKey,
242
- resolveAgentRoute,
243
- },
244
- session: {
245
- resolveStorePath: vi.fn(() => "/tmp"),
246
- readSessionUpdatedAt,
247
- recordInboundSession: vi.fn(async () => {}),
248
- },
249
- reply: {
250
- resolveEnvelopeFormatOptions: vi.fn(() => undefined),
251
- formatAgentEnvelope: vi.fn(({ body }) => body),
252
- finalizeInboundContext: vi.fn((ctx) => ctx),
253
- dispatchReplyWithBufferedBlockDispatcher,
254
- },
255
- turn: {
256
- run: runTurn as unknown as PluginRuntime["channel"]["turn"]["run"],
257
- buildContext: buildContext as unknown as PluginRuntime["channel"]["turn"]["buildContext"],
258
- },
259
- text: {
260
- resolveMarkdownTableMode: vi.fn(() => "code"),
261
- convertMarkdownTables: vi.fn((text: string) => text),
262
- resolveChunkMode: vi.fn(() => "length"),
263
- resolveTextChunkLimit: vi.fn(() => 1200),
264
- chunkMarkdownTextWithMode: vi.fn((text: string) => [text]),
265
- },
266
- },
267
- } as unknown as PluginRuntime);
268
-
269
- return {
270
- dispatchReplyWithBufferedBlockDispatcher,
271
- resolveAgentRoute,
272
- resolveCommandAuthorizedFromAuthorizers,
273
- readAllowFromStore,
274
- readSessionUpdatedAt,
275
- buildAgentSessionKey,
276
- };
277
- }
278
-
279
- function installGroupCommandAuthRuntime() {
280
- return installRuntime({
281
- resolveCommandAuthorizedFromAuthorizers: ({ useAccessGroups, authorizers }) =>
282
- useAccessGroups && authorizers.some((entry) => entry.configured && entry.allowed),
283
- });
284
- }
285
-
286
- async function processGroupControlCommand(params: {
287
- account: ResolvedZalouserAccount;
288
- content?: string;
289
- commandContent?: string;
290
- }) {
291
- await __testing.processMessage({
292
- message: createGroupMessage({
293
- content: params.content ?? "/new",
294
- commandContent: params.commandContent ?? "/new",
295
- hasAnyMention: true,
296
- wasExplicitlyMentioned: true,
297
- }),
298
- account: params.account,
299
- config: createConfig(),
300
- runtime: createRuntimeEnv(),
301
- });
302
- }
303
-
304
- function createGroupMessage(overrides: Partial<ZaloInboundMessage> = {}): ZaloInboundMessage {
305
- return {
306
- threadId: "g-1",
307
- isGroup: true,
308
- senderId: "123",
309
- senderName: "Alice",
310
- groupName: "Team",
311
- content: "hello",
312
- timestampMs: Date.now(),
313
- msgId: "m-1",
314
- hasAnyMention: false,
315
- wasExplicitlyMentioned: false,
316
- canResolveExplicitMention: true,
317
- implicitMention: false,
318
- raw: { source: "test" },
319
- ...overrides,
320
- };
321
- }
322
-
323
- function createDmMessage(overrides: Partial<ZaloInboundMessage> = {}): ZaloInboundMessage {
324
- return {
325
- threadId: "u-1",
326
- isGroup: false,
327
- senderId: "321",
328
- senderName: "Bob",
329
- groupName: undefined,
330
- content: "hello",
331
- timestampMs: Date.now(),
332
- msgId: "dm-1",
333
- raw: { source: "test" },
334
- ...overrides,
335
- };
336
- }
337
-
338
- describe("zalouser monitor group mention gating", () => {
339
- beforeEach(() => {
340
- sendMessageZalouserMock.mockClear();
341
- sendTypingZalouserMock.mockClear();
342
- sendDeliveredZalouserMock.mockClear();
343
- sendSeenZalouserMock.mockClear();
344
- });
345
-
346
- async function processMessageWithDefaults(params: {
347
- message: ZaloInboundMessage;
348
- account?: ResolvedZalouserAccount;
349
- historyState?: {
350
- historyLimit: number;
351
- groupHistories: Map<
352
- string,
353
- Array<{ sender: string; body: string; timestamp?: number; messageId?: string }>
354
- >;
355
- };
356
- }) {
357
- await __testing.processMessage({
358
- message: params.message,
359
- account: params.account ?? createAccount(),
360
- config: createConfig(),
361
- runtime: createZalouserRuntimeEnv(),
362
- historyState: params.historyState,
363
- });
364
- }
365
-
366
- async function expectSkippedGroupMessage(message?: Partial<ZaloInboundMessage>) {
367
- const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
368
- commandAuthorized: false,
369
- });
370
- await processMessageWithDefaults({
371
- message: createGroupMessage(message),
372
- });
373
- expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
374
- expect(sendTypingZalouserMock).not.toHaveBeenCalled();
375
- }
376
-
377
- async function expectGroupCommandAuthorizers(params: {
378
- accountConfig: ResolvedZalouserAccount["config"];
379
- expectedAuthorizers: Array<{ configured: boolean; allowed: boolean }>;
380
- }) {
381
- const { dispatchReplyWithBufferedBlockDispatcher, resolveCommandAuthorizedFromAuthorizers } =
382
- installGroupCommandAuthRuntime();
383
- await processGroupControlCommand({
384
- account: {
385
- ...createAccount(),
386
- config: params.accountConfig,
387
- },
388
- });
389
- expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
390
- const authCall = resolveCommandAuthorizedFromAuthorizers.mock.calls[0]?.[0];
391
- expect(authCall?.authorizers).toEqual(params.expectedAuthorizers);
392
- }
393
-
394
- async function processOpenDmMessage(params?: {
395
- message?: Partial<ZaloInboundMessage>;
396
- readSessionUpdatedAt?: (input?: {
397
- storePath: string;
398
- sessionKey: string;
399
- }) => number | undefined;
400
- }) {
401
- const runtime = installRuntime({
402
- commandAuthorized: false,
403
- });
404
- if (params?.readSessionUpdatedAt) {
405
- runtime.readSessionUpdatedAt.mockImplementation(params.readSessionUpdatedAt);
406
- }
407
- const account = createAccount();
408
- await processMessageWithDefaults({
409
- message: createDmMessage(params?.message),
410
- account: {
411
- ...account,
412
- config: {
413
- ...account.config,
414
- dmPolicy: "open",
415
- },
416
- },
417
- });
418
- return runtime;
419
- }
420
-
421
- async function expectDangerousNameMatching(params: {
422
- dangerouslyAllowNameMatching?: boolean;
423
- expectedDispatches: number;
424
- }) {
425
- const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
426
- commandAuthorized: false,
427
- });
428
- await processMessageWithDefaults({
429
- message: createGroupMessage({
430
- threadId: "g-attacker-001",
431
- groupName: "Trusted Team",
432
- senderId: "666",
433
- hasAnyMention: true,
434
- wasExplicitlyMentioned: true,
435
- content: "ping @bot",
436
- }),
437
- account: {
438
- ...createAccount(),
439
- config: {
440
- ...createAccount().config,
441
- ...(params.dangerouslyAllowNameMatching ? { dangerouslyAllowNameMatching: true } : {}),
442
- groupPolicy: "allowlist",
443
- groupAllowFrom: ["*"],
444
- groups: {
445
- "group:g-trusted-001": { enabled: true },
446
- "Trusted Team": { enabled: true },
447
- },
448
- },
449
- },
450
- });
451
- expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(
452
- params.expectedDispatches,
453
- );
454
- return dispatchReplyWithBufferedBlockDispatcher;
455
- }
456
-
457
- async function dispatchGroupMessage(params: {
458
- commandAuthorized: boolean;
459
- message: Partial<ZaloInboundMessage>;
460
- }) {
461
- const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
462
- commandAuthorized: params.commandAuthorized,
463
- });
464
- await processMessageWithDefaults({
465
- message: createGroupMessage(params.message),
466
- });
467
- expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
468
- return dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
469
- }
470
-
471
- it("skips unmentioned group messages when requireMention=true", async () => {
472
- await expectSkippedGroupMessage();
473
- });
474
-
475
- it("blocks mentioned group messages by default when groupPolicy is omitted", async () => {
476
- const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
477
- commandAuthorized: false,
478
- });
479
- const cfg: OpenClawConfig = {
480
- channels: {
481
- zalouser: {
482
- enabled: true,
483
- },
484
- },
485
- };
486
- const account = resolveZalouserAccountSync({ cfg, accountId: "default" });
487
-
488
- await __testing.processMessage({
489
- message: createGroupMessage({
490
- content: "ping @bot",
491
- hasAnyMention: true,
492
- wasExplicitlyMentioned: true,
493
- }),
494
- account,
495
- config: cfg,
496
- runtime: createRuntimeEnv(),
497
- });
498
-
499
- expect(account.config.groupPolicy).toBe("allowlist");
500
- expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
501
- });
502
-
503
- it("fails closed when requireMention=true but mention detection is unavailable", async () => {
504
- await expectSkippedGroupMessage({
505
- canResolveExplicitMention: false,
506
- hasAnyMention: false,
507
- wasExplicitlyMentioned: false,
508
- });
509
- });
510
-
511
- it("dispatches explicitly-mentioned group messages and marks WasMentioned", async () => {
512
- const callArg = await dispatchGroupMessage({
513
- commandAuthorized: false,
514
- message: {
515
- hasAnyMention: true,
516
- wasExplicitlyMentioned: true,
517
- content: "ping @bot",
518
- },
519
- });
520
- expect(callArg?.ctx?.WasMentioned).toBe(true);
521
- expect(callArg?.ctx?.To).toBe("zalouser:group:g-1");
522
- expect(callArg?.ctx?.OriginatingTo).toBe("zalouser:group:g-1");
523
- expect(sendTypingZalouserMock).toHaveBeenCalledWith("g-1", {
524
- profile: "default",
525
- isGroup: true,
526
- });
527
- });
528
-
529
- it("allows authorized control commands to bypass mention gating", async () => {
530
- const callArg = await dispatchGroupMessage({
531
- commandAuthorized: true,
532
- message: {
533
- content: "/status",
534
- hasAnyMention: false,
535
- wasExplicitlyMentioned: false,
536
- },
537
- });
538
- expect(callArg?.ctx?.WasMentioned).toBe(true);
539
- });
540
-
541
- it("passes long markdown replies through once so formatting happens before chunking", async () => {
542
- const replyText = `**${"a".repeat(2501)}**`;
543
- installRuntime({
544
- commandAuthorized: false,
545
- replyPayload: { text: replyText },
546
- });
547
-
548
- await __testing.processMessage({
549
- message: createDmMessage({
550
- content: "hello",
551
- }),
552
- account: {
553
- ...createAccount(),
554
- config: {
555
- ...createAccount().config,
556
- dmPolicy: "open",
557
- },
558
- },
559
- config: createConfig(),
560
- runtime: createRuntimeEnv(),
561
- });
562
-
563
- expect(sendMessageZalouserMock).toHaveBeenCalledTimes(1);
564
- expect(sendMessageZalouserMock).toHaveBeenCalledWith(
565
- "u-1",
566
- replyText,
567
- expect.objectContaining({
568
- isGroup: false,
569
- profile: "default",
570
- textMode: "markdown",
571
- textChunkMode: "length",
572
- textChunkLimit: 1200,
573
- }),
574
- );
575
- });
576
-
577
- it("uses commandContent for mention-prefixed control commands", async () => {
578
- const callArg = await dispatchGroupMessage({
579
- commandAuthorized: true,
580
- message: {
581
- content: "@Bot /new",
582
- commandContent: "/new",
583
- hasAnyMention: true,
584
- wasExplicitlyMentioned: true,
585
- },
586
- });
587
- expect(callArg?.ctx?.CommandBody).toBe("/new");
588
- expect(callArg?.ctx?.BodyForCommands).toBe("/new");
589
- });
590
-
591
- it("allows group control commands when only allowFrom is configured", async () => {
592
- await expectGroupCommandAuthorizers({
593
- accountConfig: {
594
- ...createAccount().config,
595
- allowFrom: ["123"],
596
- },
597
- expectedAuthorizers: [
598
- { configured: true, allowed: true },
599
- { configured: true, allowed: true },
600
- ],
601
- });
602
- });
603
-
604
- it("blocks routed allowlist groups without an explicit group sender allowlist", async () => {
605
- const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
606
- commandAuthorized: false,
607
- });
608
- await __testing.processMessage({
609
- message: createGroupMessage({
610
- content: "ping @bot",
611
- hasAnyMention: true,
612
- wasExplicitlyMentioned: true,
613
- senderId: "456",
614
- }),
615
- account: {
616
- ...createAccount(),
617
- config: {
618
- ...createAccount().config,
619
- groupPolicy: "allowlist",
620
- allowFrom: ["123"],
621
- groups: {
622
- "group:g-1": { enabled: true, requireMention: true },
623
- },
624
- },
625
- },
626
- config: createConfig(),
627
- runtime: createRuntimeEnv(),
628
- });
629
-
630
- expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
631
- });
632
-
633
- it("blocks group messages when sender is not in groupAllowFrom", async () => {
634
- const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
635
- commandAuthorized: false,
636
- });
637
- await __testing.processMessage({
638
- message: createGroupMessage({
639
- content: "ping @bot",
640
- hasAnyMention: true,
641
- wasExplicitlyMentioned: true,
642
- }),
643
- account: {
644
- ...createAccount(),
645
- config: {
646
- ...createAccount().config,
647
- groupPolicy: "allowlist",
648
- allowFrom: ["999"],
649
- groupAllowFrom: ["999"],
650
- },
651
- },
652
- config: createConfig(),
653
- runtime: createRuntimeEnv(),
654
- });
655
-
656
- expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
657
- });
658
-
659
- it("does not accept a different group id by matching only the mutable group name by default", async () => {
660
- await expectDangerousNameMatching({ expectedDispatches: 0 });
661
- });
662
-
663
- it("accepts mutable group-name matches only when dangerouslyAllowNameMatching is enabled", async () => {
664
- const dispatchReplyWithBufferedBlockDispatcher = await expectDangerousNameMatching({
665
- dangerouslyAllowNameMatching: true,
666
- expectedDispatches: 1,
667
- });
668
- const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
669
- expect(callArg?.ctx?.To).toBe("zalouser:group:g-attacker-001");
670
- });
671
-
672
- it("allows group control commands when sender is in groupAllowFrom", async () => {
673
- await expectGroupCommandAuthorizers({
674
- accountConfig: {
675
- ...createAccount().config,
676
- allowFrom: ["999"],
677
- groupAllowFrom: ["123"],
678
- },
679
- expectedAuthorizers: [
680
- { configured: true, allowed: false },
681
- { configured: true, allowed: true },
682
- ],
683
- });
684
- });
685
-
686
- it("routes DM messages with direct peer kind", async () => {
687
- const { dispatchReplyWithBufferedBlockDispatcher, resolveAgentRoute, buildAgentSessionKey } =
688
- await processOpenDmMessage();
689
-
690
- expect(resolveAgentRoute).toHaveBeenCalledWith(
691
- expect.objectContaining({
692
- peer: { kind: "direct", id: "321" },
693
- }),
694
- );
695
- expect(buildAgentSessionKey).toHaveBeenCalledWith(
696
- expect.objectContaining({
697
- peer: { kind: "direct", id: "321" },
698
- dmScope: "per-channel-peer",
699
- }),
700
- );
701
- const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
702
- expect(callArg?.ctx?.SessionKey).toBe("agent:main:zalouser:direct:321");
703
- });
704
-
705
- it("reuses the legacy DM session key when only the old group-shaped session exists", async () => {
706
- const { dispatchReplyWithBufferedBlockDispatcher } = await processOpenDmMessage({
707
- readSessionUpdatedAt: (input?: { storePath: string; sessionKey: string }) =>
708
- input?.sessionKey === "agent:main:zalouser:group:321" ? 123 : undefined,
709
- });
710
-
711
- const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
712
- expect(callArg?.ctx?.SessionKey).toBe("agent:main:zalouser:group:321");
713
- });
714
-
715
- it("skips pairing store read for open DM control commands", async () => {
716
- const { readAllowFromStore } = installRuntime({
717
- commandAuthorized: false,
718
- });
719
- const account = createAccount();
720
- await __testing.processMessage({
721
- message: createDmMessage({ content: "/new", commandContent: "/new" }),
722
- account: {
723
- ...account,
724
- config: {
725
- ...account.config,
726
- dmPolicy: "open",
727
- },
728
- },
729
- config: createConfig(),
730
- runtime: createRuntimeEnv(),
731
- });
732
-
733
- expect(readAllowFromStore).not.toHaveBeenCalled();
734
- });
735
-
736
- it("skips pairing store read for open DM non-command messages", async () => {
737
- const { readAllowFromStore } = installRuntime({
738
- commandAuthorized: false,
739
- });
740
- const account = createAccount();
741
- await __testing.processMessage({
742
- message: createDmMessage({ content: "hello there" }),
743
- account: {
744
- ...account,
745
- config: {
746
- ...account.config,
747
- dmPolicy: "open",
748
- },
749
- },
750
- config: createConfig(),
751
- runtime: createRuntimeEnv(),
752
- });
753
-
754
- expect(readAllowFromStore).not.toHaveBeenCalled();
755
- });
756
-
757
- it("includes skipped group messages as InboundHistory on the next processed message", async () => {
758
- const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
759
- commandAuthorized: false,
760
- });
761
- const historyState = {
762
- historyLimit: 5,
763
- groupHistories: new Map<
764
- string,
765
- Array<{ sender: string; body: string; timestamp?: number; messageId?: string }>
766
- >(),
767
- };
768
- const account = createAccount();
769
- const config = createConfig();
770
- await __testing.processMessage({
771
- message: createGroupMessage({
772
- content: "first unmentioned line",
773
- hasAnyMention: false,
774
- wasExplicitlyMentioned: false,
775
- }),
776
- account,
777
- config,
778
- runtime: createRuntimeEnv(),
779
- historyState,
780
- });
781
- expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
782
-
783
- await __testing.processMessage({
784
- message: createGroupMessage({
785
- content: "second line @bot",
786
- hasAnyMention: true,
787
- wasExplicitlyMentioned: true,
788
- }),
789
- account,
790
- config,
791
- runtime: createRuntimeEnv(),
792
- historyState,
793
- });
794
- expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
795
- const firstDispatch = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
796
- expect(firstDispatch?.ctx?.InboundHistory).toEqual([
797
- expect.objectContaining({ sender: "Alice", body: "first unmentioned line" }),
798
- ]);
799
- expect(String(firstDispatch?.ctx?.Body ?? "")).toContain("first unmentioned line");
800
-
801
- await __testing.processMessage({
802
- message: createGroupMessage({
803
- content: "third line @bot",
804
- hasAnyMention: true,
805
- wasExplicitlyMentioned: true,
806
- }),
807
- account,
808
- config,
809
- runtime: createRuntimeEnv(),
810
- historyState,
811
- });
812
- expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(2);
813
- const secondDispatch = dispatchReplyWithBufferedBlockDispatcher.mock.calls[1]?.[0];
814
- expect(secondDispatch?.ctx?.InboundHistory).toEqual([]);
815
- });
816
- });