import assert from 'node:assert/strict'; import test from 'node:test'; import { BoundedMap, BoundedSet, MAX_TRACKED_MAILS, MAX_TRACKED_SESSIONS } from '../lib/bounded.js'; // ─── 上限常量 ─── test('两个上限的相对大小编码了「丢一条的后果」', () => { // 会话级映射丢一条会让那条会话失去平台侧上下文(真的行为退化), // 而 deliveredMails 丢一条只是理论上可能重复投递一封几千封之前的邮件。 // 所以邮件窗口可以给得比会话映射宽。 assert.ok(MAX_TRACKED_MAILS >= MAX_TRACKED_SESSIONS, '已投递邮件的窗口应当比会话映射更宽(它的淘汰代价更小)'); assert.ok(MAX_TRACKED_SESSIONS > 0); }); // ─── BoundedMap ─── test('BoundedMap 未达上限时与普通 Map 行为一致', () => { const m = new BoundedMap(10); m.set('a', 1).set('b', 2); assert.equal(m.size, 2); assert.equal(m.get('a'), 1); assert.equal(m.get('b'), 2); assert.equal(m.has('a'), true); assert.equal(m.has('zzz'), false); assert.equal(m.get('zzz'), undefined); assert.equal(m.evicted, 0); }); test('BoundedMap 超过上限时丢最老的,size 不再增长', () => { const m = new BoundedMap(3); m.set('a', 1).set('b', 2).set('c', 3).set('d', 4); assert.equal(m.size, 3, '上限之后 size 必须封顶 —— 这正是泄露的反面'); assert.equal(m.has('a'), false, 'a 是最老的,应当被淘汰'); assert.deepEqual([...m.keys()], ['b', 'c', 'd']); assert.equal(m.evicted, 1); }); test('BoundedMap 的 get 刷新活跃度,长期被读的键不会被淘汰', () => { const m = new BoundedMap(3); m.set('a', 1).set('b', 2).set('c', 3); m.get('a'); // a 变成最新 m.set('d', 4); // 淘汰最老的 —— 现在是 b,不是 a assert.equal(m.has('a'), true, '读也算访问:还在收信的会话不该因为建得早被丢'); assert.equal(m.has('b'), false); }); test('BoundedMap 的 peek 不刷新活跃度', () => { const m = new BoundedMap(3); m.set('a', 1).set('b', 2).set('c', 3); m.peek('a'); m.set('d', 4); assert.equal(m.has('a'), false, 'peek 是「只看一眼」,不该改变淘汰顺序'); }); test('BoundedMap 重复 set 同一个键只占一个位置且刷新顺序', () => { const m = new BoundedMap(2); m.set('a', 1).set('b', 2).set('a', 9); assert.equal(m.size, 2); assert.equal(m.get('a'), 9); m.set('c', 3); assert.equal(m.has('b'), false, 'a 被重新 set 过,b 才是最老的'); assert.equal(m.has('a'), true); }); test('BoundedMap 支持 delete / clear / 迭代', () => { const m = new BoundedMap(5); m.set('a', 1).set('b', 2); assert.equal(m.delete('a'), true); assert.equal(m.delete('a'), false); assert.deepEqual([...m.entries()], [['b', 2]]); assert.deepEqual([...m.values()], [2]); assert.deepEqual([...m], [['b', 2]]); m.clear(); assert.equal(m.size, 0); }); // ─── BoundedSet ─── test('BoundedSet 超过上限时丢最老的成员', () => { const s = new BoundedSet(3); s.add('m1').add('m2').add('m3').add('m4'); assert.equal(s.size, 3); assert.equal(s.peek('m1'), false); assert.deepEqual([...s.values()], ['m2', 'm3', 'm4']); assert.equal(s.evicted, 1); }); test('BoundedSet 的 has 刷新活跃度', () => { const s = new BoundedSet(3); s.add('a').add('b').add('c'); assert.equal(s.has('a'), true); s.add('d'); assert.equal(s.peek('a'), true, '刚被去重挡下的那封应当留得更久'); assert.equal(s.peek('b'), false); }); test('BoundedSet 重复 add 不占额外位置', () => { const s = new BoundedSet(2); s.add('a').add('a').add('a'); assert.equal(s.size, 1); }); test('BoundedSet 支持 delete / clear / 迭代,且能喂给 new Set()', () => { const s = new BoundedSet(5); s.add('a').add('b'); assert.equal(s.delete('a'), true); assert.deepEqual([...s], ['b']); // pool.mailDrivenIDs() 会 `new Set(retired)` —— 少了 Symbol.iterator 就炸 assert.deepEqual([...new Set(s)], ['b']); s.clear(); assert.equal(s.size, 0); }); // ─── 负向对照:非法上限必须当场报错 ─── test('上限为 0 时抛错,而不是静默变成一张永远空着的表', () => { // 0 的后果最隐蔽:每次 set 之后立刻把自己淘汰掉,于是去重全部失效, // 邮件被反复投递,而代码里一行错误都不打。 assert.throws(() => new BoundedMap(0), RangeError); assert.throws(() => new BoundedSet(0), RangeError); }); test('上限为 NaN / 负数 / 非数字时抛错,而不是退化成无界', () => { for (const bad of [NaN, -1, 'abc', undefined, null]) { assert.throws(() => new BoundedMap(bad), RangeError, `BoundedMap(${String(bad)}) 应当抛错`); assert.throws(() => new BoundedSet(bad), RangeError, `BoundedSet(${String(bad)}) 应当抛错`); } }); test('小数上限向下取整', () => { const m = new BoundedMap(2.9); assert.equal(m.limit, 2); m.set('a', 1).set('b', 2).set('c', 3); assert.equal(m.size, 2); }); // ─── 压力:确认 size 真的封顶(这条是「不泄露」的直接断言)─── test('灌一万条之后 size 仍等于上限', () => { const s = new BoundedSet(100); for (let i = 0; i < 10_000; i++) s.add(`mail-${i}`); assert.equal(s.size, 100); assert.equal(s.evicted, 9900); assert.equal(s.peek('mail-9999'), true, '最新的必须还在'); assert.equal(s.peek('mail-0'), false); const m = new BoundedMap(100); for (let i = 0; i < 10_000; i++) m.set(`s-${i}`, { n: i }); assert.equal(m.size, 100); });