Files
dsh_shenxian/test/plugin-shared-catalog.test.mjs
T
admin e6207aa691
build / build-and-scan (push) Waiting to run
chore(仓库对齐): 文档库结构治理 + IM/插件线落地
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。

IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。

插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。

仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
2026-09-24 07:25:16 +08:00

175 lines
6.7 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.
// 平台共享只读包库 · 用户面可见性清单(`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 })
}
})