Files
MailUI4Agents/plugins/dsh-mail-bridge/test/persisted-cwd-not-found.test.mjs
JianFeeeee 753310d12f test(桥): 把「三个变异逐个验过」从**手跑**变成**判据**(原来文件里只钉了变异①)
pi 复核我那笔 `1de93fe` 时指出:提交信息里写了"三个变异逐个验过",
但判据文件里**只给变异①配了自检** —— 那三个是手跑的,手跑过一次 ≠ 以后还会红;
谁把断言改松,另两种变异不会有任何人发现。

原文件的问题(这次才看清):
- 变异②(`if (true) return undefined`:catch 形状不变、也"看"了 code,只是判断写反)
  与变异①在**判据层面完全同形** —— 只用"有没有 code 比较"去判,两者都抓不到;
- 变异③(`throw e` 提到 code 判断之前)**根本不改"是否区分"**:
  `distinguishes()` 对它恒为真。它测的是**顺序**,所以必须单独一条 `orderHolds()`。
  这正是我原来漏掉③的真正原因 —— 不是"忘了写",是**当时没有能表达它的判据**。

本次改动:
- 把判据抽成 `distinguishes()`(区分)与 `orderHolds()`(顺序)两条,`criterionPasses` 取合取;
- 三条变异各一条断言 + 一条**前置**(原样源码必须通过 ⇒ 否则三条自检恒假,等于没写);
- 每条变异都断言"真的落在 persistedCwd 内"(全局正则会命中文件里第一个无关的
  `catch (e: any)`,变异没落下而判据全绿 —— 本仓踩过,`assert.notEqual` 守住);
- 变异③额外断言 `distinguishes(mutated) === true`:**证明它测的是顺序而不是退化成①**。

验证(对**真源码**做变异,不是只喂文本):
  基线 pass=5 fail=0 | ①pass=0 fail=5 | ②pass=1 fail=4 | ③pass=3 fail=2 | 还原 pass=5 fail=0
  源码逐字节还原(cmp 过)。门禁 `npx tsc && npm test`:402/402。
2026-09-25 04:30:53 +08:00

173 lines
9.0 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 { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';
/*
* 「读不出来」不是「不存在」—— 这一行曾把 dsh 的邮件通道**整条弄断**。
*
* 事故(2026-09-19,DSH 升到 0.1.5-rc.2 后):`persistedCwd()` 的
* `catch { return undefined }` 把三种抛出情形压成同一个「磁盘上没有」,而调用方
* 只把 `undefined` 读作"可以 create"。于是格式迁移拒绝 → 当成不存在 → 走 create
* → 磁盘上**确实有**那个 id ⇒ `session "…" already exists` ⇒ 该会话的邮件
* 全部投不进去。40 个 `mail-*` 会话全部命中,且日志里只有 create 的错,
* **真正的读失败被那个 catch 吃掉了**。
*
* 为什么这个形状值得单独钉:它在"不报错的错"这一族里最典型 ——
* 读失败被降级成"没有",bug 就从"报一句错"变成"静默走错分支"。
* 本仓已经为同一形状付过几次学费(判据自匹配、`read_thread` 会话作用域、
* 「取链里第一张证书」),所以这里用判据把区别固定下来。
*
* 判据钉的是**区分本身 + 两支的顺序**,而不是"有没有 try/catch"——
* 后者在错误修法下照样能通过。
*
* ★ 2026-09-25(pi 复核时指出):原来这里只给**变异①**配了自检,而提交信息里
* 声称"三个变异逐个验过" —— 那三个是**手跑**的,文件里**只钉住了一个**。
* 手跑过一次 ≠ 以后还会红。现在三条变异 + 一条前置全部成为断言。
*/
const HERE = dirname(fileURLToPath(import.meta.url));
const SRC = readFileSync(join(HERE, '..', 'src', 'index.ts'), 'utf8');
/** 剥掉注释后再断言"代码里有什么"(本仓规范:判代码读剥离版,判理由读原文) */
const codeOnly = src =>
src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/[^\n]*/g, '$1');
/** 取某个函数/分支的正文(按花括号配对,不看窗口) */
function blockAt(src, needle) {
const at = src.indexOf(needle);
if (at < 0) return '';
const open = src.indexOf('{', at);
let depth = 0;
for (let i = open; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') { depth--; if (depth === 0) return src.slice(open + 1, i); }
}
return '';
}
const FN = 'async function persistedCwd(';
/** dsh-session-query 的分类里,「确实不存在」是**唯一**允许 create 的那个 */
const ABSENT = 'SESSION_QUERY_SESSION_NOT_FOUND';
/** catch 里那把「读失败」与「不存在」分开的判据(抽出来,才能喂变异后的文本) */
function distinguishes(src) {
const body = codeOnly(blockAt(src, FN));
return new RegExp(`code\\s*===\\s*['"\`]${ABSENT}['"\`]`).test(body)
&& /throw e\b/.test(body);
}
/**
* 顺序判据:「不存在」的 return 必须排在 `throw e` **之前**。
*
* 单独一条,因为**它抓的是一种 `distinguishes()` 抓不到的错**:
* 把 `throw e` 提到 code 判断之前,代码**仍然区分了** code(正则照样命中),
* 但「确实不存在」也会抛 ⇒ create 永远不可达 ⇒ 邮件会话再也建不出来。
*/
function orderHolds(src) {
const body = codeOnly(blockAt(src, FN));
const cb = body.slice(body.indexOf('catch'));
const at = cb.indexOf(ABSENT);
const th = cb.indexOf('throw e');
return at >= 0 && th > at;
}
/** 判据整体:既要区分,也要顺序对 */
const criterionPasses = src => distinguishes(src) && orderHolds(src);
test('★ persistedCwd 必须区分「不存在」与「读不出来」(只有前者能 create)', () => {
assert.ok(SRC.indexOf(FN) >= 0, '找不到 persistedCwd —— 结构变了,判据要跟着改');
const body = codeOnly(blockAt(SRC, FN));
assert.ok(body.length > 50, 'persistedCwd 正文太短,八成是结构变了');
// ① 存在性判据:必须**只**对"确实不存在"这个 code 返回 undefined
assert.match(body, new RegExp(`code\\s*===\\s*['"\`]${ABSENT}['"\`]`),
`只有 ${ABSENT} 才允许当成"磁盘上没有"。`
+ '少了这个 code 判断,就是把"读失败"和"不存在"又压回同一个返回值 —— '
+ '那正是让 40 个会话的邮件全投不进去的那一行。');
assert.match(body, /return undefined/,
'「不存在」这一支仍要返回 undefined(它是调用方 create 的唯一依据)');
// ② 非存在性:读失败必须**抛出**,不许静默吞掉
assert.match(body, /throw e\b/,
'读失败必须抛出(让原文错误浮到调用方)—— 降级成 undefined 会让 bug '
+ '从"报一句错"变成"静默走错分支",而日志里只剩 create 那句误导人的错。');
// ③ 顺序/可达性:throw 必须在 code 判断**之后**
assert.ok(orderHolds(SRC),
'`throw e` 必须排在 code 判断之后 —— 反过来的话,"确实不存在"也会抛,'
+ 'create 分支永远不可达(邮件会话再也建不出来)。');
// ④ 残余风险:catch 里不许出现无条件的 `return undefined`
const catchBlock = body.slice(body.indexOf('catch'));
assert.doesNotMatch(catchBlock, /catch\s*\([^)]*\)\s*\{\s*return undefined\s*;?\s*\}/,
'catch 里不许无条件 `return undefined` —— 那等于把这次的修复又改回去了。');
});
/*
* ── 变异自检:判据本身要能红 ──────────────────────────────────────
*
* 每条都遵循同一个形状(本仓踩过的坑):
* ① 变异必须**真的落在 persistedCwd 内** —— 用全局正则会命中文件里第一个无关的
* `catch (e: any)`,变异没落下、判据反而全绿(自检变成恒真的假判据);
* ② 变异后 `criterionPasses` 必须为**假**(判据能发现这个回归);
* ③ 原样源码必须为**真**(前置那条断言保证,否则这三条全是假判据)。
*/
/** 在 persistedCwd 的**函数体内**做替换,并断言替换真的发生了 */
function mutateInside(fnBody, pattern, replacement) {
const next = fnBody.replace(pattern, replacement);
assert.notEqual(next, fnBody,
'变异没落在 persistedCwd 内(模式未命中)—— 这种"变异无声失败"会让自检恒真。'
+ `\n模式: ${pattern}`);
return next;
}
/** 把变异后的函数体装回整份源码,供 blockAt 复用同一套解析 */
function spliceBody(fnBody) {
const at = SRC.indexOf(FN);
const bodyStart = SRC.indexOf('{', at) + 1;
return SRC.slice(0, bodyStart) + fnBody + SRC.slice(bodyStart + blockAt(SRC, FN).length);
}
test('★ 自检前置:原样源码必须通过判据(否则下面三条全是假判据)', () => {
assert.ok(criterionPasses(SRC),
'未变异的源码不通过判据 ⇒ 下面的变异自检恒假,等于没写。'
+ '先修被测代码或判据,再看变异。');
});
test('★ 变异①:catch 改回无条件 return undefined(事故版本)⇒ 判据必须红', () => {
const body = mutateInside(blockAt(SRC, FN),
/catch \(e: any\) \{[\s\S]*\n \}/, 'catch {\n return undefined;\n }');
assert.equal(criterionPasses(spliceBody(body)), false,
'把 catch 改回无条件的 `return undefined` 后,判据必须为假 —— 若仍为真,它发现不了这个回归。');
});
test('★ 变异②:任何抛错都返回 undefined(catch 形状不变、也"看"了 code,但判断写反)⇒ 必须红', () => {
// 与①的区别:这是更**像**修复的错法 —— 仍然比较 code,只是结果写反。
// 只按"有没有 code 比较"写松的判据会被它骗过。
const body = mutateInside(blockAt(SRC, FN),
/if \(e\?\.code === 'SESSION_QUERY_SESSION_NOT_FOUND'\) return undefined;/,
'if (true) return undefined;');
assert.equal(criterionPasses(spliceBody(body)), false,
'任何抛错都返回 undefined 时,判据必须为假。');
});
test('★ 变异③:throw 提到 code 判断之前("不存在"也抛 ⇒ create 永不可达)⇒ 必须红', () => {
// ★ 这条**不改"是否区分"** —— `distinguishes()` 对它恒为真。
// 只有 `orderHolds()` 能抓住它,所以两条判据缺一不可:
// 我原来就是只写了"区分"、漏了顺序,才让这个变异第一次跑出全绿。
const body = mutateInside(blockAt(SRC, FN),
/(\n)( if \(e\?\.code === 'SESSION_QUERY_SESSION_NOT_FOUND'\) return undefined;)/,
'$1 throw e;$1$2');
const mutated = spliceBody(body);
assert.equal(distinguishes(mutated), true,
'(前提)这个变异**保留**了 code 比较 —— 否则它测的就不是"顺序"了,'
+ '而是退化成变异①,这条自检便不再证明 orderHolds 的必要性。');
assert.equal(orderHolds(mutated), false,
'`throw e` 提前后,顺序判据必须为假。');
assert.equal(criterionPasses(mutated), false,
'判据整体必须为假 —— 反过来的话,连"确实不存在"也会抛,create 永远不可达。');
});