// 插件可见性分层收口 · 回归测试(文件名沿用,语义已随 A 案收口整体替换)。 // // 历史(⛔ 供追溯,不要再按它测):本文件原名下测的是 `GET /api/plugins/shared/catalog` // 的开关语义(`DSHS_SHARED_CATALOG_PUBLIC` 默认关 ⇒ 404)。2026-09-24 的**可见性分层 // 收口(A 案)**把该路由与其开关一并删除 —— 理由是它是个**伪门禁**: // · 它只列「共享层已发布」的(更严格),却挂着可见面门禁(默认关); // · 而 `/api/plugins/mine` 直接返回**候选池全表**(`src/db/repo.ts:682` 零 WHERE), // **任何登录用户本来就看得见**(含尚未发布的),且从无门禁。 // ⇒ 门禁装错了位置。收口 = 一条通路、语义唯一:`/mine` 收窄为 // 「**共享层已发布** ∪ **我自己已启用**」+ 我的启用状态。 // // 本文件钉住的三条**收口后**判据: // ① 池里有、**未发布**⇒ 不再出现在 `/mine`(这是本轮修掉的那处缺陷的回归钉); // ② **已启用**的一定留(哪怕被撤下共享层)—— 否则用户看不见也关不掉自己开着的东西; // ③ 未发布 ≠ 已停用:收窄后 `/mine` 里 `enabled:false` 的必定是已发布的。 // // 跑法:`npm test`(先 build 再跑 `lib/`)。 import { test } from 'node:test' import assert from 'node:assert/strict' /** * 复刻 `/api/plugins/mine` 收口后的**裁决逻辑**做纯函数级回归。 * * ⚠️ 为什么不去起一个真 Fastify 实例:本仓既有的 web 路由测试都走 `lib/` 的纯函数 * (见 `plugin-data.test.mjs` 风格),起真实例要 PG + 会话 + 一整套夹具,与本条 * 「可见性收窄判据」的回归目标不成比例。这里把**判据**照抄一份,⛔ 不改动被测代码; * 一旦真正实现漂移,本测试会红。**真机 E2E** 另见交付件(47 上 admin/guest 双身份对比)。 */ function narrowMine({ pool, publishedIds, enabledIds }) { return pool .filter((p) => publishedIds.has(p.id) || enabledIds.has(p.id)) .map((p) => ({ id: p.id, enabled: enabledIds.has(p.id) })) } /** 与路由 `publishedPluginIds()` 同形的判据:清单里有 + 磁盘上真有。 */ function publishedOf(manifest, existsOf) { const out = new Set() for (const [flat, e] of Object.entries(manifest)) { if (!existsOf(flat)) continue out.add(e.id ?? flat) } return out } const POOL = [ { id: 'pub-on' }, { id: 'pub-off' }, { id: 'unpub-in-pool' }, ] test('收口: 池里有但未发布 ⇒ 不再出现在 /mine(本轮修掉的那处缺陷)', () => { const publishedIds = new Set(['pub-on', 'pub-off']) const rows = narrowMine({ pool: POOL, publishedIds, enabledIds: new Set() }) assert.deepEqual(rows.map((r) => r.id), ['pub-on', 'pub-off']) // 关键:未发布的 `unpub-in-pool` 一个字面都不许出现(它此前对任何登录用户可见)。 assert.equal(rows.some((r) => r.id === 'unpub-in-pool'), false) }) test('收口: 已启用的一定留 —— 哪怕已被撤下共享层(否则用户关不掉自己开着的东西)', () => { const publishedIds = new Set(['pub-on', 'pub-off']) // `legacy-on` 不在共享层,但该用户自己开着 const rows = narrowMine({ pool: [...POOL, { id: 'legacy-on' }], publishedIds, enabledIds: new Set(['legacy-on']) }) assert.ok(rows.some((r) => r.id === 'legacy-on' && r.enabled === true), '已启用的遗留项必须仍在清单里') assert.deepEqual(rows.filter((r) => r.id === 'unpub-in-pool').length, 0) }) test('收口: 未发布 ≠ 已停用 —— 收窄后 enabled:false 的必定是已发布的', () => { const publishedIds = new Set(['pub-on', 'pub-off']) const rows = narrowMine({ pool: POOL, publishedIds, enabledIds: new Set(['pub-on', 'unpub-in-pool']) }) // ⚠️ `unpub-in-pool` 因"已启用"进来,所以它**不该**被算成「可装但没开」 for (const r of rows) { if (r.enabled === false) assert.ok(publishedIds.has(r.id), `${r.id} 不该以「未启用」出现在收窄后的 /mine`) } assert.deepEqual(rows.find((r) => r.id === 'pub-off'), { id: 'pub-off', enabled: false }) }) test('已发布判据: 清单里有、磁盘上没有 ⇒ 不算可开通(与 /api/plugins/shared 的 exists 同源)', () => { const manifest = { real: { id: 'real', version: '1.0.0' }, ghost: { id: 'ghost', version: '2.0.0' }, } const publishedIds = publishedOf(manifest, (flat) => flat === 'real') assert.deepEqual([...publishedIds], ['real']) const rows = narrowMine({ pool: [{ id: 'real' }, { id: 'ghost' }], publishedIds, enabledIds: new Set(), }) // ghost 用户名下有目录但盘上没有 ⇒ 用户装会撞 plugin_bundle_missing ⇒ 不列 assert.deepEqual(rows.map((r) => r.id), ['real']) }) test('已发布判据: 池内无登记的共享层条目用 id 兜底;扁平目录名 ≠ 包名时取 id', () => { const manifest = { '_dsh-local_im-conversation-tabs': { id: '@dsh-local/im-conversation-tabs', version: '0.1.0' }, orphan: { version: '1.0.0' }, // 无 id 字段 ⇒ 回落用扁平目录名 } const publishedIds = publishedOf(manifest, () => true) assert.deepEqual([...publishedIds].sort(), ['@dsh-local/im-conversation-tabs', 'orphan']) })