Files
MailUI4Agents/plugins/pi-mail-bridge/test/pi-paths.test.mjs
JianFeeeee 011957ac97 refactor(pi-bridge): A 方案落地 —— 平台兜底值搬回平台侧,workspace.js 恢复逐字节相同
pi 定的 A(2026-09-15)。要点是:**契约的逃逸口不是豁免清单,而是
`resolveWorkspaceCwd(workspace, fallback)` 的第二个参数** —— 平台兜底值本来就该由平台侧传进去。
四个桥对照很清楚:

| 桥       | 平台兜底值在哪                                   | 怎么交给共用函数 |
|----------|--------------------------------------------------|------------------|
| opencode | 自己的 `index.js`                                | 传 `directory`   |
| zcode    | 自己的 `src/index.mjs`(`zcodeSessionFallback`) | 传进去           |
| dsh      | 共用的 `mailSessionFallback`(写死 `.dsh`)      | 直接用           |
| pi       | **原来造在共用模块里**(本次出的错)             | → 现在也传进去   |

⇒ `docs/PLUGIN-CONTRACT.md` 第 1150 行**不用改、也不该加旁路**:pi 只是唯一一个把平台值
造在共用模块里的,挪回平台侧就恢复了规矩。

**改动(与 pi 预测的形状一致)**:`workspace.js` 删掉那 15 行、`pool.mjs`/`worker.mjs`
各改一行 import,外加新文件 `src/paths.mjs`。`git diff --stat` 实测
`15 -` / `3 +-` / `3 +-` —— 没有多余改动。

**为什么新家是 `src/paths.mjs` 而不是 `lib/sandbox.js`**(pi 给了两个选项,我选前者):
`lib/` 按契约是"**候选共用**"目录,把一个 pi 专有文件放进去**正是这次出事的形状** ——
下一个人会问"它为什么不在 `ALL_LIBS` 里"。`src/` 下同名文件不会引起这个问题。
父子同源(worker 的沙箱 rw 由父进程算)由"两边 import 同一个模块"继续满足。

**验收四条(pi 给的,逐条实测)**:
1. `cmp opencode/lib/workspace.js pi/lib/workspace.js` **相同**;
   `check-shared-libs.sh` **exit 0**;`install.sh --check` **exit 0** ✓
2. 测试数**不降**:491 → **496**(+5,见下)✓
3. 给 `piMailFallback` **补测试**(原先一条都没有 —— 这正是当初的不对称:
   四个桥 `npm test` 全绿、只有 `check-shared-libs` 抓得到)→ 新增
   `test/pi-paths.test.mjs` 5 条 ✓
4. 四个调用点改 import 后 diff 只应是 import 行 + 删掉的那 15 行 ✓

**新测试为什么是独立文件**:`test/workspace.test.mjs` 在四个桥里**逐字节相同**
(md5 一致,属共用测试),往里加 pi 专有断言会把共用测试也弄分叉 —— 与 `lib/` 同一条规矩。

**判据含结构断言 + 行为断言**,并已按纪律先验区分力:
· 变异 1(把 `piMailFallback` 塞回共用模块 = 本次分叉的形状)⇒ 结构判据**变红**;
· 变异 2(把 `.pi` 改成 `.dsh`)⇒ 三条行为判据**变红**;
两次变异都用 `cp` 恢复并以 `cmp` 校验一致。

★ 顺带记下 pi 指出的一条:`check-deploy-drift.mjs` 的判据 ① 是我扩到"比全部 133 个文件"的,
所以这次分叉**它能抓到** —— 但 `check-shared-libs` 先红了,说明两道门的分工是对的。
2026-09-15 06:31:18 +08:00

77 lines
4.2 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 { piMailFallback } from '../src/paths.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(HERE, '..');
const HOME = homedir();
/**
* 这一组判据补的是一个**不对称**(pi 评审 2026-09-15):
* 当初把 `piMailFallback` 造在共用的 `lib/workspace.js` 里时,
* **四个桥的 `npm test` 全绿,只有 `check-shared-libs.sh` 抓得到**
* —— 因为 `piMailFallback` 一条测试都没有,没有任何东西读它。
*
* 为什么这个测试文件是**独立**的(不放进 `test/workspace.test.mjs`):
* 那个文件在四个桥里**逐字节相同**(md5 一致,属于共用测试),
* 往里加 pi 专有的断言会把共用测试也弄分叉 —— 与 `lib/` 是同一条规矩。
*/
test('★ piMailFallback 落在 ~/.pi/mail-sessions/<key>(不是 .dsh)', () => {
// 目录名必须是 `.pi`:落进 `~/.dsh/` 会让人以为是 DSH 在干活。
const p = piMailFallback('abc-123');
assert.equal(p, join(HOME, '.pi', 'mail-sessions', 'abc-123'));
// 反向断言:绝不能带 `.dsh`(这正是当初要分叉出来的原因,写死它防回归)
assert.ok(!p.includes('.dsh'), `兜底目录不该落在 dsh 名下:${p}`);
});
test('空 key 收敛成 default(而不是 undefined/literal 空串)', () => {
// 缺失 session_id 时不能拼出 `.../undefined` 或 `.../`:
// 那会让所有缺 key 的会话挤进同一个目录,且目录名像个 bug。
assert.equal(piMailFallback(undefined), join(HOME, '.pi', 'mail-sessions', 'default'));
assert.equal(piMailFallback(''), join(HOME, '.pi', 'mail-sessions', 'default'));
assert.equal(piMailFallback(null), join(HOME, '.pi', 'mail-sessions', 'default'));
});
test('数字/非字符串 key 也收敛成字符串(不产生 NaN 路径)', () => {
assert.equal(piMailFallback(42), join(HOME, '.pi', 'mail-sessions', '42'));
});
test('★ 结构判据:平台兜底值住在 src/,不在共用的 lib/ 里', () => {
// 这是这次分叉的**结构性**回归判据 —— 光测行为测不出"它住在哪"。
const pathsSrc = readFileSync(join(ROOT, 'src', 'paths.mjs'), 'utf8');
assert.match(pathsSrc, /export function piMailFallback/, '兜底函数应当在 src/paths.mjs');
const sharedWorkspace = readFileSync(join(ROOT, 'lib', 'workspace.js'), 'utf8');
assert.ok(!sharedWorkspace.includes('piMailFallback'),
'共用 lib/workspace.js 里不该再有平台专有的 piMailFallback(它就是分叉的原因)');
assert.ok(!sharedWorkspace.includes("'.pi'"),
'共用模块里不该出现 pi 的目录名常量');
// `.dsh` 仍留在共用模块里是**对的**:那是 dsh 自己的兜底值,
// 也是共用模块保真的"唯一一份"。规矩保的是"只有一份",不是"不许有平台数据"。
assert.ok(sharedWorkspace.includes('mailSessionFallback'),
'共用模块里 dsh 的兜底函数应当保留(它不是 pi 的,不该跟着搬)');
});
test('★ 父子同源:pool 与 worker 从同一个模块取兜底值', () => {
// 为什么这条要紧:worker 的沙箱 rw 清单由**父进程**(pool.mjs)算。
// 父进程猜一个目录、worker 落在另一个 ⇒ 症状是"界内也写不了"。
// 所以两个文件必须 import 同一个函数,而不是各自拼一份。
for (const f of ['pool.mjs', 'worker.mjs']) {
const src = readFileSync(join(ROOT, 'src', f), 'utf8');
assert.match(src, /import \{ piMailFallback \} from '\.\/paths\.mjs'/,
`${f} 应当从 ./paths.mjs import piMailFallback`);
// 反向:不能再从共用的 lib/workspace.js import 它
const workspaceImport = src.split('\n').find((l) => l.startsWith('import') && l.includes('workspace.js'));
assert.ok(workspaceImport !== undefined, `${f} 仍应从 lib/workspace.js import 别的函数`);
assert.ok(!workspaceImport.includes('piMailFallback'),
`${f} 不该再从共用模块 import piMailFallback:${workspaceImport}`);
}
});