@openclaw/zalouser 2026.3.12 → 2026.5.1-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/README.md +4 -3
- package/api.ts +9 -0
- package/channel-plugin-api.ts +3 -0
- package/contract-api.ts +2 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +29 -24
- package/openclaw.plugin.json +288 -1
- package/package.json +38 -11
- package/runtime-api.ts +67 -0
- package/secret-contract-api.ts +4 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +2 -0
- package/src/accounts.runtime.ts +1 -0
- package/src/accounts.test-mocks.ts +14 -0
- package/src/accounts.test.ts +53 -1
- package/src/accounts.ts +52 -37
- package/src/channel-api.ts +20 -0
- package/src/channel.adapters.ts +390 -0
- package/src/channel.directory.test.ts +48 -61
- package/src/channel.runtime.ts +12 -0
- package/src/channel.sendpayload.test.ts +42 -37
- package/src/channel.setup.test.ts +33 -0
- package/src/channel.setup.ts +12 -0
- package/src/channel.test.ts +258 -56
- package/src/channel.ts +176 -692
- package/src/config-schema.ts +5 -5
- package/src/directory.ts +54 -0
- package/src/doctor-contract.ts +156 -0
- package/src/doctor.test.ts +77 -0
- package/src/doctor.ts +37 -0
- package/src/group-policy.test.ts +4 -4
- package/src/group-policy.ts +4 -2
- package/src/monitor.account-scope.test.ts +4 -10
- package/src/monitor.group-gating.test.ts +319 -190
- package/src/monitor.ts +233 -182
- package/src/probe.ts +3 -2
- package/src/qr-temp-file.ts +1 -1
- package/src/reaction.ts +5 -2
- package/src/runtime.ts +6 -3
- package/src/security-audit.test.ts +80 -0
- package/src/security-audit.ts +71 -0
- package/src/send.test.ts +2 -2
- package/src/send.ts +3 -3
- package/src/session-route.ts +121 -0
- package/src/setup-core.ts +33 -0
- package/src/setup-surface.test.ts +363 -0
- package/src/setup-surface.ts +470 -0
- package/src/setup-test-helpers.ts +42 -0
- package/src/shared.ts +92 -0
- package/src/status-issues.test.ts +5 -17
- package/src/status-issues.ts +18 -30
- package/src/test-helpers.ts +26 -0
- package/src/text-styles.test.ts +1 -1
- package/src/text-styles.ts +5 -2
- package/src/tool.test.ts +66 -3
- package/src/tool.ts +76 -14
- package/src/types.ts +3 -3
- package/src/zalo-js.credentials.test.ts +465 -0
- package/src/zalo-js.test-mocks.ts +89 -0
- package/src/zalo-js.ts +491 -274
- package/src/zca-client.test.ts +24 -0
- package/src/zca-client.ts +24 -58
- package/src/zca-constants.ts +55 -0
- package/test-api.ts +21 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -101
- package/src/onboarding.ts +0 -340
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/zalouser";
|
|
2
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
|
|
3
3
|
import "./monitor.send-mocks.js";
|
|
4
|
+
import "./zalo-js.test-mocks.js";
|
|
5
|
+
import { resolveZalouserAccountSync } from "./accounts.js";
|
|
4
6
|
import { __testing } from "./monitor.js";
|
|
5
7
|
import {
|
|
6
8
|
sendDeliveredZalouserMock,
|
|
@@ -9,6 +11,7 @@ import {
|
|
|
9
11
|
sendTypingZalouserMock,
|
|
10
12
|
} from "./monitor.send-mocks.js";
|
|
11
13
|
import { setZalouserRuntime } from "./runtime.js";
|
|
14
|
+
import { createZalouserRuntimeEnv } from "./test-helpers.js";
|
|
12
15
|
import type { ResolvedZalouserAccount, ZaloInboundMessage } from "./types.js";
|
|
13
16
|
|
|
14
17
|
function createAccount(): ResolvedZalouserAccount {
|
|
@@ -18,6 +21,8 @@ function createAccount(): ResolvedZalouserAccount {
|
|
|
18
21
|
profile: "default",
|
|
19
22
|
authenticated: true,
|
|
20
23
|
config: {
|
|
24
|
+
dmPolicy: "open",
|
|
25
|
+
allowFrom: ["*"],
|
|
21
26
|
groupPolicy: "open",
|
|
22
27
|
groups: {
|
|
23
28
|
"*": { requireMention: true },
|
|
@@ -31,6 +36,8 @@ function createConfig(): OpenClawConfig {
|
|
|
31
36
|
channels: {
|
|
32
37
|
zalouser: {
|
|
33
38
|
enabled: true,
|
|
39
|
+
dmPolicy: "open",
|
|
40
|
+
allowFrom: ["*"],
|
|
34
41
|
groups: {
|
|
35
42
|
"*": { requireMention: true },
|
|
36
43
|
},
|
|
@@ -39,15 +46,7 @@ function createConfig(): OpenClawConfig {
|
|
|
39
46
|
};
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
log: vi.fn(),
|
|
45
|
-
error: vi.fn(),
|
|
46
|
-
exit: ((code: number): never => {
|
|
47
|
-
throw new Error(`exit ${code}`);
|
|
48
|
-
}) as RuntimeEnv["exit"],
|
|
49
|
-
};
|
|
50
|
-
}
|
|
49
|
+
const createRuntimeEnv = () => createZalouserRuntimeEnv();
|
|
51
50
|
|
|
52
51
|
function installRuntime(params: {
|
|
53
52
|
commandAuthorized?: boolean;
|
|
@@ -90,6 +89,91 @@ function installRuntime(params: {
|
|
|
90
89
|
const readSessionUpdatedAt = vi.fn(
|
|
91
90
|
(_params?: { storePath: string; sessionKey: string }): number | undefined => undefined,
|
|
92
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
|
+
);
|
|
93
177
|
const buildAgentSessionKey = vi.fn(
|
|
94
178
|
(input: {
|
|
95
179
|
agentId: string;
|
|
@@ -141,8 +225,9 @@ function installRuntime(params: {
|
|
|
141
225
|
resolveRequireMention: vi.fn((input) => {
|
|
142
226
|
const cfg = input.cfg as OpenClawConfig;
|
|
143
227
|
const groupCfg = cfg.channels?.zalouser?.groups ?? {};
|
|
144
|
-
const
|
|
145
|
-
const
|
|
228
|
+
const typedGroupCfg = groupCfg as Record<string, { requireMention?: boolean }>;
|
|
229
|
+
const groupEntry = input.groupId ? typedGroupCfg[input.groupId] : undefined;
|
|
230
|
+
const defaultEntry = typedGroupCfg["*"];
|
|
146
231
|
if (typeof groupEntry?.requireMention === "boolean") {
|
|
147
232
|
return groupEntry.requireMention;
|
|
148
233
|
}
|
|
@@ -167,6 +252,10 @@ function installRuntime(params: {
|
|
|
167
252
|
finalizeInboundContext: vi.fn((ctx) => ctx),
|
|
168
253
|
dispatchReplyWithBufferedBlockDispatcher,
|
|
169
254
|
},
|
|
255
|
+
turn: {
|
|
256
|
+
run: runTurn as unknown as PluginRuntime["channel"]["turn"]["run"],
|
|
257
|
+
buildContext: buildContext as unknown as PluginRuntime["channel"]["turn"]["buildContext"],
|
|
258
|
+
},
|
|
170
259
|
text: {
|
|
171
260
|
resolveMarkdownTableMode: vi.fn(() => "code"),
|
|
172
261
|
convertMarkdownTables: vi.fn((text: string) => text),
|
|
@@ -187,6 +276,31 @@ function installRuntime(params: {
|
|
|
187
276
|
};
|
|
188
277
|
}
|
|
189
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
|
+
|
|
190
304
|
function createGroupMessage(overrides: Partial<ZaloInboundMessage> = {}): ZaloInboundMessage {
|
|
191
305
|
return {
|
|
192
306
|
threadId: "g-1",
|
|
@@ -229,57 +343,180 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
229
343
|
sendSeenZalouserMock.mockClear();
|
|
230
344
|
});
|
|
231
345
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
}) {
|
|
236
357
|
await __testing.processMessage({
|
|
237
|
-
message:
|
|
238
|
-
account: createAccount(),
|
|
358
|
+
message: params.message,
|
|
359
|
+
account: params.account ?? createAccount(),
|
|
239
360
|
config: createConfig(),
|
|
240
|
-
runtime:
|
|
361
|
+
runtime: createZalouserRuntimeEnv(),
|
|
362
|
+
historyState: params.historyState,
|
|
241
363
|
});
|
|
364
|
+
}
|
|
242
365
|
|
|
366
|
+
async function expectSkippedGroupMessage(message?: Partial<ZaloInboundMessage>) {
|
|
367
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
368
|
+
commandAuthorized: false,
|
|
369
|
+
});
|
|
370
|
+
await processMessageWithDefaults({
|
|
371
|
+
message: createGroupMessage(message),
|
|
372
|
+
});
|
|
243
373
|
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
244
374
|
expect(sendTypingZalouserMock).not.toHaveBeenCalled();
|
|
245
|
-
}
|
|
375
|
+
}
|
|
246
376
|
|
|
247
|
-
|
|
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
|
+
}) {
|
|
248
425
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
249
426
|
commandAuthorized: false,
|
|
250
427
|
});
|
|
251
|
-
await
|
|
428
|
+
await processMessageWithDefaults({
|
|
252
429
|
message: createGroupMessage({
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
430
|
+
threadId: "g-attacker-001",
|
|
431
|
+
groupName: "Trusted Team",
|
|
432
|
+
senderId: "666",
|
|
433
|
+
hasAnyMention: true,
|
|
434
|
+
wasExplicitlyMentioned: true,
|
|
435
|
+
content: "ping @bot",
|
|
256
436
|
}),
|
|
257
|
-
account:
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
},
|
|
260
450
|
});
|
|
451
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(
|
|
452
|
+
params.expectedDispatches,
|
|
453
|
+
);
|
|
454
|
+
return dispatchReplyWithBufferedBlockDispatcher;
|
|
455
|
+
}
|
|
261
456
|
|
|
262
|
-
|
|
263
|
-
|
|
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();
|
|
264
473
|
});
|
|
265
474
|
|
|
266
|
-
it("
|
|
475
|
+
it("blocks mentioned group messages by default when groupPolicy is omitted", async () => {
|
|
267
476
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
268
477
|
commandAuthorized: false,
|
|
269
478
|
});
|
|
479
|
+
const cfg: OpenClawConfig = {
|
|
480
|
+
channels: {
|
|
481
|
+
zalouser: {
|
|
482
|
+
enabled: true,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
const account = resolveZalouserAccountSync({ cfg, accountId: "default" });
|
|
487
|
+
|
|
270
488
|
await __testing.processMessage({
|
|
271
489
|
message: createGroupMessage({
|
|
490
|
+
content: "ping @bot",
|
|
272
491
|
hasAnyMention: true,
|
|
273
492
|
wasExplicitlyMentioned: true,
|
|
274
|
-
content: "ping @bot",
|
|
275
493
|
}),
|
|
276
|
-
account
|
|
277
|
-
config:
|
|
494
|
+
account,
|
|
495
|
+
config: cfg,
|
|
278
496
|
runtime: createRuntimeEnv(),
|
|
279
497
|
});
|
|
280
498
|
|
|
281
|
-
expect(
|
|
282
|
-
|
|
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
|
+
});
|
|
283
520
|
expect(callArg?.ctx?.WasMentioned).toBe(true);
|
|
284
521
|
expect(callArg?.ctx?.To).toBe("zalouser:group:g-1");
|
|
285
522
|
expect(callArg?.ctx?.OriginatingTo).toBe("zalouser:group:g-1");
|
|
@@ -290,22 +527,14 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
290
527
|
});
|
|
291
528
|
|
|
292
529
|
it("allows authorized control commands to bypass mention gating", async () => {
|
|
293
|
-
const
|
|
530
|
+
const callArg = await dispatchGroupMessage({
|
|
294
531
|
commandAuthorized: true,
|
|
295
|
-
|
|
296
|
-
await __testing.processMessage({
|
|
297
|
-
message: createGroupMessage({
|
|
532
|
+
message: {
|
|
298
533
|
content: "/status",
|
|
299
534
|
hasAnyMention: false,
|
|
300
535
|
wasExplicitlyMentioned: false,
|
|
301
|
-
}
|
|
302
|
-
account: createAccount(),
|
|
303
|
-
config: createConfig(),
|
|
304
|
-
runtime: createRuntimeEnv(),
|
|
536
|
+
},
|
|
305
537
|
});
|
|
306
|
-
|
|
307
|
-
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
308
|
-
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
309
538
|
expect(callArg?.ctx?.WasMentioned).toBe(true);
|
|
310
539
|
});
|
|
311
540
|
|
|
@@ -346,60 +575,33 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
346
575
|
});
|
|
347
576
|
|
|
348
577
|
it("uses commandContent for mention-prefixed control commands", async () => {
|
|
349
|
-
const
|
|
578
|
+
const callArg = await dispatchGroupMessage({
|
|
350
579
|
commandAuthorized: true,
|
|
351
|
-
|
|
352
|
-
await __testing.processMessage({
|
|
353
|
-
message: createGroupMessage({
|
|
580
|
+
message: {
|
|
354
581
|
content: "@Bot /new",
|
|
355
582
|
commandContent: "/new",
|
|
356
583
|
hasAnyMention: true,
|
|
357
584
|
wasExplicitlyMentioned: true,
|
|
358
|
-
}
|
|
359
|
-
account: createAccount(),
|
|
360
|
-
config: createConfig(),
|
|
361
|
-
runtime: createRuntimeEnv(),
|
|
585
|
+
},
|
|
362
586
|
});
|
|
363
|
-
|
|
364
|
-
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
365
|
-
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
366
587
|
expect(callArg?.ctx?.CommandBody).toBe("/new");
|
|
367
588
|
expect(callArg?.ctx?.BodyForCommands).toBe("/new");
|
|
368
589
|
});
|
|
369
590
|
|
|
370
591
|
it("allows group control commands when only allowFrom is configured", async () => {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
});
|
|
376
|
-
await __testing.processMessage({
|
|
377
|
-
message: createGroupMessage({
|
|
378
|
-
content: "/new",
|
|
379
|
-
commandContent: "/new",
|
|
380
|
-
hasAnyMention: true,
|
|
381
|
-
wasExplicitlyMentioned: true,
|
|
382
|
-
}),
|
|
383
|
-
account: {
|
|
384
|
-
...createAccount(),
|
|
385
|
-
config: {
|
|
386
|
-
...createAccount().config,
|
|
387
|
-
allowFrom: ["123"],
|
|
388
|
-
},
|
|
592
|
+
await expectGroupCommandAuthorizers({
|
|
593
|
+
accountConfig: {
|
|
594
|
+
...createAccount().config,
|
|
595
|
+
allowFrom: ["123"],
|
|
389
596
|
},
|
|
390
|
-
|
|
391
|
-
|
|
597
|
+
expectedAuthorizers: [
|
|
598
|
+
{ configured: true, allowed: true },
|
|
599
|
+
{ configured: true, allowed: true },
|
|
600
|
+
],
|
|
392
601
|
});
|
|
393
|
-
|
|
394
|
-
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
395
|
-
const authCall = resolveCommandAuthorizedFromAuthorizers.mock.calls[0]?.[0];
|
|
396
|
-
expect(authCall?.authorizers).toEqual([
|
|
397
|
-
{ configured: true, allowed: true },
|
|
398
|
-
{ configured: true, allowed: true },
|
|
399
|
-
]);
|
|
400
602
|
});
|
|
401
603
|
|
|
402
|
-
it("blocks
|
|
604
|
+
it("blocks routed allowlist groups without an explicit group sender allowlist", async () => {
|
|
403
605
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
404
606
|
commandAuthorized: false,
|
|
405
607
|
});
|
|
@@ -408,13 +610,17 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
408
610
|
content: "ping @bot",
|
|
409
611
|
hasAnyMention: true,
|
|
410
612
|
wasExplicitlyMentioned: true,
|
|
613
|
+
senderId: "456",
|
|
411
614
|
}),
|
|
412
615
|
account: {
|
|
413
616
|
...createAccount(),
|
|
414
617
|
config: {
|
|
415
618
|
...createAccount().config,
|
|
416
619
|
groupPolicy: "allowlist",
|
|
417
|
-
allowFrom: ["
|
|
620
|
+
allowFrom: ["123"],
|
|
621
|
+
groups: {
|
|
622
|
+
"group:g-1": { enabled: true, requireMention: true },
|
|
623
|
+
},
|
|
418
624
|
},
|
|
419
625
|
},
|
|
420
626
|
config: createConfig(),
|
|
@@ -424,29 +630,23 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
424
630
|
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
425
631
|
});
|
|
426
632
|
|
|
427
|
-
it("
|
|
633
|
+
it("blocks group messages when sender is not in groupAllowFrom", async () => {
|
|
428
634
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
429
635
|
commandAuthorized: false,
|
|
430
636
|
});
|
|
431
637
|
await __testing.processMessage({
|
|
432
638
|
message: createGroupMessage({
|
|
433
|
-
|
|
434
|
-
groupName: "Trusted Team",
|
|
435
|
-
senderId: "666",
|
|
639
|
+
content: "ping @bot",
|
|
436
640
|
hasAnyMention: true,
|
|
437
641
|
wasExplicitlyMentioned: true,
|
|
438
|
-
content: "ping @bot",
|
|
439
642
|
}),
|
|
440
643
|
account: {
|
|
441
644
|
...createAccount(),
|
|
442
645
|
config: {
|
|
443
646
|
...createAccount().config,
|
|
444
647
|
groupPolicy: "allowlist",
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
"group:g-trusted-001": { allow: true },
|
|
448
|
-
"Trusted Team": { allow: true },
|
|
449
|
-
},
|
|
648
|
+
allowFrom: ["999"],
|
|
649
|
+
groupAllowFrom: ["999"],
|
|
450
650
|
},
|
|
451
651
|
},
|
|
452
652
|
config: createConfig(),
|
|
@@ -456,92 +656,36 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
456
656
|
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
457
657
|
});
|
|
458
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
|
+
|
|
459
663
|
it("accepts mutable group-name matches only when dangerouslyAllowNameMatching is enabled", async () => {
|
|
460
|
-
const
|
|
461
|
-
|
|
664
|
+
const dispatchReplyWithBufferedBlockDispatcher = await expectDangerousNameMatching({
|
|
665
|
+
dangerouslyAllowNameMatching: true,
|
|
666
|
+
expectedDispatches: 1,
|
|
462
667
|
});
|
|
463
|
-
await __testing.processMessage({
|
|
464
|
-
message: createGroupMessage({
|
|
465
|
-
threadId: "g-attacker-001",
|
|
466
|
-
groupName: "Trusted Team",
|
|
467
|
-
senderId: "666",
|
|
468
|
-
hasAnyMention: true,
|
|
469
|
-
wasExplicitlyMentioned: true,
|
|
470
|
-
content: "ping @bot",
|
|
471
|
-
}),
|
|
472
|
-
account: {
|
|
473
|
-
...createAccount(),
|
|
474
|
-
config: {
|
|
475
|
-
...createAccount().config,
|
|
476
|
-
dangerouslyAllowNameMatching: true,
|
|
477
|
-
groupPolicy: "allowlist",
|
|
478
|
-
groupAllowFrom: ["*"],
|
|
479
|
-
groups: {
|
|
480
|
-
"group:g-trusted-001": { allow: true },
|
|
481
|
-
"Trusted Team": { allow: true },
|
|
482
|
-
},
|
|
483
|
-
},
|
|
484
|
-
},
|
|
485
|
-
config: createConfig(),
|
|
486
|
-
runtime: createRuntimeEnv(),
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
490
668
|
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
491
669
|
expect(callArg?.ctx?.To).toBe("zalouser:group:g-attacker-001");
|
|
492
670
|
});
|
|
493
671
|
|
|
494
672
|
it("allows group control commands when sender is in groupAllowFrom", async () => {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
await __testing.processMessage({
|
|
501
|
-
message: createGroupMessage({
|
|
502
|
-
content: "/new",
|
|
503
|
-
commandContent: "/new",
|
|
504
|
-
hasAnyMention: true,
|
|
505
|
-
wasExplicitlyMentioned: true,
|
|
506
|
-
}),
|
|
507
|
-
account: {
|
|
508
|
-
...createAccount(),
|
|
509
|
-
config: {
|
|
510
|
-
...createAccount().config,
|
|
511
|
-
allowFrom: ["999"],
|
|
512
|
-
groupAllowFrom: ["123"],
|
|
513
|
-
},
|
|
673
|
+
await expectGroupCommandAuthorizers({
|
|
674
|
+
accountConfig: {
|
|
675
|
+
...createAccount().config,
|
|
676
|
+
allowFrom: ["999"],
|
|
677
|
+
groupAllowFrom: ["123"],
|
|
514
678
|
},
|
|
515
|
-
|
|
516
|
-
|
|
679
|
+
expectedAuthorizers: [
|
|
680
|
+
{ configured: true, allowed: false },
|
|
681
|
+
{ configured: true, allowed: true },
|
|
682
|
+
],
|
|
517
683
|
});
|
|
518
|
-
|
|
519
|
-
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
520
|
-
const authCall = resolveCommandAuthorizedFromAuthorizers.mock.calls[0]?.[0];
|
|
521
|
-
expect(authCall?.authorizers).toEqual([
|
|
522
|
-
{ configured: true, allowed: false },
|
|
523
|
-
{ configured: true, allowed: true },
|
|
524
|
-
]);
|
|
525
684
|
});
|
|
526
685
|
|
|
527
686
|
it("routes DM messages with direct peer kind", async () => {
|
|
528
687
|
const { dispatchReplyWithBufferedBlockDispatcher, resolveAgentRoute, buildAgentSessionKey } =
|
|
529
|
-
|
|
530
|
-
commandAuthorized: false,
|
|
531
|
-
});
|
|
532
|
-
const account = createAccount();
|
|
533
|
-
await __testing.processMessage({
|
|
534
|
-
message: createDmMessage(),
|
|
535
|
-
account: {
|
|
536
|
-
...account,
|
|
537
|
-
config: {
|
|
538
|
-
...account.config,
|
|
539
|
-
dmPolicy: "open",
|
|
540
|
-
},
|
|
541
|
-
},
|
|
542
|
-
config: createConfig(),
|
|
543
|
-
runtime: createRuntimeEnv(),
|
|
544
|
-
});
|
|
688
|
+
await processOpenDmMessage();
|
|
545
689
|
|
|
546
690
|
expect(resolveAgentRoute).toHaveBeenCalledWith(
|
|
547
691
|
expect.objectContaining({
|
|
@@ -559,31 +703,16 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
559
703
|
});
|
|
560
704
|
|
|
561
705
|
it("reuses the legacy DM session key when only the old group-shaped session exists", async () => {
|
|
562
|
-
const { dispatchReplyWithBufferedBlockDispatcher
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
readSessionUpdatedAt.mockImplementation((input?: { storePath: string; sessionKey: string }) =>
|
|
566
|
-
input?.sessionKey === "agent:main:zalouser:group:321" ? 123 : undefined,
|
|
567
|
-
);
|
|
568
|
-
const account = createAccount();
|
|
569
|
-
await __testing.processMessage({
|
|
570
|
-
message: createDmMessage(),
|
|
571
|
-
account: {
|
|
572
|
-
...account,
|
|
573
|
-
config: {
|
|
574
|
-
...account.config,
|
|
575
|
-
dmPolicy: "open",
|
|
576
|
-
},
|
|
577
|
-
},
|
|
578
|
-
config: createConfig(),
|
|
579
|
-
runtime: createRuntimeEnv(),
|
|
706
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = await processOpenDmMessage({
|
|
707
|
+
readSessionUpdatedAt: (input?: { storePath: string; sessionKey: string }) =>
|
|
708
|
+
input?.sessionKey === "agent:main:zalouser:group:321" ? 123 : undefined,
|
|
580
709
|
});
|
|
581
710
|
|
|
582
711
|
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
583
712
|
expect(callArg?.ctx?.SessionKey).toBe("agent:main:zalouser:group:321");
|
|
584
713
|
});
|
|
585
714
|
|
|
586
|
-
it("
|
|
715
|
+
it("skips pairing store read for open DM control commands", async () => {
|
|
587
716
|
const { readAllowFromStore } = installRuntime({
|
|
588
717
|
commandAuthorized: false,
|
|
589
718
|
});
|
|
@@ -601,7 +730,7 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
601
730
|
runtime: createRuntimeEnv(),
|
|
602
731
|
});
|
|
603
732
|
|
|
604
|
-
expect(readAllowFromStore).
|
|
733
|
+
expect(readAllowFromStore).not.toHaveBeenCalled();
|
|
605
734
|
});
|
|
606
735
|
|
|
607
736
|
it("skips pairing store read for open DM non-command messages", async () => {
|