/** * 接线断言:服务端 409 里说「本会话是 full 档」时,dsh 桥必须**放行**而不是拒绝。 * * # 为什么需要这条(与 pi 桥同一处缺陷,同日一起修) * * 409 原本只有一个含义:「这条任务链上没有人类」→ 当场拒绝(`return 'rejected'`)。 * 但 `handler/permission.go` 在**档位不该问人**时也回 409,并在回包里带 * `permission_mode`。两种 409 的正确反应相反:无人可问 → 拒绝;full 档 → **放行**。 * * dsh 桥还有一层放大:档位是从邮件事件 `applyPermissionMode()` 应用的, * 补投路径漏传档位时(`lib/catchup.js`,同日修)会话被**降级**成 workspace —— * 沙箱 danger-full-access → workspace-write、审批 never → ask, * 于是每个受守卫的工具调用都去问一次,再被 409 拒绝。一条 full 档会话只要有一封 * 补投邮件,这一轮的工具调用就全被自己人拦死。 * * # 为什么读源码而不是跑起来 * * 与 `permission-forward-wiring.test.mjs` 同一取舍:入口不是可导入的模块 * (插件工厂 + Cordis 依赖)。这里要钉的只有一件事:那个放行分支还在、且认服务端给的档位。 * * # 判据自检 * * 拿掉放行分支的源码喂给它,必须红 —— 否则这条断言永远为真,比没有更糟。 */ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const SRC = readFileSync(join(HERE, '..', 'src', 'index.ts'), 'utf8'); /** 取 409 相关分支的正文(按花括号配对,不看窗口) */ function blockAt(src, needle) { const at = src.indexOf(needle); if (at < 0) return ''; const open = src.indexOf('{', at); let depth = 0; for (let i = open; i < src.length; i++) { if (src[i] === '{') depth++; else if (src[i] === '}') { depth--; if (depth === 0) return src.slice(open + 1, i); } } return ''; } test('★ 409 + full 档 → 放行;plan / 无人可问 → 仍拒绝', () => { const full = blockAt(SRC, "if (e?.status === 409 && String(e?.body?.permission_mode || '') === 'full')"); assert.ok(full.length > 40, '要能找到「409 + full」分支(找不到说明结构变了,判据要跟着改)'); assert.match(full, /return 'allowed-once'/, 'full 档要放行(DSH 的 ApprovalOutcome 只认 allowed-once/rejected/…)'); const plain = blockAt(SRC, 'if (e?.status === 409) {'); assert.ok(plain.length > 100, '要能找到普通 409 分支'); assert.match(plain, /return 'rejected'/, '非 full 的 409 仍要当场拒绝'); /* * ⚠️ 「代码里有没有放行」必须读**剥掉注释**的正文:普通 409 分支的注释里正好写着 * "ApprovalOutcome 只认 allowed-once / rejected / …"(解释为什么写 'rejected' * 而不是 'denied')—— 读原文会把那句话当成"这里会放行"而误报。 * 这是本仓规范里那条:判"代码里有什么"读剥离版,判"理由写清了没"读原文。 */ const plainCode = plain.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, ''); assert.ok(!/allowed-once/.test(plainCode), `普通 409 分支不许放行(那就等于"没人可问也放行"):${(plainCode.match(/allowed-once/) || [''])[0]}`); // 顺序:full 分支必须在普通 409 之前,否则永远不可达 assert.ok(SRC.indexOf("permission_mode || '') === 'full'") < SRC.indexOf('if (e?.status === 409) {'), '放行分支要排在普通 409 之前(否则被前面的分支吃掉,永远不可达)'); }); test('★ 判据自检:拿掉放行分支,上面那条必须红', () => { const crippled = SRC.replace(/if \(e\?\.status === 409 && String\(e\?\.body\?\.permission_mode \|\| ''\) === 'full'\)[\s\S]*?\n \}\n/, ''); assert.equal(blockAt(crippled, "if (e?.status === 409 && String(e?.body?.permission_mode || '') === 'full')").length, 0, '自检:判据要能检出"放行分支被拿掉"这件事'); });