// IM **连接层后端抽象**回归测试(node:test)—— IM 线 · 第 12 棒。 // // 跑的是**构建产物** `lib/`(`npm test` 先 build)。判据编号写在用例名里 ⇒ // 「哪条验收对哪个用例」一眼可查,第三方可复现。 // // ## 覆盖策略 // ① **等价性判据**(最重要):native 后端 / 未注入扇出端口时,`ImHub.deliver` 与 // `flushPresence` 的行为必须与第 6 棒落地时**逐字相同**(帧文本、`delivered` 口径、 // "无人订阅则不产帧")。这是"零行为变化"的可执行证据。 // ② **能力自述判据**:两种后端的 `capabilities` 逐项对照 + 结构性约束(不发心跳 // 却不声明需要垫片 ⇒ 构造即抛 —— 把 E-2 的「26 s 全断」钉成编译期/构造期约束)。 // ③ **自建保活判据**(硬实现项 ①):计时起点**必须**绑该连接自己的认证完成时刻; // 并对"整进程统一计时"这个**结构性反面**做对照断言(不编造掉线数,只断死线差异)。 // ④ **gateway 判据**:发布一次 / 帧原样装信封 / 配置缺项与背压都**具名**且**不抛** / // 失败只计数 / `presenceIngest` 自曝未接线。 // ⑤ **不变量判据**:`/api/im/ws` 路径常量、帧上限、`DSH_IM_WS=0` 回滚开关、 // 「后端概念不得泄漏进内核文件」的源码级断言。 // // ⚠️ 本文件**不碰网络**(gateway 用注入的 `fetchImpl`)、**不碰 DB**(store 用桩)。 import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { join } from 'node:path' import { BACKEND_KINDS, IM_BACKEND_ENV, IM_GATEWAY_OBSERVED_IDLE_MS, assertCapabilities, assertKeepalivePlan, planKeepalive, selectConnectionBackend, } from '../lib/im/connection-backend.js' import { GATEWAY_CAPABILITIES, GATEWAY_PRESENCE_INGEST, IM_GATEWAY_DEFAULT_CHANNEL_PREFIX, channelFor, createGatewayConnectionBackend, gatewayPayloadOf, } from '../lib/im/backends/gateway.js' import { NATIVE_CAPABILITIES, createNativeConnectionBackend } from '../lib/im/backends/native.js' import { ImHub } from '../lib/im/hub.js' import { syntheticConnection } from '../lib/im/ws.js' import { ImKeepaliveShim } from '../lib/im/keepalive.js' const ROOT = process.cwd() const readSrc = (rel) => readFileSync(join(ROOT, rel), 'utf8') /** 最小 store 桩(hub 只在 `send` / `resume` 路径上用真 store;本文件不碰)。 */ const storeStub = () => ({}) /** runtime 桩(native / gateway 后端只转发它的三个方法)。 */ const runtimeStub = (over = {}) => ({ dispatch: () => false, connectionCount: () => 0, close: () => undefined, ...over, }) function hubWith(options = {}) { return new ImHub(storeStub(), options) } // ── 判据 1–3 · 后端选择(纯函数 ⇒ 直接断)───────────────────────────────────── test('B-1 · 后端选择:未声明 ⇒ native/default(⛔ 不静默切到外部层)', () => { assert.deepEqual(selectConnectionBackend({}), { kind: 'native', reason: 'default' }) assert.deepEqual(selectConnectionBackend({ [IM_BACKEND_ENV]: '' }), { kind: 'native', reason: 'default' }) assert.deepEqual(selectConnectionBackend({ [IM_BACKEND_ENV]: ' ' }), { kind: 'native', reason: 'default' }) }) test('B-2 · 后端选择:显式声明 ⇒ explicit(大小写不敏感)', () => { assert.deepEqual(selectConnectionBackend({ [IM_BACKEND_ENV]: 'native' }), { kind: 'native', reason: 'explicit' }) assert.deepEqual(selectConnectionBackend({ [IM_BACKEND_ENV]: 'gateway' }), { kind: 'gateway', reason: 'explicit' }) assert.deepEqual(selectConnectionBackend({ [IM_BACKEND_ENV]: ' GateWay ' }), { kind: 'gateway', reason: 'explicit' }) assert.deepEqual(BACKEND_KINDS, ['native', 'gateway']) }) test('B-3 · 后端选择:不认识的值 ⇒ 退回 native 但**带回原值**(调用方必须落具名告警)', () => { const sel = selectConnectionBackend({ [IM_BACKEND_ENV]: 'centrifugo-direct' }) assert.equal(sel.kind, 'native') assert.equal(sel.reason, 'unknown-fallback') assert.equal(sel.raw, 'centrifugo-direct') }) // ── 判据 4–7 · 能力自述与结构性约束 ────────────────────────────────────────── test('B-4 · native 能力自述:自己终结客户端、自己发 WS ping、扇出在本进程、⛔ 不需要垫片', () => { assert.deepEqual(NATIVE_CAPABILITIES, { terminatesClients: true, providesServerHeartbeat: true, fanoutOffsite: false, needsKeepaliveShim: false, presenceSource: 'local', }) const backend = createNativeConnectionBackend({ hub: hubWith(), runtime: runtimeStub() }) assert.equal(backend.kind, 'native') assert.deepEqual(backend.capabilities, NATIVE_CAPABILITIES) }) test('B-5 · gateway 能力自述:扇出外移、**会**发协议级 ping(第 16 棒修正)⇒ 仍须声明垫片、在线态取自后端', () => { assert.deepEqual(GATEWAY_CAPABILITIES, { terminatesClients: false, // 🔴 第 16 棒修正:原 `false` 出自对 `wsPingRecv=0` 的误读 —— 那条读数数的是 // **WS 控制帧**,而外部连接层发的是**协议级 JSON ping**(空对象)。改判依据 = // 选型单 §15.11.3 / §15.11.4 在 47 就地部署实例上的三向实测(off 断 / conn 掉 0 / global 断)。 providesServerHeartbeat: true, fanoutOffsite: true, needsKeepaliveShim: true, presenceSource: 'backend', }) }) test('B-6 · 结构性约束:① 不发心跳却不说需要垫片 ⇒ 抛 ② **扇出外移却不说需要垫片 ⇒ 抛**(第 16 棒新增)③ 自相矛盾 ⇒ 抛', () => { assert.throws( () => assertCapabilities('native', { ...NATIVE_CAPABILITIES, providesServerHeartbeat: false, needsKeepaliveShim: false }), /backend-without-heartbeat-needs-shim/, ) // 🔴 第 16 棒新增:`providesServerHeartbeat` 改 `true` 后,上一条的网关分支不再生效 // (`true` + 任意 shim 值都过)⇒ 必须补这一条,否则"外移却不下发保活要求"会静默通过。 assert.throws( () => assertCapabilities('gateway', { ...GATEWAY_CAPABILITIES, needsKeepaliveShim: false }), /backend-fanout-offsite-needs-shim/, ) assert.throws( () => assertCapabilities('gateway', { ...GATEWAY_CAPABILITIES, terminatesClients: true }), /backend-fanout-offsite-cannot-terminate-clients/, ) // 合规的两组不抛(正向对照) assert.doesNotThrow(() => assertCapabilities('native', NATIVE_CAPABILITIES)) assert.doesNotThrow(() => assertCapabilities('gateway', GATEWAY_CAPABILITIES)) }) test('B-7 · 保活计划:默认由实测空窗反推,锚点**只能是**每连接认证完成时刻', () => { const plan = planKeepalive() assert.equal(plan.idleTimeoutMs, IM_GATEWAY_OBSERVED_IDLE_MS) assert.equal(plan.idleTimeoutMs, 26_000) assert.equal(plan.intervalMs, Math.floor(26_000 / 3)) assert.ok(plan.intervalMs > 0 && plan.intervalMs < plan.idleTimeoutMs, '间隔必须严格小于空窗') assert.ok(plan.intervalMs * 2 < plan.idleTimeoutMs, '丢一次 ping 也不该被判死') assert.equal(plan.anchor, 'per-connection-auth-complete') // 篡改锚点 / 把间隔调到 ≥ 空窗 ⇒ 必须抛(⛔ 不带病上线) assert.throws(() => assertKeepalivePlan({ ...plan, anchor: 'process-start' }), /keepalive-anchor-invalid/) assert.throws(() => assertKeepalivePlan({ ...plan, intervalMs: plan.idleTimeoutMs }), /keepalive-interval-invalid/) assert.doesNotThrow(() => assertKeepalivePlan(plan)) }) // ── 判据 8–11 · 内核等价性("零行为变化"的可执行证据)───────────────────────── test('B-8 · 等价性(native/未注入端口):`deliver` 读数 = 在线连接数;**无人订阅则不产帧**', () => { const hub = hubWith() const a = syntheticConnection('c1', 'u1') const b = syntheticConnection('c2', 'u1') hub.attach(a) hub.attach(b) // 无人订阅该房 ⇒ delivered/skipped 恒 0(与第 6 棒逐字相同) const empty = hub.deliver('room-x', { id: 'm1', seq: 1 }) assert.deepEqual(empty, { roomId: 'room-x', messageId: 'm1', delivered: 0, skipped: 0 }) const sent = [] const conn = { ...syntheticConnection('c3', 'u2'), send: (t) => (sent.push(t), true) } hub.attach(conn) hub.subscribe(conn, 'room-1') const r = hub.deliver('room-1', { id: 'm2', seq: 2 }) assert.equal(r.delivered, 1) assert.equal(r.skipped, 0) // 帧文本与改动前逐字相同(客户端只有一种解包路径) assert.equal(sent[0], JSON.stringify({ op: 'message', roomId: 'room-1', messages: [{ id: 'm2', seq: 2 }] })) }) test('B-9 · 等价性(native/未注入端口):`deliver` 一条消息对**同一连接只推 1 次**', () => { const hub = hubWith() const sent = [] const conn = { ...syntheticConnection('c1', 'u1'), send: (t) => (sent.push(t), true) } hub.attach(conn) hub.subscribe(conn, 'room-1') hub.subscribe(conn, 'room-1') // 幂等:重复订阅不增加投递 const r = hub.deliver('room-1', { id: 'm1', seq: 1 }) assert.equal(r.delivered, 1) assert.equal(sent.length, 1) }) test('B-10 · 等价性(native/未注入端口):`flushPresence` 每房每窗口 1 帧/连接,无人订阅不产帧', () => { // ⚠️ `now: () => 0` + `presenceBatchMs: 0`:把批窗口闸门打开且**不依赖真实时间** // (否则 `lastFlushAt` 取真实 Date.now ⇒ 首帧被闸门吃掉,判据会假绿)。 const hub = hubWith({ now: () => 0, presenceBatchMs: 0 }) const frames1 = [] const c1 = { ...syntheticConnection('c1', 'u1'), send: (t) => (frames1.push(t), true) } hub.attach(c1) hub.notePresence('room-none', 'u9', 'online', 900) assert.equal(hub.flushPresence(1000), 0, '无人订阅的房:⛔ 不产帧') hub.subscribe(c1, 'room-1', false) // ⛔ 不因订阅本身记一次 online(否则成员数组会多一条) hub.notePresence('room-1', 'u2', 'online', 1500) assert.equal(hub.flushPresence(2000), 1) assert.equal(frames1.length, 1) assert.equal( frames1[0], JSON.stringify({ op: 'presence', roomId: 'room-1', at: 2000, members: [{ memberId: 'u2', state: 'online', at: 1500 }] }), ) }) test('B-11 · 注入扇出端口(外移口径):即使本进程无人订阅也**照发**,且帧文本与 native 逐字相同', () => { const seen = [] const hub = hubWith({ now: () => 0, presenceBatchMs: 0, fanout: { deliverFrame: (roomId, text) => (seen.push([roomId, text]), { delivered: 1, skipped: 0 }) }, }) // 本进程零连接 const r = hub.deliver('room-9', { id: 'm9', seq: 9 }) assert.equal(r.delivered, 1, '外移口径下必须照发(否则一条都送不出去)') assert.equal(seen.length, 1) assert.equal(seen[0][0], 'room-9') assert.equal(seen[0][1], JSON.stringify({ op: 'message', roomId: 'room-9', messages: [{ id: 'm9', seq: 9 }] })) // presence 也走端口 hub.notePresence('room-9', 'u1', 'online', 4000) assert.equal(hub.flushPresence(5000), 1) assert.equal(seen.length, 2) assert.match(seen[1][1], /"op":"presence"/) }) // ── 判据 12–16 · 自建保活垫片(硬实现项 ①)────────────────────────────────── test('B-12 · 保活锚点:两条相隔 5000 ms 认证的连接 ⇒ 死线之差**恰为** 5000 ms(⛔ 不是同一条死线)', () => { const pings = [] const shim = new ImKeepaliveShim({ sink: { ping: (id) => pings.push(id) }, now: () => 0, sweepMs: 3600_000 }) try { shim.start('c-early', 1000) shim.start('c-late', 6000) const [early, late] = shim.anchors() assert.equal(early.connId, 'c-early') assert.equal(late.connId, 'c-late') assert.equal(early.anchorAt, 1000) assert.equal(late.anchorAt, 6000) assert.equal(late.nextPingAt - early.nextPingAt, 5000, '死线必须跟着各连接自己的认证时刻走') assert.equal(early.nextPingAt, 1000 + shim.planRef.intervalMs) assert.equal(late.nextPingAt, 6000 + shim.planRef.intervalMs) } finally { shim.close() } }) test('B-13 · 保活锚点(结构性反面):整进程统一计时会给**同一条死线** —— 两种模型必须可区分', () => { // 对照模型(**不是**本实现,只用来证明"两种模型不同"):死线由进程起点决定,与认证时刻无关。 const sharedModel = (t0) => (authTimes) => authTimes.map(() => t0 + 1) const perConn = (authTimes, interval) => authTimes.map((t) => t + interval) const authTimes = [1000, 6000, 11000] assert.equal(new Set(sharedModel(0)(authTimes)).size, 1, '统一计时 ⇒ 死线全相等(这正是 E-2 的缺陷形态)') assert.equal(new Set(perConn(authTimes, 8000)).size, 3, '每连接计时 ⇒ 死线各不相同') // 本实现落在"每连接"那一侧 const shim = new ImKeepaliveShim({ sink: { ping: () => undefined }, now: () => 0, sweepMs: 3600_000 }) try { for (const t of authTimes) shim.start(`c-${t}`, t) assert.equal(new Set(shim.anchors().map((a) => a.nextPingAt)).size, 3) } finally { shim.close() } }) test('B-14 · 保活节奏:到点发 ping、每连接各自计数;入向流量刷新 lastSeen ⇒ 不误判死', () => { const pings = [] const shim = new ImKeepaliveShim({ sink: { ping: (id) => pings.push(id) }, now: () => 0, sweepMs: 3600_000 }) try { const iv = shim.planRef.intervalMs shim.start('c1', 0) shim.tick(iv - 1) assert.equal(pings.length, 0, '未到死线不发') shim.tick(iv) assert.deepEqual(pings, ['c1']) shim.tick(iv * 2) assert.deepEqual(pings, ['c1', 'c1']) assert.equal(shim.anchors()[0].pingsSent, 2) // 持续有入向流量 ⇒ 永不过期 shim.noteSeen('c1', iv * 2) shim.tick(iv * 2 + shim.planRef.idleTimeoutMs - 1) assert.equal(shim.stats().expired, 0) assert.equal(shim.size(), 1) } finally { shim.close() } }) test('B-15 · 保活判死:静默超过空窗 ⇒ 恰好 expire 一次(⛔ 不重复计数)+ 报出 idleMs', () => { const expired = [] const shim = new ImKeepaliveShim({ sink: { ping: () => undefined }, now: () => 0, sweepMs: 3600_000, onExpire: (id, idleMs) => expired.push([id, idleMs]), }) try { shim.start('c1', 0) shim.tick(shim.planRef.idleTimeoutMs) // 恰好等于空窗 ⇒ 不超 ⇒ 不判死 assert.equal(expired.length, 0) shim.tick(shim.planRef.idleTimeoutMs + 1) assert.equal(expired.length, 1) assert.equal(expired[0][0], 'c1') assert.equal(expired[0][1], shim.planRef.idleTimeoutMs + 1) shim.tick(shim.planRef.idleTimeoutMs + 5000) assert.equal(expired.length, 1, '已回收的连接不得重复计数') assert.equal(shim.size(), 0) assert.equal(shim.stats().expired, 1) } finally { shim.close() } }) test('B-16 · 保活收尾:stop/close 幂等,close 之后 tick 不再动', () => { const pings = [] const shim = new ImKeepaliveShim({ sink: { ping: (id) => pings.push(id) }, now: () => 0, sweepMs: 3600_000 }) shim.start('c1', 0) shim.start('c2', 0) shim.stop('c1') assert.equal(shim.size(), 1) shim.stop('c1') assert.equal(shim.size(), 1, 'stop 幂等') shim.close() assert.equal(shim.size(), 0) shim.tick(shim.planRef.intervalMs * 10) assert.equal(pings.length, 0, 'close 之后不再发 ping') }) // ── 判据 17–22 · gateway 后端 ──────────────────────────────────────────────── test('B-17 · 频道名归一:前缀允许 `:`(命名空间分隔符),房间 id **只放行** `[A-Za-z0-9_-]`', () => { assert.equal(channelFor('imr_abc-123', 'im-room-'), 'im-room-imr_abc-123') // 前缀里的 `:` 与 `.` 保留(否则运维没法按命名空间配置);空格仍被换掉 assert.equal(channelFor('room/with:bad chars', 'p x:'), 'p_x:room_with_bad_chars') assert.equal(channelFor('imr_1', 'im:'), 'im:imr_1') // 🔴 房间 id 里的 `:` 必须被挡掉 —— 否则能借它把帧推进**另一个命名空间**(越权面) assert.equal(channelFor('imr_1:other-ns', 'im:'), 'im:imr_1_other-ns') assert.equal(channelFor('r1'), `${IM_GATEWAY_DEFAULT_CHANNEL_PREFIX}r1`) }) test('B-18 · 帧格式不变:应用层帧**原样**装进 `{frame}`(客户端 parse 出来与 native 逐字相同)', () => { const text = JSON.stringify({ op: 'message', roomId: 'r', messages: [{ id: 'm' }] }) const payload = gatewayPayloadOf(text) assert.deepEqual(payload, { frame: text }) assert.equal(JSON.parse(JSON.stringify(payload)).frame, text) }) test('B-19 · gateway 配置缺项:**具名降级**且不抛 —— `{0,1}` + `degraded` + 只警告一次', () => { const warns = [] const noFetch = () => { throw new Error('不该发网络请求') } const backend = createGatewayConnectionBackend({ endpoint: '', runtime: runtimeStub(), fetchImpl: noFetch, log: { info: () => undefined, warn: (f, m) => warns.push([m, f]) }, }) assert.deepEqual(backend.deliverFrame('r1', 'x'), { delivered: 0, skipped: 1 }) assert.deepEqual(backend.deliverFrame('r1', 'x'), { delivered: 0, skipped: 1 }) assert.equal(backend.stats().degraded, 'missing-endpoint') assert.equal(backend.stats().publishes, 0) assert.equal(warns.filter((w) => w[0] === 'im_gateway_not_ready').length, 1, '⛔ 不刷屏') // 有 URL 无 KEY ⇒ 另一个具名原因 const backend2 = createGatewayConnectionBackend({ endpoint: 'http://127.0.0.1:1', runtime: runtimeStub(), fetchImpl: noFetch }) backend2.deliverFrame('r1', 'x') assert.equal(backend2.stats().degraded, 'missing-key') }) test('B-20 · gateway 发布:一次 `deliverFrame` ⇒ 恰一次调用,频道与信封正确,计数 +1', async () => { const calls = [] const backend = createGatewayConnectionBackend({ endpoint: 'http://gw.test:9/', key: 'k-1', runtime: runtimeStub(), fetchImpl: async (url, init) => { calls.push({ url, init }) return { ok: true, status: 200, text: async () => '' } }, }) const text = JSON.stringify({ op: 'message', roomId: 'imr_1', messages: [{ id: 'm' }] }) assert.deepEqual(backend.deliverFrame('imr_1', text), { delivered: 1, skipped: 0 }) assert.equal(calls.length, 1) assert.equal(calls[0].url, 'http://gw.test:9/api/publish', '尾斜杠必须被剥掉') assert.equal(calls[0].init.headers.authorization, 'apikey k-1') const body = JSON.parse(calls[0].init.body) assert.equal(body.channel, 'im-room-imr_1') assert.deepEqual(body.data, { frame: text }) // 发布是**异步受理** ⇒ 等一拍再读计数 await new Promise((r) => setImmediate(r)) const s = backend.stats() assert.equal(s.publishes, 1) assert.equal(s.publishFailures, 0) assert.equal(s.inflight, 0) assert.ok(s.lastPublishMs >= 0) assert.equal(s.hasKey, true) assert.equal(s.endpoint, 'http://gw.test:9') assert.equal(s.degraded, '') }) test('B-21 · gateway 发布失败:只计数 + 具名日志,**⛔ 不抛**(一条坏消息不得掀掉房间)', async () => { const warns = [] const backend = createGatewayConnectionBackend({ endpoint: 'http://gw.test:9', key: 'k', runtime: runtimeStub(), log: { info: () => undefined, warn: (f, m) => warns.push([m, f]) }, fetchImpl: async () => ({ ok: false, status: 502, text: async () => 'bad gateway' }), }) assert.doesNotThrow(() => backend.deliverFrame('r1', 'x')) await new Promise((r) => setImmediate(r)) assert.equal(backend.stats().publishFailures, 1) assert.equal(backend.stats().lastError, 'http-502') assert.ok(warns.some((w) => w[0] === 'im_gateway_publish_failed')) const backend2 = createGatewayConnectionBackend({ endpoint: 'http://gw.test:9', key: 'k', runtime: runtimeStub(), fetchImpl: async () => { throw new Error('ECONNREFUSED') }, }) assert.doesNotThrow(() => backend2.deliverFrame('r1', 'x')) await new Promise((r) => setImmediate(r)) assert.equal(backend2.stats().publishFailures, 1) assert.match(backend2.stats().lastError, /ECONNREFUSED/) }) test('B-22 · gateway 背压:在途满 ⇒ 立刻 `{0,1}` + `publishDropped`(⛔ 不无限堆积)', async () => { let release const gate = new Promise((r) => { release = r }) const backend = createGatewayConnectionBackend({ endpoint: 'http://gw.test:9', key: 'k', runtime: runtimeStub(), maxInflight: 1, fetchImpl: async () => { await gate return { ok: true, status: 200, text: async () => '' } }, }) assert.deepEqual(backend.deliverFrame('r1', 'a'), { delivered: 1, skipped: 0 }) assert.deepEqual(backend.deliverFrame('r1', 'b'), { delivered: 0, skipped: 1 }) assert.equal(backend.stats().publishDropped, 1) assert.equal(backend.stats().degraded, 'backpressure') assert.equal(backend.stats().inflight, 1) release() await new Promise((r) => setImmediate(r)) assert.equal(backend.stats().inflight, 0) assert.equal(backend.stats().publishes, 1) }) test('B-23 · gateway 通道与就绪:平台侧 `/api/im/ws` 保留(dispatch 同一函数对象)、`health()` 缺配置不发请求', async () => { const dispatch = () => true let fetchCount = 0 const backend = createGatewayConnectionBackend({ endpoint: 'http://gw.test:9', key: 'k', runtime: runtimeStub({ dispatch, connectionCount: () => 7 }), fetchImpl: async () => { fetchCount += 1 return { ok: true, status: 200, text: async () => '{}' } }, }) assert.equal(backend.dispatch, dispatch, '两种后端的 dispatch 都来自 runtime(实例内通道 + 回滚路径)') assert.equal(backend.connectionCount(), 7) assert.equal(backend.stats().presenceIngest, GATEWAY_PRESENCE_INGEST) assert.equal(GATEWAY_PRESENCE_INGEST, 'not-wired') const missing = createGatewayConnectionBackend({ endpoint: '', runtime: runtimeStub(), fetchImpl: async () => ({ ok: true, status: 200, text: async () => '' }) }) assert.deepEqual(await missing.health(), { ok: false, reason: 'missing-endpoint' }) assert.equal(fetchCount, 0, '缺配置的健康检查不得发网络请求') const ok = await backend.health() assert.equal(ok.ok, true) assert.equal(fetchCount, 1, 'health() ⛔ 不改任何发设计数') assert.equal(backend.stats().publishes, 0) }) test('B-24 · native 后端:dispatch / connectionCount / close 全部转发给 runtime(纯包装)', () => { const dispatch = () => false let closed = 0 const runtime = runtimeStub({ dispatch, connectionCount: () => 3, close: () => (closed += 1) }) const backend = createNativeConnectionBackend({ hub: hubWith(), runtime }) assert.equal(backend.dispatch, dispatch) assert.equal(backend.connectionCount(), 3) assert.equal(backend.stats().keepaliveShim, false) backend.close() assert.equal(closed, 1) }) // ── 判据 25–28 · 不变量(协议 / 开关 / 概念边界)───────────────────────────── test('B-25 · 协议不变量:`/api/im/ws` 路径与帧上限常量一字未变', async () => { const hub = await import('../lib/im/hub.js') assert.equal(hub.IM_WS_PATH, '/api/im/ws') assert.equal(hub.IM_API_PREFIX, '/api/im/') assert.equal(hub.IM_HEARTBEAT_MS, 25_000) assert.equal(hub.IM_PRESENCE_BATCH_MS, 1000) const ws = await import('../lib/im/ws.js') assert.equal(ws.IM_MAX_FRAME_BYTES, 1 << 20) assert.equal(ws.IM_MAX_PAYLOAD_BYTES, 64 << 10) }) test('B-26 · 回滚开关不变量:`DSH_IM_WS=0` 的早退仍在,且仍在插件最早处', () => { const src = readSrc('src/web/routes/im.ts') assert.match(src, /process\.env\.DSH_IM_WS === '0'/, '回滚开关必须保留') const idxGuard = src.indexOf("DSH_IM_WS === '0'") // ⚠️ 必须找**调用点**而不是名字 —— `import { createNativeConnectionBackend }` 在文件顶部, // 按名字搜会误判成"后端在开关之前建"(首版踩过)。 const idxBackend = src.indexOf('createNativeConnectionBackend({ hub') assert.ok(idxGuard > 0 && idxBackend > idxGuard, '开关必须在建后端之前 ⇒ 关掉时一行都不装') }) test('B-27 · 概念边界:内核文件里**不得出现**外部连接层的产品名 / 专属配置键', () => { // ⚠️ 必须带词边界:`openImPg(` 里含子串 "openim" ⇒ 裸 /openim/i 会假红(首版踩过)。 const banned = /\b(?:centrifugo|centrifuge|ejabberd|openim)\b/i for (const f of [ 'src/im/hub.ts', 'src/im/ws.ts', 'src/im/store.ts', 'src/im/types.ts', 'src/im/clock.ts', 'src/im/db.ts', 'src/im/agent-bridge.ts', 'src/im/instance-token.ts', 'src/im/connection-backend.ts', 'src/im/keepalive.ts', 'src/im/backends/native.ts', 'src/im/backends/gateway.ts', ]) { assert.doesNotMatch(readSrc(f), banned, `${f} 不得泄漏外部组件概念(命名必须中性)`) } // 参考部署的专属配置只允许出现在 poc/ 与文档里 assert.doesNotMatch(readSrc('src/web/routes/im.ts'), /centrifugo|centrifuge/i) }) test('B-28 · 文件名中性:`backends/` 下只有 native 与 gateway 两个后端实现', () => { const native_ = readSrc('src/im/backends/native.ts') const gateway = readSrc('src/im/backends/gateway.ts') assert.match(native_, /export const NATIVE_CAPABILITIES/) assert.match(gateway, /export const GATEWAY_CAPABILITIES/) // 接口单一定义源:扇出端口只在 hub.ts 定义,connection-backend 只再导出 const cb = readSrc('src/im/connection-backend.ts') assert.match(cb, /export type \{ ImFanoutPort, ImFanoutResult \}/) assert.doesNotMatch(cb, /export interface ImFanoutPort/) })