/** * lib/permission-mode.js 的测试 —— 四个平台逐字节共用。 * * 这些判据编码了六条 opencode 实测结论。不实测就写代码会做出「看起来对但 * 管不住」的东西,所以每条结论都在这里钉死,改坏了会当场失败。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { MODE_PLAN, MODE_WORKSPACE, MODE_FULL, MODES, DEFAULT_MODE, ENFORCE_NATIVE, ENFORCE_PARTIAL, ENFORCE_ADVISORY, normalizeMode, normalizeEnforcement, modeAtMost, modeNeedsHuman, dshSandboxMode, dshApprovalPolicy, piGuardedTools, piBlocksOutright, modeBriefing, } from '../lib/permission-mode.js'; // ─── 归一化 ─── test('合法档位原样返回', () => { for (const m of MODES) assert.equal(normalizeMode(m), m); }); test('非法档位 fail-closed 到默认档,不是 full', () => { for (const bad of ['', 'FULL', 'full-access', 'workspace-write', null, undefined, 42, {}]) { assert.equal(normalizeMode(bad), DEFAULT_MODE, `${String(bad)} 应当归到默认档`); } assert.notEqual(DEFAULT_MODE, MODE_FULL, '默认档不能是 full'); }); test('强制力保守方向是 advisory', () => { for (const ok of [ENFORCE_NATIVE, ENFORCE_PARTIAL, ENFORCE_ADVISORY]) { assert.equal(normalizeEnforcement(ok), ok, `${ok} 是平台自报的事实,必须原样保留`); } for (const bad of ['', 'NATIVE', 'enforced', null, undefined]) { assert.equal(normalizeEnforcement(bad), ENFORCE_ADVISORY); } }); test('档位顺序必须是 plan < workspace < full(modeAtMost 的依据)', () => { assert.deepEqual(MODES, [MODE_PLAN, MODE_WORKSPACE, MODE_FULL]); }); // ─── modeAtMost ─── test('modeAtMost 取更严的一档', () => { assert.equal(modeAtMost(MODE_PLAN, MODE_FULL), MODE_PLAN); assert.equal(modeAtMost(MODE_FULL, MODE_PLAN), MODE_PLAN); assert.equal(modeAtMost(MODE_WORKSPACE, MODE_FULL), MODE_WORKSPACE); assert.equal(modeAtMost(MODE_FULL, MODE_FULL), MODE_FULL); }); // Gateway 侧曾因为「未知值当最严 vs 归到默认档」两套语义而不可交换, // 单元测试当场抓到。两边保持同一套语义。 test('modeAtMost 可交换(脏值也不例外)', () => { const all = [...MODES, 'garbage', '', null]; for (const a of all) { for (const b of all) { assert.equal(modeAtMost(a, b), modeAtMost(b, a), `不可交换:(${a},${b})`); } } }); test('脏值不得把 plan 抬成更宽松的档', () => { assert.equal(modeAtMost('garbage', MODE_PLAN), MODE_PLAN); }); // ─── modeNeedsHuman ─── test('只有 workspace 档需要人点头', () => { assert.equal(modeNeedsHuman(MODE_PLAN), false, 'plan 档当场拒绝,不问人'); assert.equal(modeNeedsHuman(MODE_WORKSPACE), true); assert.equal(modeNeedsHuman(MODE_FULL), false, 'full 档自动放行,不问人'); }); test('脏档位按默认档处理,即需要人(宁可多问一次)', () => { assert.equal(modeNeedsHuman('garbage'), true); assert.equal(modeNeedsHuman(''), true); }); // ─── DSH ─── test('DSH 三档与原生沙箱一一对应', () => { assert.equal(dshSandboxMode(MODE_PLAN), 'read-only'); assert.equal(dshSandboxMode(MODE_WORKSPACE), 'workspace-write'); assert.equal(dshSandboxMode(MODE_FULL), 'danger-full-access'); }); // 关键实测:danger-full-access → approval:"never" → decide() 在 waterfall // 之前短路 return "rejected",approval/request 钩子根本不触发。 test('DSH 审批策略只在 workspace 档是 ask', () => { assert.equal(dshApprovalPolicy(MODE_WORKSPACE), 'ask'); assert.equal(dshApprovalPolicy(MODE_PLAN), 'never'); assert.equal(dshApprovalPolicy(MODE_FULL), 'never'); }); test('DSH 脏档位按默认档(workspace-write + ask)', () => { assert.equal(dshSandboxMode('garbage'), 'workspace-write'); assert.equal(dshApprovalPolicy('garbage'), 'ask'); }); // ─── pi ─── test('pi 在 full 档不守卫任何工具', () => { assert.deepEqual(piGuardedTools(MODE_FULL), []); }); test('pi 在 plan / workspace 档守卫 bash / write / edit', () => { for (const m of [MODE_PLAN, MODE_WORKSPACE]) { const g = piGuardedTools(m); assert.ok(g.includes('bash')); assert.ok(g.includes('write')); assert.ok(g.includes('edit')); } }); test('pi 不守卫读类工具', () => { const g = piGuardedTools(MODE_WORKSPACE); for (const t of ['read', 'grep', 'find', 'ls']) { assert.equal(g.includes(t), false, `${t} 是读类工具,不该守卫`); } }); test('pi 在 plan 档直接拒绝,不走问人流程', () => { assert.equal(piBlocksOutright(MODE_PLAN), true); assert.equal(piBlocksOutright(MODE_WORKSPACE), false); assert.equal(piBlocksOutright(MODE_FULL), false); }); // ─── modeBriefing ─── test('full 档的说明不提授权', () => { const s = modeBriefing({ mode: MODE_FULL, enforcement: ENFORCE_NATIVE }); assert.match(s, /full/); assert.equal(/授权/.test(s.replace('不需要额外授权', '')), false); }); // advisory 与 native 措辞必须不同:假装 advisory 是强制的会让模型以为 // 越界会被拦,于是不必自己小心 —— 那比做不到本身更危险。 test('advisory 必须明说平台无法强制这一档', () => { const adv = modeBriefing({ mode: MODE_PLAN, enforcement: ENFORCE_ADVISORY }); const nat = modeBriefing({ mode: MODE_PLAN, enforcement: ENFORCE_NATIVE }); assert.match(adv, /无法强制/); assert.equal(/无法强制/.test(nat), false, 'native 不该说无法强制'); assert.notEqual(adv, nat, '两种强制力的措辞必须不同'); }); test('workspace 档的 advisory 版同样明说', () => { const adv = modeBriefing({ mode: MODE_WORKSPACE, enforcement: ENFORCE_ADVISORY, workspace: '/tmp/x' }); assert.match(adv, /无法强制/); assert.match(adv, /\/tmp\/x/, '要带上具体目录'); }); test('native 的 workspace 说明要交代「授权可能被拒」', () => { const s = modeBriefing({ mode: MODE_WORKSPACE, enforcement: ENFORCE_NATIVE, workspace: '/srv/app' }); assert.match(s, /\/srv\/app/); assert.match(s, /拒绝/, '被拒时该怎么办必须说清楚,否则模型会反复重试'); }); test('plan 档的说明必须告诉模型「把方案写在回信里」', () => { for (const e of [ENFORCE_NATIVE, ENFORCE_ADVISORY]) { const s = modeBriefing({ mode: MODE_PLAN, enforcement: e }); assert.match(s, /回信/, '不给出路的话模型只会反复撞墙'); } }); test('缺 workspace 时用兜底措辞,不出现 undefined', () => { const s = modeBriefing({ mode: MODE_WORKSPACE, enforcement: ENFORCE_NATIVE }); assert.equal(/undefined/.test(s), false); assert.equal(/`` /.test(s), false); }); test('脏输入不炸且按默认档', () => { const s = modeBriefing({ mode: 'garbage', enforcement: 'garbage' }); assert.match(s, /workspace/); assert.match(s, /无法强制/, '脏强制力按 advisory 处理'); }); // ─── partial:有拦截点但覆盖不完整 ─── // // 这个取值的全部意义就是「不许说假话」。因此判据不是「措辞好看」, // 而是它与两个极端的说法**都不同**,且明确交代「不要依赖会被拦」。 test('partial 三档措辞两两不同(不能与任一极端混同)', () => { for (const mode of [MODE_PLAN, MODE_WORKSPACE]) { const nat = modeBriefing({ mode, enforcement: ENFORCE_NATIVE }); const par = modeBriefing({ mode, enforcement: ENFORCE_PARTIAL }); const adv = modeBriefing({ mode, enforcement: ENFORCE_ADVISORY }); assert.notEqual(par, nat, `${mode}: partial 不能与 native 同措辞(那是高估)`); assert.notEqual(par, adv, `${mode}: partial 不能与 advisory 同措辞(那是低估)`); } }); test('partial 必须交代「覆盖不完整」且不得说「无法强制」', () => { for (const mode of [MODE_PLAN, MODE_WORKSPACE]) { const par = modeBriefing({ mode, enforcement: ENFORCE_PARTIAL }); assert.match(par, /不完整|缺口/, `${mode}: 必须说清覆盖不完整`); assert.equal(/无法强制/.test(par), false, `${mode}: partial 平台确实在拦,「无法强制」是错的`); } }); test('partial 不能把「会被拦下」当成保证', () => { const plan = modeBriefing({ mode: MODE_PLAN, enforcement: ENFORCE_PARTIAL }); // native 版说「都会被平台拦下」,partial 版必须收回这个承诺 assert.equal(/都会被平台拦下/.test(plan), false, 'partial 下承诺「都会拦下」会让模型不必自己小心 —— 那是 native 才成立的话'); assert.match(plan, /不要依赖|主动/, '必须给出「主动自律」的指引'); }); test('partial 与 native 一样要求把方案写在回信里(出路不能消失)', () => { const s = modeBriefing({ mode: MODE_PLAN, enforcement: ENFORCE_PARTIAL }); assert.match(s, /回信/); });