build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(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/;交接单不入库(政策)。
278 lines
14 KiB
JavaScript
278 lines
14 KiB
JavaScript
// 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 的内部结构')
|
||
})
|