把散落在代码里的真实部署值统一收进 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 未被误建)。
172 lines
9.2 KiB
JavaScript
172 lines
9.2 KiB
JavaScript
/**
|
||
* 覆盖网络线 序 ㉕ · 「逐步拉起」单测 —— **纯函数面**(零 IO、零 systemd、零生产副作用)。
|
||
*
|
||
* ## 为什么只测纯函数
|
||
* 被替换掉的是「启动即 `cleanAllStaleScopes()`」。它的输入是 **OS 层既有 scope**
|
||
* (`systemctl list-units` + `systemctl show -p Description`),在 Windows 开发机上
|
||
* 拿不到 ⇒ 真机部分由 §6/S6 在 106 上验收。
|
||
* 但**最容易出错、也最致命**的那一段恰好是纯的 —— **把 `Description` 解析回实例三要素**:
|
||
* 解析错 ⇒ 后续替换时定位到别人的实例(跨租户最坏情形)。故这一段必须穷举式钉住。
|
||
*
|
||
* 另加两条**源码级守卫**(照 序⑦ `T38` 先例):认领逻辑必须真的接在构造函数上、
|
||
* 且⛔ 不许把「认领」写成「什么都不做」(那会让档案 30 的孤儿失控)。
|
||
*
|
||
* 运行:`node --test test/orchestrator-rehydrate.test.mjs`(⚠️ **刻意不进 `npm test`** ——
|
||
* 本序要求零回归基线 `npm test` 176/175/0/1 **逐字不变**,加进去会改测试总数)。
|
||
*
|
||
* @module test/orchestrator-rehydrate
|
||
*/
|
||
|
||
import assert from 'node:assert/strict'
|
||
import { readFile } from 'node:fs/promises'
|
||
import { test } from 'node:test'
|
||
import {
|
||
decideScopeAction,
|
||
parseScopeDescription,
|
||
parseScopeUnitName,
|
||
} from '../lib/supervisor/orchestrator.js'
|
||
|
||
/**
|
||
* 夹具 = 106 上 **真实** 实例 `dsh-100002-ef8d1d12.scope` 的 `Description` 精简等价形态
|
||
* (保留全部关键 flag 与其真实相对顺序,省掉无关的 `--ro-bind-try` 白名单项)。
|
||
* ⚠️ 顺序刻意保留:`--chdir` 在 bwrap 段、`--profile/--port` 在 setpriv 之后。
|
||
*/
|
||
const REAL_DESC =
|
||
'/usr/bin/bwrap --ro-bind /usr /usr --tmpfs /etc ' +
|
||
'--bind /var/lib/dsh-test/users/4092b965-2f68-4977-9989-68b3966f7df0/tmp /tmp ' +
|
||
'--bind /var/lib/dsh-test/users/4092b965-2f68-4977-9989-68b3966f7df0 /var/lib/dsh-test/users/4092b965-2f68-4977-9989-68b3966f7df0 ' +
|
||
'--unshare-pid ' +
|
||
'--chdir /var/lib/dsh-test/users/4092b965-2f68-4977-9989-68b3966f7df0/ws ' +
|
||
'-- setpriv --reuid 100002 --regid 100002 --clear-groups ' +
|
||
'/usr/bin/dsh --profile web --host 127.0.0.1 --port 21000'
|
||
|
||
const UID = 100002
|
||
const USER = '4092b965-2f68-4977-9989-68b3966f7df0'
|
||
const FOLDER = `/var/lib/dsh-test/users/${USER}/ws`
|
||
|
||
/* ── R1–R5:scope 名解析(⛔ 只认本平台自己的形态) ─────────────────────────── */
|
||
|
||
test('R1 真机形态:dsh-100002-ef8d1d12.scope ⇒ uid 100002', () => {
|
||
assert.equal(parseScopeUnitName('dsh-100002-ef8d1d12.scope'), 100002)
|
||
})
|
||
|
||
test('R2 ⛔ 大写 hex / 非 scope / 别的单元一律不认', () => {
|
||
assert.equal(parseScopeUnitName('dsh-100002-EF8D1D12.scope'), undefined)
|
||
assert.equal(parseScopeUnitName('dsh-100002-ef8d1d12.service'), undefined)
|
||
assert.equal(parseScopeUnitName('dshs-relay.service'), undefined)
|
||
assert.equal(parseScopeUnitName('dsh-100002-ef8d1d12.scope.bak'), undefined)
|
||
assert.equal(parseScopeUnitName(''), undefined)
|
||
})
|
||
|
||
test('R3 ⛔ uid=0 / 缺段 一律不认(防误伤 systemd 自身与门户进程)', () => {
|
||
assert.equal(parseScopeUnitName('dsh-0-ef8d1d12.scope'), undefined)
|
||
assert.equal(parseScopeUnitName('dsh--ef8d1d12.scope'), undefined)
|
||
assert.equal(parseScopeUnitName('dsh-100002.scope'), undefined)
|
||
})
|
||
|
||
/* ── R6–R11:Description 解析(真机夹具) ──────────────────────────────────── */
|
||
|
||
test('R6 真机夹具:三要素全部解出且 userId 取自 --chdir(⛔ 不是取自 --bind)', () => {
|
||
const info = parseScopeDescription(REAL_DESC, UID)
|
||
assert.deepEqual(info, { userId: USER, role: 'main', port: 21000, folder: FOLDER })
|
||
})
|
||
|
||
test('R7 ⛔ uid 交叉校验不符 ⇒ 拒(scope 名与 argv 非同源 = 半截信息,必须停)', () => {
|
||
assert.equal(parseScopeDescription(REAL_DESC, 100003), undefined)
|
||
assert.equal(parseScopeDescription(REAL_DESC.replace('--reuid 100002', '--reuid 100009'), UID), undefined)
|
||
})
|
||
|
||
test('R8 ⛔ 缺 --profile 或 profile 非 web/headless ⇒ 拒(role 猜不得)', () => {
|
||
assert.equal(parseScopeDescription(REAL_DESC.replace('--profile web ', ''), UID), undefined)
|
||
assert.equal(parseScopeDescription(REAL_DESC.replace('--profile web', '--profile weird'), UID), undefined)
|
||
})
|
||
|
||
test('R9 ⛔ main 缺 --port / 端口非数字 / 越界 ⇒ 拒', () => {
|
||
assert.equal(parseScopeDescription(REAL_DESC.replace('--port 21000', ''), UID), undefined)
|
||
assert.equal(parseScopeDescription(REAL_DESC.replace('--port 21000', '--port abc'), UID), undefined)
|
||
assert.equal(parseScopeDescription(REAL_DESC.replace('--port 21000', '--port 0'), UID), undefined)
|
||
assert.equal(parseScopeDescription(REAL_DESC.replace('--port 21000', '--port 70000'), UID), undefined)
|
||
})
|
||
|
||
test('R10 ⛔ 缺 --chdir / 相对路径 / 路径里没有 /users/ 段 ⇒ 拒(拿不到 userId)', () => {
|
||
assert.equal(parseScopeDescription(REAL_DESC.replace('--chdir ' + FOLDER + ' ', ''), UID), undefined)
|
||
assert.equal(parseScopeDescription(REAL_DESC.replace(FOLDER, 'ws'), UID), undefined)
|
||
assert.equal(
|
||
parseScopeDescription(REAL_DESC.replace(FOLDER, '/srv/ws').replace(/\/var\/lib\/dshs\/users\//g, '/srv/'), UID),
|
||
undefined,
|
||
)
|
||
})
|
||
|
||
test('R11 watchdog:headless 且有端口 ⇒ 拒(不自洽);headless 无端口 ⇒ 解出但 role=watchdog', () => {
|
||
const headlessWithPort = REAL_DESC.replace('--profile web', '--profile headless')
|
||
assert.equal(parseScopeDescription(headlessWithPort, UID), undefined)
|
||
const headless = headlessWithPort.replace(' --port 21000', '')
|
||
const info = parseScopeDescription(headless, UID)
|
||
assert.equal(info?.role, 'watchdog')
|
||
assert.equal(info?.port, undefined)
|
||
assert.equal(info?.userId, USER)
|
||
})
|
||
|
||
/* ── R12–R15:处置判定(认领 vs 按旧行为停) ───────────────────────────────── */
|
||
|
||
test('R12 合法 main ⇒ adopt', () => {
|
||
const info = parseScopeDescription(REAL_DESC, UID)
|
||
assert.deepEqual(decideScopeAction(info, false), { kind: 'adopt' })
|
||
})
|
||
|
||
test('R13 ⛔ 同 uid 多 scope ⇒ 全部停(档案 30 的风险本体:多实例共 profile)', () => {
|
||
const info = parseScopeDescription(REAL_DESC, UID)
|
||
assert.deepEqual(decideScopeAction(info, true), { kind: 'stop', reason: 'dup-uid' })
|
||
})
|
||
|
||
test('R14 ⛔ 解析失败 ⇒ 停(宁可清掉,不留半截实例)', () => {
|
||
assert.deepEqual(decideScopeAction(undefined, false), { kind: 'stop', reason: 'unparsable' })
|
||
})
|
||
|
||
test('R15 ⛔ watchdog ⇒ 停(一次性 headless、无监听端口 ⇒ 无法确认健康,留着无收益)', () => {
|
||
const info = parseScopeDescription(REAL_DESC.replace('--profile web', '--profile headless').replace(' --port 21000', ''), UID)
|
||
assert.deepEqual(decideScopeAction(info, false), { kind: 'stop', reason: 'no-probe-target' })
|
||
})
|
||
|
||
/* ── R16–R18:源码级接线守卫(照 序⑦ T38 先例) ────────────────────────────── */
|
||
|
||
const SRC = await readFile(new URL('../src/supervisor/orchestrator.ts', import.meta.url), 'utf8')
|
||
|
||
test('R16 接线:构造函数必须调 rehydrateAdoptedScopes(⛔ 不是裸 cleanAllStaleScopes)', () => {
|
||
assert.ok(
|
||
/this\.portGuard = createPortGuard\(config\.portGuard\)[\s\S]{0,400}this\.rehydrateAdoptedScopes\(\)/.test(SRC),
|
||
'constructor 未接认领逻辑(仍是启动即清空)',
|
||
)
|
||
})
|
||
|
||
test('R17 ⛔ cleanAllStaleScopes 不许被删(回滚路径 + 非 account 回退都还在)', () => {
|
||
assert.ok(SRC.includes('private cleanAllStaleScopes(): void'))
|
||
assert.ok(
|
||
/private rehydrateAdoptedScopes\(\): void \{[\s\S]{0,400}this\.cleanAllStaleScopes\(\)/.test(SRC),
|
||
'非 account 回退丢了',
|
||
)
|
||
})
|
||
|
||
test('R18 ⛔ 认领不得写进 mains(写进去 ⇒ enter 复用分支拿不到 token ⇒ 503)', () => {
|
||
const body = SRC.slice(SRC.indexOf('private adoptOne('), SRC.indexOf('private probeAdopted('))
|
||
assert.ok(body.includes('this.adopted.set('), 'adoptOne 未登记到 adopted')
|
||
assert.ok(!body.includes('this.mains.set('), '⛔ adoptOne 把实例写进了 mains ⇒ 用户会被 503 挡住')
|
||
assert.ok(!body.includes('spawn('), '⛔ adoptOne 里出现了 spawn ⇒ 违反"不 spawn"')
|
||
const afterAdopt = body.slice(body.indexOf('this.adopted.set('))
|
||
assert.ok(!afterAdopt.includes('stopUnit('), '⛔ adopt 路径上还停了实例(认领应当只登记 + 探活)')
|
||
})
|
||
|
||
test('R19 ⛔ 认领/回收路径里不得出现凭据落盘(R11:安全维度不许净变差)', () => {
|
||
const body = SRC.slice(SRC.indexOf('private rehydrateAdoptedScopes('), SRC.indexOf('/** 档案 30:清掉指定 uid'))
|
||
for (const banned of ['writeFileSync', 'appendFileSync', 'launchToken']) {
|
||
assert.ok(!body.includes(banned), `rehydrate 路径里出现了 ${banned}`)
|
||
}
|
||
})
|
||
|
||
test('R20 ⛔ 节流:认领必须逐条经 setTimeout 排队(不得同步一次全跑)', () => {
|
||
const body = SRC.slice(SRC.indexOf('private rehydrateAdoptedScopes('), SRC.indexOf('private scanExistingScopes('))
|
||
assert.ok(body.includes('setTimeout('), '认领没有节流')
|
||
assert.ok(SRC.includes('DSHS_REHYDRATE_STAGGER_MS'), '节流阈值不是可配的环境键')
|
||
assert.ok(SRC.includes('DSHS_REHYDRATE_PROBE_MS'), '探活超时不是可配的环境键')
|
||
})
|