Files
dsh_shenxian/test/plugin-assembly.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

1048 lines
52 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.
/**
* 插件投放与分库线 ① · **S3 跨机装配**的回归测试。
*
* ## 这个文件防的是什么(每一件都对应一条会静默失效的判据)
*
* 1. **`RemoteSpawner.applyPlugins` 真的发到"实例所在那台"** —— 与 `restartAndProbe`
* 同一套 `hostIdFor` 路由。⛔ 若它回落到默认 host,就会把请求发给**另一台机**
* (那台上没有这个用户的 profile ⇒ 落盘落到一个空目录、或给错人装插件),
* 而症状只有"插件没生效",平台零日志。
* 2. **请求体形状与 agent 侧的入参校验对得上** —— `{userId, items:[{id,version,enabled,src}]}`。
* 两侧字段名漂一个字(如 `src` vs `source`)在 TS 里编译得过(一个 `Record<string,unknown>`),
* 但运行期是 400 ⇒ "用户点了启用永远失败"。
* 3. **`Spawner.applyPlugins` 是接口成员**:`LeasedSpawner` / `LocalSpawner` / `RemoteSpawner`
* 三处都得有 —— 漏一处 TS 会拦(这条由 `tsc` 保证),但**透传语义**(LeasedSpawner ⛔ 不碰
* 租约)是编译期看不出来的,故在此钉住。
* 4. **共享层路径换算两台机必须逐字一致**(`sharedDirNameOf`)—— manager 算出
* `dsh-plugin-mcn-suite` 而 worker 上铺的是别的名字 ⇒ `pnpm install` 报 ENOENT,
* 症状只是"某个用户装不上插件"。
* 5. **装配失败要"报错可读"**(S3-E ⑤):指向不存在的版本 ⇒ 抛带 `code` 的错,
* ⛔ 不静默跳过 —— 静默跳过会让任务显示 `success` 而插件根本没装。
*
* 运行:`node --test test/plugin-assembly.test.mjs`(已登记进 `npm test`)。
*
* @module test/plugin-assembly
*/
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, symlinkSync, lstatSync, readlinkSync, existsSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
applyProfileChanges,
depRefOf,
profileDirOf,
readProfileManifest,
sharedDirNameOf,
sharedDirOf,
syncWebProviderPatch,
wsDirOf,
WEB_PROVIDER_CLOSE,
WEB_PROVIDER_OPEN,
isWorkspaceRoot,
pnpmInstallArgs,
workspaceRootFlag,
pnpmExecError,
pnpmDiagArgs,
// 共享层自足性(第 18 棒)+ 回滚残影清理
ensureSharedScopeLink,
healSharedLayerSelfSufficiency,
pruneOrphanDepLinks,
restoreProfile,
snapshotProfile,
rebuildMissingDepLinks,
linkRefsOf,
resolveDshPackageRoot,
resetDshPackageRootCache,
// 越权软链的临时摘除(第 20 棒)
quarantineSharedLayerLinks,
restoreQuarantinedLinks,
} from '../lib/supervisor/plugin-assembly.js'
import { RemoteSpawner } from '../lib/supervisor/remote-spawner.js'
/* ─────────── 小工具 ─────────── */
/** 记下每次请求(url / method / body),回同一个形状的 JSON。 */
function recordingFetch(payload) {
const calls = []
const impl = async (url, init) => {
calls.push({ url: `${init?.method ?? 'GET'} ${url}`, body: init?.body === undefined ? undefined : JSON.parse(init.body) })
return { ok: true, status: 200, json: async () => payload, text: async () => JSON.stringify(payload) }
}
impl.calls = calls
return impl
}
/** 106 那台 worker(`via` 缺省 ⇒ 直连,不做 relay 翻译)。 */
const W106 = {
hostId: 'w-2',
agentUrl: 'http://127.0.0.1:19000',
token: 'tok-106',
}
/* ─────────── ① 跨机路由 ─────────── */
test('S3: applyPlugins 落到"该用户实例所在那台",⛔ 不回落到默认 host', async () => {
const fetchImpl = recordingFetch({ enabled: ['@dsh-local/trpg-kit'], disabled: [], bundles: ['@dsh-local/trpg-kit'] })
const sp = new RemoteSpawner({
agentUrl: 'http://127.0.0.1:19100', // 默认 host(w-1)
token: 'tok-local',
defaultHostId: 'w-1',
hostIdFor: async () => 'w-2', // 该用户的实例在 106
hostsProvider: async () => [W106],
fetchImpl,
})
const out = await sp.applyPlugins('u-1', [{ id: '@dsh-local/trpg-kit', enabled: true }])
assert.equal(fetchImpl.calls.length, 1)
// 🔴 打到 w-2(19000)而不是默认 host(19100)—— 这就是"装在 A、实例在 B"的判据。
assert.match(fetchImpl.calls[0].url, /^POST http:\/\/127\.0\.0\.1:19000\/plugins\/apply$/)
assert.deepEqual(out.bundles, ['@dsh-local/trpg-kit'])
})
test('S3: applyPlugins 请求体形状 = agent 侧约定(字段名漂一个就是 400)', async () => {
const fetchImpl = recordingFetch({ enabled: [], disabled: [], bundles: [] })
const sp = new RemoteSpawner({
agentUrl: 'http://127.0.0.1:19100',
token: 'tok',
fetchImpl,
})
await sp.applyPlugins('u-1', [
{ id: '@dsh-local/trpg-kit', version: '1.2.3', enabled: true },
{ id: 'mcn-suite', version: null, enabled: false, src: 'file' },
])
// ⚠️ v17 起 `essential` 是**协议的一部分**(基础插件"关不掉"的拒绝必须落在落盘那端
// ⇒ 这个标记必须过网)。本断言钉的就是"两边字段名与形状逐字一致"。
assert.deepEqual(fetchImpl.calls[0].body, {
userId: 'u-1',
items: [
{ id: '@dsh-local/trpg-kit', version: '1.2.3', enabled: true, src: 'bundled', essential: false },
{ id: 'mcn-suite', version: null, enabled: false, src: 'file', essential: false },
],
})
})
test('S3: applyPlugins 在 hostIdFor 缺失(单机形态)时走默认 host', async () => {
const fetchImpl = recordingFetch({ enabled: [], disabled: [], bundles: [] })
const sp = new RemoteSpawner({ agentUrl: 'http://127.0.0.1:19100', token: 'tok', fetchImpl })
await sp.applyPlugins('u-1', [])
assert.match(fetchImpl.calls[0].url, /127\.0\.0\.1:19100\/plugins\/apply/)
})
test('S3: agent 回 4xx ⇒ 原样带上状态码与响应体抛错(⛔ 不被重试吞掉)', async () => {
let calls = 0
const impl = async () => {
calls += 1
return {
ok: false,
status: 409,
json: async () => ({}),
text: async () => '{"error":"plugin_bundle_missing"}',
}
}
const sp = new RemoteSpawner({ agentUrl: 'http://127.0.0.1:19100', token: 'tok', fetchImpl: impl })
await assert.rejects(
() => sp.applyPlugins('u-1', [{ id: 'x', enabled: true }]),
(err) => {
assert.match(String(err.message), /409/)
assert.match(String(err.message), /plugin_bundle_missing/)
return true
},
)
// 4xx = 协议/参数错 ⇒ 重试没意义(与 call() 的既有纪律一致)
assert.equal(calls, 1)
})
/* ─────────── ② 路径换算:两台机必须逐字一致 ─────────── */
test('S3: 共享层目录名 = 扁平化包名(scope 的 `/` 变 `_`,⛔ 不带版本号)', () => {
assert.equal(sharedDirNameOf('@dsh-local/trpg-kit'), '_dsh-local_trpg-kit')
assert.equal(sharedDirNameOf('dsh-plugin-mcn-suite'), 'dsh-plugin-mcn-suite')
assert.equal(sharedDirNameOf('[email protected]'), 'univer-office_1.2.3')
// ⛔ 版本号不进路径(B 档:同名只留最新一版,写进路径会让老 profile 悬空)
assert.equal(sharedDirNameOf('dsh-plugin-mcn-suite'), sharedDirNameOf('dsh-plugin-mcn-suite'))
})
test('S3: sharedDirOf / profileDirOf / wsDirOf 的路径数学', () => {
// ⚠️ 用 `join` 拼期望值:本机是 Windows(`\`),而 47/106 是 Linux(`/`)——
// 判据要的是**路径数学**(拼哪个子目录),不是分隔符字面。
assert.equal(
sharedDirOf('/var/lib/dshs/bundled-plugins', '@dsh-local/trpg-kit'),
join('/var/lib/dshs/bundled-plugins', '_dsh-local_trpg-kit'),
)
assert.equal(profileDirOf('/var/lib/dshs/users/u-1'), join('/var/lib/dshs/users/u-1', 'home', 'profiles', 'web'))
assert.equal(wsDirOf('/var/lib/dshs/users/u-1'), join('/var/lib/dshs/users/u-1', 'ws'))
})
/* ─────────── ③ profile 读写与 web provider 段 ─────────── */
test('S3: readProfileManifest 读不出 ⇒ 空清单(⛔ 不抛)', () => {
const dir = mkdtempSync(join(tmpdir(), 'pa-'))
try {
assert.deepEqual(readProfileManifest(dir), { bundles: [], deps: {} })
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
test('S3: syncWebProviderPatch 幂等(跑两次结果逐字相同)', () => {
const dir = mkdtempSync(join(tmpdir(), 'pa-'))
try {
writeFileSync(join(dir, 'package.json'), JSON.stringify({
name: 'dsh-profile-web',
private: true,
dependencies: { 'anysearch': 'file:/tmp/anysearch' },
dsh: { profile: { bundles: ['anysearch'] } },
}, null, 2))
// 插件自带 `cordis.patch.yml` 声明 searchProvider(`collectWebProviderClaims` 读它)
mkdirSync(join(dir, 'node_modules', 'anysearch'), { recursive: true })
writeFileSync(join(dir, 'node_modules', 'anysearch', 'cordis.patch.yml'), [
'- id: web',
' config:',
' searchProvider: anysearch',
'',
].join('\n'))
syncWebProviderPatch(dir)
const first = readFileSync(join(dir, 'cordis.patch.yml'), 'utf8')
syncWebProviderPatch(dir)
const second = readFileSync(join(dir, 'cordis.patch.yml'), 'utf8')
assert.equal(first, second)
// 托管段两侧标记必须都在(验证脚本按字面找它们 —— ⛔ 改名会让 verify-*.mjs 静默不判)
assert.ok(first.includes(WEB_PROVIDER_OPEN))
assert.ok(first.includes(WEB_PROVIDER_CLOSE))
assert.match(first, /searchProvider: anysearch/)
assert.match(first, /fetchProvider: http/)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
test('S3: bundles 为空 ⇒ 托管段被整段摘掉(回到"自动选")', () => {
const dir = mkdtempSync(join(tmpdir(), 'pa-'))
try {
writeFileSync(join(dir, 'package.json'), JSON.stringify({
name: 'dsh-profile-web',
private: true,
dependencies: {},
dsh: { profile: { bundles: [] } },
}, null, 2))
writeFileSync(join(dir, 'cordis.patch.yml'), [
'other: true',
'',
WEB_PROVIDER_OPEN,
'- id: web',
' config:',
' searchProvider: anysearch',
WEB_PROVIDER_CLOSE,
'',
].join('\n'))
syncWebProviderPatch(dir)
const text = readFileSync(join(dir, 'cordis.patch.yml'), 'utf8')
assert.ok(!text.includes(WEB_PROVIDER_OPEN))
assert.ok(!text.includes('searchProvider'))
assert.match(text, /other: true/)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
/* ─────────── ④ 装配失败必须"报错可读"(S3-E ⑤) ─────────── */
test('S3 反证: 共享层里没有该包 ⇒ applyPlugins 抛错(⛔ 不静默跳过)', async () => {
// 用 RemoteSpawner 模拟:agent 回 409 plugin_bundle_missing
const impl = async () => ({
ok: false,
status: 409,
json: async () => ({}),
text: async () => '{"error":"plugin_bundle_missing","message":"共享层里没有该插件的包:@dsh-local/ghost"}',
})
const sp = new RemoteSpawner({ agentUrl: 'http://127.0.0.1:19100', token: 'tok', fetchImpl: impl })
await assert.rejects(
() => sp.applyPlugins('u-1', [{ id: '@dsh-local/ghost', enabled: true }]),
/plugin_bundle_missing/,
)
})
/* ─────────── ⑤ 软链语义(D2:一份实体、多份链接) ─────────── */
test('S3: D2 语义 —— node_modules/<pkg> 是指向共享层的软链,⛔ 不是实体副本', () => {
const root = mkdtempSync(join(tmpdir(), 'pa-'))
try {
const shared = join(root, 'bundled-plugins', '_dsh-local_trpg-kit')
mkdirSync(shared, { recursive: true })
writeFileSync(join(shared, 'package.json'), '{"name":"@dsh-local/trpg-kit"}')
const profile = join(root, 'users', 'u-1', 'home', 'profiles', 'web')
mkdirSync(join(profile, 'node_modules'), { recursive: true })
symlinkSync(shared, join(profile, 'node_modules', '@dsh-local_trpg-kit'), 'dir')
const link = join(profile, 'node_modules', '@dsh-local_trpg-kit')
assert.ok(lstatSync(link).isSymbolicLink(), '必须是软链(实体只有共享层那一份)')
assert.equal(readlinkSync(link), shared, '必须指向共享层的那一份实体')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
/* ─────────── ⑥ pnpm 参数契约(防"裸写无效 flag ⇒ 静默装不上")─────────── */
test('S3: pnpm 参数 ⛔ 不含裸写的 ignore-workspace-root-check(pnpm 9 会 Unknown option 直接失败)', () => {
// 真机实测(106 · pnpm 9.15.9):`pnpm install --ignore-workspace-root-check` 报
// ERROR Unknown option: 'ignore-workspace-root-check'
// 且**退出即失败** ⇒ 装配整条链断在 pnpm 这一步,而症状只是"插件没装上"。
const dir = mkdtempSync(join(tmpdir(), 'pa-args-'))
try {
// 有 workspace 文件 ⇒ 就是 workspace 根:`-w` 必须带(放行 workspace root 只能靠它)
writeFileSync(join(dir, 'pnpm-workspace.yaml'), 'packages:\n - .\n')
const withWs = pnpmInstallArgs(dir)
assert.ok(
!withWs.includes('--ignore-workspace-root-check'),
'⛔ 不得出现裸写的 --ignore-workspace-root-check(pnpm 9 不认)',
)
assert.ok(withWs.includes('-w'), 'profile 是 workspace 根 ⇒ 必须带 -w 显式放行')
assert.equal(withWs[0], 'install', '子命令必须是 install')
// ⛔ 命令行不重复传 auto-install-peers:dsh 插件的 @deepseek-ai/dsh-client-* 是 optional peer,
// 公网 registry 没有 ⇒ 自动装 peer 必失败;该开关由 profile 内 workspace 文件承载。
assert.ok(
!withWs.some((a) => String(a).startsWith('--config.auto-install-peers')),
'⛔ 别在命令行传 auto-install-peers(由 profile 的 workspace 文件承载)',
)
// 🔴 **没有** workspace 文件 ⇒ ⛔ 绝不能带 `-w`:pnpm 9 会报
// `ERROR --workspace-root may only be used inside a workspace` 并立即退出。
// 这正是 47 上 admin 实例的真实形态(平台自己铺的 profile 没有 pnpm-workspace.yaml)
// ⇒ 旧写法(常量里写死 `-w`)让门户「启停插件」整条链断在 pnpm 这一步。
rmSync(join(dir, 'pnpm-workspace.yaml'))
assert.equal(isWorkspaceRoot(dir), false, '没有 pnpm-workspace.yaml ⇒ 不是 workspace 根')
assert.deepEqual(workspaceRootFlag(dir), [], '⛔ 非 workspace 根不得带 -w')
assert.ok(
!pnpmInstallArgs(dir).includes('-w'),
'⛔ 非 workspace 根的 profile 不得带 -w(pnpm 9 直接报错退出)',
)
assert.equal(pnpmInstallArgs(dir)[0], 'install', '子命令必须是 install')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
/* ─────────── ⑪ 静默 reporter 的失败取证(第 20 棒)─────────── */
test('S3: 失败取证 —— `--reporter silent` 会被换成 `default` 重跑(否则原因拿不到)', () => {
// 第 19 棒真机:12 s 内连续 4 次 `exit 255`,而 `--reporter silent` 把原因**全吞掉**
// (`stderr=- stdout=-`)⇒ 错误对象里只剩命令与退出码。本组把"诊断重跑"的参数形态钉住。
const src = ['install', '-w', '--reporter', 'silent']
assert.deepEqual(
pnpmDiagArgs(src),
['install', '-w', '--reporter', 'default'],
'silent ⇒ default,其余参数逐字保留、顺序不变',
)
assert.deepEqual(src, ['install', '-w', '--reporter', 'silent'], '⛔ 不许就地改写调用方数组(失败报错仍用原参数)')
// 原本没写 reporter ⇒ 补一个:不同 pnpm 版本的默认 reporter 不同,不补则证据不可比。
assert.deepEqual(pnpmDiagArgs(['remove', 'pkg']), ['remove', 'pkg', '--reporter', 'default'])
// 已写别的 reporter ⇒ 同样换成 default(诊断要的是"最多信息",不是"沿用原档位")
assert.deepEqual(pnpmDiagArgs(['install', '--reporter', 'append-only']), ['install', '--reporter', 'default'])
})
test('S3: 失败取证 —— 诊断读数进 message(跨机回传后只剩 message 可读)', () => {
const err = pnpmExecError(
{ status: 255, stderr: '', stdout: '' },
['install', '--reporter', 'silent'],
'/var/lib/dshs/users/x/home/profiles/web',
114801,
'诊断重跑(--reporter default)exit=255 stderr=ERR_PNPM_UNEXPECTED_STORE stdout=-',
)
assert.ok(err.message.includes('ERR_PNPM_UNEXPECTED_STORE'), '诊断重跑拿到的真原因必须进 message')
assert.ok(err.message.includes('exit=255'), '退出码必须在 message 里')
assert.ok(String(err.pnpmDiag).includes('重跑'), '结构化字段 pnpmDiag 同步挂上(同机可直接取)')
// ⛔ 不改既有语义:不传 diag ⇒ 与旧版逐字相同(旧断言仍绿,这里钉下界)。
const plain = pnpmExecError({ status: 1, stderr: 'boom' }, ['install'], '/x', 7)
assert.equal(plain.pnpmDiag, '')
assert.ok(!plain.message.includes('诊断重跑'), '无诊断时 ⛔ 不许凭空多一句话')
})
/* ─────────── ⑫ 越权软链的临时摘除(2026-09-25 · 第 20 棒 · 真机 exit 255 的真根因) ─────────── */
//
// 真机原文(47 · admin profile,复刻平台调用、只换 reporter):
// `EPERM EPERM: operation not permitted, chmod '…/profiles/web/node_modules/xlsx/bin/xlsx.njs'`
// `at async linkBin (pnpm.cjs) ← installInContext`
// 机制 = 顶层那条**未声明**的软链解到共享层,而共享层文件属主是 **root** ⇒ 用户侧 pnpm
// 在 `linkBin` 里对它无脑 `chmod` ⇒ 非属主 EPERM(是 EPERM 不是 EACCES ⇒ 与 mode 无关)。
// 本组钉住"三条摘除判据 + finally 放回 + ⛔ 不覆盖"。
test('越权软链:只摘「顶层 ∧ 未声明 ∧ 落在共享层」的软链,其余一律不碰', (t) => {
const root = mkdtempSync(join(tmpdir(), 'pa-quar-'))
try {
const shared = join(root, 'bundled-plugins')
const sharedPkg = join(shared, 'dsh-plugin-mcn-suite')
mkdirSync(join(sharedPkg, 'node_modules', 'xlsx', 'bin'), { recursive: true })
writeFileSync(join(sharedPkg, 'node_modules', 'xlsx', 'bin', 'xlsx.njs'), '#!/usr/bin/env node\n')
const profile = join(root, 'home', 'profiles', 'web')
const nm = join(profile, 'node_modules')
mkdirSync(nm, { recursive: true })
// 声明集合(与平台同一份口径:`dependencies` ∪ `dsh.profile.bundles`)
const declared = new Set(['dsh-plugin-mcn-suite'])
writeFileSync(
join(profile, 'package.json'),
JSON.stringify({
dependencies: { 'dsh-plugin-mcn-suite': `link:${sharedPkg}` },
dsh: { profile: { bundles: ['dsh-plugin-mcn-suite'] } },
}),
)
if (!tryLink(sharedPkg, join(nm, 'dsh-plugin-mcn-suite'))) {
t.skip('symlink creation unavailable on this host')
return
}
// 真凶:**未声明**、指向共享层,且中间隔一层 fallback(与真机同形)
const fb = join(profile, '.dsh-module-fallback', 'node_modules')
mkdirSync(fb, { recursive: true })
tryLink(join(sharedPkg, 'node_modules', 'xlsx'), join(fb, 'xlsx'))
tryLink(join(fb, 'xlsx'), join(nm, 'xlsx'))
// 干扰项:真目录 / 指向 profile 内(不落共享层)的软链 / 悬空软链
mkdirSync(join(nm, 'realpkg'), { recursive: true })
mkdirSync(join(profile, 'local-target'), { recursive: true })
tryLink(join(profile, 'local-target'), join(nm, 'locallink'))
tryLink(join(root, 'nope'), join(nm, 'dangling'))
const q = quarantineSharedLayerLinks(profile, shared, declared)
assert.deepEqual(q.map((x) => x.name), ['xlsx'], '只摘真凶一条(声明内 / 真目录 / 非共享层 / 悬空链都不摘)')
assert.ok(!existsSync(join(nm, 'xlsx')), '摘除后原路径必须不在(pnpm 才看不见它)')
assert.ok(existsSync(join(nm, '.dsh-quarantine-xlsx')), '摘除 = 改名(⛔ 不是删除)')
assert.ok(lstatSync(join(nm, 'dsh-plugin-mcn-suite')).isSymbolicLink(), '⛔ 声明内的 link: 一律不碰')
assert.ok(lstatSync(join(nm, 'realpkg')).isDirectory(), '⛔ 真目录一律不碰')
assert.ok(existsSync(join(nm, 'locallink')), '⛔ 不落共享层的软链不碰')
assert.ok(lstatSync(join(nm, 'dangling')).isSymbolicLink(), '⛔ 悬空链不碰(解析不了 ⇒ 不猜)')
// 放回:目标逐字还原、隐藏项消失、重复调用幂等
const kept = readlinkSync(join(nm, '.dsh-quarantine-xlsx'))
assert.deepEqual(restoreQuarantinedLinks(q), [], '放回必须全部成功')
assert.ok(!existsSync(join(nm, '.dsh-quarantine-xlsx')), '放回后 ⛔ 不许留下隐藏项')
assert.equal(readlinkSync(join(nm, 'xlsx')), kept, '放回后的链接目标逐字不变')
// 可重复:判据没变 ⇒ 再摘仍只摘这一条;再放回仍逐字还原(摘/放是一对,不累积副作用)
const q2 = quarantineSharedLayerLinks(profile, shared, declared)
assert.deepEqual(q2.map((x) => x.name), ['xlsx'], '再摘同一份清单(不重复摘、不重复计数)')
assert.deepEqual(restoreQuarantinedLinks(q2), [], '第二轮放回同样全部成功')
assert.equal(readlinkSync(join(nm, 'xlsx')), kept, '两轮之后链接仍逐字不变')
assert.ok(!existsSync(join(nm, '.dsh-quarantine-xlsx')), '⛔ 两轮之后不许留隐藏项')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('越权软链:放回时原名被占 ⇒ 跳过并记名(⛔ 不覆盖 pnpm 刚装的东西,⛔ 不丢隐藏项)', (t) => {
const root = mkdtempSync(join(tmpdir(), 'pa-quar2-'))
try {
const nm = join(root, 'node_modules')
mkdirSync(nm, { recursive: true })
mkdirSync(join(root, 'local-target'), { recursive: true })
if (!tryLink(join(root, 'local-target'), join(nm, '.dsh-quarantine-xlsx'))) {
t.skip('symlink creation unavailable on this host')
return
}
// pnpm 跑完后原名位置上出现了别的东西(真目录)⇒ 不许被覆盖
mkdirSync(join(nm, 'xlsx'), { recursive: true })
const failed = restoreQuarantinedLinks([{ name: 'xlsx', linkPath: join(nm, 'xlsx'), hidden: join(nm, '.dsh-quarantine-xlsx') }])
assert.deepEqual(failed, ['xlsx'], '放不回的要**具名**返回(调用方据此落日志)')
assert.ok(lstatSync(join(nm, 'xlsx')).isDirectory(), '占位项一个字不动')
assert.ok(existsSync(join(nm, '.dsh-quarantine-xlsx')), '⛔ 隐藏项不许被丢掉(丢掉 = 用户盘上少一个包)')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('越权软链:applyProfileChanges 跑完 pnpm ⇒ 越权软链仍在盘上(finally 放回,⛔ 不留隐藏态)', (t) => {
process.env.DSHS_TEST_NO_PNPM = '1'
const root = mkdtempSync(join(tmpdir(), 'pa-quar3-'))
try {
const userRoot = root
const profile = join(userRoot, 'home', 'profiles', 'web')
const nm = join(profile, 'node_modules')
mkdirSync(nm, { recursive: true })
mkdirSync(join(userRoot, 'ws'), { recursive: true })
const sharedRoot = join(root, 'bundled-plugins')
const declDir = join(sharedRoot, '_dsh-local_decl')
const mcnDir = join(sharedRoot, 'dsh-plugin-mcn-suite')
mkdirSync(declDir, { recursive: true })
mkdirSync(join(mcnDir, 'node_modules', 'xlsx'), { recursive: true })
writeFileSync(join(declDir, 'package.json'), '{"name":"@dsh-local/decl"}')
writeFileSync(join(mcnDir, 'package.json'), '{"name":"dsh-plugin-mcn-suite"}')
const canary = join(root, 'canary')
if (!tryLink(sharedRoot, canary)) {
t.skip('symlink creation unavailable on this host')
return
}
rmSync(canary, { force: true })
writeFileSync(
join(profile, 'package.json'),
JSON.stringify({ dependencies: { 'dsh-plugin-mcn-suite': `link:${mcnDir}` }, dsh: { profile: { bundles: [] } } }),
)
tryLink(mcnDir, join(nm, 'dsh-plugin-mcn-suite'))
// 未声明的越权软链(真凶)
tryLink(join(mcnDir, 'node_modules', 'xlsx'), join(nm, 'xlsx'))
const r = applyProfileChanges([{ id: '@dsh-local/decl', enabled: true, src: 'bundled' }], {
userRoot,
bundledPluginDir: sharedRoot,
resolveBundleDir: (id) => join(sharedRoot, sharedDirNameOf(id)),
})
assert.deepEqual(r.enabled, ['@dsh-local/decl'], '启用项要真的落盘(否则本用例没走到 pnpm 那一步)')
assert.ok(lstatSync(join(nm, 'xlsx')).isSymbolicLink(), '⛔ apply 之后必须把越权软链放回(不许把用户盘留在隐藏态)')
assert.ok(!existsSync(join(nm, '.dsh-quarantine-xlsx')), '⛔ 不许留隐藏项')
} finally {
delete process.env.DSHS_TEST_NO_PNPM
rmSync(root, { recursive: true, force: true })
}
})
test('S3: pnpm 子进程失败 ⇒ 错误对象带 stdout/stderr 证据(⛔ 不许只剩"安装失败"四个字)', () => {
// 第 16 棒真机教训:`applyPlugins` 失败时原因**一个字都不落日志**,两次复现都只能看到
// 用户侧的"安装失败,请重试或联系管理员" ⇒ 根因无从定位。本断言把"带证据"钉成契约。
const err = pnpmExecError(
{ status: 1, stderr: Buffer.from('ERROR --workspace-root may only be used inside a workspace\n'), stdout: Buffer.from('') },
['remove', 'dsh-plugin-mcn-suite', '-w', '--reporter', 'silent'],
'/tmp/somewhere',
114801,
)
assert.equal(err.code, 'pnpm_failed')
assert.ok(err.message.includes('--workspace-root may only be used inside a workspace'), '原因必须进 message(跨机后只剩 message 可读)')
assert.ok(err.message.includes('remove dsh-plugin-mcn-suite'), '命令必须进 message')
assert.ok(String(err.pnpmStderr).includes('may only be used inside a workspace'), 'stderr 必须挂成结构化字段')
assert.equal(err.pnpmCwd, '/tmp/somewhere')
// 超时要单独可辨(上层据此判 timeout 而不是 failed)
const to = pnpmExecError({ code: 'ETIMEDOUT', killed: true, stderr: '' }, ['install'], '/tmp/x', 1)
assert.equal(to.code, 'pnpm_timeout')
})
/* ─────────── ⑦ 原子性:缺失包 ⇒ 抛错且 profile 原样不动 ─────────── */
test('S3: 有包不在共享层 ⇒ applyProfileChanges 抛 plugin_bundle_missing 且 package.json 未被改动', () => {
// 这条防的是**半装状态**:若先写依赖引用再跑 pnpm,失败时 package.json 里会留下
// 悬挂引用而实际未安装 ⇒ 下次实例冷启动直接崩,用户却看到"装过了"。
// 判据:抛错 + 磁盘上的 package.json 与调用前**逐字节相同**。
const root = mkdtempSync(join(tmpdir(), 'pa-atomic-'))
try {
const userRoot = root
mkdirSync(join(userRoot, 'home', 'profiles', 'web'), { recursive: true })
mkdirSync(join(userRoot, 'ws'), { recursive: true })
const profile = join(userRoot, 'home', 'profiles', 'web')
const before = {
name: 'dsh-profile-web',
private: true,
dependencies: { 'keep-me': 'file:/somewhere/keep-me' },
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base'], patchReload: 'live' } },
}
const beforeText = JSON.stringify(before, null, 2) + '\n'
writeFileSync(join(profile, 'package.json'), beforeText)
const sharedRoot = join(root, 'shared')
const present = join(sharedRoot, 'present-plugin')
mkdirSync(present, { recursive: true })
writeFileSync(join(present, 'package.json'), '{"name":"present-plugin","version":"1.0.0"}')
assert.throws(
() => applyProfileChanges(
[
{ id: 'present-plugin', enabled: true, src: 'bundled' },
{ id: 'ghost-plugin', enabled: true, src: 'bundled' },
],
{
userRoot,
bundledPluginDir: sharedRoot,
resolveBundleDir: (id) => {
const d = join(sharedRoot, id)
return existsSync(join(d, 'package.json')) ? d : undefined
},
},
),
(e) => e && e.code === 'plugin_bundle_missing',
'缺失包必须抛带 code=plugin_bundle_missing 的错(⛔ 不静默跳过)',
)
// 🔴 核心断言:一个字节都不能变 —— 连"好的那个包"也不许被写进去
assert.equal(
readFileSync(join(profile, 'package.json'), 'utf8'),
beforeText,
'⛔ 有任一包缺失时 package.json 必须原样不动(否则留下半装状态)',
)
} finally {
rmSync(root, { recursive: true, force: true })
}
})
/* ─────────── ⑧ 装配协议 = `link:`(2026-09-23 拍板,推翻原 D-h 的 `file:`)─────────── */
test('S3: 装配协议必须是 link: 且指向共享层 —— ⛔ 不得回到 file:(pnpm 9 会整份复制 6.5 MiB/用户/包)', () => {
// 真机实测(第 6 棒 · 106 · pnpm 9.15.9):
// `file:`(目录型)⇒ 插件包被**整份复制**进该用户 `.pnpm/`(6.5 MiB、116 文件、
// `links=1`、inode 与源不同)⇒ 不满足 D2「⛔ 不给每个用户复制一份」,用户数一涨磁盘线性膨胀。
// `link:` ⇒ 纯软链直达共享层(≈ 4.0 KiB),且**抗 reconcile**(加依赖 / `remove` 后软链仍在,
// 即 D-h 当初选 `file:` 的唯一理由依然成立)。
const target = '/var/lib/dshs/bundled-plugins/_dsh-local_storyforge'
assert.equal(
depRefOf(target),
`link:${target}`,
'⛔ 协议必须是 link: —— 回到 file: 会让每个用户各复制一份实体(D2 不成立)',
)
// 写进 profile 的**真实**依赖值也必须带 link: 前缀(防"只在工具函数里改了、调用点漏改")
//
// ⚠️ `DSHS_TEST_NO_PNPM=1` = 允许跳过 `setpriv`(Linux 专有;本机 Windows 上必 ENOENT)。
// 但**写 package.json 的那段照常跑** ⇒ 断言测的是真产物,不是桩。
// ⛔ 在 Linux 真机上该开关**不生效**(`pnpmExecDisabled` 会探测到 setpriv 存在)⇒ 生产不可能被绕过。
process.env.DSHS_TEST_NO_PNPM = '1'
const root = mkdtempSync(join(tmpdir(), 'pa-ref-'))
try {
const userRoot = root
const profile = join(userRoot, 'home', 'profiles', 'web')
mkdirSync(profile, { recursive: true })
mkdirSync(join(userRoot, 'ws'), { recursive: true })
writeFileSync(join(profile, 'package.json'), '{"name":"p","private":true}\n')
const sharedRoot = join(root, 'bundled-plugins')
const pkgDir = join(sharedRoot, '_dsh-local_probe')
mkdirSync(pkgDir, { recursive: true })
writeFileSync(join(pkgDir, 'package.json'), '{"name":"@dsh-local/probe"}')
applyProfileChanges([{ id: '@dsh-local/probe', enabled: true, src: 'bundled' }], {
userRoot,
bundledPluginDir: sharedRoot,
resolveBundleDir: (id) =>
existsSync(join(sharedRoot, sharedDirNameOf(id), 'package.json'))
? join(sharedRoot, sharedDirNameOf(id))
: undefined,
})
const written = JSON.parse(readFileSync(join(profile, 'package.json'), 'utf8'))
assert.equal(
written.dependencies['@dsh-local/probe'],
`link:${pkgDir}`,
'profile 里写死的依赖值必须是 link: 前缀',
)
assert.ok(
!String(written.dependencies['@dsh-local/probe']).startsWith('file:'),
'⛔ 不得写成 file:(会触发 pnpm 整份复制)',
)
} finally {
rmSync(root, { recursive: true, force: true })
delete process.env.DSHS_TEST_NO_PNPM
}
})
/* ─────────── ⑦ 共享层自足性:补 dsh 宿主包软链(2026-09-25 · 第 18 棒) ─────────── */
//
// 判据来源:第 16/17 棒真机实证 —— 共享层里零个 `@deepseek-ai` ⇒ 实例加载插件报
// `ERR_MODULE_NOT_FOUND: @deepseek-ai/dsh-llm` ⇒ crash-restart ⇒ 插件被自动禁用。
// 本组用例钉住三件会**静默退化**的事:
// ① 缺链时**真的补上**,且指向**本机** scope 目录(⛔ 不是 Manager 算好发下来的绝对路径
// —— 47 在 `/usr/local/lib`,106 在 `/usr/lib`,跨机就是悬空链);
// ② 幂等(对准了不动)+ 悬空/指错**要改回来** + **真目录⛔ 不碰**(删了不可逆);
// ③ ⛔ 不能把软链放到共享层根(那会让节点对账面永远多出 `extra`、判据"对账收敛"失效)
// —— 故所有用例都断言链接落在 `<flat>/node_modules/` 之下。
/** 建软链;本机不允许(无特权 Windows)⇒ 返回 false,用例自行 skip。 */
function tryLink(target, path) {
try {
symlinkSync(target, path, 'dir')
return true
} catch {
return false
}
}
test('共享层自足性:缺链 ⇒ 补上 <flat>/node_modules/@deepseek-ai(指向本机 scope 目录)', (t) => {
const root = mkdtempSync(join(tmpdir(), 'pa-scope-'))
try {
const flat = join(root, 'bundled-plugins', 'dsh-plugin-mcn-suite')
mkdirSync(join(flat, 'lib'), { recursive: true })
writeFileSync(join(flat, 'package.json'), '{"name":"dsh-plugin-mcn-suite"}')
const scope = join(root, 'dsh-root', 'node_modules', '@deepseek-ai')
mkdirSync(scope, { recursive: true })
// 先探一次能不能建链(Windows 无特权会 EPERM)
const canary = join(root, 'canary')
if (!tryLink(scope, canary)) {
t.skip('symlink creation unavailable on this host')
return
}
rmSync(canary, { force: true })
const r = ensureSharedScopeLink(flat, scope)
assert.equal(r.action, 'linked')
const link = join(flat, 'node_modules', '@deepseek-ai')
assert.ok(lstatSync(link).isSymbolicLink(), '必须落到 <flat>/node_modules/@deepseek-ai')
assert.equal(readlinkSync(link), scope, '目标必须是本机 scope 目录(绝对路径)')
assert.ok(existsSync(link), '链必须能解开(悬空链等于没补)')
// 幂等:再跑一次 ⇒ kept,且一个字节都没动
const before = lstatSync(link).mtimeMs
const again = ensureSharedScopeLink(flat, scope)
assert.equal(again.action, 'kept')
assert.equal(lstatSync(link).mtimeMs, before, 'kept ⇒ ⛔ 不许重建')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('共享层自足性:悬空/指错 ⇒ 换成本机的;真目录占位 ⇒ ⛔ 不碰', (t) => {
const root = mkdtempSync(join(tmpdir(), 'pa-scope2-'))
try {
const flat = join(root, 'bundled-plugins', 'flat-a')
mkdirSync(join(flat, 'node_modules'), { recursive: true })
const scope = join(root, 'dsh-root', 'node_modules', '@deepseek-ai')
mkdirSync(scope, { recursive: true })
const canary = join(root, 'canary')
if (!tryLink(scope, canary)) {
t.skip('symlink creation unavailable on this host')
return
}
rmSync(canary, { force: true })
// ① 悬空链(模拟"Manager 生成的绝对目标在本机不存在")
const link = join(flat, 'node_modules', '@deepseek-ai')
symlinkSync(join(root, 'nowhere', '@deepseek-ai'), link, 'dir')
assert.ok(!existsSync(link), '前置:这条链确实是悬空的')
assert.equal(ensureSharedScopeLink(flat, scope).action, 'relinked')
assert.equal(readlinkSync(link), scope)
assert.ok(existsSync(link), '换过之后必须能解开')
// ② 真目录占位(包自带 @deepseek-ai)⇒ 不碰
rmSync(link, { force: true })
mkdirSync(link, { recursive: true })
writeFileSync(join(link, 'keep.txt'), 'x')
assert.equal(ensureSharedScopeLink(flat, scope).action, 'skipped_real_dir')
assert.ok(existsSync(join(link, 'keep.txt')), '⛔ 真目录一个字节都不许动')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('共享层自足性:解析不到本机 scope ⇒ no_scope,一个文件都不碰', (t) => {
const root = mkdtempSync(join(tmpdir(), 'pa-scope3-'))
try {
const flat = join(root, 'bundled-plugins', 'flat-a')
mkdirSync(flat, { recursive: true })
const r = ensureSharedScopeLink(flat, null)
assert.equal(r.action, 'no_scope')
assert.equal(r.target, null)
assert.ok(!existsSync(join(flat, 'node_modules')), '⛔ 不许留下空 node_modules')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('共享层自足性:全量补(含早先铺好的包),跳过 . 开头的旁挂物', (t) => {
const root = mkdtempSync(join(tmpdir(), 'pa-heal-'))
const saved = { DSH_PACKAGE_DIR: process.env.DSH_PACKAGE_DIR, DSHS_DSH_BIN: process.env.DSHS_DSH_BIN }
try {
const sharedRoot = join(root, 'bundled-plugins')
for (const flat of ['dsh-plugin-mcn-suite', '_dsh-local_storyforge']) {
mkdirSync(join(sharedRoot, flat), { recursive: true })
}
// . 开头的旁挂物(.store / .manifest.json 的目录位)不是包目录 ⇒ 不许被当成包
mkdirSync(join(sharedRoot, '.store'), { recursive: true })
const fakeRoot = join(root, 'dsh-root')
const scope = join(fakeRoot, 'node_modules', '@deepseek-ai')
mkdirSync(scope, { recursive: true })
const canary = join(root, 'canary')
if (!tryLink(scope, canary)) {
t.skip('symlink creation unavailable on this host')
return
}
rmSync(canary, { force: true })
// 通过 env 指定"本机 dsh 包根"⇒ heal 内部现解析(与真机同一条路径)
process.env.DSH_PACKAGE_DIR = fakeRoot
resetDshPackageRootCache()
const rep = healSharedLayerSelfSufficiency(sharedRoot)
assert.equal(rep.scope, scope, 'scope 必须是 <DSH_PACKAGE_DIR>/node_modules/@deepseek-ai')
assert.deepEqual(rep.linked, ['_dsh-local_storyforge', 'dsh-plugin-mcn-suite'], '两个包都要补(不止刚铺的那个)')
assert.deepEqual(rep.skipped, [], '.store 这类旁挂物⛔ 不许被当成包目录')
assert.deepEqual(rep.failed, [])
for (const flat of ['dsh-plugin-mcn-suite', '_dsh-local_storyforge']) {
assert.ok(existsSync(join(sharedRoot, flat, 'node_modules', '@deepseek-ai')), '链要能解开:' + flat)
}
assert.ok(!existsSync(join(sharedRoot, 'node_modules')), '⛔ 链接不许落在共享层根(会让节点对账永远多出 extra)')
// 第二轮必须收敛成 kept(幂等)
const rep2 = healSharedLayerSelfSufficiency(sharedRoot)
assert.deepEqual(rep2.linked, [])
assert.deepEqual(rep2.relinked, [])
assert.equal(rep2.kept.length, 2)
} finally {
if (saved.DSH_PACKAGE_DIR === undefined) delete process.env.DSH_PACKAGE_DIR
else process.env.DSH_PACKAGE_DIR = saved.DSH_PACKAGE_DIR
if (saved.DSHS_DSH_BIN === undefined) delete process.env.DSHS_DSH_BIN
else process.env.DSHS_DSH_BIN = saved.DSHS_DSH_BIN
resetDshPackageRootCache()
rmSync(root, { recursive: true, force: true })
}
})
test('dsh 安装位置解析:只有一份实现(① 层 dsh-install 与 ③ 层同源、缓存一并清)', async () => {
const saved = { DSH_PACKAGE_DIR: process.env.DSH_PACKAGE_DIR }
try {
const install = await import('../lib/web/dsh-install.js')
process.env.DSH_PACKAGE_DIR = '/first-root'
resetDshPackageRootCache()
install.resetInstallPathsCache()
assert.equal(resolveDshPackageRoot(), '/first-root')
assert.equal(install.dshPackageRoot(), '/first-root', '① 层必须复用 ③ 层的结果')
assert.equal(install.dshScopeDir(), join('/first-root', 'node_modules', '@deepseek-ai'))
// 🔴 漂移点:①层的 reset 必须同时清掉 ③ 层的缓存,否则 env 改了不生效(verify-dsh-install 第 4 组用例)
process.env.DSH_PACKAGE_DIR = '/second-root'
assert.equal(install.dshPackageRoot(), '/first-root', '前置:不 reset 就该是旧值(缓存生效)')
install.resetInstallPathsCache()
assert.equal(install.dshPackageRoot(), '/second-root', 'resetInstallPathsCache 必须连带清 ③ 层缓存')
} finally {
if (saved.DSH_PACKAGE_DIR === undefined) delete process.env.DSH_PACKAGE_DIR
else process.env.DSH_PACKAGE_DIR = saved.DSH_PACKAGE_DIR
resetDshPackageRootCache()
}
})
/* ─────────── ⑧ 回滚不留残影(2026-09-25 · 第 18 棒 · P1) ─────────── */
test('回滚:restoreProfile 清掉无主软链,⛔ 不动声明内 / 真目录 / bundles / . 旁挂物', (t) => {
const root = mkdtempSync(join(tmpdir(), 'pa-rb-'))
try {
const shared = join(root, 'bundled-plugins')
const profile = join(root, 'home', 'profiles', 'web')
const nm = join(profile, 'node_modules')
mkdirSync(nm, { recursive: true })
mkdirSync(shared, { recursive: true }) // 链的目标要真存在(否则 existsSync 会因为悬空而骗人)
const scope = join(root, 'dsh-root')
mkdirSync(scope, { recursive: true })
const canary = join(root, 'canary')
if (!tryLink(scope, canary)) {
t.skip('symlink creation unavailable on this host')
return
}
rmSync(canary, { force: true })
// 装配前:依赖声明里已有 mcn-suite(+ bundles 里有一个只出现在 bundles 的包)
writeFileSync(
join(profile, 'package.json'),
JSON.stringify({
dependencies: { 'dsh-plugin-mcn-suite': 'link:/x/dsh-plugin-mcn-suite' },
dsh: { profile: { bundles: ['@dsh-local/bundle-only'] } },
}),
)
const snap = snapshotProfile(profile)
assert.ok(snap['package.json'] !== null)
// 装配之后盘上的样子(含本次留下的残影)
symlinkSync(shared, join(nm, 'dsh-plugin-mcn-suite'), 'dir') // 声明内 ⇒ 留
mkdirSync(join(nm, '@dsh-local'), { recursive: true })
symlinkSync(shared, join(nm, '@dsh-local', 'bundle-only'), 'dir') // bundles 内 ⇒ 留
symlinkSync(shared, join(nm, 'dsh-plugin-orphan'), 'dir') // 无主 ⇒ 删
symlinkSync(shared, join(nm, '@dsh-local', 'ghost'), 'dir') // 无主(scoped)⇒ 删
mkdirSync(join(nm, 'real-dir-pkg'), { recursive: true }) // 真目录 ⇒ 留
mkdirSync(join(nm, '.pnpm'), { recursive: true }) // pnpm 内部结构 ⇒ 留
const removed = pruneOrphanDepLinks(profile, snap)
assert.deepEqual(removed.sort(), ['@dsh-local/ghost', 'dsh-plugin-orphan'])
assert.ok(existsSync(join(nm, 'dsh-plugin-mcn-suite')), '声明内的依赖⛔ 不许删')
assert.ok(existsSync(join(nm, '@dsh-local', 'bundle-only')), 'bundles 里的包⛔ 不许删')
assert.ok(existsSync(join(nm, 'real-dir-pkg')), '真目录⛔ 一律不碰')
assert.ok(existsSync(join(nm, '.pnpm')), '`.` 开头的旁挂物⛔ 不碰')
// restoreProfile 是同一个判据的入口(回滚路径只走它)
symlinkSync(shared, join(nm, 'dsh-plugin-orphan'), 'dir')
restoreProfile(profile, snap)
assert.ok(!existsSync(join(nm, 'dsh-plugin-orphan')), 'restoreProfile 必须清掉残影')
assert.deepEqual(JSON.parse(readFileSync(join(profile, 'package.json'), 'utf8')), JSON.parse(snap['package.json']))
} finally {
rmSync(root, { recursive: true, force: true })
}
})
/* ─────────── ⑨ 回滚的「缺」方向:重建缺失的 link: 软链(2026-09-25 · 第 19 棒 · P0) ─────────── */
/**
* 第 18 棒真机实证:`off` 撞 pnpm 偶发失败后,`package.json` 的声明**还在**、
* `node_modules/<pkg>` 软链**已被删** ⇒ 实例崩 `cannot resolve profile bundle` ×5
* → `crash-loop-circuit-open`。旧 `restoreProfile` 只治"多",治不了这个"缺"。
*/
test('回滚:restoreProfile 重建缺失的 link: 软链,⛔ 不碰已存在 / 真目录 / 非 link: / 悬空目标', (t) => {
const root = mkdtempSync(join(tmpdir(), 'pa-rb2-'))
try {
const shared = join(root, 'bundled-plugins')
const profile = join(root, 'home', 'profiles', 'web')
const nm = join(profile, 'node_modules')
mkdirSync(nm, { recursive: true })
for (const d of ['dsh-plugin-mcn-suite', '_dsh-local_storyforge', '_dsh-local_im-conversation-tabs']) {
mkdirSync(join(shared, d), { recursive: true })
}
const canary = join(root, 'canary')
if (!tryLink(shared, canary)) {
t.skip('symlink creation unavailable on this host')
return
}
rmSync(canary, { force: true })
const mcnTarget = join(shared, 'dsh-plugin-mcn-suite')
const sfTarget = join(shared, '_dsh-local_storyforge')
const imTarget = join(shared, '_dsh-local_im-conversation-tabs')
writeFileSync(
join(profile, 'package.json'),
JSON.stringify({
dependencies: {
'dsh-plugin-mcn-suite': 'link:' + mcnTarget, // 缺 ⇒ 重建
'@dsh-local/storyforge': 'link:' + sfTarget, // 缺(scoped)⇒ 重建 + 造出 @dsh-local/
'@dsh-local/im-conversation-tabs': 'link:' + imTarget, // 已在 ⇒ kept
'dsh-plugin-real-dir': 'link:' + join(shared, 'dsh-plugin-real-dir'), // 盘上是真目录 ⇒ 不碰
'dsh-plugin-file-ref': 'file:/nowhere/dsh-plugin-file-ref.tgz', // 非 link: ⇒ 一律不动
'dsh-plugin-dangling': 'link:' + join(root, 'no-such-dir'), // 目标不存在 ⇒ ⛔ 不建悬空链
},
dsh: { profile: { bundles: [] } },
}),
)
const snap = snapshotProfile(profile)
// 装配之后的盘上样子:「已在」那条留着;真目录也留着(⛔ 分不清是 pnpm 摊平还是用户自己的)
mkdirSync(join(nm, '@dsh-local'), { recursive: true })
symlinkSync(imTarget, join(nm, '@dsh-local', 'im-conversation-tabs'), 'dir')
mkdirSync(join(nm, 'dsh-plugin-real-dir'), { recursive: true })
// 判据 ① 缺链重建(含 scoped 自动造父目录)
const rebuilt = rebuildMissingDepLinks(profile)
assert.deepEqual(rebuilt.sort(), ['@dsh-local/storyforge', 'dsh-plugin-mcn-suite'])
assert.equal(readlinkSync(join(nm, 'dsh-plugin-mcn-suite')), mcnTarget, '重建的链必须指向声明里的目标')
assert.equal(readlinkSync(join(nm, '@dsh-local', 'storyforge')), sfTarget, 'scoped 包要连带造出 @dsh-local/')
// 判据 ② 已存在 ⇒ 不重建(返回值里没有它)
assert.ok(!rebuilt.includes('@dsh-local/im-conversation-tabs'))
// 判据 ③ 真目录 / 非 link: / 悬空目标 ⇒ 一个都不动
assert.ok(lstatSync(join(nm, 'dsh-plugin-real-dir')).isDirectory(), '真目录⛔ 一律不碰')
assert.ok(!existsSync(join(nm, 'dsh-plugin-file-ref')), '非 link: 声明⛔ 不许建链')
assert.ok(!existsSync(join(nm, 'dsh-plugin-dangling')), '目标不存在⇒⛔ 不许建悬空链')
// 幂等:再跑一次 = 空操作(已正确 ⇒ kept)
assert.deepEqual(rebuildMissingDepLinks(profile), [])
// 判据 ④ 回滚路径的唯一入口 = restoreProfile(两个方向一起收敛)
rmSync(join(nm, 'dsh-plugin-mcn-suite'), { force: true })
restoreProfile(profile, snap)
assert.equal(readlinkSync(join(nm, 'dsh-plugin-mcn-suite')), mcnTarget, 'restoreProfile 必须补回缺的链')
assert.deepEqual(JSON.parse(readFileSync(join(profile, 'package.json'), 'utf8')), JSON.parse(snap['package.json']))
} finally {
rmSync(root, { recursive: true, force: true })
}
})
test('回滚:node_modules 不存在 / 无 link: 声明 ⇒ 直接跳过(⛔ 不造半安装态)', (t) => {
const root = mkdtempSync(join(tmpdir(), 'pa-rb3-'))
try {
const shared = join(root, 'bundled-plugins')
mkdirSync(join(shared, 'p1'), { recursive: true })
const profile = join(root, 'home', 'profiles', 'web')
mkdirSync(profile, { recursive: true })
// ① 没有 node_modules ⇒ 跳过(那是"从没装过",该走 pnpm install)
writeFileSync(join(profile, 'package.json'), JSON.stringify({ dependencies: { p1: 'link:' + join(shared, 'p1') } }))
assert.deepEqual(rebuildMissingDepLinks(profile), [])
assert.ok(!existsSync(join(profile, 'node_modules')), '⛔ 不许凭空造出 node_modules')
// ② 有 node_modules 但声明里没有 link: ⇒ 空操作
mkdirSync(join(profile, 'node_modules'), { recursive: true })
writeFileSync(join(profile, 'package.json'), JSON.stringify({ dependencies: { p1: 'file:/x/p1.tgz' } }))
assert.deepEqual(rebuildMissingDepLinks(profile), [])
assert.deepEqual([...linkRefsOf(profile).keys()], [])
} finally {
rmSync(root, { recursive: true, force: true })
}
})
/* ─────────── ⑩ 基础插件「默认开启 + 用户不可关闭」(2026-09-25 · 第 19 棒 · §5-15) ─────────── */
/**
* 口径 = 用户 2026-09-25 09:3x 原话「这些 dsh 自带的基础插件 应该是 默认开启无法关闭的」。
*
* 本组钉的是**落盘那端**(真正改盘的地方,集群形态下在 worker 上跑):
* 路由侧的 409 只管住门户这条路径;把 `essential` 随装配项发过来,这里必须**结构性**拒绝关闭
* —— 否则"关不掉"只是"按钮藏起来了",绕过 UI 照样能关。
*/
test('基础插件:落盘那端拒绝关闭(状态一个字不动);普通插件照旧可关;缺链顺手补回', (t) => {
process.env.DSHS_TEST_NO_PNPM = '1'
const root = mkdtempSync(join(tmpdir(), 'pa-ess-'))
try {
const userRoot = root
const profile = join(userRoot, 'home', 'profiles', 'web')
const nm = join(profile, 'node_modules')
mkdirSync(nm, { recursive: true })
mkdirSync(join(userRoot, 'ws'), { recursive: true })
const sharedRoot = join(root, 'bundled-plugins')
const essDir = join(sharedRoot, '_dsh-local_ess')
const normDir = join(sharedRoot, '_dsh-local_norm')
mkdirSync(essDir, { recursive: true })
mkdirSync(normDir, { recursive: true })
const canary = join(root, 'canary')
if (!tryLink(sharedRoot, canary)) {
t.skip('symlink creation unavailable on this host')
return
}
rmSync(canary, { force: true })
// 前置状态:两个插件**都已经装上**(声明在 + bundles 在)
writeFileSync(
join(profile, 'package.json'),
JSON.stringify({
dependencies: { '@dsh-local/ess': `link:${essDir}`, '@dsh-local/norm': `link:${normDir}` },
dsh: { profile: { bundles: ['@dsh-local/ess', '@dsh-local/norm'] } },
}),
)
const opts = {
userRoot,
bundledPluginDir: sharedRoot,
resolveBundleDir: (id) => join(sharedRoot, sharedDirNameOf(id)),
}
// ① 基础插件:请求"关" ⇒ **被拒**,且盘上一个字都没动
const r1 = applyProfileChanges([{ id: '@dsh-local/ess', enabled: false, src: 'bundled', essential: true }], opts)
assert.deepEqual(r1.refused, [{ id: '@dsh-local/ess', reason: 'essential_plugin' }], '必须回**具名**拒绝理由')
assert.deepEqual(r1.disabled, [], '被拒的项⛔ 不许出现在 disabled 里(那等于宣称"关掉了")')
const after1 = JSON.parse(readFileSync(join(profile, 'package.json'), 'utf8'))
assert.equal(after1.dependencies['@dsh-local/ess'], `link:${essDir}`, '被拒 ⇒ 依赖声明⛔ 不许删')
assert.ok(after1.dsh.profile.bundles.includes('@dsh-local/ess'), '被拒 ⇒ bundles⛔ 不许摘')
// ② 普通插件(不在基础插件名单里)⇒ 走的是**"能关"的那条路**(`disabled` 里有它)
// ⇒ 证明门禁是"点名"的,⛔ 不是一刀切。
// ⚠️ 这里**不**拿"磁盘上删没删"当判据:本机测试用 `DSHS_TEST_NO_PNPM=1` ⇒ pnpm 不跑,
// 而 `package.json` 里那一行的删除**是 pnpm remove 自己落盘的**(平台侧只改内存里的
// `deps` 快照)。磁盘级判据属**真机验收**,⛔ 不在单测里假装。
const r2 = applyProfileChanges([{ id: '@dsh-local/norm', enabled: false, src: 'bundled' }], opts)
assert.deepEqual(r2.disabled, ['@dsh-local/norm'], '普通插件的关闭必须被**放行**')
assert.deepEqual(r2.refused, [], '普通插件⛔ 不许被基础插件门禁拦住')
// ③ 第 1 件的"缺"方向也要在装配路径上收敛:ess 声明还在、链此前没建 ⇒ 本次顺手补回
assert.ok(r1.repaired.includes('@dsh-local/ess'), '装配路径必须顺手补回缺失的 link: 软链')
assert.equal(readlinkSync(join(nm, '@dsh-local', 'ess')), essDir)
} finally {
rmSync(root, { recursive: true, force: true })
delete process.env.DSHS_TEST_NO_PNPM
}
})
test('基础插件:种子文案与拒绝码**同名不重复**(UI/路由/装配三处引用同一个来源)', async () => {
const { ESSENTIAL_REFUSED_REASON } = await import('../lib/supervisor/plugin-assembly.js')
const { ESSENTIAL_DENY_CODE, essentialDenyMessage } = await import('../lib/web/essential-plugins.js')
assert.equal(ESSENTIAL_DENY_CODE, ESSENTIAL_REFUSED_REASON, '门户 409 的 error 码必须与装配层 reason 逐字一致')
assert.equal(ESSENTIAL_DENY_CODE, 'essential_plugin')
const msg = essentialDenyMessage('@softspark/dsh-file-preview')
assert.ok(msg.includes('@softspark/dsh-file-preview'), '拒绝文案必须**具名**(否则用户不知道是哪个插件)')
assert.ok(msg.includes('不可关闭'), '拒绝文案必须说清"为什么"')
})