/** * 插件投放与分库线 ① · **S3 跨机装配**的回归测试。 * * ## 这个文件防的是什么(每一件都对应一条会静默失效的判据) * * 1. **`RemoteSpawner.applyPlugins` 真的发到"实例所在那台"** —— 与 `restartAndProbe` * 同一套 `hostIdFor` 路由。⛔ 若它回落到默认 host,就会把请求发给**另一台机** * (那台上没有这个用户的 profile ⇒ 落盘落到一个空目录、或给错人装插件), * 而症状只有"插件没生效",平台零日志。 * 2. **请求体形状与 agent 侧的入参校验对得上** —— `{userId, items:[{id,version,enabled,src}]}`。 * 两侧字段名漂一个字(如 `src` vs `source`)在 TS 里编译得过(一个 `Record`), * 但运行期是 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('univer-office@1.2.3'), '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/ 是指向共享层的软链,⛔ 不是实体副本', () => { 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`、判据"对账收敛"失效) // —— 故所有用例都断言链接落在 `/node_modules/` 之下。 /** 建软链;本机不允许(无特权 Windows)⇒ 返回 false,用例自行 skip。 */ function tryLink(target, path) { try { symlinkSync(target, path, 'dir') return true } catch { return false } } test('共享层自足性:缺链 ⇒ 补上 /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(), '必须落到 /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 必须是 /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/` 软链**已被删** ⇒ 实例崩 `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('不可关闭'), '拒绝文案必须说清"为什么"') })