319 lines
13 KiB
JavaScript
319 lines
13 KiB
JavaScript
/**
|
||
* 跨机可见性状态回流(形态 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'
|
||
|
||
/** 造一棵真实目录树:`<dataRoot>/users/<uid>/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)
|
||
})
|