build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(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/;交接单不入库(政策)。
175 lines
6.7 KiB
JavaScript
175 lines
6.7 KiB
JavaScript
// 平台共享只读包库 · 用户面可见性清单(`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 })
|
||
}
|
||
})
|