Files
dsh_shenxian/test/orchestrator-rehydrate.test.mjs
T

171 lines
9.2 KiB
JavaScript
Raw Normal View History

/**
* 覆盖网络线 序 ㉕ · 「逐步拉起」单测 —— **纯函数面**(零 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'), '探活超时不是可配的环境键')
})