Files
dsh_shenxian/test/remote-user-fs.test.mjs
admin 452924d89c feat(config): 涉密内容外置到配置目录(档案 140)
把散落在代码里的真实部署值统一收进 config/,代码改为引用配置,
使仓库副本/开源导出不再带出生产域名、IP、内网路径与凭据。

新增 config/:platform.env.example(模板)· load.sh(shell 加载器)·
index.cjs(node 加载器)· README.md(键一览与优先级)。
真实值放 config/platform.env —— 已 .gitignore 排除,不入库、不进导出。

TS 侧新增 src/platform-paths.ts 作部署路径的唯一解析处(零副作用):
platformDir/stateDir/backupDir/artifactDir/installDir/scriptPath。
config.ts 接入这些字段;内置中继种子由生产 URL 改为空(改由
DSHS_OVERLAY_BOOTSTRAP_SEEDS 提供)。修掉 5 处硬编码绝对路径,
src/** 注释中性化 116 行/53 文件。

scripts/** 36 个内部运维脚本:真令牌/PG 口令/隧道目标/主机号/路径
一律改从配置取;web/wake.html 的注册域白名单改为运行时从
location.hostname 推导;test/** 夹具 119 行/13 文件改 RFC 2606/5737
保留值,并把「内置种子必须为空」固化为回归断言。

取证:tsc 0 错;npm test 373/375(唯一失败 lease 属既有);
全仓扫描(大小写不敏感)代码面涉密标识 = 0;已部署 47 并零回归
(/opt/dsh/* 未搬家,/var/lib/dshs/platform 未被误建)。
2026-09-19 15:12:19 +08:00

189 lines
7.0 KiB
JavaScript
Raw Permalink 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-2 的 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-2',
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-2,不许打默认机')
})
test('U7 命中时**不**做补齐(正常路径零开销:不查库)', async () => {
let ensured = 0
const { fs, fetchImpl } = mk({
hostIdFor: async () => 'w-2',
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-2',
agentFor: () => undefined, // 目录里始终没有 w-2
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-2',
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-2',
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`])
})