/** * 跨机可见性状态回流(形态 D 主路径)· 回归测试 —— 插件投放与分库线 §5-1。 * * ## 钉住的是什么 * 第 8 棒真机实证的缺陷:`/api/plugins/mine` **无条件**读 Manager 本机 profile ⇒ 实例在 worker * 上的用户恒显 `enabled:false`。本项的修法 = 「**先判本机(直读、零网络)⇒ 非本机定向拉对端现算真值**」。 * * 因此本文件测的是**两条判据**,而不是"函数长得对不对": * ① **本机不发请求** —— 一旦接反,症状是静默多点一次网络,且"实例未拉起时"会把真值误降级成未知; * ② **"零插件"与"不知道"必须可分** —— `ok + bundles:[]` ≠ `not_here` ≠ `unknown`。 * 混用就是把"不知道"显示成"未启用",那正是本能力要治的病(⛔ 不许在新代码里复制它)。 * * ⚠️ 测的是**真实实现**(`lib/` 里编译出来的 `profileBundlesAt` / `LeasedSpawner.readUserBundles`), * ⛔ 不是把判据照抄一份 —— 照抄的测试只会在两边一起错的时候保持绿。 * * 跑法:`npm test`(先 `npm run build`)。 * * @module test/user-bundles-readback */ import assert from 'node:assert/strict' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'node:test' import { AGENT_TOKEN_HEADER, profileBundlesAt } from '../lib/worker/agent.js' import { LeasedSpawner } from '../lib/supervisor/leased-spawner.js' import { profileDirOf } from '../lib/supervisor/plugin-assembly.js' import { userRoot } from '../lib/fs/workspace.js' /* ─────────── ① worker 侧:读本机 profile 清单 ─────────── */ const UID = '4092b965-0000-4000-8000-000000000001' /** 造一棵真实目录树:`/users//home/profiles/web/package.json`。 */ function withUserTree(bundles, run) { const dataRoot = mkdtempSync(join(tmpdir(), 'dshs-bundles-')) try { const dir = profileDirOf(userRoot(dataRoot, UID)) mkdirSync(dir, { recursive: true }) if (bundles !== null) { writeFileSync(join(dir, 'package.json'), JSON.stringify({ dsh: { profile: { bundles } } })) } return run(dataRoot) } finally { rmSync(dataRoot, { recursive: true, force: true }) } } test('worker: 树在 ⇒ found:true 且带出真值 bundles', () => { withUserTree(['@dsh-local/storyforge', '@dsh-local/mcn-suite'], (dataRoot) => { const r = profileBundlesAt(dataRoot, UID) assert.equal(r.found, true) assert.deepEqual(r.bundles, ['@dsh-local/storyforge', '@dsh-local/mcn-suite']) assert.ok(typeof r.asOf === 'number' && r.asOf > 0, 'asOf 必须带(§5-1:不是可选项)') }) }) test('worker: 树在、但一个插件都没开 ⇒ found:true + 空清单(⛔ 这不是 not_here)', () => { withUserTree([], (dataRoot) => { const r = profileBundlesAt(dataRoot, UID) assert.equal(r.found, true, '树在本机 ⇒ found 必须为 true') assert.deepEqual(r.bundles, []) }) }) test('worker: 树不在本机 ⇒ found:false(= not_here 的来源,⛔ 与"零插件"可区分)', () => { withUserTree([], (dataRoot) => { const missing = profileBundlesAt(dataRoot, '00000000-0000-4000-8000-00000000dead') assert.equal(missing.found, false) // 关键:not_here 与"零插件"的**判别位是 found**,不是 bundles 的长度 const empty = profileBundlesAt(dataRoot, UID) assert.equal(empty.found, true) assert.notEqual(missing.found, empty.found) }) }) test('worker: 树在但 package.json 缺失/损坏 ⇒ 不抛、按空清单(首次装配语义)', () => { withUserTree(null, (dataRoot) => { const r = profileBundlesAt(dataRoot, UID) assert.equal(r.found, true) assert.deepEqual(r.bundles, []) }) }) /* ─────────── ② Manager 侧:定向拉的定位链与降级语义 ─────────── */ /** 假 agent 回包。 */ function reply(payload, status = 200) { return { ok: status >= 200 && status < 300, status, json: async () => payload } } /** * 造一个 LeasedSpawner:`instanceHost` = 归属表里该用户实例落在哪台。 * `calls` 收集**真实发出的请求** —— 「本机不发请求」这条判据只有它证得了。 * `ensureHost` 调用次数单独记(第 12 棒加):它是"目录补齐"的唯一入口, * 「本机也去补一次表」= 白白多点一次 PG,必须有判据钉住。 */ function makeLeased({ instanceHost = 'w-2', found = true, status = 200, payload, lookupThrows = false, agentFor, ensureHost, } = {}) { const calls = [] const ensureCalls = [] const db = { findUserInstance: async () => { if (lookupThrows) throw new Error('pg down') return instanceHost === null ? undefined : { userId: UID, hostId: instanceHost, epoch: 1 } }, } const fetchImpl = async (url, init) => { calls.push({ url, init }) if (status !== 200) return reply({ error: 'boom' }, status) return reply(payload ?? { found, bundles: found ? ['@dsh-local/storyforge'] : [], asOf: 111 }) } const s = new LeasedSpawner( {}, db, { hostId: 'w-1', agentUrl: 'http://127.0.0.1:19100', agentToken: 'tok-local', manual: true, fetchImpl, agentFor: agentFor ?? ((hostId) => hostId === 'w-2' ? { agentUrl: 'http://10.0.0.2:19000', token: 'tok-106' } : undefined), ...(ensureHost === undefined ? {} : { ensureHost: async (hostId) => { ensureCalls.push(hostId) return ensureHost(hostId) }, }), }, ) return { s, calls, ensureCalls } } test('manager: 实例在别的机器 ⇒ 带 token 定向 POST 到**那台**的 /profile-bundles', async () => { const { s, calls } = makeLeased() const r = await s.readUserBundles(UID) assert.equal(r.kind, 'ok') assert.deepEqual(r.bundles, ['@dsh-local/storyforge']) assert.equal(r.hostId, 'w-2') assert.equal(calls.length, 1, '必须恰好一次请求') assert.equal(calls[0].url, 'http://10.0.0.2:19000/profile-bundles') assert.equal(calls[0].init.method, 'POST') // ★ 凭据判据:与 `/fence` 同级 —— 走的就是既有 token 头(⛔ 不是免凭据口) assert.equal(calls[0].init.headers[AGENT_TOKEN_HEADER], 'tok-106') assert.deepEqual(JSON.parse(calls[0].init.body), { userId: UID }) }) test('manager: 实例就在本机 ⇒ 返回 null 且**一个请求都不发**(零网络往返)', async () => { const { s, calls } = makeLeased({ instanceHost: 'w-1' }) const r = await s.readUserBundles(UID) assert.equal(r, null, 'null = "实例在本机,调用方自己读盘"') assert.equal(calls.length, 0, '★ 本机再去拨远端 = 纯性能退化') }) test('manager: 归属未登记 ⇒ 同样按本机处理(与 fenceOnAgent 同姿势),不发请求', async () => { const { s, calls } = makeLeased({ instanceHost: null }) assert.equal(await s.readUserBundles(UID), null) assert.equal(calls.length, 0) }) test('manager: 对端回 found:false ⇒ not_here(⛔ 不许折成"零插件")', async () => { const { s } = makeLeased({ found: false }) const r = await s.readUserBundles(UID) assert.equal(r.kind, 'not_here') assert.equal(r.bundles, undefined, 'not_here 不携带 bundles —— 没有真值可给') }) test('manager: 对端回 found:true + 空清单 ⇒ ok/[](= 确实没开任何插件)', async () => { const { s } = makeLeased({ found: true, payload: { found: true, bundles: [], asOf: 222 } }) const r = await s.readUserBundles(UID) assert.equal(r.kind, 'ok') assert.deepEqual(r.bundles, []) }) test('manager: 对端 5xx ⇒ unknown 且**不产出空清单**(⛔ 绝不显示成"未启用")', async () => { const { s } = makeLeased({ status: 500 }) const r = await s.readUserBundles(UID) assert.equal(r.kind, 'unknown') assert.match(r.reason, /agent_unreachable/) assert.equal(r.bundles, undefined) }) test('manager: host 不在目录里 ⇒ unknown(⛔ 不回落本机、不发请求、不猜)', async () => { const { s, calls } = makeLeased({ agentFor: () => undefined }) const r = await s.readUserBundles(UID) assert.equal(r.kind, 'unknown') assert.match(r.reason, /host_not_in_directory/) assert.equal(calls.length, 0) }) test('manager: 回包不成形(found 缺失)⇒ unknown(失败关闭,别猜成 not_here)', async () => { const { s } = makeLeased({ payload: {} }) const r = await s.readUserBundles(UID) assert.equal(r.kind, 'unknown') assert.match(r.reason, /malformed_reply/) }) test('manager: 归属查询本身失败 ⇒ unknown(⛔ 不许当成"本机"读出一份假空清单)', async () => { const { s, calls } = makeLeased({ lookupThrows: true }) const r = await s.readUserBundles(UID) assert.equal(r.kind, 'unknown') assert.match(r.reason, /instance_lookup_failed/) assert.equal(calls.length, 0) }) test('manager: 三态互斥 —— ok / not_here / unknown 不可被压成同一个值', async () => { const ok = (await makeLeased({ payload: { found: true, bundles: [], asOf: 1 } }).s.readUserBundles(UID)).kind const notHere = (await makeLeased({ found: false }).s.readUserBundles(UID)).kind const unknown = (await makeLeased({ status: 503 }).s.readUserBundles(UID)).kind assert.equal(new Set([ok, notHere, unknown]).size, 3) }) /* ─────────── ③ 目录新鲜度:未命中先补一次表(第 12 棒 · 47 真机缺口) ─────────── */ test('manager: host 不在目录里 ⇒ 先补一次目录,补上后走定向读(重启窗口期也能生效)', async () => { // 复刻 47 现场:`restart dshs` 后 `hostDirectory` 只含本机 ⇒ 第一次查 miss; // `ensureHost()` 打一次 `hostsProvider()` ⇒ 表变全 ⇒ 再判一次就命中。 let fresh = false const { s, calls, ensureCalls } = makeLeased({ agentFor: () => (fresh ? { agentUrl: 'http://10.0.0.2:19000', token: 'tok-106' } : undefined), ensureHost: async () => { fresh = true }, }) const r = await s.readUserBundles(UID) assert.deepEqual(ensureCalls, ['w-2'], '补齐必须打在**目标机**上,⛔ 不是随便补一台') assert.equal(r.kind, 'ok') assert.equal(r.hostId, 'w-2') assert.equal(calls.length, 1, '补上后必须真的发出那一次定向读') assert.equal(calls[0].url, 'http://10.0.0.2:19000/profile-bundles') assert.equal(calls[0].init.headers[AGENT_TOKEN_HEADER], 'tok-106') }) test('manager: 补齐后仍不在目录 ⇒ unknown(⛔ 补不出地址就照旧"不知道",不猜、不回落默认机)', async () => { const { s, calls, ensureCalls } = makeLeased({ agentFor: () => undefined, ensureHost: async () => undefined, }) const r = await s.readUserBundles(UID) assert.equal(ensureCalls.length, 1, '只补一次(冷却由补齐点自己管,⛔ 不在这里循环重试)') assert.equal(r.kind, 'unknown') assert.match(r.reason, /host_not_in_directory/) assert.equal(calls.length, 0, '⛔ 补不到位时绝不能拨默认机') }) test('manager: 补齐本身抛错 ⇒ unknown(不是 not_here、更不是空清单)', async () => { const { s, calls } = makeLeased({ agentFor: () => undefined, ensureHost: async () => { throw new Error('pg down') }, }) const r = await s.readUserBundles(UID) assert.equal(r.kind, 'unknown') assert.match(r.reason, /host_not_in_directory/) assert.equal(r.bundles, undefined) assert.equal(calls.length, 0) }) test('manager: 目录已命中 ⇒ **不补**(正常路径零额外开销,⛔ 别把 PG 打成热点)', async () => { const { s, ensureCalls } = makeLeased({ ensureHost: async () => { throw new Error('不该被调用') }, }) const r = await s.readUserBundles(UID) assert.equal(r.kind, 'ok') assert.equal(ensureCalls.length, 0) }) test('manager: 实例在本机 ⇒ **不补也不发**(本机路径零网络、零查库)', async () => { const { s, calls, ensureCalls } = makeLeased({ instanceHost: 'w-1', ensureHost: async () => { throw new Error('不该被调用') }, }) assert.equal(await s.readUserBundles(UID), null) assert.equal(ensureCalls.length, 0) assert.equal(calls.length, 0) }) test('manager: ⛔ 根本没接 agentFor ⇒ 不去补(补了也读不出地址,纯浪费一次查库)', async () => { const calls = [] const s = new LeasedSpawner( {}, { findUserInstance: async () => ({ userId: UID, hostId: 'w-2', epoch: 1 }) }, { hostId: 'w-1', agentUrl: 'http://127.0.0.1:19100', agentToken: 'tok-local', manual: true, fetchImpl: async (url, init) => { calls.push({ url, init }) return reply({ found: true, bundles: [] }) }, ensureHost: async () => { throw new Error('不该被调用') }, }, ) const r = await s.readUserBundles(UID) assert.equal(r.kind, 'unknown') assert.match(r.reason, /host_not_in_directory/) assert.equal(calls.length, 0) })