fix(桥): 「读不出来」被当成「不存在」—— 这一行把 dsh 的邮件通道整条弄断

`persistedCwd()` 原来是 `catch { return undefined }`,把 readSession 的**三种**
抛出情形压成同一个「磁盘上没有」。调用方只把 `undefined` 读作"可以 create":

  读失败(格式迁移拒绝 / 日志损坏)→ 当成不存在 → 走 create
  → 磁盘上**确实有**那个 id ⇒ `session "…" already exists`
  ⇒ 该会话的邮件全投不进去,而日志里只有 create 的错,
     **真正的读失败被那个 catch 吃掉了**。

2026-09-19 DSH 升到 0.1.5-rc.2 后 40 个 `mail-*` 会话全部命中。

修法:`readSession` 的报错**本来就带可区分的 code**
(`dsh-session-query` 的 `notFound()` 给 `SESSION_QUERY_SESSION_NOT_FOUND`;
格式/损坏给 `SESSION_QUERY_CORRUPT_SESSION` / `SESSION_QUERY_PERSISTENCE_FAILED`)。
现在**只有 `SESSION_QUERY_SESSION_NOT_FOUND` 返回 `undefined`**,其余一律抛出,
让原文错误浮到调用方 —— 不再降级成"不存在"。

判据 `test/persisted-cwd-not-found.test.mjs`(2 条,已进 `npm test` 门禁)钉的是
**区分本身**,不是"有没有 try/catch"。三个变异逐个验过:
  ① catch 改回无条件 `return undefined` ⇒ 红
  ② 任何抛错都返回 undefined ⇒ 红
  ③ `throw e` 提到 code 判断之前("不存在"也抛 ⇒ create 不可达)⇒ 红

★ 变异③第一次**没落在目标上**:全局正则命中了文件里第一个无关的
`catch (e: any)`,判据全绿 —— 于是把它写成自检里的一条断言(变异必须真的落下),
避免这条自检本身变成恒真的假判据。

顺带记两个事实:
- `src/index.ts` 是桥的真源,`dist/` 是部署产物(`.gitignore` 忽略);已 `tsc` 重建并在产物里复验。
- 姊妹桥(pi/opencode/zcode)不含 `persistedCwd`,本缺陷只在 dsh 这条链上。
This commit is contained in:
2026-09-25 04:15:37 +08:00
parent 1158681f01
commit 1de93feabc
2 changed files with 134 additions and 5 deletions

View File

@ -687,9 +687,26 @@ export function apply(ctx: any, config: PluginConfig): void {
* 「新开会话」那条路,用回同一个 `mail-<session_id>` —— 而那个 id 上一次
* 已经落过盘。只能问持久化层,因为这是重启后唯一还存在的事实来源。
*
* 读不到就当作不存在:`readSession` 在会话不存在、日志不可读、replay 校验
* 不过时都会抛。三种情形里只有第一种适合 create,但后两种 resume 也一样
* 救不回来 —— 那就让 create 去报它自己的错。
* ★★★ 2026-09-25:「读不出来」**不是**「不存在」(这行曾把邮件通道整条弄断)
*
* 原来这里 `catch { return undefined }` —— 把**三种**抛出情形压成同一个
* 「磁盘上没有」。而调用方只把 `undefined` 读作"可以 create",于是:
*
* 读失败(格式迁移拒绝 / 日志损坏) → 被当成"不存在" → 走 create
* → 可磁盘上**确实有**那个 id ⇒ `session "…" already exists`
* ⇒ 该会话的邮件全部投不进去,且日志里只有 create 的错,
* **看不到真正的读失败**(它在另一个 catch 里被吃掉了)。
*
* 实测(2026-09-19,DSH 0.1.5-rc.2 升级后):40 个 `mail-*` 会话全部命中,
* 整条邮件通道全断。`readSession` 的报错**本来就带可区分的 code**,
* 是这里丢掉的区别(`dsh-session-query`:`notFound()` 给
* `SESSION_QUERY_SESSION_NOT_FOUND`;格式/损坏给
* `SESSION_QUERY_CORRUPT_SESSION` / `SESSION_QUERY_PERSISTENCE_FAILED`)。
*
* 现在:**只有 `SESSION_QUERY_SESSION_NOT_FOUND` 才返回 `undefined`**(真的可以 create);
* 其余一律**抛出**(让原文错误浮到调用方,别再降级成"不存在")。
* 这与本仓反复吃到的同一形状:「我没看到」≠「不存在」——
* 一旦把读失败降级成"没有",bug 就从"报一句错"变成"静默走错分支"。
*/
async function persistedCwd(sessionId: string): Promise<string | undefined> {
const q: any = (ctx as any).get?.('sessionQuery');
@ -697,8 +714,12 @@ export function apply(ctx: any, config: PluginConfig): void {
try {
const snap = await q.readSession(sessionId);
return snap?.header?.cwd ?? '';
} catch {
return undefined;
} catch (e: any) {
// 只有「确实不在磁盘上」才允许 create;其余是**读失败**,必须出声。
if (e?.code === 'SESSION_QUERY_SESSION_NOT_FOUND') return undefined;
console.error(`[dsh-mail-bridge] 读会话 ${sessionId} 失败(**不是**"不存在",不降级为 create): `
+ `${e?.code || '(无 code)'}: ${e?.message || e}`);
throw e;
}
}