Files
dsh_shenxian/test/im-connection-backend.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

548 lines
25 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 线 · 第 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/)
})