Files
dsh_shenxian/test/im-presence-ingest.test.mjs
T
admin e6207aa691
build / build-and-scan (push) Waiting to run
chore(仓库对齐): 文档库结构治理 + IM/插件线落地
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。

IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。

插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。

仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
2026-09-24 07:25:16 +08:00

278 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 的内部结构')
})