chore(仓库对齐): 文档库结构治理 + IM/插件线落地
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/;交接单不入库(政策)。
This commit is contained in:
admin committed 2026-09-24 07:25:16 +08:00
1 parent 3d8f50e366
commit e6207aa691
239 files changed
+34477 -14633

No files matched your search

+547
View File
@@ -0,0 +1,547 @@
// 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/)
})