@openclaw/zalouser 2026.3.7 → 2026.3.10
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/CHANGELOG.md +24 -0
- package/package.json +6 -1
- package/src/channel.directory.test.ts +72 -0
- package/src/channel.sendpayload.test.ts +72 -54
- package/src/channel.ts +122 -14
- package/src/config-schema.ts +12 -9
- package/src/monitor.group-gating.test.ts +379 -11
- package/src/monitor.ts +412 -114
- package/src/onboarding.ts +8 -31
- package/src/runtime.ts +4 -12
- package/src/types.ts +3 -0
- package/src/zalo-js.ts +269 -22
- package/src/zca-client.ts +45 -1
|
@@ -49,11 +49,67 @@ function createRuntimeEnv(): RuntimeEnv {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function installRuntime(params: {
|
|
52
|
+
function installRuntime(params: {
|
|
53
|
+
commandAuthorized?: boolean;
|
|
54
|
+
resolveCommandAuthorizedFromAuthorizers?: (params: {
|
|
55
|
+
useAccessGroups: boolean;
|
|
56
|
+
authorizers: Array<{ configured: boolean; allowed: boolean }>;
|
|
57
|
+
}) => boolean;
|
|
58
|
+
}) {
|
|
53
59
|
const dispatchReplyWithBufferedBlockDispatcher = vi.fn(async ({ dispatcherOptions, ctx }) => {
|
|
54
60
|
await dispatcherOptions.typingCallbacks?.onReplyStart?.();
|
|
55
61
|
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 }, ctx };
|
|
56
62
|
});
|
|
63
|
+
const resolveCommandAuthorizedFromAuthorizers = vi.fn(
|
|
64
|
+
(input: {
|
|
65
|
+
useAccessGroups: boolean;
|
|
66
|
+
authorizers: Array<{ configured: boolean; allowed: boolean }>;
|
|
67
|
+
}) => {
|
|
68
|
+
if (params.resolveCommandAuthorizedFromAuthorizers) {
|
|
69
|
+
return params.resolveCommandAuthorizedFromAuthorizers(input);
|
|
70
|
+
}
|
|
71
|
+
return params.commandAuthorized ?? false;
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
const resolveAgentRoute = vi.fn((input: { peer?: { kind?: string; id?: string } }) => {
|
|
75
|
+
const peerKind = input.peer?.kind === "direct" ? "direct" : "group";
|
|
76
|
+
const peerId = input.peer?.id ?? "1";
|
|
77
|
+
return {
|
|
78
|
+
agentId: "main",
|
|
79
|
+
sessionKey:
|
|
80
|
+
peerKind === "direct" ? "agent:main:main" : `agent:main:zalouser:${peerKind}:${peerId}`,
|
|
81
|
+
accountId: "default",
|
|
82
|
+
mainSessionKey: "agent:main:main",
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
const readAllowFromStore = vi.fn(async () => []);
|
|
86
|
+
const readSessionUpdatedAt = vi.fn(
|
|
87
|
+
(_params?: { storePath: string; sessionKey: string }): number | undefined => undefined,
|
|
88
|
+
);
|
|
89
|
+
const buildAgentSessionKey = vi.fn(
|
|
90
|
+
(input: {
|
|
91
|
+
agentId: string;
|
|
92
|
+
channel: string;
|
|
93
|
+
accountId?: string;
|
|
94
|
+
peer?: { kind?: string; id?: string };
|
|
95
|
+
dmScope?: string;
|
|
96
|
+
}) => {
|
|
97
|
+
const peerKind = input.peer?.kind === "direct" ? "direct" : "group";
|
|
98
|
+
const peerId = input.peer?.id ?? "1";
|
|
99
|
+
if (peerKind === "direct") {
|
|
100
|
+
if (input.dmScope === "per-account-channel-peer") {
|
|
101
|
+
return `agent:${input.agentId}:${input.channel}:${input.accountId ?? "default"}:direct:${peerId}`;
|
|
102
|
+
}
|
|
103
|
+
if (input.dmScope === "per-peer") {
|
|
104
|
+
return `agent:${input.agentId}:direct:${peerId}`;
|
|
105
|
+
}
|
|
106
|
+
if (input.dmScope === "main" || !input.dmScope) {
|
|
107
|
+
return "agent:main:main";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return `agent:${input.agentId}:${input.channel}:${peerKind}:${peerId}`;
|
|
111
|
+
},
|
|
112
|
+
);
|
|
57
113
|
|
|
58
114
|
setZalouserRuntime({
|
|
59
115
|
logging: {
|
|
@@ -61,13 +117,13 @@ function installRuntime(params: { commandAuthorized: boolean }) {
|
|
|
61
117
|
},
|
|
62
118
|
channel: {
|
|
63
119
|
pairing: {
|
|
64
|
-
readAllowFromStore
|
|
120
|
+
readAllowFromStore,
|
|
65
121
|
upsertPairingRequest: vi.fn(async () => ({ code: "PAIR", created: true })),
|
|
66
122
|
buildPairingReply: vi.fn(() => "pair"),
|
|
67
123
|
},
|
|
68
124
|
commands: {
|
|
69
125
|
shouldComputeCommandAuthorized: vi.fn((body: string) => body.trim().startsWith("/")),
|
|
70
|
-
resolveCommandAuthorizedFromAuthorizers
|
|
126
|
+
resolveCommandAuthorizedFromAuthorizers,
|
|
71
127
|
isControlCommandMessage: vi.fn((body: string) => body.trim().startsWith("/")),
|
|
72
128
|
shouldHandleTextCommands: vi.fn(() => true),
|
|
73
129
|
},
|
|
@@ -93,16 +149,12 @@ function installRuntime(params: { commandAuthorized: boolean }) {
|
|
|
93
149
|
}),
|
|
94
150
|
},
|
|
95
151
|
routing: {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
sessionKey: "agent:main:zalouser:group:1",
|
|
99
|
-
accountId: "default",
|
|
100
|
-
mainSessionKey: "agent:main:main",
|
|
101
|
-
})),
|
|
152
|
+
buildAgentSessionKey,
|
|
153
|
+
resolveAgentRoute,
|
|
102
154
|
},
|
|
103
155
|
session: {
|
|
104
156
|
resolveStorePath: vi.fn(() => "/tmp"),
|
|
105
|
-
readSessionUpdatedAt
|
|
157
|
+
readSessionUpdatedAt,
|
|
106
158
|
recordInboundSession: vi.fn(async () => {}),
|
|
107
159
|
},
|
|
108
160
|
reply: {
|
|
@@ -120,7 +172,14 @@ function installRuntime(params: { commandAuthorized: boolean }) {
|
|
|
120
172
|
},
|
|
121
173
|
} as unknown as PluginRuntime);
|
|
122
174
|
|
|
123
|
-
return {
|
|
175
|
+
return {
|
|
176
|
+
dispatchReplyWithBufferedBlockDispatcher,
|
|
177
|
+
resolveAgentRoute,
|
|
178
|
+
resolveCommandAuthorizedFromAuthorizers,
|
|
179
|
+
readAllowFromStore,
|
|
180
|
+
readSessionUpdatedAt,
|
|
181
|
+
buildAgentSessionKey,
|
|
182
|
+
};
|
|
124
183
|
}
|
|
125
184
|
|
|
126
185
|
function createGroupMessage(overrides: Partial<ZaloInboundMessage> = {}): ZaloInboundMessage {
|
|
@@ -142,6 +201,21 @@ function createGroupMessage(overrides: Partial<ZaloInboundMessage> = {}): ZaloIn
|
|
|
142
201
|
};
|
|
143
202
|
}
|
|
144
203
|
|
|
204
|
+
function createDmMessage(overrides: Partial<ZaloInboundMessage> = {}): ZaloInboundMessage {
|
|
205
|
+
return {
|
|
206
|
+
threadId: "u-1",
|
|
207
|
+
isGroup: false,
|
|
208
|
+
senderId: "321",
|
|
209
|
+
senderName: "Bob",
|
|
210
|
+
groupName: undefined,
|
|
211
|
+
content: "hello",
|
|
212
|
+
timestampMs: Date.now(),
|
|
213
|
+
msgId: "dm-1",
|
|
214
|
+
raw: { source: "test" },
|
|
215
|
+
...overrides,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
145
219
|
describe("zalouser monitor group mention gating", () => {
|
|
146
220
|
beforeEach(() => {
|
|
147
221
|
sendMessageZalouserMock.mockClear();
|
|
@@ -165,6 +239,25 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
165
239
|
expect(sendTypingZalouserMock).not.toHaveBeenCalled();
|
|
166
240
|
});
|
|
167
241
|
|
|
242
|
+
it("fails closed when requireMention=true but mention detection is unavailable", async () => {
|
|
243
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
244
|
+
commandAuthorized: false,
|
|
245
|
+
});
|
|
246
|
+
await __testing.processMessage({
|
|
247
|
+
message: createGroupMessage({
|
|
248
|
+
canResolveExplicitMention: false,
|
|
249
|
+
hasAnyMention: false,
|
|
250
|
+
wasExplicitlyMentioned: false,
|
|
251
|
+
}),
|
|
252
|
+
account: createAccount(),
|
|
253
|
+
config: createConfig(),
|
|
254
|
+
runtime: createRuntimeEnv(),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
258
|
+
expect(sendTypingZalouserMock).not.toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
|
|
168
261
|
it("dispatches explicitly-mentioned group messages and marks WasMentioned", async () => {
|
|
169
262
|
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
170
263
|
commandAuthorized: false,
|
|
@@ -183,6 +276,8 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
183
276
|
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
184
277
|
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
185
278
|
expect(callArg?.ctx?.WasMentioned).toBe(true);
|
|
279
|
+
expect(callArg?.ctx?.To).toBe("zalouser:group:g-1");
|
|
280
|
+
expect(callArg?.ctx?.OriginatingTo).toBe("zalouser:group:g-1");
|
|
186
281
|
expect(sendTypingZalouserMock).toHaveBeenCalledWith("g-1", {
|
|
187
282
|
profile: "default",
|
|
188
283
|
isGroup: true,
|
|
@@ -208,4 +303,277 @@ describe("zalouser monitor group mention gating", () => {
|
|
|
208
303
|
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
209
304
|
expect(callArg?.ctx?.WasMentioned).toBe(true);
|
|
210
305
|
});
|
|
306
|
+
|
|
307
|
+
it("uses commandContent for mention-prefixed control commands", async () => {
|
|
308
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
309
|
+
commandAuthorized: true,
|
|
310
|
+
});
|
|
311
|
+
await __testing.processMessage({
|
|
312
|
+
message: createGroupMessage({
|
|
313
|
+
content: "@Bot /new",
|
|
314
|
+
commandContent: "/new",
|
|
315
|
+
hasAnyMention: true,
|
|
316
|
+
wasExplicitlyMentioned: true,
|
|
317
|
+
}),
|
|
318
|
+
account: createAccount(),
|
|
319
|
+
config: createConfig(),
|
|
320
|
+
runtime: createRuntimeEnv(),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
324
|
+
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
325
|
+
expect(callArg?.ctx?.CommandBody).toBe("/new");
|
|
326
|
+
expect(callArg?.ctx?.BodyForCommands).toBe("/new");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("allows group control commands when only allowFrom is configured", async () => {
|
|
330
|
+
const { dispatchReplyWithBufferedBlockDispatcher, resolveCommandAuthorizedFromAuthorizers } =
|
|
331
|
+
installRuntime({
|
|
332
|
+
resolveCommandAuthorizedFromAuthorizers: ({ useAccessGroups, authorizers }) =>
|
|
333
|
+
useAccessGroups && authorizers.some((entry) => entry.configured && entry.allowed),
|
|
334
|
+
});
|
|
335
|
+
await __testing.processMessage({
|
|
336
|
+
message: createGroupMessage({
|
|
337
|
+
content: "/new",
|
|
338
|
+
commandContent: "/new",
|
|
339
|
+
hasAnyMention: true,
|
|
340
|
+
wasExplicitlyMentioned: true,
|
|
341
|
+
}),
|
|
342
|
+
account: {
|
|
343
|
+
...createAccount(),
|
|
344
|
+
config: {
|
|
345
|
+
...createAccount().config,
|
|
346
|
+
allowFrom: ["123"],
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
config: createConfig(),
|
|
350
|
+
runtime: createRuntimeEnv(),
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
354
|
+
const authCall = resolveCommandAuthorizedFromAuthorizers.mock.calls[0]?.[0];
|
|
355
|
+
expect(authCall?.authorizers).toEqual([
|
|
356
|
+
{ configured: true, allowed: true },
|
|
357
|
+
{ configured: true, allowed: true },
|
|
358
|
+
]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("blocks group messages when sender is not in groupAllowFrom/allowFrom", async () => {
|
|
362
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
363
|
+
commandAuthorized: false,
|
|
364
|
+
});
|
|
365
|
+
await __testing.processMessage({
|
|
366
|
+
message: createGroupMessage({
|
|
367
|
+
content: "ping @bot",
|
|
368
|
+
hasAnyMention: true,
|
|
369
|
+
wasExplicitlyMentioned: true,
|
|
370
|
+
}),
|
|
371
|
+
account: {
|
|
372
|
+
...createAccount(),
|
|
373
|
+
config: {
|
|
374
|
+
...createAccount().config,
|
|
375
|
+
groupPolicy: "allowlist",
|
|
376
|
+
allowFrom: ["999"],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
config: createConfig(),
|
|
380
|
+
runtime: createRuntimeEnv(),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("allows group control commands when sender is in groupAllowFrom", async () => {
|
|
387
|
+
const { dispatchReplyWithBufferedBlockDispatcher, resolveCommandAuthorizedFromAuthorizers } =
|
|
388
|
+
installRuntime({
|
|
389
|
+
resolveCommandAuthorizedFromAuthorizers: ({ useAccessGroups, authorizers }) =>
|
|
390
|
+
useAccessGroups && authorizers.some((entry) => entry.configured && entry.allowed),
|
|
391
|
+
});
|
|
392
|
+
await __testing.processMessage({
|
|
393
|
+
message: createGroupMessage({
|
|
394
|
+
content: "/new",
|
|
395
|
+
commandContent: "/new",
|
|
396
|
+
hasAnyMention: true,
|
|
397
|
+
wasExplicitlyMentioned: true,
|
|
398
|
+
}),
|
|
399
|
+
account: {
|
|
400
|
+
...createAccount(),
|
|
401
|
+
config: {
|
|
402
|
+
...createAccount().config,
|
|
403
|
+
allowFrom: ["999"],
|
|
404
|
+
groupAllowFrom: ["123"],
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
config: createConfig(),
|
|
408
|
+
runtime: createRuntimeEnv(),
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
412
|
+
const authCall = resolveCommandAuthorizedFromAuthorizers.mock.calls[0]?.[0];
|
|
413
|
+
expect(authCall?.authorizers).toEqual([
|
|
414
|
+
{ configured: true, allowed: false },
|
|
415
|
+
{ configured: true, allowed: true },
|
|
416
|
+
]);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("routes DM messages with direct peer kind", async () => {
|
|
420
|
+
const { dispatchReplyWithBufferedBlockDispatcher, resolveAgentRoute, buildAgentSessionKey } =
|
|
421
|
+
installRuntime({
|
|
422
|
+
commandAuthorized: false,
|
|
423
|
+
});
|
|
424
|
+
const account = createAccount();
|
|
425
|
+
await __testing.processMessage({
|
|
426
|
+
message: createDmMessage(),
|
|
427
|
+
account: {
|
|
428
|
+
...account,
|
|
429
|
+
config: {
|
|
430
|
+
...account.config,
|
|
431
|
+
dmPolicy: "open",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
config: createConfig(),
|
|
435
|
+
runtime: createRuntimeEnv(),
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
expect(resolveAgentRoute).toHaveBeenCalledWith(
|
|
439
|
+
expect.objectContaining({
|
|
440
|
+
peer: { kind: "direct", id: "321" },
|
|
441
|
+
}),
|
|
442
|
+
);
|
|
443
|
+
expect(buildAgentSessionKey).toHaveBeenCalledWith(
|
|
444
|
+
expect.objectContaining({
|
|
445
|
+
peer: { kind: "direct", id: "321" },
|
|
446
|
+
dmScope: "per-channel-peer",
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
450
|
+
expect(callArg?.ctx?.SessionKey).toBe("agent:main:zalouser:direct:321");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("reuses the legacy DM session key when only the old group-shaped session exists", async () => {
|
|
454
|
+
const { dispatchReplyWithBufferedBlockDispatcher, readSessionUpdatedAt } = installRuntime({
|
|
455
|
+
commandAuthorized: false,
|
|
456
|
+
});
|
|
457
|
+
readSessionUpdatedAt.mockImplementation((input?: { storePath: string; sessionKey: string }) =>
|
|
458
|
+
input?.sessionKey === "agent:main:zalouser:group:321" ? 123 : undefined,
|
|
459
|
+
);
|
|
460
|
+
const account = createAccount();
|
|
461
|
+
await __testing.processMessage({
|
|
462
|
+
message: createDmMessage(),
|
|
463
|
+
account: {
|
|
464
|
+
...account,
|
|
465
|
+
config: {
|
|
466
|
+
...account.config,
|
|
467
|
+
dmPolicy: "open",
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
config: createConfig(),
|
|
471
|
+
runtime: createRuntimeEnv(),
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const callArg = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
475
|
+
expect(callArg?.ctx?.SessionKey).toBe("agent:main:zalouser:group:321");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("reads pairing store for open DM control commands", async () => {
|
|
479
|
+
const { readAllowFromStore } = installRuntime({
|
|
480
|
+
commandAuthorized: false,
|
|
481
|
+
});
|
|
482
|
+
const account = createAccount();
|
|
483
|
+
await __testing.processMessage({
|
|
484
|
+
message: createDmMessage({ content: "/new", commandContent: "/new" }),
|
|
485
|
+
account: {
|
|
486
|
+
...account,
|
|
487
|
+
config: {
|
|
488
|
+
...account.config,
|
|
489
|
+
dmPolicy: "open",
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
config: createConfig(),
|
|
493
|
+
runtime: createRuntimeEnv(),
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
expect(readAllowFromStore).toHaveBeenCalledTimes(1);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("skips pairing store read for open DM non-command messages", async () => {
|
|
500
|
+
const { readAllowFromStore } = installRuntime({
|
|
501
|
+
commandAuthorized: false,
|
|
502
|
+
});
|
|
503
|
+
const account = createAccount();
|
|
504
|
+
await __testing.processMessage({
|
|
505
|
+
message: createDmMessage({ content: "hello there" }),
|
|
506
|
+
account: {
|
|
507
|
+
...account,
|
|
508
|
+
config: {
|
|
509
|
+
...account.config,
|
|
510
|
+
dmPolicy: "open",
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
config: createConfig(),
|
|
514
|
+
runtime: createRuntimeEnv(),
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(readAllowFromStore).not.toHaveBeenCalled();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("includes skipped group messages as InboundHistory on the next processed message", async () => {
|
|
521
|
+
const { dispatchReplyWithBufferedBlockDispatcher } = installRuntime({
|
|
522
|
+
commandAuthorized: false,
|
|
523
|
+
});
|
|
524
|
+
const historyState = {
|
|
525
|
+
historyLimit: 5,
|
|
526
|
+
groupHistories: new Map<
|
|
527
|
+
string,
|
|
528
|
+
Array<{ sender: string; body: string; timestamp?: number; messageId?: string }>
|
|
529
|
+
>(),
|
|
530
|
+
};
|
|
531
|
+
const account = createAccount();
|
|
532
|
+
const config = createConfig();
|
|
533
|
+
await __testing.processMessage({
|
|
534
|
+
message: createGroupMessage({
|
|
535
|
+
content: "first unmentioned line",
|
|
536
|
+
hasAnyMention: false,
|
|
537
|
+
wasExplicitlyMentioned: false,
|
|
538
|
+
}),
|
|
539
|
+
account,
|
|
540
|
+
config,
|
|
541
|
+
runtime: createRuntimeEnv(),
|
|
542
|
+
historyState,
|
|
543
|
+
});
|
|
544
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled();
|
|
545
|
+
|
|
546
|
+
await __testing.processMessage({
|
|
547
|
+
message: createGroupMessage({
|
|
548
|
+
content: "second line @bot",
|
|
549
|
+
hasAnyMention: true,
|
|
550
|
+
wasExplicitlyMentioned: true,
|
|
551
|
+
}),
|
|
552
|
+
account,
|
|
553
|
+
config,
|
|
554
|
+
runtime: createRuntimeEnv(),
|
|
555
|
+
historyState,
|
|
556
|
+
});
|
|
557
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
558
|
+
const firstDispatch = dispatchReplyWithBufferedBlockDispatcher.mock.calls[0]?.[0];
|
|
559
|
+
expect(firstDispatch?.ctx?.InboundHistory).toEqual([
|
|
560
|
+
expect.objectContaining({ sender: "Alice", body: "first unmentioned line" }),
|
|
561
|
+
]);
|
|
562
|
+
expect(String(firstDispatch?.ctx?.Body ?? "")).toContain("first unmentioned line");
|
|
563
|
+
|
|
564
|
+
await __testing.processMessage({
|
|
565
|
+
message: createGroupMessage({
|
|
566
|
+
content: "third line @bot",
|
|
567
|
+
hasAnyMention: true,
|
|
568
|
+
wasExplicitlyMentioned: true,
|
|
569
|
+
}),
|
|
570
|
+
account,
|
|
571
|
+
config,
|
|
572
|
+
runtime: createRuntimeEnv(),
|
|
573
|
+
historyState,
|
|
574
|
+
});
|
|
575
|
+
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(2);
|
|
576
|
+
const secondDispatch = dispatchReplyWithBufferedBlockDispatcher.mock.calls[1]?.[0];
|
|
577
|
+
expect(secondDispatch?.ctx?.InboundHistory).toEqual([]);
|
|
578
|
+
});
|
|
211
579
|
});
|