import assert from 'node:assert/strict'; import test from 'node:test'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; import { homedir } from 'node:os'; import { resolveSessionReuse, resolveTurnCwd } from '../src/turn-cwd.mjs'; import { piMailFallback } from '../src/paths.mjs'; import { resolveWorkspaceCwd } from '../lib/workspace.js'; import { sandboxWritePaths } from '../lib/sandbox.js'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(HERE, '..'); const HOME = homedir(); const call = (o) => resolveTurnCwd({ fallback: piMailFallback, resolve: resolveWorkspaceCwd, ...o, }); /** * 这一组补的是 pi 评审 2026-09-15 用探针实测出来的**输入**分叉: * * 父进程:`resolveWorkspaceCwd(to_workspace, …)` ← 来源:**这封信的地址** * 子进程:`job.session.cwd || resolveWorkspaceCwd(…)` ← 来源:**会话上次实际用的 cwd** * * 原先那条"父子同源"判据断言的是**函数**同源(两边 import 同一个 `piMailFallback`), * 它**测不出输入分叉** —— 所以这里改成断言一条**等式**: * "`--rw` 的 cwd(父进程算) === worker 会用的 cwd"。 */ test('★ 复用会话时:cwd 取 state.cwd,而不是按这封信的地址重算', () => { // 实测场景:会话 d042cc4c 的 state.cwd = /home/program/agentmail, // 此时来一封写给 `pi@.<会话>`(不带 path)的信。 const r = call({ sessionReused: true, storedCwd: '/home/program/agentmail', toWorkspace: undefined, // 地址不带 path ⇒ 按地址算会得到兜底目录 sessionKey: 'sess-x', }); assert.equal(r.cwd, '/home/program/agentmail', '复用会话时必须用会话上次实际用的 cwd —— 按地址重算就是 rw 与 worker 分叉'); assert.equal(r.source, 'reused'); // 对照:按地址算会得到兜底目录,与 state.cwd 不同 ⇒ 如果不修,rw 就是错的 const byAddress = resolveWorkspaceCwd(undefined, piMailFallback('sess-x')); assert.notEqual(byAddress.cwd, r.cwd, '两种来源确实不同名 —— 这正是分叉的成因'); }); test('★ 等式:--rw 里必须有 worker 会用的那个 cwd', () => { // 这是 pi 要的那条可断言等式。用真 sandboxWritePaths 算 rw, // 断言 worker 实际会用的 cwd **在 rw 里**。 const stored = '/home/program/agentmail'; const r = call({ sessionReused: true, storedCwd: stored, toWorkspace: undefined, sessionKey: 'sess-x' }); const exists = (p) => [stored, '/tmp', `${HOME}/.pi/agent`, `${HOME}/.pi/agent/sessions`, `${HOME}/.agentmail-pi`, '/dev/null'].includes(p); const { dirs } = sandboxWritePaths({ cwd: r.cwd, env: { TMPDIR: '/tmp', HOME }, exists, home: HOME }); assert.ok(dirs.includes(r.cwd), `--rw 里必须有 worker 会用的 cwd(${r.cwd}):rw=${dirs.join(',')}`); // 反面对照:如果不改(按地址算),rw 里就没有那个目录 —— 证明这条判据有区分力 const wrong = call({ sessionReused: false, toWorkspace: undefined, sessionKey: 'sess-x' }); const wrongDirs = sandboxWritePaths({ cwd: wrong.cwd, env: { TMPDIR: '/tmp', HOME }, exists, home: HOME }).dirs; assert.ok(!wrongDirs.includes(stored), '按地址算出来的 rw 不该含 state.cwd —— 这就是修复前的错位状态'); }); test('没有复用时仍按地址算(不能一律取 state.cwd)', () => { const r = call({ sessionReused: false, storedCwd: '/home/program/agentmail', toWorkspace: '/home/program/agentmail', sessionKey: 'k' }); assert.equal(r.source, 'address', '没复用就该按地址算'); assert.equal(r.cwd, '/home/program/agentmail'); }); test('复用了但 state.cwd 为空 ⇒ 退回按地址算(老状态没有 cwd)', () => { const r = call({ sessionReused: true, storedCwd: '', toWorkspace: '/tmp', sessionKey: 'k' }); assert.equal(r.source, 'address'); }); test('★ 结构判据:pool 算 launch 与 worker 取值必须来自同一个决定', () => { const pool = readFileSync(join(ROOT, 'src', 'pool.mjs'), 'utf8'); assert.match(pool, /resolveTurnCwd\(/, 'pool 必须用这个纯函数决定 cwd'); assert.match(pool, /cwd: turnCwd\.cwd/, 'pool 必须把该决定交给 workerLaunch(--rw 按它算)'); assert.match(pool, /resolvedCwd: turnCwd\.cwd/, 'pool 必须把该决定注入 job 给 worker'); // worker 侧:★ 这条断言改过一版,因为**第一版不具区分力**(我自己变异测出来的)。 // 第一版只断言"文件里出现 `job.session?.resolvedCwd`" —— 把消费那一支删掉、 // 退回 `job.session.cwd`,那条正则**仍然匹配得上**(因为别处还留着它), // 于是变异后测试依旧全绿 ⇒ 这条判据护不住它声称要护的东西。 // 现在断言的是**优先级**:每一处取 cwd 的表达式都必须先看 `resolvedCwd`。 const worker = readFileSync(join(ROOT, 'src', 'worker.mjs'), 'utf8'); const cwdExprs = [...worker.matchAll(/const cwd = ([^;]+);/g)].map((m) => m[1]); assert.ok(cwdExprs.length >= 1, '应当至少有一处取 cwd 的表达式'); for (const expr of cwdExprs) { assert.ok(expr.includes('resolvedCwd'), `取 cwd 的表达式必须先看父进程的决定(resolvedCwd),实际是:${expr.trim()}`); } // 新建分支那处同样是"父进程优先" assert.match(worker, /const turnCwd = job\.session\?\.resolvedCwd \|\| cwd/, '新建分支也必须以父进程的决定为准'); }); test('★ 等式:每个分支 `openSession({cwd:X})` 与随后的 `return {…cwd:X}` 必须是同一个符号', () => { // pi 评审 2026-09-15 §三 抓到的那一处:新建分支 `openSession` 用 `turnCwd`, // 而 `return { ...opened, cwd, reused: false }` 回报的是本地推导的 `cwd`。 // 这个返回值经 `session_opened` → `state.cwd`,而 `state.cwd` 正是下一轮 // `resolveTurnCwd` 的 `storedCwd`(下一轮 `--rw` 的输入)⇒ // 等式只在"本轮 rw vs 本轮 openSession"上闭合,**没在"本轮 rw vs 下一轮 rw"上闭合**。 // // 判据写成**配对**(同一手法:断言"同一个符号",而不是断言"出现过某个名字"—— // 后者正是我上一版判据不具区分力的原因)。 const worker = readFileSync(join(ROOT, 'src', 'worker.mjs'), 'utf8'); // ★ 第一版写法**不具区分力**(我又一次被自己的变异测出来):用两串正则分别抓 // `openSession` 的 cwd 与 `return` 的 cwd,结果因为 `matchAll` 的懒惰量词 // 只抓到**各一个**,而"只有一个"时包含关系天然成立 ⇒ 变异后照样全绿。 // 改为**按分支**配对:以每个 `return { ...opened, ... }` 为锚,回溯它前面最近的 // `openSession(` —— 这才是"这一支用了什么、又回报了什么"。 // ★ `\w+` 要求至少一个字符 ⇒ 抓不到简写 `cwd,`(实际三处里有两处是简写)。 // 量词写成 `*`:`cwd: X` 抓到 X,`cwd,` 抓到空串(= 简写,符号就是 cwd)。 const returns = [...worker.matchAll(/return \{ \.\.\.opened, cwd(?::\s*([\w.?]+))?\s*,\s*reused/g)]; assert.ok(returns.length >= 3, `应当抓到三处回报,实际 ${returns.length}`); for (const m of returns) { const before = worker.slice(0, m.index); const openIdx = before.lastIndexOf('openSession('); assert.ok(openIdx > 0, '每个回报前面应当有一个 openSession'); // 该 openSession 调用里取的 cwd 符号(`cwd` 或 `cwd: X`) const call = worker.slice(openIdx, openIdx + 260); const got = call.match(/cwd(?::\s*([\w.?]+))?\s*,/); assert.ok(got, `解析不出 openSession 的 cwd:${call.slice(0, 80)}`); const usedCwd = got[1] || 'cwd'; // `cwd,` 是简写 ⇒ 符号就是 cwd const reported = m[1] || 'cwd'; // 简写 `cwd,` ⇒ 回报的符号就是 cwd assert.equal(reported, usedCwd, `回报的必须与 openSession 实际用的是同一个符号:用了 ${usedCwd},回报了 ${reported}`); } }); test('★ 单点:复用判定只有一处实现,worker 不再自己 existsSync 一份', () => { // pi 评审 §三 的结构性那条:父进程与 worker 各写一份判定,是 `resolvedCwd` // 交错生效的前提 —— 两边一不一致,resolvedCwd 就会被交给不消费它的分支。 const reuse = readFileSync(join(ROOT, 'src', 'turn-cwd.mjs'), 'utf8'); assert.match(reuse, /export function resolveSessionReuse/, '判定的唯一实现应当在 turn-cwd.mjs'); const pool = readFileSync(join(ROOT, 'src', 'pool.mjs'), 'utf8'); assert.match(pool, /resolveSessionReuse\(/, 'pool 必须用这个唯一的判定'); const worker = readFileSync(join(ROOT, 'src', 'worker.mjs'), 'utf8'); // worker 消费父进程的判定;只有在父进程没给(老协议)时才退回自己判 assert.match(worker, /const decidedReused = job\.session\?\.sessionReused/, 'worker 必须消费父进程的复用判定'); assert.match(worker, /decidedReused === undefined/, '并且只在这个判定缺失时才退回自己判'); // ★ ③ 这条是 pi 让我补的,而它**前两版都有洞**(都是 pi 核出来的): // 第 1 版:判据名说"单点",实际只断言了"有没有消费父进程的结论" —— 名字替实现作证; // 第 2 版:改成"剥注释 + 只允许一种合法位置",但 // (a) **一次断言都没跑**:白名单等的是 `existsSync(`(带括号),而委托行写的是 // `exists: existsSync`(**传函数引用,不是调用**)⇒ `callLines` 是空数组, // 那个 for 循环**零次执行**。它能变红只是因为变异后那行**含** `existsSync(`。 // ⇒ **"判据跑没跑"从绿上看不出来**(判据自己也需要一条"我跑了"的判据); // (b) 白名单正则匹配不到它要放行的那一行:行里 `existsSync` 后面是 ` }` 再 `)`, // 正则却要求紧跟 `)`。今天无害(合法行进不了循环),但**是埋伏**: // 哪天有人写成等价的 `exists: (p) => existsSync(p)`,那行就进了循环、 // 白名单匹配不上 ⇒ **在正确的改动上变红**("红了但红错地方")。 // // 修法(pi 给的,两个洞一起堵):白名单锚在 `exists:\s*existsSync`(**不看括号**), // 并**先断言那一行存在**(这一条让它不再空转)。 // 剥注释仍然是前提:讲这件事的注释里就写着 existsSync。 const workerCode = worker.split('\n').filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)); const uses = workerCode.filter((l) => /existsSync/.test(l)); // ★ 白名单必须**只看"这一行把 existsSync 交给了唯一规则"**,不锚具体写法。 // 我按 pi 给的改法(`exists:\s*existsSync\b`)试过,**变异 C 仍然误红** —— // 因为等价的 `exists: (p) => existsSync(p) {…}` 里 `existsSync` 后面是 `(`,不匹配 `\b` 收尾。 // ⇒ 判据要断言的是"**同一行里既有 `exists:` 又有 `existsSync`**"(= 交给规则), // 而不是"按某种写法写出来"。这样:换等价写法不红;把 existsSync 用到别处仍红。 const isAllowed = (l) => /^\s*import\b/.test(l) || delegating(l); // ★ 这条与 `isAllowed` **必须用同一个谓词**:我第一次改的时候只改了 `isAllowed`、 // 把这条留成旧写法(`/exists:\s*existsSync\b/`),于是"等价写法"这条变异仍然误红 —— // 两处判据在描述同一件事却各写一份,正是这一路在消的形状("两处各写一份")。 const delegating = (l) => /exists:/.test(l) && /existsSync/.test(l); assert.ok(uses.some(delegating), 'worker 必须把 existsSync 交给唯一规则(这一行不能被删掉)—— 没有这一条,下面的断言可能空转'); assert.deepEqual(uses.filter((l) => !isAllowed(l)), [], `除 import 与"交给唯一规则"那一行外,worker 不该再用 existsSync;越界的行:${ JSON.stringify(uses.filter((l) => !isAllowed(l)).map((l) => l.trim()))}`); }); test('resolveSessionReuse:文件在/不在、cwd 有无,四种组合', () => { const exists = () => true; const no = () => false; assert.deepEqual(resolveSessionReuse({ sessionFile: '/f', storedCwd: '/c', exists }), { reused: true, reuseFile: '/f' }); assert.equal(resolveSessionReuse({ sessionFile: '/f', storedCwd: '', exists }).reused, false, '没有 storedCwd 不算复用(父进程下一轮 --rw 就没有依据)'); assert.equal(resolveSessionReuse({ sessionFile: '/f', storedCwd: '/c', exists: no }).reused, false, '文件不在不算复用'); assert.equal(resolveSessionReuse({ sessionFile: '', storedCwd: '/c', exists }).reused, false, '没有文件不算复用'); });