/** * 覆盖网络 S0 · 可达性 / 会合接口单测(纯函数,不连网、不起进程)。 * 运行:node --test test/reachability.test.mjs(已含在 npm test / npm verify 中) * * ## 这个测试的职责 * S0 的唯一验收标准是「**行为零变化**」。所以本文件的核心断言是 * **「可达性解析前后的 agent 基址逐字相等」** —— 把现网 `dsh_hosts` 的真实两行 * 原样写进判据里(**不靠肉眼、不靠人工比对**): * * | hostId | dsh_hosts.endpoint(2026-09-16 实测) | 真实语义 | * |---|---|---| * | `w-2` | `http://127.0.0.1:19000` | 经 47 上 sshd 的反向隧道落点 | * | `w-1` | `http://127.0.0.1:19100` | 同机直连(node 自己监听) | */ import { test } from 'node:test' import assert from 'node:assert/strict' import { agentBaseUrl, agentBaseUrlOf, parseReachability, toEndpoint, VIA_LOCAL, VIA_MANAGER_SSH, } from '../lib/net/reachability.js' import { LocalRendezvous, ManagerSshRendezvous, RendezvousRegistry } from '../lib/net/rendezvous.js' /** 现网真实两条(2026-09-16 在 47 上 `SELECT id, endpoint FROM dsh_hosts` 实测)。 */ const LIVE_HOSTS = [ { hostId: 'w-2', endpoint: 'http://127.0.0.1:19000', via: VIA_MANAGER_SSH }, { hostId: 'w-1', endpoint: 'http://127.0.0.1:19100', via: VIA_LOCAL }, ] test('S0 等价性:旧 agentUrl 取址与可达性取址逐字相等', () => { for (const h of LIVE_HOSTS) { // 旧路径(今天生产用的):直接把 endpoint 当 agent 基址 const legacy = h.endpoint.replace(/\/$/, '') // 新路径(S0 起的唯一入口):没给 reachability ⇒ 必须回退到同一个值 assert.equal(agentBaseUrlOf({ hostId: h.hostId, agentUrl: h.endpoint }), legacy, h.hostId) } }) test('parseReachability → agentBaseUrl 对现网两行是往返恒等的', () => { for (const h of LIVE_HOSTS) { const reach = parseReachability(h.hostId, h.endpoint, h.via) assert.equal(agentBaseUrl(reach), h.endpoint, `${h.hostId} 往返不一致`) assert.equal(toEndpoint(reach), h.endpoint, `${h.hostId} toEndpoint 不一致`) assert.equal(reach.scheme, 'http') assert.deepEqual( { hostId: reach.hostId, networkId: reach.networkId, via: reach.via, address: reach.address }, // 裸 hostId ⇒ 落运维网(P0-3 的过渡期兼容:旧调用方一个字都不用改) { hostId: h.hostId, networkId: 'ops', via: h.via, address: h.endpoint.slice('http://'.length) }, ) } }) test('parseReachability:https / 裸 host:port / 尾斜杠 三种兼容面', () => { assert.deepEqual(parseReachability('h', 'https://a.example:8443', 'x'), { hostId: 'h', networkId: 'ops', via: 'x', address: 'a.example:8443', scheme: 'https', }) // 没写 scheme ⇒ 按 http(与 fetch 的补全行为一致) assert.deepEqual(parseReachability('h', '10.0.0.5:19000', 'x'), { hostId: 'h', networkId: 'ops', via: 'x', address: '10.0.0.5:19000', scheme: 'http', }) assert.equal(agentBaseUrl(parseReachability('h', 'http://127.0.0.1:19000/', 'x')), 'http://127.0.0.1:19000') }) test('可达性优先于旧 agentUrl', () => { const host = { hostId: 'w-2', agentUrl: 'http://stale.example:1', reachability: { hostId: 'w-2', via: VIA_MANAGER_SSH, address: '127.0.0.1:19000', scheme: 'http' }, } assert.equal(agentBaseUrlOf(host), 'http://127.0.0.1:19000') }) test('两者皆缺 ⇒ 抛错(禁止静默打到空地址)', () => { assert.throws(() => agentBaseUrlOf({ hostId: 'w-x' }), /既无 reachability 也无 agentUrl/) assert.throws(() => agentBaseUrlOf({ hostId: 'w-x', agentUrl: '' }), /既无 reachability 也无 agentUrl/) }) test('LocalRendezvous:命中给 local,未命中回 undefined 不抛', async () => { const table = new Map([['w-1', '127.0.0.1:19100']]) const rv = new LocalRendezvous((id) => table.get(id)) assert.equal(rv.id, VIA_LOCAL) assert.equal(rv.dialTarget(), '(direct)') assert.deepEqual(await rv.resolve('w-1'), { hostId: 'w-1', networkId: 'ops', via: VIA_LOCAL, address: '127.0.0.1:19100', scheme: 'http', }) assert.equal(await rv.resolve('w-2'), undefined) }) test('ManagerSshRendezvous:解析出的基址必须等于现网 endpoint(S2 迁移判据)', async () => { const table = new Map([ ['w-2', '127.0.0.1:19000'], ['w-1', '127.0.0.1:19100'], ]) const rv = new ManagerSshRendezvous({ target: 'root@203.0.113.10:32022', addressOf: (id) => table.get(id), }) assert.equal(rv.id, VIA_MANAGER_SSH) assert.equal(rv.dialTarget(), 'root@203.0.113.10:32022') for (const h of LIVE_HOSTS) { const resolved = await rv.resolve(h.hostId) assert.equal(agentBaseUrl(resolved), h.endpoint, `${h.hostId} 迁移后基址变了`) } }) test('RendezvousRegistry:按 via 取实现', () => { const local = new LocalRendezvous(() => undefined) const ssh = new ManagerSshRendezvous({ target: 't', addressOf: () => undefined }) const reg = new RendezvousRegistry([local, ssh]) assert.equal(reg.get(VIA_LOCAL), local) assert.equal(reg.get(VIA_MANAGER_SSH), ssh) assert.equal(reg.get('relay:backbone-1'), undefined) assert.deepEqual(reg.ids().sort(), [VIA_LOCAL, VIA_MANAGER_SSH].sort()) }) // ── S2 验收判据 ──────────────────────────────────────────────────────────── // 「迁移前 `agentUrl`」必须 **逐条逐字等于** 「迁移后 `resolve()` 的结果」。 // 这里把 `hostsProvider` 的真实接线原样复刻一遍(先读 via → 选实现 → 解析 → 拼基址), // 两条数据用现网实测值写死 ⇒ **不靠肉眼、不靠人工比对**。 test('S2:via → Rendezvous → Reachability 后取址与旧 agentUrl 逐条相等', async () => { // 现网 `dsh_hosts` 真实两行(2026-09-16 实测;`via` = 回填后的目标值) const rows = LIVE_HOSTS.map((h) => ({ id: h.hostId, endpoint: h.endpoint, via: h.via })) // P0-3:控制面的键 = **逻辑名**(现网全在 `ops` ⇒ 与裸 id 等价) const nameOf = (h) => `ops/${h.id}` const hostAddresses = new Map() const rendezvous = new RendezvousRegistry([ new LocalRendezvous((id) => hostAddresses.get(id)), new ManagerSshRendezvous({ target: 'ssh://root@203.0.113.10:32022', addressOf: (id) => hostAddresses.get(id) }), ]) // hostsProvider 第一遍:同步地址表 for (const row of rows) { hostAddresses.set(nameOf(row), parseReachability(nameOf(row), row.endpoint, row.via).address) } for (const row of rows) { const impl = rendezvous.get(row.via) ?? rendezvous.get(VIA_MANAGER_SSH) assert.notEqual(impl, undefined, `${row.id} 的 via=${row.via} 必须能选到实现`) const reach = await impl.resolve(nameOf(row)) assert.notEqual(reach, undefined, `${row.id} 必须能解析出可达性`) assert.equal(reach.via, row.via, `${row.id} 解析后 via 变了`) assert.equal(reach.hostId, row.id, `${row.id} 解析出的 hostId 必须是**裸** id`) assert.equal(reach.networkId, 'ops', `${row.id} 解析出的网络段必须是 ops`) // 判据:新路径拼出来的基址 == 旧路径直接用的 endpoint assert.equal(agentBaseUrlOf({ hostId: row.id, reachability: reach }), row.endpoint, `${row.id} 取址变了`) assert.equal(agentBaseUrlOf({ hostId: row.id, agentUrl: row.endpoint }), row.endpoint, `${row.id} 旧路径变了`) } }) test('S2:via 认不出来 ⇒ 回退 manager-ssh,不抛(过渡期要能跑)', async () => { const hostAddresses = new Map([['w-x', '10.0.0.9:19000']]) const rendezvous = new RendezvousRegistry([ new LocalRendezvous((id) => hostAddresses.get(id)), new ManagerSshRendezvous({ target: '', addressOf: (id) => hostAddresses.get(id) }), ]) const impl = rendezvous.get('relay:not-deployed-yet') ?? rendezvous.get(VIA_MANAGER_SSH) const reach = await impl.resolve('w-x') assert.equal(reach.via, VIA_MANAGER_SSH) assert.equal(agentBaseUrlOf({ hostId: 'w-x', reachability: reach }), 'http://10.0.0.9:19000') })