Files
dsh_shenxian/test/plugin-assembly.test.mjs
T

1047 lines
52 KiB
JavaScript
Raw Normal View History

/**
* 插件投放与分库线 ① · **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('不可关闭'), '拒绝文案必须说清"为什么"')
})