@zhin.js/core 1.0.46 → 1.0.48

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.
@@ -0,0 +1,566 @@
1
+ /**
2
+ * MessageFilterFeature 测试
3
+ */
4
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
5
+ import {
6
+ MessageFilterFeature,
7
+ FilterRules,
8
+ type FilterRule,
9
+ type MessageFilterConfig,
10
+ } from '../src/built/message-filter.js';
11
+ import type { Message, MessageBase } from '../src/message.js';
12
+
13
+ // ============================================================================
14
+ // 辅助函数
15
+ // ============================================================================
16
+
17
+ function makeMessage(overrides: Partial<MessageBase> = {}): Message<any> {
18
+ return {
19
+ $id: '1',
20
+ $adapter: 'test' as any,
21
+ $bot: 'bot1',
22
+ $content: [],
23
+ $raw: '',
24
+ $sender: { id: 'user1', name: 'User' },
25
+ $channel: { id: 'ch1', type: 'group' },
26
+ $timestamp: Date.now(),
27
+ $reply: vi.fn(),
28
+ $recall: vi.fn(),
29
+ ...overrides,
30
+ } as any;
31
+ }
32
+
33
+ // ============================================================================
34
+ // 元数据
35
+ // ============================================================================
36
+
37
+ describe('MessageFilterFeature', () => {
38
+ let filter: MessageFilterFeature;
39
+
40
+ beforeEach(() => {
41
+ filter = new MessageFilterFeature();
42
+ });
43
+
44
+ it('应有正确的元数据', () => {
45
+ expect(filter.name).toBe('message-filter');
46
+ expect(filter.icon).toBe('Filter');
47
+ expect(filter.desc).toBe('消息过滤');
48
+ });
49
+
50
+ it('默认策略为 allow', () => {
51
+ expect(filter.defaultPolicy).toBe('allow');
52
+ });
53
+
54
+ it('无规则时所有消息允许通过', () => {
55
+ const result = filter.test(makeMessage());
56
+ expect(result.allowed).toBe(true);
57
+ expect(result.matchedRule).toBeNull();
58
+ });
59
+
60
+ // ============================================================================
61
+ // 默认策略
62
+ // ============================================================================
63
+
64
+ describe('默认策略', () => {
65
+ it('设置 deny 后无规则匹配时应拒绝', () => {
66
+ filter.defaultPolicy = 'deny';
67
+ const result = filter.test(makeMessage());
68
+ expect(result.allowed).toBe(false);
69
+ expect(result.matchedRule).toBeNull();
70
+ expect(result.reason).toContain('deny');
71
+ });
72
+
73
+ it('设置 allow 后无规则匹配时应放行', () => {
74
+ filter.defaultPolicy = 'allow';
75
+ const result = filter.test(makeMessage());
76
+ expect(result.allowed).toBe(true);
77
+ });
78
+ });
79
+
80
+ // ============================================================================
81
+ // 规则 CRUD
82
+ // ============================================================================
83
+
84
+ describe('规则 CRUD', () => {
85
+ it('add 应注册规则', () => {
86
+ const rule: FilterRule = { name: 'r1', action: 'deny' };
87
+ filter.add(rule, 'test-plugin');
88
+
89
+ expect(filter.count).toBe(1);
90
+ expect(filter.getRule('r1')).toBe(rule);
91
+ expect(filter.byName.has('r1')).toBe(true);
92
+ });
93
+
94
+ it('add 返回的 dispose 应移除规则', () => {
95
+ const rule: FilterRule = { name: 'r1', action: 'deny' };
96
+ const dispose = filter.add(rule, 'test-plugin');
97
+
98
+ dispose();
99
+ expect(filter.count).toBe(0);
100
+ expect(filter.getRule('r1')).toBeUndefined();
101
+ });
102
+
103
+ it('remove 应移除规则并清理索引', () => {
104
+ const rule: FilterRule = { name: 'r1', action: 'deny' };
105
+ filter.add(rule, 'test-plugin');
106
+
107
+ filter.remove(rule);
108
+ expect(filter.count).toBe(0);
109
+ expect(filter.byName.has('r1')).toBe(false);
110
+ });
111
+
112
+ it('getByPlugin 应返回该插件的规则', () => {
113
+ filter.add({ name: 'a', action: 'deny' }, 'plugin-a');
114
+ filter.add({ name: 'b', action: 'deny' }, 'plugin-b');
115
+ filter.add({ name: 'c', action: 'deny' }, 'plugin-a');
116
+
117
+ expect(filter.getByPlugin('plugin-a')).toHaveLength(2);
118
+ expect(filter.getByPlugin('plugin-b')).toHaveLength(1);
119
+ });
120
+ });
121
+
122
+ // ============================================================================
123
+ // 优先级排序
124
+ // ============================================================================
125
+
126
+ describe('优先级排序', () => {
127
+ it('高优先级规则应先匹配', () => {
128
+ filter.add({ name: 'low', action: 'deny', priority: 0 }, 'test');
129
+ filter.add({ name: 'high', action: 'allow', priority: 100 }, 'test');
130
+
131
+ const result = filter.test(makeMessage());
132
+ expect(result.matchedRule).toBe('high');
133
+ expect(result.allowed).toBe(true);
134
+ });
135
+
136
+ it('相同优先级应按添加顺序', () => {
137
+ filter.add({ name: 'first', action: 'deny' }, 'test');
138
+ filter.add({ name: 'second', action: 'allow' }, 'test');
139
+
140
+ const result = filter.test(makeMessage());
141
+ expect(result.matchedRule).toBe('first');
142
+ });
143
+
144
+ it('添加/移除规则后缓存应刷新', () => {
145
+ filter.add({ name: 'r1', action: 'deny', priority: 10 }, 'test');
146
+ expect(filter.sortedRules[0].name).toBe('r1');
147
+
148
+ const dispose = filter.add({ name: 'r2', action: 'allow', priority: 20 }, 'test');
149
+ expect(filter.sortedRules[0].name).toBe('r2');
150
+
151
+ dispose();
152
+ expect(filter.sortedRules[0].name).toBe('r1');
153
+ });
154
+ });
155
+
156
+ // ============================================================================
157
+ // enabled 字段
158
+ // ============================================================================
159
+
160
+ describe('enabled 字段', () => {
161
+ it('disabled 规则应被跳过', () => {
162
+ filter.add({ name: 'disabled', action: 'deny', enabled: false, priority: 100 }, 'test');
163
+ filter.add({ name: 'enabled', action: 'allow' }, 'test');
164
+
165
+ const result = filter.test(makeMessage());
166
+ expect(result.matchedRule).toBe('enabled');
167
+ });
168
+
169
+ it('enabled: true 应正常匹配', () => {
170
+ filter.add({ name: 'r1', action: 'deny', enabled: true }, 'test');
171
+ const result = filter.test(makeMessage());
172
+ expect(result.matchedRule).toBe('r1');
173
+ });
174
+
175
+ it('未设置 enabled 默认为启用', () => {
176
+ filter.add({ name: 'r1', action: 'deny' }, 'test');
177
+ expect(filter.sortedRules).toHaveLength(1);
178
+ });
179
+ });
180
+
181
+ // ============================================================================
182
+ // scope 匹配
183
+ // ============================================================================
184
+
185
+ describe('scope 匹配', () => {
186
+ it('scope 匹配时应命中', () => {
187
+ filter.add({ name: 'r1', action: 'deny', scopes: ['group'] }, 'test');
188
+
189
+ const msg = makeMessage({ $channel: { id: 'ch1', type: 'group' } });
190
+ expect(filter.test(msg).allowed).toBe(false);
191
+ });
192
+
193
+ it('scope 不匹配时应跳过', () => {
194
+ filter.add({ name: 'r1', action: 'deny', scopes: ['private'] }, 'test');
195
+
196
+ const msg = makeMessage({ $channel: { id: 'ch1', type: 'group' } });
197
+ expect(filter.test(msg).allowed).toBe(true); // 无规则匹配,走默认 allow
198
+ });
199
+
200
+ it('多 scope 匹配任一即可', () => {
201
+ filter.add({ name: 'r1', action: 'deny', scopes: ['group', 'channel'] }, 'test');
202
+
203
+ expect(filter.test(makeMessage({ $channel: { id: 'c1', type: 'channel' } })).allowed).toBe(false);
204
+ expect(filter.test(makeMessage({ $channel: { id: 'g1', type: 'group' } })).allowed).toBe(false);
205
+ expect(filter.test(makeMessage({ $channel: { id: 'p1', type: 'private' } })).allowed).toBe(true);
206
+ });
207
+
208
+ it('未设置 scope 应匹配所有类型', () => {
209
+ filter.add({ name: 'r1', action: 'deny' }, 'test');
210
+
211
+ expect(filter.test(makeMessage({ $channel: { id: 'g1', type: 'group' } })).allowed).toBe(false);
212
+ expect(filter.test(makeMessage({ $channel: { id: 'p1', type: 'private' } })).allowed).toBe(false);
213
+ expect(filter.test(makeMessage({ $channel: { id: 'c1', type: 'channel' } })).allowed).toBe(false);
214
+ });
215
+ });
216
+
217
+ // ============================================================================
218
+ // 多维条件匹配
219
+ // ============================================================================
220
+
221
+ describe('多维条件匹配', () => {
222
+ it('adapter 匹配', () => {
223
+ filter.add({ name: 'r1', action: 'deny', adapters: ['icqq'] }, 'test');
224
+
225
+ expect(filter.test(makeMessage({ $adapter: 'icqq' as any })).allowed).toBe(false);
226
+ expect(filter.test(makeMessage({ $adapter: 'discord' as any })).allowed).toBe(true);
227
+ });
228
+
229
+ it('bot 匹配', () => {
230
+ filter.add({ name: 'r1', action: 'deny', bots: ['bot-a'] }, 'test');
231
+
232
+ expect(filter.test(makeMessage({ $bot: 'bot-a' })).allowed).toBe(false);
233
+ expect(filter.test(makeMessage({ $bot: 'bot-b' })).allowed).toBe(true);
234
+ });
235
+
236
+ it('channel 匹配', () => {
237
+ filter.add({ name: 'r1', action: 'deny', channels: ['group-123'] }, 'test');
238
+
239
+ expect(filter.test(makeMessage({ $channel: { id: 'group-123', type: 'group' } })).allowed).toBe(false);
240
+ expect(filter.test(makeMessage({ $channel: { id: 'group-456', type: 'group' } })).allowed).toBe(true);
241
+ });
242
+
243
+ it('sender 匹配', () => {
244
+ filter.add({ name: 'r1', action: 'deny', senders: ['baduser'] }, 'test');
245
+
246
+ expect(filter.test(makeMessage({ $sender: { id: 'baduser', name: 'Bad' } })).allowed).toBe(false);
247
+ expect(filter.test(makeMessage({ $sender: { id: 'gooduser', name: 'Good' } })).allowed).toBe(true);
248
+ });
249
+
250
+ it('AND 逻辑:所有条件必须同时满足', () => {
251
+ filter.add({
252
+ name: 'r1',
253
+ action: 'deny',
254
+ scopes: ['group'],
255
+ adapters: ['icqq'],
256
+ senders: ['spammer'],
257
+ }, 'test');
258
+
259
+ // 全部满足 → deny
260
+ expect(filter.test(makeMessage({
261
+ $adapter: 'icqq' as any,
262
+ $channel: { id: 'g1', type: 'group' },
263
+ $sender: { id: 'spammer', name: 'S' },
264
+ })).allowed).toBe(false);
265
+
266
+ // scope 不满足 → 跳过规则
267
+ expect(filter.test(makeMessage({
268
+ $adapter: 'icqq' as any,
269
+ $channel: { id: 'p1', type: 'private' },
270
+ $sender: { id: 'spammer', name: 'S' },
271
+ })).allowed).toBe(true);
272
+
273
+ // adapter 不满足 → 跳过规则
274
+ expect(filter.test(makeMessage({
275
+ $adapter: 'discord' as any,
276
+ $channel: { id: 'g1', type: 'group' },
277
+ $sender: { id: 'spammer', name: 'S' },
278
+ })).allowed).toBe(true);
279
+
280
+ // sender 不满足 → 跳过规则
281
+ expect(filter.test(makeMessage({
282
+ $adapter: 'icqq' as any,
283
+ $channel: { id: 'g1', type: 'group' },
284
+ $sender: { id: 'normal', name: 'N' },
285
+ })).allowed).toBe(true);
286
+ });
287
+
288
+ it('OR 逻辑:条件内多个值匹配任一', () => {
289
+ filter.add({ name: 'r1', action: 'deny', channels: ['g1', 'g2', 'g3'] }, 'test');
290
+
291
+ expect(filter.test(makeMessage({ $channel: { id: 'g1', type: 'group' } })).allowed).toBe(false);
292
+ expect(filter.test(makeMessage({ $channel: { id: 'g2', type: 'group' } })).allowed).toBe(false);
293
+ expect(filter.test(makeMessage({ $channel: { id: 'g4', type: 'group' } })).allowed).toBe(true);
294
+ });
295
+ });
296
+
297
+ // ============================================================================
298
+ // Pattern 匹配
299
+ // ============================================================================
300
+
301
+ describe('Pattern 匹配', () => {
302
+ it('精确字符串匹配', () => {
303
+ filter.add({ name: 'r1', action: 'deny', channels: ['exact-id'] }, 'test');
304
+ expect(filter.test(makeMessage({ $channel: { id: 'exact-id', type: 'group' } })).allowed).toBe(false);
305
+ expect(filter.test(makeMessage({ $channel: { id: 'exact-id-extra', type: 'group' } })).allowed).toBe(true);
306
+ });
307
+
308
+ it('通配符 * 匹配所有', () => {
309
+ filter.add({ name: 'r1', action: 'deny', scopes: ['group'], channels: ['*'] }, 'test');
310
+ expect(filter.test(makeMessage({ $channel: { id: 'any-id', type: 'group' } })).allowed).toBe(false);
311
+ expect(filter.test(makeMessage({ $channel: { id: 'another', type: 'group' } })).allowed).toBe(false);
312
+ });
313
+
314
+ it('正则表达式匹配', () => {
315
+ filter.add({ name: 'r1', action: 'deny', channels: [/^test-/] }, 'test');
316
+ expect(filter.test(makeMessage({ $channel: { id: 'test-abc', type: 'group' } })).allowed).toBe(false);
317
+ expect(filter.test(makeMessage({ $channel: { id: 'prod-abc', type: 'group' } })).allowed).toBe(true);
318
+ });
319
+
320
+ it('混合 pattern 类型', () => {
321
+ filter.add({ name: 'r1', action: 'deny', senders: ['exact-user', /^bot-/] }, 'test');
322
+ expect(filter.test(makeMessage({ $sender: { id: 'exact-user', name: 'U' } })).allowed).toBe(false);
323
+ expect(filter.test(makeMessage({ $sender: { id: 'bot-123', name: 'B' } })).allowed).toBe(false);
324
+ expect(filter.test(makeMessage({ $sender: { id: 'normal', name: 'N' } })).allowed).toBe(true);
325
+ });
326
+ });
327
+
328
+ // ============================================================================
329
+ // first-match-wins 语义
330
+ // ============================================================================
331
+
332
+ describe('first-match-wins', () => {
333
+ it('高优先级 allow 应覆盖低优先级 deny', () => {
334
+ filter.add({ name: 'deny-all', action: 'deny', priority: 0 }, 'test');
335
+ filter.add({ name: 'allow-vip', action: 'allow', priority: 100, senders: ['vip'] }, 'test');
336
+
337
+ const vipMsg = makeMessage({ $sender: { id: 'vip', name: 'VIP' } });
338
+ const normalMsg = makeMessage({ $sender: { id: 'normal', name: 'N' } });
339
+
340
+ expect(filter.test(vipMsg).allowed).toBe(true);
341
+ expect(filter.test(vipMsg).matchedRule).toBe('allow-vip');
342
+
343
+ expect(filter.test(normalMsg).allowed).toBe(false);
344
+ expect(filter.test(normalMsg).matchedRule).toBe('deny-all');
345
+ });
346
+
347
+ it('白名单模式:allow 特定 + deny 兜底', () => {
348
+ filter.add({ name: 'allow-group', action: 'allow', scopes: ['group'], channels: ['allowed-group'], priority: 1 }, 'test');
349
+ filter.add({ name: 'deny-rest', action: 'deny', scopes: ['group'], priority: -100 }, 'test');
350
+
351
+ expect(filter.test(makeMessage({ $channel: { id: 'allowed-group', type: 'group' } })).allowed).toBe(true);
352
+ expect(filter.test(makeMessage({ $channel: { id: 'other-group', type: 'group' } })).allowed).toBe(false);
353
+ // private 消息不受影响
354
+ expect(filter.test(makeMessage({ $channel: { id: 'p1', type: 'private' } })).allowed).toBe(true);
355
+ });
356
+ });
357
+
358
+ // ============================================================================
359
+ // 配置加载
360
+ // ============================================================================
361
+
362
+ describe('配置加载', () => {
363
+ it('从 config 加载 default_policy', () => {
364
+ const f = new MessageFilterFeature({ default_policy: 'deny' });
365
+ expect(f.defaultPolicy).toBe('deny');
366
+ });
367
+
368
+ it('从 config 加载规则', () => {
369
+ const config: MessageFilterConfig = {
370
+ rules: [
371
+ { name: 'r1', action: 'deny', scopes: ['group'], channels: ['123'] },
372
+ { name: 'r2', action: 'allow', priority: 10, senders: ['admin'] },
373
+ ],
374
+ };
375
+ const f = new MessageFilterFeature(config);
376
+
377
+ expect(f.count).toBe(2);
378
+ expect(f.getRule('r1')).toBeDefined();
379
+ expect(f.getRule('r2')).toBeDefined();
380
+ });
381
+
382
+ it('配置中的正则字符串应被解析', () => {
383
+ const config: MessageFilterConfig = {
384
+ rules: [
385
+ { name: 'regex-rule', action: 'deny', channels: ['/^test-/i'] },
386
+ ],
387
+ };
388
+ const f = new MessageFilterFeature(config);
389
+
390
+ expect(f.test(makeMessage({ $channel: { id: 'Test-123', type: 'group' } })).allowed).toBe(false);
391
+ expect(f.test(makeMessage({ $channel: { id: 'prod-123', type: 'group' } })).allowed).toBe(true);
392
+ });
393
+
394
+ it('配置中的普通字符串不应被误解析为正则', () => {
395
+ const config: MessageFilterConfig = {
396
+ rules: [
397
+ { name: 'str-rule', action: 'deny', channels: ['plain-string'] },
398
+ ],
399
+ };
400
+ const f = new MessageFilterFeature(config);
401
+
402
+ expect(f.test(makeMessage({ $channel: { id: 'plain-string', type: 'group' } })).allowed).toBe(false);
403
+ expect(f.test(makeMessage({ $channel: { id: 'plain-string-extra', type: 'group' } })).allowed).toBe(true);
404
+ });
405
+
406
+ it('空 config 应无规则', () => {
407
+ const f = new MessageFilterFeature({});
408
+ expect(f.count).toBe(0);
409
+ expect(f.defaultPolicy).toBe('allow');
410
+ });
411
+
412
+ it('undefined config 应无规则', () => {
413
+ const f = new MessageFilterFeature(undefined);
414
+ expect(f.count).toBe(0);
415
+ });
416
+ });
417
+
418
+ // ============================================================================
419
+ // FilterRules 工厂
420
+ // ============================================================================
421
+
422
+ describe('FilterRules 工厂', () => {
423
+ it('deny() 创建 deny 规则', () => {
424
+ const rule = FilterRules.deny('test-deny', { scopes: ['group'], channels: ['g1'] });
425
+ expect(rule.name).toBe('test-deny');
426
+ expect(rule.action).toBe('deny');
427
+ expect(rule.scopes).toEqual(['group']);
428
+ });
429
+
430
+ it('allow() 创建 allow 规则', () => {
431
+ const rule = FilterRules.allow('test-allow', { senders: ['admin'] });
432
+ expect(rule.name).toBe('test-allow');
433
+ expect(rule.action).toBe('allow');
434
+ });
435
+
436
+ it('blacklist() 创建 deny 规则', () => {
437
+ const rule = FilterRules.blacklist('group', ['g1', 'g2']);
438
+ expect(rule.action).toBe('deny');
439
+ expect(rule.scopes).toEqual(['group']);
440
+ expect(rule.channels).toEqual(['g1', 'g2']);
441
+ expect(rule.name).toBe('group-blacklist');
442
+ });
443
+
444
+ it('blacklist() 支持自定义名称', () => {
445
+ const rule = FilterRules.blacklist('private', ['u1'], 'custom-name');
446
+ expect(rule.name).toBe('custom-name');
447
+ });
448
+
449
+ it('whitelist() 创建 [allow, deny-catch-all] 规则对', () => {
450
+ const [allow, deny] = FilterRules.whitelist('channel', ['c1', 'c2']);
451
+ expect(allow.action).toBe('allow');
452
+ expect(allow.scopes).toEqual(['channel']);
453
+ expect(allow.channels).toEqual(['c1', 'c2']);
454
+ expect(allow.priority).toBe(1);
455
+
456
+ expect(deny.action).toBe('deny');
457
+ expect(deny.scopes).toEqual(['channel']);
458
+ expect(deny.priority).toBe(-100);
459
+ });
460
+
461
+ it('whitelist 集成到 filter 的完整行为', () => {
462
+ const [allow, denyRest] = FilterRules.whitelist('group', ['allowed-group']);
463
+ filter.add(allow, 'test');
464
+ filter.add(denyRest, 'test');
465
+
466
+ expect(filter.test(makeMessage({ $channel: { id: 'allowed-group', type: 'group' } })).allowed).toBe(true);
467
+ expect(filter.test(makeMessage({ $channel: { id: 'other-group', type: 'group' } })).allowed).toBe(false);
468
+ expect(filter.test(makeMessage({ $channel: { id: 'p1', type: 'private' } })).allowed).toBe(true);
469
+ });
470
+ });
471
+
472
+ // ============================================================================
473
+ // toJSON
474
+ // ============================================================================
475
+
476
+ describe('toJSON', () => {
477
+ it('应序列化所有规则', () => {
478
+ filter.add({ name: 'r1', action: 'deny', channels: ['ch1'] }, 'p1');
479
+ filter.add({ name: 'r2', action: 'allow', senders: [/^admin/], priority: 10 }, 'p2');
480
+
481
+ const json = filter.toJSON();
482
+ expect(json.name).toBe('message-filter');
483
+ expect(json.count).toBe(2);
484
+ expect(json.items).toHaveLength(2);
485
+ });
486
+
487
+ it('应按插件名过滤', () => {
488
+ filter.add({ name: 'r1', action: 'deny' }, 'p1');
489
+ filter.add({ name: 'r2', action: 'deny' }, 'p2');
490
+
491
+ expect(filter.toJSON('p1').count).toBe(1);
492
+ expect(filter.toJSON('p1').items[0].name).toBe('r1');
493
+ });
494
+
495
+ it('RegExp 应被序列化为 source 字符串', () => {
496
+ filter.add({ name: 'r1', action: 'deny', channels: [/^test-/] }, 'test');
497
+ const json = filter.toJSON();
498
+ expect(json.items[0].channels).toEqual(['^test-']);
499
+ });
500
+
501
+ it('字符串 pattern 应保持原样', () => {
502
+ filter.add({ name: 'r1', action: 'deny', channels: ['exact'] }, 'test');
503
+ const json = filter.toJSON();
504
+ expect(json.items[0].channels).toEqual(['exact']);
505
+ });
506
+ });
507
+
508
+ // ============================================================================
509
+ // 综合场景
510
+ // ============================================================================
511
+
512
+ describe('综合场景', () => {
513
+ it('防火墙规则链:VIP 放行 + 群黑名单 + 默认 allow', () => {
514
+ filter.add({ name: 'vip', action: 'allow', priority: 100, senders: ['vip-user'] }, 'test');
515
+ filter.add({ name: 'block-groups', action: 'deny', scopes: ['group'], channels: ['spam-group'] }, 'test');
516
+
517
+ // VIP 即使在黑名单群也能通过
518
+ expect(filter.test(makeMessage({
519
+ $sender: { id: 'vip-user', name: 'V' },
520
+ $channel: { id: 'spam-group', type: 'group' },
521
+ })).allowed).toBe(true);
522
+
523
+ // 普通用户在黑名单群被拦截
524
+ expect(filter.test(makeMessage({
525
+ $sender: { id: 'normal', name: 'N' },
526
+ $channel: { id: 'spam-group', type: 'group' },
527
+ })).allowed).toBe(false);
528
+
529
+ // 普通用户在正常群通过
530
+ expect(filter.test(makeMessage({
531
+ $sender: { id: 'normal', name: 'N' },
532
+ $channel: { id: 'good-group', type: 'group' },
533
+ })).allowed).toBe(true);
534
+ });
535
+
536
+ it('纯白名单模式:default deny + 特定规则放行', () => {
537
+ filter.defaultPolicy = 'deny';
538
+ filter.add({ name: 'allow-admin', action: 'allow', senders: ['admin'] }, 'test');
539
+ filter.add({ name: 'allow-work', action: 'allow', scopes: ['group'], channels: ['work-group'] }, 'test');
540
+
541
+ expect(filter.test(makeMessage({ $sender: { id: 'admin', name: 'A' } })).allowed).toBe(true);
542
+ expect(filter.test(makeMessage({ $channel: { id: 'work-group', type: 'group' } })).allowed).toBe(true);
543
+ expect(filter.test(makeMessage({ $channel: { id: 'random-group', type: 'group' } })).allowed).toBe(false);
544
+ expect(filter.test(makeMessage({ $channel: { id: 'p1', type: 'private' } })).allowed).toBe(false);
545
+ });
546
+
547
+ it('多适配器隔离:只允许 QQ 群聊,拒绝 QQ 私聊', () => {
548
+ filter.add({ name: 'deny-qq-private', action: 'deny', adapters: ['icqq'], scopes: ['private'] }, 'test');
549
+
550
+ expect(filter.test(makeMessage({
551
+ $adapter: 'icqq' as any,
552
+ $channel: { id: 'p1', type: 'private' },
553
+ })).allowed).toBe(false);
554
+
555
+ expect(filter.test(makeMessage({
556
+ $adapter: 'icqq' as any,
557
+ $channel: { id: 'g1', type: 'group' },
558
+ })).allowed).toBe(true);
559
+
560
+ expect(filter.test(makeMessage({
561
+ $adapter: 'discord' as any,
562
+ $channel: { id: 'p1', type: 'private' },
563
+ })).allowed).toBe(true);
564
+ });
565
+ });
566
+ });