fix(pi-bridge): 回报的 cwd 必须是实际用的那个 + 复用判定收成一处(pi 评审 §三)
pi 2026-09-15 §三 报的两条,我都逐行核了,**都成立**。
## 一、新建分支回报的 cwd ≠ 它实际用的 cwd(他给的最小修法)
```js
const opened = await openSession({ cwd: turnCwd, … }); // ← 用的是 turnCwd
return { ...opened, cwd, reused: false }; // ← 回报的是本地推导的 cwd
```
这个返回值经 `session_opened` → `state.cwd`,而 `state.cwd` **正是下一轮
`resolveTurnCwd` 的 `storedCwd`**(也即下一轮 `--rw` 的输入)。
⇒ `7fe2796` 建立的那条"**记下来的必须是实际用的**"不变量在这一支上不成立:
等式只在"本轮 rw vs 本轮 openSession"上闭合,**没在"本轮 rw vs 下一轮 rw"上闭合**。
改成 `return { ...opened, cwd: turnCwd, reused: false }`。
★ 可达性我说实话:**窄**。要 `resolvedCwd !== 本地 cwd` 得"父进程判复用而 worker 落到
新建分支",目前只有"父进程判完之后会话文件消失"这条 TOCTOU 窗口能造出来。
所以它现在**不是 bug,是一条会随别人改动而变成 bug 的不变量缺口** —— pi 的定性准确,
我照他的定性记,不夸大。
## 二、复用判定两处各写一份(结构性,而且是上面那条的前提)
```
pool : state.sessionFile && state.cwd && existsSync(state.sessionFile)
worker : given && existsSync(given)
```
这正是前两轮刚消掉的那种"两处各写一份",而且它决定了 `resolvedCwd` 会不会被交给
一个**不消费它的分支** —— 上面那条能出问题,根子在这儿。
新增 `src/turn-cwd.mjs` 的 `resolveSessionReuse({sessionFile, storedCwd, exists})`
(**唯一一处实现**,`exists` 注入以便判据覆盖"在/不在"两种情形),
pool 用它判、并把结论一并注入 job(`session.sessionReused`),worker **消费**它。
## 三、判据(pi 建议的两条,都做了,且都验过区分力)
1. **等式/配对**:按分支回溯 —— 以每个 `return { ...opened, cwd:? X, reused` 为锚,
回溯它前面最近的 `openSession(`,断言**同一个符号**。
★ 这条我**写坏过两次**,两次都是变异测出来的,都记在测试文件里:
· 第一版用两串正则分别抓,`matchAll` 的懒惰量词**只抓到各一个**,
而"只有一个"时包含关系天然成立 ⇒ 变异后照样全绿;
· 第二版修好配对后,`[\w.?]+` 要求**至少一个字符** ⇒ 抓不到简写 `cwd,`
(实际三处里两处是简写)⇒ 报"应当抓到三处,实际 1"。
⇒ **判据红了要查清是产线错了还是判据错了**;这两次都是判据错,不是产线错。
2. **单点**:`resolveSessionReuse` 必须是唯一实现;pool 必须用它;worker 必须消费
`job.session.sessionReused`,且只在它缺失时才退回自己判。
3. 另加 `resolveSessionReuse` 四种输入组合(文件在/不在 × cwd 有/无)。
**变异实测(三条,均 `cp` 恢复 + `cmp` 校验)**:
· 把回报改回 `cwd`(= pi 报的那个 bug)⇒ 配对判据**变红**;
· worker 又自己判一份(`decidedReused = undefined`)⇒ 单点判据**变红**;
· pool 绕回两处各写一份 ⇒ 单点判据**变红**。
## 四、一处我要标出来的(结构上被保留、实际不可达的分支)
worker 里那条保底分支 `decidedReused === undefined ? 自己判 : 消费父进程的`
**实际上走不到**:`sessionReused` 为真要求 `state.sessionFile && state.cwd`,
而这两个字段只在 `session_opened` 里被**一起**写入 ⇒ 有 `sessionReused` 就必有 `sessionFile`。
保留它是为了老协议/异常帧不至于静默落到"新建会话"(比报错更糟),
但它**没有判据覆盖**,也没法用真协议触发 —— 按"跑不到的分支"记账,不假装它被验过。
验证:pi 桥 **505/505**(+3);`check-shared-libs` exit 0;`install.sh --check` exit 0;
`drift` 报 5 处待部署(与先前一致 —— 本轮只改已有文件,未新增文件)。
This commit is contained in:
@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
import { resolveTurnCwd } from '../src/turn-cwd.mjs';
|
||||
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';
|
||||
@ -98,3 +98,66 @@ test('★ 结构判据:pool 算 launch 与 worker 取值必须来自同一个
|
||||
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/, '并且只在这个判定缺失时才退回自己判');
|
||||
});
|
||||
|
||||
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,
|
||||
'没有文件不算复用');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user