// IM **在线态取数接线**回归测试(node:test)—— IM 线 · 第 15 棒。 // // 跑的是**构建产物** `lib/`(`npm test` 先 build)。判据编号写在用例名里 ⇒ // 「哪条验收对哪个用例」一眼可查,第三方可复现。 // // ## 覆盖策略 // ① **具名取值判据**:`'local-hub'` / `'gateway-join-leave'` / `'not-wired'` 三个取值 // 互不相同,且"没接"这件事**必须**仍然可读出来(⛔ 不许把哨兵改掉)。 // ② **接线判据**:注入端口 ⇒ `stats().presenceIngest` 具名;未注入 ⇒ 仍是哨兵。 // ③ **落到内核判据**(最重要):事件必须走进 `hub.notePresence()` ⇒ 批合并 / 收敛 / // 帧文本 / "只推订阅该房的连接"**全部与 native 逐字相同**(本模块不自己造帧)。 // ④ **不信任外部输入**:`apply()` 丢弃并计数(不抛);`parsePresenceEvents()` 整批拒收 // (fail-closed,具名原因)。 // ⑤ **回调面结构性判据**(源码级):fail-closed 的 404 分支、常量时间比对、key 不进读数。 // // ⚠️ 本文件**不碰网络**、**不碰 DB**(hub 用桩 store,连接用 `syntheticConnection`)。 import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { join } from 'node:path' import { ImHub } from '../lib/im/hub.js' import { PRESENCE_EVENT_KINDS, PRESENCE_INGEST_GATEWAY, PRESENCE_INGEST_LOCAL, PRESENCE_INGEST_NOT_WIRED, createHubPresenceIngest, parsePresenceEvents, secretEquals, } from '../lib/im/presence-ingest.js' import { GATEWAY_PRESENCE_INGEST, GATEWAY_PRESENCE_INGEST_WIRED, createGatewayConnectionBackend, } from '../lib/im/backends/gateway.js' import { createNativeConnectionBackend } from '../lib/im/backends/native.js' import { syntheticConnection } from '../lib/im/ws.js' const ROOT = process.cwd() const readSrc = (rel) => readFileSync(join(ROOT, rel), 'utf8') /** 最小 store 桩(本文件不碰 send / resume 路径)。 */ const storeStub = () => ({}) /** runtime 桩(native / gateway 后端只转发它的三个方法)。 */ const runtimeStub = (over = {}) => ({ dispatch: () => false, connectionCount: () => 0, close: () => undefined, ...over, }) /** * 批窗口闸门打开的 hub:`now: () => 0` + `presenceBatchMs: 0` —— ⛔ 不依赖真实时间 * (否则 `lastFlushAt` 取真实 `Date.now` ⇒ 首帧被闸门吃掉,判据会**假绿**;第 12 棒踩过)。 */ const hubOpenBatch = () => new ImHub(storeStub(), { now: () => 0, presenceBatchMs: 0 }) // ── 判据 ① · 具名取值 ───────────────────────────────────────────────────────── test('P-1 · 取数路径三个取值互不相同;哨兵仍是 `not-wired`(⛔ 不许把"没接"伪装成"接了")', () => { const all = [PRESENCE_INGEST_NOT_WIRED, PRESENCE_INGEST_LOCAL, PRESENCE_INGEST_GATEWAY] assert.equal(new Set(all).size, 3) assert.equal(PRESENCE_INGEST_NOT_WIRED, 'not-wired') assert.equal(PRESENCE_INGEST_LOCAL, 'local-hub') assert.equal(PRESENCE_INGEST_GATEWAY, 'gateway-join-leave') // gateway 模块的导出与内核常量**同源**(⛔ 不是两处各写一遍字面量) assert.equal(GATEWAY_PRESENCE_INGEST, PRESENCE_INGEST_NOT_WIRED) assert.equal(GATEWAY_PRESENCE_INGEST_WIRED, PRESENCE_INGEST_GATEWAY) assert.deepEqual(PRESENCE_EVENT_KINDS, ['join', 'leave', 'snapshot']) }) // ── 判据 ② · 接线与否在 stats 里自曝 ────────────────────────────────────────── test('P-2 · 未注入取数端口 ⇒ gateway 后端自曝 `not-wired`,且不挂 `presenceIngest` 字段', () => { const backend = createGatewayConnectionBackend({ endpoint: 'http://127.0.0.1:1', key: 'k', runtime: runtimeStub() }) assert.equal(backend.stats().presenceIngest, PRESENCE_INGEST_NOT_WIRED) assert.equal(backend.presenceIngest, undefined) }) test('P-3 · 注入取数端口 ⇒ `stats().presenceIngest` 具名,且后端把同一端口挂出来', () => { const hub = hubOpenBatch() const port = createHubPresenceIngest(hub, { source: PRESENCE_INGEST_GATEWAY }) const backend = createGatewayConnectionBackend({ endpoint: 'http://127.0.0.1:1', key: 'k', runtime: runtimeStub(), presence: port, }) assert.equal(backend.stats().presenceIngest, PRESENCE_INGEST_GATEWAY) assert.equal(backend.presenceIngest, port) assert.equal(backend.presenceIngest.source, PRESENCE_INGEST_GATEWAY) }) test('P-9 · native 后端如实报 `local-hub`(在线态权威就在本进程 ⇒ ⛔ 不再是 not-wired)', () => { const hub = hubOpenBatch() const backend = createNativeConnectionBackend({ hub, runtime: runtimeStub() }) assert.equal(backend.stats().presenceIngest, PRESENCE_INGEST_LOCAL) }) // ── 判据 ③ · 事件真的落进内核(两条读数对照)────────────────────────────────── test('P-4 · 两条读数对照:取数前该成员不在快照里 ⇒ 取数后为 `online`', () => { const hub = hubOpenBatch() const port = createHubPresenceIngest(hub) // 读数 1(取数前):快照里没有任何人,在线聚合数 = 0 const before = port.snapshot('room-1') assert.deepEqual(before, {}) assert.equal(port.onlineCount('room-1'), 0) assert.equal(port.ledgerOnline(), 0) // 喂两条 join(不同成员,同一房) const r = port.apply([ { roomId: 'room-1', memberId: 'u1', kind: 'join' }, { roomId: 'room-1', memberId: 'u2', kind: 'join' }, ]) assert.deepEqual(r, { accepted: 2, ignored: 0, rooms: 1, reason: '' }) // 读数 2(取数后) const after = port.snapshot('room-1') assert.deepEqual(after, { u1: 'online', u2: 'online' }) assert.equal(port.onlineCount('room-1'), 2) assert.equal(port.ledgerOnline(), 2) assert.notDeepEqual(before, after) }) test('P-5 · `leave` 才是离线;`snapshot` **只做加法**(⛔ 不把"缺席"当"离开")', () => { const hub = hubOpenBatch() const port = createHubPresenceIngest(hub) port.apply([{ roomId: 'room-1', memberId: 'u1', kind: 'join' }]) port.apply([{ roomId: 'room-1', memberId: 'u2', kind: 'snapshot' }]) assert.deepEqual(port.snapshot('room-1'), { u1: 'online', u2: 'online' }) // 显式 leave ⇒ 从**在线快照**里消失(账本记 offline,但快照只回在线 —— 与 hub 口径一致) port.apply([{ roomId: 'room-1', memberId: 'u1', kind: 'leave' }]) assert.deepEqual(port.snapshot('room-1'), { u2: 'online' }) assert.equal(port.onlineCount('room-1'), 1) // ⚠️ 快照只含 u2 ⇒ u1 **不因缺席被改写**(谁在线由显式 leave / join 决定) const r = port.apply([{ roomId: 'room-1', memberId: 'u2', kind: 'snapshot' }]) assert.equal(r.accepted, 1) assert.deepEqual(port.snapshot('room-1'), { u2: 'online' }) }) test('P-6 · 批合并 + 帧文本:一个窗口内同成员多次变更 ⇒ 每房每连接**恰 1 帧**,且只推订阅者', () => { const hub = hubOpenBatch() const port = createHubPresenceIngest(hub) const got = [] const sub = { ...syntheticConnection('c1', 'u1'), send: (t) => (got.push(t), true) } const other = { ...syntheticConnection('c2', 'u2'), send: () => true } hub.attach(sub) hub.attach(other) hub.subscribe(sub, 'room-1', false) // ⛔ 订阅本身不记 online(否则成员数组会多一条) // 同一批里 u2 三次变更(join → leave → join)⇒ 收敛成最后一次 const r = port.apply([ { roomId: 'room-1', memberId: 'u2', kind: 'join' }, { roomId: 'room-1', memberId: 'u2', kind: 'leave' }, { roomId: 'room-1', memberId: 'u2', kind: 'join' }, ]) assert.equal(r.accepted, 3) const frames = hub.flushPresence() assert.equal(frames, 1, '一个窗口 ⇒ 每房每连接恰 1 帧') assert.equal(got.length, 1) assert.deepEqual(JSON.parse(got[0]), { op: 'presence', roomId: 'room-1', at: 0, members: [{ memberId: 'u2', state: 'online', at: 0 }], }) // ⛔ 没订阅该房的连接一帧都收不到 assert.equal(got.length, 1) }) test('P-6b · 无人订阅该房 ⇒ 事件照收(进快照),但**不产帧**', () => { const hub = hubOpenBatch() const port = createHubPresenceIngest(hub) port.apply([{ roomId: 'room-silent', memberId: 'u9', kind: 'join' }]) assert.equal(hub.flushPresence(), 0) assert.deepEqual(port.snapshot('room-silent'), { u9: 'online' }) }) test('P-6c · native 口径(`ownsPresence=false`):快照 / 在线人数**直通 hub**,账本不启用', () => { const hub = hubOpenBatch() const port = createHubPresenceIngest(hub, { source: PRESENCE_INGEST_LOCAL }) assert.equal(port.ownsPresence, false) // hub 里有一条真连接并订阅该房 ⇒ 快照能看到它 const conn = { ...syntheticConnection('c1', 'u1'), send: () => true } hub.attach(conn) hub.subscribe(conn, 'room-1', false) assert.deepEqual(port.snapshot('room-1'), { u1: 'online' }) assert.equal(port.onlineCount('room-1'), 1) // ⛔ 喂事件**不会**写进账本(native 的权威是 hub 自己) port.apply([{ roomId: 'room-1', memberId: 'u9', kind: 'join' }]) assert.deepEqual(port.snapshot('room-1'), { u1: 'online' }) assert.equal(port.onlineCount('room-1'), 1) assert.equal(port.ledgerOnline(), hub.stats().connections) }) // ── 判据 ④ · 不信任外部输入 ─────────────────────────────────────────────────── test('P-7 · `apply()` 对畸形/超量**丢弃并计数**,⛔ 不抛(一条坏事件不得掀掉整批)', () => { const hub = hubOpenBatch() const port = createHubPresenceIngest(hub, { maxEvents: 2 }) const r = port.apply([ { roomId: 'room-1', memberId: 'u1', kind: 'join' }, // 处理 1 { roomId: '', memberId: 'u2', kind: 'join' }, // 处理 2,畸形 ⇒ 丢 { roomId: 'room-1', memberId: 'u3', kind: 'join' }, // 超量 ⇒ 丢(上限 = 已处理条数) ]) assert.equal(r.accepted, 1) assert.equal(r.ignored, 2) assert.equal(r.reason, 'too-many-events') assert.deepEqual(port.snapshot('room-1'), { u1: 'online' }) }) test('P-8 · `parsePresenceEvents()` **整批拒收**(fail-closed)+ 具名原因', () => { assert.deepEqual(parsePresenceEvents({ events: [] }), { events: [] }) assert.deepEqual( parsePresenceEvents({ events: [{ roomId: 'r', memberId: 'u', kind: 'join' }] }), { events: [{ roomId: 'r', memberId: 'u', kind: 'join' }] }, ) assert.deepEqual(parsePresenceEvents(null), { error: 'bad-body' }) assert.deepEqual(parsePresenceEvents('{"events":[]}'), { error: 'bad-body' }) assert.deepEqual(parsePresenceEvents({}), { error: 'bad-events' }) assert.deepEqual(parsePresenceEvents({ events: [{ roomId: 'r', memberId: 'u' }] }), { error: 'bad-kind' }) assert.deepEqual(parsePresenceEvents({ events: [{ roomId: '', memberId: 'u', kind: 'join' }] }), { error: 'bad-room-id' }) assert.deepEqual(parsePresenceEvents({ events: [{ roomId: 'r', memberId: '', kind: 'join' }] }), { error: 'bad-member-id' }) assert.deepEqual(parsePresenceEvents({ events: [{ roomId: 'r', memberId: 'u', kind: 'pong' }] }), { error: 'bad-kind' }) assert.deepEqual(parsePresenceEvents({ events: ['x'] }), { error: 'bad-event' }) assert.deepEqual(parsePresenceEvents({ events: [[]] }), { error: 'bad-event' }) assert.deepEqual(parsePresenceEvents([]), { error: 'bad-body' }) assert.deepEqual( parsePresenceEvents({ events: new Array(3).fill({ roomId: 'r', memberId: 'u', kind: 'join' }) }, 2), { error: 'too-many-events' }, ) }) test('P-11 · `secretEquals()`:长度不等先拒、逐字符比较、空期望值恒拒', () => { assert.equal(secretEquals('abc', 'abc'), true) assert.equal(secretEquals('abc', 'abd'), false) assert.equal(secretEquals('abc', 'abcd'), false) assert.equal(secretEquals('', ''), false, '期望值为空 ⇒ 恒拒(⛔ 不许"空对空"放行)') assert.equal(secretEquals('', 'x'), false) }) // ── 判据 ⑤ · 回调面的结构性判据(源码级,⛔ 不靠"读代码猜")────────────────── test('P-10 · 回调面:非 gateway 模式 / 未配 key ⇒ 恒 404;鉴别走常量时间比对;key 不进读数', () => { const src = readSrc('src/web/routes/im.ts') // ① fail-closed 的 404 分支存在,且判据同时含"模式"与"key 非空" assert.match(src, /app\.post\('\/api\/im\/gateway\/presence'/) assert.match(src, /if \(selection\.kind !== 'gateway' \|\| gatewayKey === ''\)/) assert.match(src, /reply\.code\(404\)\.send\(\{ error: 'not-found' \}\)/) // ② 鉴别用 secretEquals(⛔ 不是 ===) assert.match(src, /secretEquals\(supplied, gatewayKey\)/) assert.match(src, /x-dsh-im-gateway-key/) // ③ key 只从 env 读,且不出现在 stats 的返回里 assert.match(src, /const gatewayKey = process\.env\[IM_GATEWAY_KEY_ENV\] \?\? ''/) const statsBlock = src.slice(src.indexOf("app.get('/api/im/stats'")) assert.ok(!statsBlock.includes('gatewayKey'), 'stats 读数里 ⛔ 不得出现 key') assert.match(src, /presenceIngest: presenceIngest\.source/) assert.match(src, /presenceOnline: presenceIngest\.ledgerOnline\(\)/) // ⑤ 读"谁在线"的两处也走端口(⛔ 否则 gateway 模式下恒空 / 恒 0,读数会骗人) assert.match(src, /presence: presenceIngest\.snapshot\(ctx\.roomId\)/) assert.match(src, /online: presenceIngest\.onlineCount\(ctx\.roomId\)/) // ④ 事件落点是 hub.notePresence(⛔ 内核不新增第二套在线态) const ingestSrc = readSrc('src/im/presence-ingest.ts') assert.match(ingestSrc, /hub\.notePresence\(event\.roomId, event\.memberId, state\)/) assert.ok(!ingestSrc.includes('pendingPresence'), '⛔ 不得碰 hub 的内部结构') })