Files
dsh_ai1net_server/test/remote-user-fs.test.mjs
T
admin 146c3d25ef feat(overlay): 覆盖网络线序①–⑮ 代码与测试产物入库
覆盖网络线累积产物(此前只在工作区、未入版本库):
- 新增 relay 子系统 src/net/relay/**(wire/duplex/server/client/dialer/switcher/directory/identity/keys/placement/network/addr-override/main/index)
- 新增 src/worker/relay-tunnel.ts、src/web/routes/overlay.ts
- 新增观测/演练脚本 overlay-probe、overlay-failover-drill、overlay-keyring、overlay-holepunch、overlay-jitter、overlay-wan、overlay-relaykey-add、relay-mem-calibrate
- 新增测试 12 个(relay / relay-failover / remote-spawner / instance-port / overlay-{network,auth,bootstrap,identity} / remote-user-fs 等)

验收基线:npm test = 162 pass / 0 fail / 1 skip;--scene all = 12 PASS / 0 SKIP / 0 FAIL;overlay-probe = 12/12
2026-09-17 17:03:27 +08:00

189 lines
7.0 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.
/**
* `RemoteUserFs` 的**按用户路由**单测(覆盖网络线缺陷 A1)。
*
* ## 被钉死的缺陷(2026-09-16 现场实测)
* `RemoteUserFs.target()` 旧实现对三种失败**一律静默回退默认机**:查库抛错 / 没给 `agentFor` /
* `agentFor` 查不到。而默认机 = **Manager 自己那台**,它对*别的机*的用户只有两种回答:
* ① 那台 agent 上没有这个用户 ⇒ `{error:"not_found"}` —— 与「**文件夹不存在**」**完全同形**,
* 用户读成"我的文件丢了",真因却是"请求根本没出这台机";
* ② 本地恰好有同名目录 ⇒ 文件被写进一份**没人在看的副本**(更糟的静默写坏)。
*
* 触发窗口是**真实存在的、且每次重启必现**:`server.ts` 的 `hostDirectory` 是惰性 Map,
* 唯一写入者 `hostsProvider()` 此前只被 `RemoteSpawner.ensureHosts()` 调用 ⇒ Manager 重启后
* 若用户先碰文件面,表里只有本机。实测:连发 3 次 launch **全 404,relay 零 `DIAL`、
* 拨号池零落点**(判别器 = relay 有没有 `DIAL`)。
*
* ## 修法与断言
* ① **治本**:新增可选 `ensureHost(hostId)` —— 未命中时先**按需补齐**目录再判(U1),
* 所以正常冷启动请求不会被下面的失败关闭波及;
* ② **治安全**:补齐后仍取不到 ⇒ `UserFsError('host_unresolved')` → **503**,且
* **一个字节都不许发往默认机**(U2/U3/U4/U8 的 `fetch` 断言)。
* ③ **不退化**:"确实还没有归属"(`hostIdFor` 正常返回 `undefined`)与单机形态仍是默认机(U5/U6)。
*
* 运行:`node --test test/remote-user-fs.test.mjs`(已登记进 `npm test` / `npm run verify`)。
*
* @module test/remote-user-fs
*/
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { RemoteUserFs } from '../lib/fs/remote-user-fs.js'
/* ─────────── 小工具 ─────────── */
/** Manager 自己那台(= 默认 / 回退 agent)—— 任何"打到这里"都是路由失败。 */
const DEFAULT_AGENT = 'http://127.0.0.1:19100'
/** 归属机:w-106 的 agent。 */
const W106_AGENT = 'http://127.0.0.1:19000'
/** 记账用 fetch:记下每一发请求,永不真的出网。 */
function spyFetch() {
const calls = []
const impl = async (url) => {
calls.push(String(url))
return { ok: true, status: 200, text: async () => JSON.stringify([]) }
}
impl.calls = calls
return impl
}
function mk(opts) {
const fetchImpl = spyFetch()
const fs = new RemoteUserFs({
agentUrl: DEFAULT_AGENT,
token: 'tok-default',
workerDataRoot: '/var/lib/dsh/data',
hostIdFor: opts.hostIdFor,
agentFor: opts.agentFor,
ensureHost: opts.ensureHost,
fetchImpl,
})
return { fs, fetchImpl }
}
/** 断言"抛出的一定是 host_unresolved/503"。 */
function isHostUnresolved(err) {
assert.equal(err.code, 'host_unresolved')
assert.equal(err.status, 503)
return true
}
/* ─────────── ① 治本:未命中先补齐 ─────────── */
test('U1 目录未命中:ensureHost 补齐后打到**归属机器**(不再回退默认机)', async () => {
const dir = new Map() // 模拟 hostsProvider() 尚未填过的空目录
let ensured = 0
const { fs, fetchImpl } = mk({
hostIdFor: async () => 'w-106',
agentFor: (h) => dir.get(h),
ensureHost: async (h) => {
ensured += 1
dir.set(h, { agentUrl: W106_AGENT, token: 'tok-106' })
},
})
await fs.listDir('u1', '')
assert.equal(ensured, 1, '未命中必须触发一次补齐')
assert.deepEqual(fetchImpl.calls, [`${W106_AGENT}/fs/list`], '必须打到 w-106,不许打默认机')
})
test('U7 命中时**不**做补齐(正常路径零开销:不查库)', async () => {
let ensured = 0
const { fs, fetchImpl } = mk({
hostIdFor: async () => 'w-106',
agentFor: () => ({ agentUrl: W106_AGENT, token: 'tok-106' }),
ensureHost: async () => {
ensured += 1
},
})
await fs.listDir('u1', '')
assert.equal(ensured, 0, '命中后再查库 = 纯浪费')
assert.deepEqual(fetchImpl.calls, [`${W106_AGENT}/fs/list`])
})
/* ─────────── ② 治安全:取不到 ⇒ 失败关闭 ─────────── */
test('U2 补齐后仍取不到地址:503 host_unresolved,且**零请求发往默认机**(写操作也拦住)', async () => {
let ensured = 0
const { fs, fetchImpl } = mk({
hostIdFor: async () => 'w-106',
agentFor: () => undefined, // 目录里始终没有 w-106
ensureHost: async () => {
ensured += 1
},
})
// 用 mkdir 而不是读:这正是"把目录建到错机上"的那类操作
await assert.rejects(() => fs.mkdir('u1', 'MCN短视频创作'), isHostUnresolved)
assert.equal(ensured, 1, '失败前仍应尝试过补齐')
assert.deepEqual(fetchImpl.calls, [], '⛔ 一个字节都不许发往默认机')
})
test('U3 未提供 ensureHost(老调用点):未命中同样失败关闭,**不退化**为静默回退', async () => {
const { fs, fetchImpl } = mk({
hostIdFor: async () => 'w-106',
agentFor: () => undefined,
// ensureHost 故意不传
})
await assert.rejects(() => fs.listDir('u1', ''), isHostUnresolved)
assert.deepEqual(fetchImpl.calls, [])
})
test('U8 ensureHost 自己抛错(如查库失败):仍失败关闭,不吞成"默认机"', async () => {
const { fs, fetchImpl } = mk({
hostIdFor: async () => 'w-106',
agentFor: () => undefined,
ensureHost: async () => {
throw new Error('pg is down')
},
})
await assert.rejects(() => fs.listDir('u1', ''), isHostUnresolved)
assert.deepEqual(fetchImpl.calls, [])
})
test('U4 hostIdFor 抛错(查库失败):失败关闭,不静默回退默认机', async () => {
const { fs, fetchImpl } = mk({
hostIdFor: async () => {
throw new Error('pg is down')
},
agentFor: () => ({ agentUrl: W106_AGENT, token: 'tok-106' }),
})
await assert.rejects(() => fs.readFile('u1', 'a.txt'), isHostUnresolved)
assert.deepEqual(fetchImpl.calls, [], '归属都查不出来时,任何一台都不该被打')
})
/* ─────────── ③ 不退化:设计内的默认机路径 ─────────── */
test('U5 hostIdFor 返回 undefined(确实还没有归属):仍用默认 agent', async () => {
const { fs, fetchImpl } = mk({
hostIdFor: async () => undefined,
agentFor: () => ({ agentUrl: W106_AGENT, token: 'tok-106' }),
})
await fs.initUserRoot('u1', 1001)
assert.deepEqual(fetchImpl.calls, [`${DEFAULT_AGENT}/fs/init`], '首触达/无归属走默认机是设计内契约')
})
test('U6 未提供 hostIdFor(单机形态):默认 agent,且不触发任何路由表', async () => {
let probed = 0
const { fs, fetchImpl } = mk({
agentFor: () => {
probed += 1
return undefined
},
})
await fs.listDir('u1', '')
assert.equal(probed, 0)
assert.deepEqual(fetchImpl.calls, [`${DEFAULT_AGENT}/fs/list`])
})