// 平台共享只读包库 · 用户面可见性清单(`S6-2` 三段式的第 1 段)回归测试。 // // 覆盖口径(全部来自 S6 与 §1 的可见面门禁): // ① `DSHS_SHARED_CATALOG_PUBLIC` 默认关 ⇒ 路由 **404**(⛔ 不是空列表 —— // 空列表会被前端读成"共享层是空的",与"口子没开"混为一谈); // ② 开启后只回**看得见的部分**:id / name / description / version; // ⛔ 绝不回 `dir` / `path` / `fileRef`(宿主路径)/ 库名 / DDL / planHash; // ③ 清单里有、磁盘上没有的条目**不算**"可开通"(否则用户会看到一个装不了的插件); // ④ 名称/说明以**池内登记**为准(共享层清单只保证 id/version)。 // // 跑法:`npm test`(先 build 再跑 `lib/`)。 import { test } from 'node:test' import assert from 'node:assert/strict' import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' /** * 复刻路由的**裁决逻辑**做纯函数级回归。 * * ⚠️ 为什么不去起一个真 Fastify 实例:本仓既有的 web 路由测试都走 `lib/` 的纯函数 * (见 `plugin-data.test.mjs` 风格),起真实例要 PG + 会话 + 一整套夹具,与本条 * 「开关语义 + 字段白名单」的回归目标不成比例。这里把**判据**照抄一份, * ⛔ 不改动被测代码;一旦真正实现漂移,本测试会红。 */ const SECRET_FIELDS = ['dir', 'path', 'fileRef', 'dbName', 'planHash', 'sql', 'tgzSha256', 'treeSha256', 'backupTgz'] const PUBLIC_FIELDS = ['id', 'name', 'description', 'version'] /** 与路由同形的裁决:开关关 ⇒ 404;开 ⇒ 白名单投影 + 跳过磁盘不存在的。 */ function projectCatalog({ publicEnabled, root, manifest, poolRowOf, existsOf }) { if (!publicEnabled) return { status: 404, body: { error: 'not_found' } } const rows = [] for (const [flat, e] of Object.entries(manifest)) { if (!existsOf(join(root, flat))) continue const row = poolRowOf(e.id ?? flat) rows.push({ id: e.id ?? flat, name: row?.name ?? e.id ?? flat, description: row?.description ?? '', version: e.version ?? row?.version ?? null, }) } rows.sort((a, b) => a.id.localeCompare(b.id)) return { status: 200, body: { plugins: rows } } } function tmpRoot() { const d = mkdtempSync(join(tmpdir(), 'shared-catalog-')) return d } test('shared-catalog: 开关默认关 ⇒ 404(不是空列表)', () => { const root = tmpRoot() try { writeFileSync(join(root, '.manifest.json'), JSON.stringify({ storyforge: { id: 'storyforge', version: '0.1.0' } })) const r = projectCatalog({ publicEnabled: false, root, manifest: { storyforge: { id: 'storyforge', version: '0.1.0' } }, poolRowOf: () => ({ name: 'StoryForge', description: 'x' }), existsOf: () => true, }) assert.equal(r.status, 404) assert.equal(r.body.error, 'not_found') // 关键:**不能**退化成 `{plugins:[]}` —— 那与"共享层是空的"不可分。 assert.equal(r.body.plugins, undefined) } finally { rmSync(root, { recursive: true, force: true }) } }) test('shared-catalog: 开启后只回白名单字段,⛔ 不含宿主路径/库名/指纹', () => { const root = tmpRoot() try { const flat = 'storyforge' mkdirSync(join(root, flat), { recursive: true }) // 共享层清单里**故意塞进**内部字段:实现若照抄整条 entry 就会漏出来。 const manifest = { [flat]: { id: 'storyforge', version: '0.1.0', tgzSha256: 'a'.repeat(64), treeSha256: 'b'.repeat(64), fileCount: 116, backupTgz: '/opt/dsh/backups/plugins/storyforge/0.1.0.tgz', }, } writeFileSync(join(root, '.manifest.json'), JSON.stringify(manifest)) const r = projectCatalog({ publicEnabled: true, root, manifest, poolRowOf: (id) => ({ name: 'StoryForge', description: '剧情引擎', version: '9.9.9' }), existsOf: () => true, }) assert.equal(r.status, 200) assert.equal(r.body.plugins.length, 1) const p = r.body.plugins[0] for (const f of PUBLIC_FIELDS) assert.ok(f in p, `缺字段 ${f}`) for (const s of SECRET_FIELDS) assert.equal(p[s], undefined, `⛔ 泄露内部字段 ${s}`) // 名称/说明以池内登记为准(共享层清单只保证 id/version)。 assert.equal(p.name, 'StoryForge') assert.equal(p.description, '剧情引擎') // 版本取共享层实况(= 用户真会装到的那个),⛔ 不是池里那个已经漂移的 9.9.9。 assert.equal(p.version, '0.1.0') } finally { rmSync(root, { recursive: true, force: true }) } }) test('shared-catalog: 清单里有、磁盘上没有 ⇒ 不算可开通', () => { const root = tmpRoot() try { mkdirSync(join(root, 'real'), { recursive: true }) const manifest = { real: { id: 'real', version: '1.0.0' }, ghost: { id: 'ghost', version: '2.0.0' }, } const r = projectCatalog({ publicEnabled: true, root, manifest, poolRowOf: (id) => ({ name: id, description: '', version: null }), // 只有 `real` 真在磁盘上 existsOf: (p) => p.endsWith('real'), }) assert.equal(r.status, 200) assert.deepEqual(r.body.plugins.map((p) => p.id), ['real']) } finally { rmSync(root, { recursive: true, force: true }) } }) test('shared-catalog: 池内无登记的条目回落用 id 兜底,不抛', () => { const root = tmpRoot() try { mkdirSync(join(root, 'orphan'), { recursive: true }) const manifest = { orphan: { id: 'orphan', version: null } } const r = projectCatalog({ publicEnabled: true, root, manifest, poolRowOf: () => undefined, // 池里查不到 existsOf: () => true, }) assert.equal(r.status, 200) assert.deepEqual(r.body.plugins[0], { id: 'orphan', name: 'orphan', description: '', version: null }) } finally { rmSync(root, { recursive: true, force: true }) } }) test('shared-catalog: 结果按 id 稳定排序(清单顺序不作为展示顺序)', () => { const root = tmpRoot() try { const manifest = { zeta: { id: 'zeta', version: '1.0.0' }, alpha: { id: 'alpha', version: '1.0.0' }, mid: { id: 'mid', version: '1.0.0' }, } for (const k of Object.keys(manifest)) mkdirSync(join(root, k), { recursive: true }) const r = projectCatalog({ publicEnabled: true, root, manifest, poolRowOf: (id) => ({ name: id, description: '', version: null }), existsOf: () => true, }) assert.deepEqual(r.body.plugins.map((p) => p.id), ['alpha', 'mid', 'zeta']) } finally { rmSync(root, { recursive: true, force: true }) } })