feat(overlay): 内容块级寻址 + 实例逐步拉起 + 骨干选路 + 组密钥加密(序24–㉛ 累积同步)

代码
- 内容分发块级寻址:新增 src/net/relay/content/{chunker,store,runtime,source,peer,crypto}.ts
- 组密钥(C 档)确定性加密:AES-256-GCM,块 id β′ = sha256(密文) 前 32 hex;双 epoch 过渡窗口
- 实例生命周期:三处 teardown() 不再杀实例(local/remote/leased-spawner);启动认领 + TCP 探活判孤儿
- 骨干选路:jitter 选路 + endpoint-target;relay client/server/wire/identity/directory/rendezvous/switcher 调整
- 工作台 src/web/server.ts、src/worker/relay-tunnel.ts 装配与候选链观测

脚本与测试
- scripts/overlay-{probe,keyring,jitter}.cjs 更新
- 探针新增 OBS-21(每连接候选数)/ OBS-22(teardown 静态守卫 + 认领面)/ OBS-23(组密钥加密)
- 新增 test/{orchestrator-teardown,orchestrator-rehydrate,overlay-content,overlay-jitter}.test.mjs;relay 两例更新

文档
- 新增交接单:覆盖网络-序24-内容分发块级寻址 / 序25-实例逐步拉起 / 序26-骨干稳定选路与加密
- INDEX.md、交接单/README.md、skills/dsh-auto-handoff-chain/SKILL.md 同步

验收(零回归,2026-09-18 08:0x 复核)
- npm test           201 tests / 200 pass / 0 fail / 1 skipped
- overlay-failover-drill --scene all --table   12 PASS / 0 SKIP / 0 FAIL
- overlay-probe --table                        23 PASS / 0 SKIP / 0 FAIL (rc=0)
This commit is contained in:
admin committed 2026-09-18 08:08:51 +08:00
1 parent 04776af4b1
commit 09ce76f3af
38 files changed
+9133 -211

No files matched your search

+694 -1
View File
@@ -14,9 +14,10 @@
import assert from 'node:assert/strict'
import { createHmac, randomBytes } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { createServer as createTcpServer, connect } from 'node:net'
import { test } from 'node:test'
import { MUX, OPS_NETWORK, RelayClient, RelayDialer, RelayServer, chooseNode, decodeMux, encodeJsonFrame, encodeMux, logicalName, parseKeysInline } from '../lib/net/relay/index.js'
import { MUX, OPS_NETWORK, RelayClient, RelayDialer, RelayRendezvous, RelayServer, chooseNode, decodeMux, encodeJsonFrame, encodeMux, hostNameIndex, logicalName, parseKeysInline, relayEndpointTarget } from '../lib/net/relay/index.js'
const BASE = 45000
const SPAN = 200
@@ -1316,3 +1317,695 @@ test('T24 拨号池:未分配槽位被一条连接命中后 ⛔ 不得自毁
assert.equal(dialer.status().pool, await liveCount(), '分配后池账仍须与实际在听恒等')
assert.ok(await poolPortBusy(local), `已分配的落点口 ${local} 应在听`)
})
/* ═══════════════════ 序⑲ presence(节点在线态)T25–T31 ═══════════════════ */
/**
* presence 时序口径的**测试档**(生产默认 = grace 10 s / debounce 30 s / batch 1 s / TTL 45 s)。
* 单测不可能真等 40 s ⇒ 全部注入。⚠️ 口径本身**不改**,只改"等多久"。
*/
const PT = { presenceGraceMs: 150, presenceOfflineDebounceMs: 250, presenceBatchMs: 40, presenceTtlMs: 3_000 }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
/** 起一套"订阅方 + 若干 worker"的现场(⛔ `sub` 只是普通客户端,不需要拨号方身份)。 */
async function presenceScene(t, workerIds, tune = {}) {
const secret = randomBytes(32).toString('hex')
const keys = new Map(['sub', ...workerIds].map((id) => [id, secret]))
const logs = []
const server = new RelayServer({
port: 0,
keys,
instancePortBase: BASE,
instancePortSpan: SPAN,
...PT,
...tune,
log: (l) => logs.push(l),
})
await server.start()
const url = `ws://127.0.0.1:${server.boundPort}${PATH}`
const clients = []
const mk = (hostId) => {
/**
* ⚠️ 两条硬约束(都是本轮实测踩到的,⛔ 别再踩):
* ① 非拨号方客户端**必须至少声明一个端口**(`handleHello` 回 `no-ports` 且 `retryable=false` ⇒ 永不 `up`)。
* 这里声明 `BASE` 只为满足该约束 —— relay 为它绑的是**动态回环口**(`listen(0)`),声明值只是转发目标,⛔ 不占本机端口。
* ② **每个 hostId 都必须有 key**(否则 `AUTH DENY unknown-host`)⇒ 按需登记,
* 这样 `presenceScene(t, [])` 之后仍可 `mk('w-xx')`(⛔ 不用把待造主机在入参里预先枚举一遍)。
*/
keys.set(hostId, secret)
const c = new RelayClient({ url, hostId, secret, ports: [BASE], log: () => {} })
clients.push(c)
c.start()
return c
}
t.after(async () => {
for (const c of clients) c.stop()
await server.stop()
})
const sub = mk('sub')
const up = (c) => waitFor(() => c.status().state === 'up', 5_000)
assert.ok(await up(sub), '订阅方未注册成功')
return { server, sub, mk, up, logs }
}
/** 读一次真 `/status`(顺便驱动 `statusHits` 判别器)。 */
async function readStatus(server) {
const res = await fetch(`http://127.0.0.1:${server.boundPort}/status`)
return res.json()
}
/**
* 极简**原始** WS 客户端 —— 只为"未知帧号必须被显式拒绝(⛔ 不静默丢弃)"这一条判据。
*
* 故意不做认证:它要在**未认证**那一刻发帧 ⇒ 不需要 MAC(也就不能伪造合法注册)。
* 返回服务端回的 WS close 码;`null` = 没等到 close(= 静默丢弃)。
*/
function rawWsProbe(port, muxType, ms = 3_000) {
return new Promise((resolve) => {
const sock = connect(port, '127.0.0.1')
let buf = Buffer.alloc(0)
let headerEnd = -1
let sent = false
let done = false
let timer
const finish = (code) => {
if (done) return
done = true
clearTimeout(timer)
// `end()` 而不是 `destroy()`:给上面那帧 "close 回应" 一个真的发出去的机会(见下)。
sock.end()
resolve(code)
}
timer = setTimeout(() => finish(null), ms)
sock.on('error', () => finish(null))
sock.on('data', (chunk) => {
buf = Buffer.concat([buf, chunk])
if (!sent) {
headerEnd = buf.indexOf('\r\n\r\n')
if (headerEnd < 0) return
sent = true
// 掩码的 binary mux 帧(客户端必须 mask,RFC 6455 §5.1)
const body = encodeMux(muxType, 0, Buffer.alloc(0))
const mask = randomBytes(4)
const masked = Buffer.from(body)
for (let i = 0; i < masked.length; i++) masked[i] ^= mask[i & 3]
const head = Buffer.alloc(6)
head[0] = 0x82
head[1] = 0x80 | masked.length
mask.copy(head, 2)
sock.write(Buffer.concat([head, masked]))
}
// 在握手之后的数据里找服务端发的 close 帧(opcode 0x8;服务端→客户端**不掩码**)
for (let i = headerEnd + 4; i + 3 < buf.length; ) {
const opcode = buf[i] & 0x0f
const len = buf[i + 1] & 0x7f
if (opcode === 0x8) {
const code = buf.readUInt16BE(i + 2)
/**
* 🔴 收到 close **必须回一个 close**(RFC 6455 §5.5.1),然后 `end()` 走优雅 TCP 收尾。
* 实测(本轮踩到):回都不回就直接 `destroy()` ⇒ 服务端的 ws 会一直等对端 close 帧
* (默认 30 s)⇒ 这条连接一直挂在 http server 上 ⇒ `server.stop()` 里的
* `http.close(cb)` **永不回调** ⇒ 整个测试文件在 T26 之后被父级取消
* (报 `Promise resolution is still pending but the event loop has already resolved`)。
* 这是**测试夹具**的坑,不是产品缺陷 —— 生产停机有 `closeAllConnections()` 兜底。
*/
const mask = randomBytes(4)
const payload = Buffer.alloc(2)
payload.writeUInt16BE(code, 0)
const masked = Buffer.from(payload)
for (let k = 0; k < masked.length; k++) masked[k] ^= mask[k & 3]
const head = Buffer.alloc(6)
head[0] = 0x88
head[1] = 0x80 | masked.length
mask.copy(head, 2)
try {
sock.write(Buffer.concat([head, masked]))
} catch {
/* 对端已经走了 */
}
finish(code)
return
}
i += 2 + len
}
})
sock.write(
`GET ${PATH} HTTP/1.1\r\nHost: 127.0.0.1\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n` +
`Sec-WebSocket-Key: ${randomBytes(16).toString('base64')}\r\nSec-WebSocket-Version: 13\r\n\r\n`,
)
})
}
test('T25 presence 生命周期:注册即在线 · 连接更替不闪烁 · 断连过 grace+debounce 才离线', async (t) => {
const { server, sub, mk, up } = await presenceScene(t, [])
const name = logicalName(OPS_NETWORK, 'w-p')
sub.subscribePresence()
assert.ok(await waitFor(() => sub.presenceStatus().state === 'subscribed', 3_000), '订阅未生效')
// E4 首帧即全量:`SNAP` **一帧拿全**,⛔ 不是逐 host 拉
assert.equal(sub.presenceStatus().snapFrames, 1, '首帧必须是 SNAP')
assert.equal(sub.presenceStatus().pushFrames, 0, '`SNAP` 之前 ⛔ 不得有增量帧')
// E1 稳态:没有任何状态变化 ⇒ ⛔ 一个帧都不推
const idle = sub.presenceStatus().pushFrames
await sleep(300)
assert.equal(sub.presenceStatus().pushFrames, idle, '稳态必须 0 帧(变化驱动,无变化不推)')
const w = mk('w-p')
assert.ok(await up(w), 'worker 未注册')
assert.ok(await waitFor(() => sub.presenceStatus().pushFrames === idle + 1, 3_000), '上线应恰好推 1 帧')
assert.equal(sub.presenceStatus().lastFrameEntries, 1, '该帧只应带这 1 条')
assert.equal(sub.presenceOf(name)?.online, true, '上线事件未更新本地镜像')
assert.equal(sub.presenceOf(name)?.devices, 1)
/**
* E6(聚合口径的**可观测后果**):同 hostId 的第二条连接注册时,服务端会**先顶掉旧会话再接入新会话**
* (`handleHello` 的 `superseded by new session`)⇒ 此刻"一条连接关了、另一条开了"。
* 聚合口径要求:**这中间不许产生任何状态事件**(否则每次重连都会让上层看到一次闪烁 = 净退化)。
*/
const w2 = mk('w-p')
assert.ok(await up(w2), '第二条连接未注册')
await sleep(400)
assert.equal(sub.presenceStatus().pushFrames, idle + 1, '连接更替(顶旧接新)⛔ 不得产生任何新帧')
assert.equal(sub.presenceOf(name)?.online, true, '更替期间必须**始终**在线(⛔ 不得闪一下离线)')
assert.equal(sub.presenceOf(name)?.devices, 1, '旧会话已被顶掉 ⇒ 活连接数仍是 1(devices 必须诚实)')
// E5:真正全断 ⇒ 先过 grace 仍在线,再过 debounce 才转离线
w2.stop()
await sleep(120)
const during = sub.presenceOf(name)
assert.equal(during?.online, true, `≤ grace(${PT.presenceGraceMs}ms) 必须仍在线(防抖动闪烁)`)
/**
* 倒计时(`offlineInMs`)**只认 `/status`(或 `SNAP`)上的值,⛔ 不从推送镜像里读**:
* 它是"生成那一刻"的相对量,而推送是**变化驱动**的(无变化不推)⇒ 镜像里那个值一发出就过期。
* 想让它实时更新就只能周期性推帧 —— 那正是 E1「稳态 0 帧」要干掉的东西。
* 换句话说:**事实走推送,带时钟刻度的心跳量走兜底读取**(D5 的兜底就不只是"降级可用",而是分工)。
*/
const st = await readStatus(server)
const mine = st.presence.find((p) => p.name === name)
assert.equal(typeof mine?.offlineInMs, 'number', 'grace 窗口内 `/status` 必须给出"还有多久转离线"')
assert.ok(
mine.offlineInMs > 0 && mine.offlineInMs <= PT.presenceGraceMs + PT.presenceOfflineDebounceMs,
`倒计时必须落在 (0, grace+debounce] 内,实测 ${mine?.offlineInMs}`,
)
assert.equal(sub.presenceStatus().pushFrames, idle + 1, 'grace 窗口内 ⛔ 一个帧都不许推(E2:一次变化 ≤1 帧)')
assert.ok(await waitFor(() => sub.presenceOf(name)?.online === false, 4_000), '过 grace+debounce 必须转离线')
assert.equal(sub.presenceStatus().pushFrames, idle + 2, '离线应恰好再推 1 帧')
})
test('T26 线协议:SUB/UNSUB/PRESENCE/SNAP 帧号**末尾追加**且与既有集合不重叠', async (t) => {
// 帧号:既有的 0x01–0x0f 语义一字未动,新帧全部 > 0x0f
assert.equal(MUX.SUB, 0x10)
assert.equal(MUX.UNSUB, 0x11)
assert.equal(MUX.PRESENCE, 0x12)
assert.equal(MUX.SNAP, 0x13)
for (const t2 of [MUX.SUB, MUX.UNSUB, MUX.PRESENCE, MUX.SNAP]) {
assert.ok(t2 > MUX.DIAL_ACK, `新帧号 ${t2} 必须**追加**在既有分配表末尾(⛔ 不改既有语义)`)
}
const codes = Object.values(MUX)
assert.equal(new Set(codes).size, codes.length, '帧号必须两两不同')
const back = decodeMux(encodeJsonFrame(MUX.SUB, 0, { all: true }))
assert.equal(back.type, MUX.SUB, 'SUB 帧编解码往返失败')
// 🔴 "未知帧号 ⇒ 显式报错,⛔ 不静默丢弃"(本线头号教训):用一个服务端**不认识**的帧号打它
const { server } = await presenceScene(t, [])
const before = server.status().counters.authFailed
const code = await rawWsProbe(server.boundPort, 0x99)
assert.equal(code, 1008, `未知帧号必须被**显式拒绝**(期望 close 1008 policy-violation,实得 ${code})`)
assert.equal(server.status().counters.authFailed, before + 1, '拒绝必须**有计数**(否则等于没记)')
})
test('T27 批合并:同一 1 s 窗口内 N 次状态变化只推 1 帧(E2/E4)', async (t) => {
const ids = Array.from({ length: 6 }, (_, i) => `w-b${i}`)
// 窗口取 300ms:6 次回环握手远小于它 ⇒ 判据确定性足够(⛔ 不靠"碰巧合上")
const { sub, mk, up } = await presenceScene(t, [], { presenceBatchMs: 300 })
sub.subscribePresence()
assert.ok(await waitFor(() => sub.presenceStatus().state === 'subscribed', 3_000), '订阅未生效')
const base = sub.presenceStatus().pushFrames
const ws = ids.map((id) => mk(id))
await Promise.all(ws.map((c) => up(c)))
const name0 = logicalName(OPS_NETWORK, ids[0])
assert.ok(await waitFor(() => sub.presenceOf(name0)?.online === true, 3_000), '上线事件未到达')
await sleep(900) // 让所有可能的批窗口都过去
assert.equal(sub.presenceStatus().pushFrames, base + 1, `${ids.length} 台同窗口上线 ⇒ 必须合并成**1 帧**`)
assert.equal(sub.presenceStatus().lastFrameEntries, ids.length, '这一帧必须**带数组**(6 条),⛔ 不是逐个 host 一条')
// 反向:同窗口全部下线 ⇒ 同样只 1 帧(合并方向也要成立,⛔ 不能只测上线)
for (const c of ws) c.stop()
await waitFor(() => sub.presenceOf(name0)?.online === false, 5_000)
await sleep(900)
assert.equal(sub.presenceStatus().pushFrames, base + 2, `${ids.length} 台同窗口下线 ⇒ 必须合并成 1 帧`)
assert.equal(sub.presenceStatus().lastFrameEntries, ids.length, '离线帧同样必须带全 6 条')
})
test('T28 订阅可见性**只收窄**:跨网订阅必须显式拒绝 + 计数(⛔ 不静默返空,E8/D6)', async (t) => {
const { server, sub } = await presenceScene(t, ['w-p'])
sub.subscribePresence([logicalName('u:5', 'd1')])
assert.ok(await waitFor(() => sub.presenceStatus().rejected === 1, 3_000), '跨网订阅必须被**显式拒绝**')
const st = await readStatus(server)
assert.equal(st.counters.rejected, 1, '服务端必须**有计数**(⛔ 静默返空 = 假绿)')
assert.equal(sub.presenceOf('u:5/d1'), undefined, '被拒的订阅 ⛔ 不得留下任何条目')
assert.equal(sub.presenceStatus().state, 'idle', '被拒后状态必须回到 idle(上层据此回退 /status)')
assert.ok((await readStatus(server)).counters.subs === 0, '被拒的订阅 ⛔ 不得计入 subs')
})
test('T29 TTL 安全网:漏掉 close 事件的"幽灵连接"超 TTL 后被摘掉并收口离线(E5 第三支)', async (t) => {
const { server, sub } = await presenceScene(t, [], {
presenceTtlMs: 400,
presenceGraceMs: 100,
presenceOfflineDebounceMs: 150,
})
sub.subscribePresence()
assert.ok(await waitFor(() => sub.presenceStatus().state === 'subscribed', 3_000), '订阅未生效')
const name = logicalName(OPS_NETWORK, 'w-ghost')
/**
* **故障注入**(⛔ 不是模拟业务,而是模拟**漏掉了 close 事件**这一种故障):
* 只往 presence 表里记一条"连接",既不建真连接、也就永远不会收到 close ⇒ 这正是 TTL 要兜的事。
* 不这么做的话,"漏事件 ⇒ 永久假在线"这条路径**根本无法被触发**(正常路径总会 dropSession)。
*/
server.presenceTouch({ id: 'ghost-1', hostId: 'w-ghost', network: OPS_NETWORK, ports: new Set() })
assert.ok(await waitFor(() => sub.presenceOf(name)?.online === true, 3_000), '幽灵连接应先被认定为在线')
assert.ok(
await waitFor(() => sub.presenceOf(name)?.online === false, 5_000),
'TTL 安全网未能自愈 ⇒ 漏掉 close 的事件会**永久**留在册',
)
})
test('T30 主路径=订阅 / 兜底=/status:订阅新鲜时 ⛔ 不读兜底;不可用时**必须**回退(D5/E7)', async (t) => {
const calls = []
const name = logicalName(OPS_NETWORK, 'w-1')
const rv = (presence) =>
new RelayRendezvous({
dialTargetUrl: 'wss://example.invalid/dshs-relay',
addressOf: () => '127.0.0.1:19100',
online: (n) => {
calls.push(`/status兜底:${n}`)
return true
},
presence,
})
// ① 订阅新鲜 ⇒ **以它为准**,兜底一次都不读
assert.ok((await rv(() => true).resolve(name)) !== undefined, '订阅说在线 ⇒ 必须解析成功')
assert.equal(calls.length, 0, '订阅新鲜时 ⛔ 不许读兜底(/status)')
// ② 订阅说"不在" ⇒ 直接不认识(同样不读兜底)
assert.equal(await rv(() => false).resolve(name), undefined, '订阅说离线 ⇒ 必须回 undefined')
assert.equal(calls.length, 0, '订阅能给答案时 ⛔ 不许读兜底')
// ③ 订阅"不知道"(undefined)⇒ **必须回退**,且照样拿到在线态
assert.ok((await rv(() => undefined).resolve(name)) !== undefined, '订阅不可用时必须能回退且不瞎')
assert.equal(calls.length, 1, '回退必须**真的调用**兜底(否则就是"订阅一断就全瞎")')
// E7 最终一致:在线态**没有任何**跨节点同步通道(两台 relay 各管各的 ⇒ 天然无脑裂源)
const secret = randomBytes(32).toString('hex')
const keys = new Map([['w-e7', secret]])
const s1 = new RelayServer({ port: 0, keys, instancePortBase: BASE, instancePortSpan: SPAN, ...PT, log: () => {} })
const s2 = new RelayServer({ port: 0, keys, instancePortBase: BASE, instancePortSpan: SPAN, ...PT, log: () => {} })
await s1.start()
await s2.start()
const c = new RelayClient({
url: `ws://127.0.0.1:${s1.boundPort}${PATH}`,
hostId: 'w-e7',
secret,
ports: [BASE],
log: () => {},
})
t.after(async () => {
c.stop()
await s1.stop()
await s2.stop()
})
c.start()
assert.ok(await waitFor(() => c.status().state === 'up', 5_000), 'client 未注册')
assert.equal(s1.status().presence.length, 1, '第一台应有该 host')
assert.equal(s2.status().presence.length, 0, '⛔ 另一台**不得**知道它(有同步才是缺陷:那是脑裂源)')
})
test('T31 presence 判别器:subs / pushed / rejected / statusHits 都能被断言(⛔ 不许只写日志)', async (t) => {
const { server, sub, mk, up } = await presenceScene(t, [])
assert.equal((await readStatus(server)).counters.subs, 0, '没人订阅 ⇒ subs=0')
sub.subscribePresence()
assert.ok(await waitFor(() => sub.presenceStatus().state === 'subscribed', 3_000), '订阅未生效')
assert.equal((await readStatus(server)).counters.subs, 1, '订阅生效 ⇒ subs=1(gauge)')
const pushed0 = (await readStatus(server)).counters.pushed
const w = mk('w-p')
assert.ok(await up(w), 'worker 未注册')
assert.ok(await waitFor(() => sub.presenceStatus().pushFrames >= 1, 3_000), '未收到推送')
await sleep(400)
const pushed1 = (await readStatus(server)).counters.pushed
assert.ok(pushed1 > pushed0, 'pushed 必须随真实推送增长(否则判别器是死的)')
await sleep(400)
assert.equal((await readStatus(server)).counters.pushed, pushed1, '稳态下 pushed 必须**停住不走**(E1)')
// statusHits:两次读数之差 = 1 ⇒ 期间**没有别人**在读 /status
const a = (await readStatus(server)).counters.statusHits
const b = (await readStatus(server)).counters.statusHits
assert.equal(b - a, 1, `两次读数之差应为 1(期间只有本测试在读),实得 ${b - a}`)
/**
* `snaps`(E4 的机器可读判据):有订阅者却 `snaps = 0` ⇒ 首帧走的不是 `SNAP`。
* 同理把 `presenceTiming` 钉住 —— 探针(`OBS-13`)拿它与参数表 `PRESENCE_*` 对口径,
* 对不上就是**口径漂移**(改了默认值却没改表 ⇒ 表在撒谎)。
*/
const st = await readStatus(server)
assert.equal(st.counters.snaps, 1, '本轮只有 1 次订阅 ⇒ 必须恰好 1 帧 SNAP')
assert.ok(st.counters.snaps <= st.counters.pushed, 'snaps 是 pushed 的子集(⛔ 不得大于)')
assert.deepEqual(
{
graceMs: st.presenceTiming.graceMs,
offlineDebounceMs: st.presenceTiming.offlineDebounceMs,
batchMs: st.presenceTiming.batchMs,
ttlMs: st.presenceTiming.ttlMs,
subMax: st.presenceTiming.subMax,
},
{
graceMs: PT.presenceGraceMs,
offlineDebounceMs: PT.presenceOfflineDebounceMs,
batchMs: PT.presenceBatchMs,
ttlMs: PT.presenceTtlMs,
subMax: 0,
},
'`presenceTiming` 必须把注入的时序口径如实下发(探针 `OBS-13` 靠它对口径)',
)
sub.stop()
await sleep(200)
assert.equal((await readStatus(server)).counters.subs, 0, '连接断了订阅必须随之消失(→ 回到 0)')
})
test('T32 落点不丢:注册即在线那一帧必须带**非 0** 落点,且端口变更会被推送(序⑲ 收口实测踩到的假死)', async (t) => {
const { sub, mk, up } = await presenceScene(t, [])
const name = logicalName(OPS_NETWORK, 'w-l')
sub.subscribePresence()
assert.ok(await waitFor(() => sub.presenceStatus().state === 'subscribed', 3_000), '订阅未生效')
/**
* 🔴 这一条对应一个**实测踩到的假死**:`presenceTouch` 曾在 `ensureEndpoint` **之前**调用 ⇒
* 首帧里 `localPorts[].localPort = 0`;而发布只在"在线态翻转"时发生 ⇒ 那个 0 永远修不回来 ⇒
* 订阅方(Manager)`addressOf` 查不到落点 ⇒ 实例页**打不开但不报错**。
* 判据必须卡在"**订阅之后**才上线的主机"上:老主机早就在册,落点已被别的路径补过。
*/
const w = mk('w-l')
assert.ok(await up(w), 'worker 未注册')
assert.ok(await waitFor(() => sub.presenceOf(name)?.online === true, 3_000), '上线事件未到达')
const lp0 = sub.presenceOf(name)?.localPorts ?? []
assert.equal(lp0.length, 1, '首帧必须带该 host 的落点条目')
assert.equal(lp0[0].port, BASE, '落点条目的 port 必须与声明一致')
assert.ok(lp0[0].localPort > 0, `落点口号必须**非 0**(实测 ${lp0[0].localPort} ⇒ 0 = addressOf 查不到 ⇒ 页面假死)`)
// 运行期加一个端口(`PORT_ADD`)⇒ 新落点也必须**推给订阅方**(否则同样查不到)
const frames0 = sub.presenceStatus().pushFrames
const NEW_PORT = BASE + 1
assert.equal(await w.addPort(NEW_PORT), true, 'PORT_ADD 未被接受')
assert.ok(await waitFor(() => (sub.presenceOf(name)?.ports ?? []).length === 2, 3_000), '端口变更未推送')
await sleep(400)
const lp1 = sub.presenceOf(name)?.localPorts ?? []
assert.equal(lp1.length, 2, '两个端口都必须在落点表里')
assert.ok(
lp1.every((x) => x.localPort > 0),
`新端口落点同样必须非 0:${JSON.stringify(lp1)}`,
)
assert.equal(sub.presenceStatus().pushFrames, frames0 + 1, '端口变更 = 一次状态变化 ⇒ 恰好 1 帧(E2 配额内)')
})
/* ═══════════ 序㉑ P-1 修复(门的判据 = 订阅已建立 ∧ 链路活着)T33–T34 ═══════════ */
/**
* 🔴 **P-1 回归**(在册缺陷,2026-09-17 序 ⑳ 实测)。
*
* 病根:门(`presenceFresh()`)原判据 = "最近一次 presence **载荷**距今 ≤ TTL"。而 presence 是
* **变化驱动**的 —— 稳态下一帧都不推 ⇒ 45 s 后必然过期 ⇒ 门自己重开、`/status` 轮询照旧在跑
* (真机实测降幅仅 **1.10×**,设计目标 ≥ 10×)。**"没有变化"被读成了"没有数据"**。
*
* 本用例把"稳态 + 超过 TTL"这个组合钉死:帧数必须仍是 0(E1 不破),门必须**仍然关着**。
* ⚠️ 旧实现下必红(载荷年龄 > TTL ⇒ `presenceFresh()` 翻假)—— 这就是"先红后绿"的那条断言。
*/
test('T33 P-1:稳态零帧下门不得自己重开(判据 = 订阅已建立 ∧ 链路活着,⛔ 不是载荷年龄)', async (t) => {
/**
* ⚠️ `hbSec: 1` 是**夹具前提**,不是产品口径:测试档把 presence TTL 压到 3 s,而生产心跳是 15 s
* ⇒ 不压心跳的话,relay 的 `presenceDevices`(`now − conns[ts] ≤ ttl`)会在 3 s 后把连接判死
* ⇒ 自己制造出"离线→在线"的状态变化(**假帧**),把本用例的稳态前提破坏掉。
* 生产里 `TTL(45 s) > 心跳(15 s)` ⇒ 不存在这个组合(这正是 TTL 因子取 3 的原因)。
*/
const { sub } = await presenceScene(t, [], { hbSec: 1 })
sub.subscribePresence()
assert.ok(await waitFor(() => sub.presenceStatus().state === 'subscribed', 3_000), '订阅未生效')
/**
* ⚠️ 先**等静默下来**再取基线:`SNAP` 之后还有一次**由落点落地驱动**的强制推(序⑲ T32 的假死修复)
* —— 它属于"上线那一件事"的收尾,⛔ 不是稳态帧。不先等它,基线就取在稳态之前。
*/
await sleep(1_000)
const idle = sub.presenceStatus().pushFrames
// 旧判据看的是"**最近一次载荷**"⇒ 前提要按它的口径算(`max(snap,push)` ⇒ 取**年龄最小**的那个)。
const ageOf = (s) => Math.min(s.lastSnapAgoMs ?? 0, s.lastPushAgoMs ?? s.lastSnapAgoMs ?? 0)
const before = sub.presenceStatus()
// 稳态(无任何状态变化)⇒ 一帧都不推;等到**超过 TTL**(测试档 3 s)再看门。
await sleep(PT.presenceTtlMs + 1_200)
const st = sub.presenceStatus()
assert.equal(st.pushFrames, idle, '稳态必须仍然是 0 帧(E1:变化驱动,无变化不推)')
assert.equal(st.state, 'subscribed', '链路上订阅应仍生效(服务端心跳在 ⇒ 半开巡检不会断它)')
assert.ok(
ageOf(st) >= PT.presenceTtlMs,
`前提未成立:最近一次载荷年龄应已超过 TTL,实得 ${ageOf(st)}ms(旧=${ageOf(before)}ms;否则这条用例证不了 P-1)`,
)
assert.equal(
st.fresh,
true,
`载荷 ${ageOf(st)}ms 没来、但链路活着(入站静默 ${st.lastInboundAgoMs}ms ≤ 上界 ${st.linkSilentMaxMs}ms)⇒ 门必须保持关闭`,
)
assert.equal(sub.presenceFresh(), true, 'P-1 回归:`presenceFresh()` ⛔ 不得因"没有变化"而翻假')
})
/**
* **反方向**(失败关闭):链路活着 ⛔ 不足以判"新鲜" —— 还必须**订阅真的生效**。
* 少这一条,"永远返回 true"也能让 T33 绿 ⇒ 过修无法被发现。
*/
test('T34 P-1 反向:未订阅 / 已退订 / 被拒 ⇒ 一律不得判"新鲜"(失败关闭,回退 `/status`)', async (t) => {
const { sub } = await presenceScene(t, [])
// ① 从没订阅 ⇒ 不新鲜
assert.equal(sub.presenceFresh(), false, '未订阅 ⇒ 必须回退 /status')
assert.equal(sub.presenceStatus().fresh, false, '状态视图必须如实报门是开的')
// ② 订阅生效 ⇒ 新鲜;退订 ⇒ **立刻**回到不新鲜(链路还活着也不例外)
sub.subscribePresence()
assert.ok(await waitFor(() => sub.presenceStatus().state === 'subscribed', 3_000), '订阅未生效')
assert.equal(sub.presenceFresh(), true, '订阅刚生效 ⇒ 应判新鲜(否则主路径永远用不上)')
sub.unsubscribePresence()
assert.equal(sub.presenceFresh(), false, '退订 ⇒ 必须立刻不新鲜(⛔ 不许靠"链路活着"继续给绿)')
// ③ 被**显式拒绝**的订阅(跨网)⇒ 同样不新鲜(⛔ 静默返空与"本网没人"同形,本线头号教训)
sub.subscribePresence(['u:5/d1'])
assert.ok(await waitFor(() => sub.presenceStatus().rejected === 1, 3_000), '跨网订阅必须被显式拒绝')
assert.equal(sub.presenceFresh(), false, '被拒的订阅 ⇒ 必须回退 /status')
// ④ 链路断 ⇒ 订阅随之消失 ⇒ 不新鲜(`onPresenceDown` 的唯一职责)
sub.subscribePresence()
assert.ok(await waitFor(() => sub.presenceStatus().state === 'subscribed', 3_000), '重新订阅未生效')
sub.stop()
await sleep(200)
assert.equal(sub.presenceFresh(), false, '链路断 ⇒ 必须不新鲜(否则会拿过期镜像当事实)')
})
/* ═══════════ 序㉑ P-2 修复(键口径 ⇒ 抽纯函数)T35–T36 ═══════════ */
/**
* 🔴 **P-2**(在册缺陷):`translateEndpoint` 的键口径 —— `hostVia` / `relayEndpoints` / 拨号池
* 全按**逻辑名**建键,而调用方 `RemoteSpawner.translateEndpoint(host.hostId, …)` 只给得到
* **裸 hostId** ⇒ `hostVia.get(hostId)` 恒 `undefined` ⇒ 早退原样透传 ⇒ **闭包整体是死分支**。
*
* 本用例钉住**判定本体**(抽成的纯函数);键口径那一半由 T36 钉。
*/
test('T35 P-2:`relayEndpointTarget` 四支判定(透传 / 拨号 / 快照 / 失败关闭)⛔ 不误触发拨号池', () => {
let dialedCalls = 0
const dial = (p) => () => {
dialedCalls += 1
return p
}
// ① 未知 host(不在 `dsh_hosts`)⇒ 原样透传,且**不许**碰拨号池
assert.deepEqual(
relayEndpointTarget({ known: false, via: undefined, dialedPort: dial(25000), snapshotLocalPort: 41000 }),
{ kind: 'passthrough', why: 'unknown-host' },
'未知 host 必须保持老行为(单机 / 默认 host 不受影响)',
)
assert.equal(dialedCalls, 0, '未知 host ⛔ 不许查拨号池(`localPortFor` 会**按需绑池口**,是有副作用的调用)')
// ② `via` 不是 relay ⇒ 原样透传(隧道同号反向转发,不需要翻译);同样不碰池
for (const via of ['local', 'manager-ssh']) {
assert.deepEqual(
relayEndpointTarget({ known: true, via, dialedPort: dial(25000) }),
{ kind: 'passthrough', why: 'not-relay' },
`via=${via} 两侧口号相同 ⇒ 翻译既不需要也不该做`,
)
}
assert.equal(dialedCalls, 0, '非 relay ⛔ 不许查拨号池')
// ③ via=relay ⇒ ①拨号落点优先(R5:落点在 Manager 本机 ⇒ relay 换机器也成立)
assert.deepEqual(
relayEndpointTarget({ known: true, via: 'relay', dialedPort: dial(25000), snapshotLocalPort: 41000 }),
{ kind: 'local', port: 25000, via: 'dialed' },
)
// ④ 拨号拿不到 ⇒ 回落 relay 快照
assert.deepEqual(
relayEndpointTarget({ known: true, via: 'relay', dialedPort: dial(undefined), snapshotLocalPort: 41000 }),
{ kind: 'local', port: 41000, via: 'snapshot' },
)
// ⑤ 两条都没有 ⇒ **失败关闭**(⛔ 不是原样透传:那会拿 Worker 侧口号拨 Manager 本机)
assert.deepEqual(
relayEndpointTarget({ known: true, via: 'relay', dialedPort: dial(undefined) }),
{ kind: 'unreachable', why: 'no-dialed-port' },
)
// ⑥ `0` 是"没有落点"的哨兵值(不是合法口号)⇒ 必须仍判失败关闭
assert.equal(
relayEndpointTarget({ known: true, via: 'relay', dialedPort: dial(0), snapshotLocalPort: 0 }).kind,
'unreachable',
'落点 0 ⇒ 无落点(与 relay `/status` 同口径)',
)
})
test('T36 P-2:键口径 —— 裸 `hostId` 必须经 `hostNameIndex` 换到逻辑名(⛔ 闭包不得再拿 hostId 当键)', async () => {
const idx = hostNameIndex([
{ id: 'w-47', networkId: 'ops' },
{ id: 'w-106', networkId: '' }, // 空 ⇒ 归属网取兜底(与 DB 列默认值同口径)
{ id: 'd1', networkId: 'u:5' },
])
assert.equal(idx.get('w-47'), 'ops/w-47')
assert.equal(idx.get('w-106'), 'ops/w-106', '空 network_id 必须按兜底网补全(否则与 DB 行写的键不一致)')
assert.equal(idx.get('d1'), 'u:5/d1', '跨网同 hostId 各算一台(P0-3)')
assert.equal(idx.get('ops/w-106'), undefined, '索引的键是**裸 hostId** ⇒ 拿逻辑名查不到(两侧口径必须显式转换)')
assert.equal(hostNameIndex([{ id: 'x', networkId: '' }], 'u:9').get('x'), 'u:9/x', '兜底网可注入(⛔ 不写死 ops)')
/**
* 源码级守卫(这类"整个闭包静默失效"的缺陷只靠运行时断言抓不到 —— 无实例时分支根本不执行):
* ⛔ 闭包不得再出现"拿裸 hostId 当控制面表的键"的写法。
*/
const src = await readFile(new URL('../src/web/server.ts', import.meta.url), 'utf8')
// ⚠️ 只看**代码行**:注释里会引用反例("原实现直接 `hostVia.get(hostId)`…"),拿整文件匹配会自伤。
const code = src
.split('\n')
.filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l))
.join('\n')
assert.equal(code.includes('hostVia.get(hostId)'), false, '⛔ 不得再用裸 hostId 查 `hostVia`(恒 undefined ⇒ 死分支)')
assert.equal(code.includes('localPortFor(hostId'), false, '⛔ 不得再用裸 hostId 查拨号池(同上)')
assert.equal(code.includes('relayEndpoints.get(`${hostId}'), false, '⛔ 快照回退键同样必须是逻辑名')
assert.ok(code.includes('hostNameById.get(hostId)'), '闭包必须经 `hostNameById` 换到逻辑名')
})
/* ═══════════ 序㉒ P-2b 修复(候选链补「订阅推送落点」一级)T37 ═══════════ */
/**
* 🔴 **P-2b**(在册缺陷):`relayEndpointTarget` 只有「拨号落点 → relay `/status` 快照」**两级**,
* 而地址解析链(`src/web/server.ts#RelayRendezvous.addressOf`)是**三级**:
* ① 拨号落点 → ② **订阅推送落点**(`presenceLocalPort`)→ ③ relay 快照。
*
* 为什么在 P-1 修好之后这条变成**真缺陷**:P-1 把门判据改成「订阅已建立 ∧ 链路活着」之后,
* 订阅新鲜期**长期成立** ⇒ 快照刷新(`relayEndpoints`)**趋冷**,而拨号池在"该 host 的槽位
* 分不出来"(跨网被拒 / 池满 / 尚未绑口)时也给不出落点 ⇒ 本判定会落到"两级都没有"
* ⇒ **判实例不可达(失败关闭)**,尽管**订阅推送里明明有落点**(同一时刻 `addressOf` 能答出来)。
* ⇒ 修法 = 把订阅推送插成 **②' 级**,与 `addressOf` 的三级链**逐级对齐**。
*
* ⚠️ 本用例只钉**优先级与失败语义**;"闭包有没有把这一级传进来"由同文件 `T38` 的源码级守卫钉。
*/
test('T37 P-2b:候选链三级(拨号 → 订阅推送 → relay 快照)逐支可判,⛔ 不误触发拨号池', () => {
let dialedCalls = 0
const dial = (p) => () => {
dialedCalls += 1
return p
}
// ① 三级全有 ⇒ **拨号落点优先**(R5:落点在 Manager 本机 ⇒ relay 换机器也成立)
assert.deepEqual(
relayEndpointTarget({
known: true,
via: 'relay',
dialedPort: dial(25000),
pushedLocalPort: 37057,
snapshotLocalPort: 41000,
}),
{ kind: 'local', port: 25000, via: 'dialed' },
'拨号落点是第一优先(它与订阅推送、快照三者必须逐支可分辨)',
)
// ② 拨号分不出槽位,但**订阅推送里有落点** ⇒ 取推送(**P-2b 的实体**:修前这一支落到 ③/失败关闭)
assert.deepEqual(
relayEndpointTarget({
known: true,
via: 'relay',
dialedPort: dial(undefined),
pushedLocalPort: 37057,
snapshotLocalPort: 41000,
}),
{ kind: 'local', port: 37057, via: 'pushed' },
'P-2b:拨号给不出时,订阅推送的落点必须先于 relay 快照被采用(与 `addressOf` 对齐)',
)
// ③ 订阅不新鲜 / 该 host 不在推送范围 ⇒ 回落到 relay 快照(③ 级语义**一行未改**)
assert.deepEqual(
relayEndpointTarget({
known: true,
via: 'relay',
dialedPort: dial(undefined),
pushedLocalPort: undefined,
snapshotLocalPort: 41000,
}),
{ kind: 'local', port: 41000, via: 'snapshot' },
'订阅给不出 ⇒ 必须仍能落到快照(⛔ 不是失败关闭)',
)
// ④ 三级都没有 ⇒ **失败关闭**(⛔ 绝不原样透传:那会拿 Worker 侧口号拨 Manager 本机)
assert.deepEqual(
relayEndpointTarget({ known: true, via: 'relay', dialedPort: dial(undefined) }),
{ kind: 'unreachable', why: 'no-dialed-port' },
'三级全无 ⇒ 失败关闭',
)
// ⑤ `0` 是"没有落点"的哨兵(与 relay `/status` 同口径)⇒ 订阅那级也必须按"没有"处理
assert.deepEqual(
relayEndpointTarget({
known: true,
via: 'relay',
dialedPort: dial(0),
pushedLocalPort: 0,
snapshotLocalPort: 41000,
}),
{ kind: 'local', port: 41000, via: 'snapshot' },
'落点 0 ⇒ 视为无落点,逐级下探(⛔ 不许把 0 当合法口号)',
)
// ⑥ 未知 host / 非 relay ⇒ **原样透传**,且订阅那一级同样不许改写结果(⛔ 不是"新增一条翻译路径")
const callsBefore6 = dialedCalls
assert.deepEqual(
relayEndpointTarget({ known: false, via: undefined, dialedPort: dial(25000), pushedLocalPort: 37057 }),
{ kind: 'passthrough', why: 'unknown-host' },
)
assert.deepEqual(
relayEndpointTarget({ known: true, via: 'local', dialedPort: dial(25000), pushedLocalPort: 37057 }),
{ kind: 'passthrough', why: 'not-relay' },
)
assert.equal(
dialedCalls - callsBefore6,
0,
'未知 host / 非 relay ⛔ 不许碰拨号池(`localPortFor` 会**按需绑池口**,是有副作用的调用)',
)
})
test('T38 P-2b:闭包必须把「订阅推送落点」传进判定(源码级守卫 —— 无实例时该分支不执行)', async () => {
const src = await readFile(new URL('../src/web/server.ts', import.meta.url), 'utf8')
const code = src
.split('\n')
.filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l))
.join('\n')
assert.ok(
code.includes('presenceLocalPort(name, ep.port)'),
'闭包必须把 `presenceLocalPort(name, ep.port)` 交给 `relayEndpointTarget`(否则三级链少一级 = P-2b 原样)',
)
assert.ok(
code.includes('pushedLocalPort:'),
'判定入参必须显式命名(⛔ 不许靠位置参数 / 事后补丁)',
)
})