Files
dsh_shenxian/test/user-bundles-readback.test.mjs
T
admin 5ec99ce227
build / build-and-scan (push) Canceled after 0s
chore: 文档库全量重新入库(2026-10-20)
2026-10-20 15:05:55 +08:00

319 lines
13 KiB
JavaScript
Raw 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.
/**
* 跨机可见性状态回流(形态 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)
})