Files
MailUI4Agents/plugins/pi-mail-bridge/test/turn-cwd.test.mjs
JianFeeeee a363bab773 fix(pi-bridge): 判据 ③ 两个洞 —— 它此前**一次断言都没跑**,且白名单在等价写法上误红
pi 复核 `00df6be` 时说这次三件都真落了,但顺手核出**判据 ③ 自己**还有两个洞。
我逐条复现,**两条都成立**:

## 洞 1:它在当前代码上**零次断言**(空转)

```
sed 's://.*::' worker.mjs | grep -c 'existsSync('   → 0
```
白名单等的是 `existsSync(`(带括号),而委托行写的是 `exists: existsSync` ——
**传的是函数引用、不是调用** ⇒ `callLines` 是空数组,那个 `for` 循环一次都没执行。
它能变红,只是因为变异后那行**含** `existsSync(`。
⇒ **"判据跑没跑"从绿上看不出来**(判据自己也需要一条"我跑了"的判据)。
这是 pi 这一路在挑的那件事的又一形态,只是这次被挑的是**我的判据的空转**。

## 洞 2:白名单正则匹配不到它要放行的那一行

合法委托行里 `existsSync` 后面是 ` }` 再 `)`,而正则要求紧跟 `)` ⇒ `false`。
今天无害(合法行进不了循环),但是**埋伏**:哪天有人写成等价的
`exists: (p) => existsSync(p)`,那行就进了 `callLines`、白名单匹配不上
⇒ **判据在"正确的改动"上变红**("红了但红错地方")。

## 改法与实测(三个变异,含一条"不该红"的)

先断言"委托那一行存在"(这条让洞 1 不再可能),白名单改为
**"同一行里既有 `exists:` 又有 `existsSync`"**(不锚具体写法):

| 变异 | 期望 | 实测 |
|---|---|---|
| 基线 | 绿 | **8/8** |
| A:删掉委托行 | 红(旧版会静默变绿) | **f 1** |
| B:加一处独立 `existsSync(given)` 调用 | 红 | **f 1** |
| C:等价写法 `exists: (p) => existsSync(p)` | **绿** | **f 1 → 已修 → 8/8** |

★ 变异 C 第一次仍然红,原因值得记:我按 pi 给的改法只改了 `isAllowed`,
**把另一条断言留成旧写法** —— 两处判据在描述同一件事却各写一份,
正是这一路在消的形状。现在两处共用同一个 `delegating` 谓词。

验证:pi 桥 509/509;三个变异行为如上(`cp` 恢复 + `cmp` 校验)。
2026-09-15 10:26:14 +08:00

198 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
'没有文件不算复用');
});