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。
This commit is contained in:
@ -19,8 +19,12 @@ import { fileURLToPath } from 'node:url';
|
||||
* 本仓已经为同一形状付过几次学费(判据自匹配、`read_thread` 会话作用域、
|
||||
* 「取链里第一张证书」),所以这里用判据把区别固定下来。
|
||||
*
|
||||
* 判据钉的是**区分本身**(哪些 code 允许 create、哪些必须抛),
|
||||
* 而不是"有没有 try/catch"——后者在错误修法下照样能通过。
|
||||
* 判据钉的是**区分本身 + 两支的顺序**,而不是"有没有 try/catch"——
|
||||
* 后者在错误修法下照样能通过。
|
||||
*
|
||||
* ★ 2026-09-25(pi 复核时指出):原来这里只给**变异①**配了自检,而提交信息里
|
||||
* 声称"三个变异逐个验过" —— 那三个是**手跑**的,文件里**只钉住了一个**。
|
||||
* 手跑过一次 ≠ 以后还会红。现在三条变异 + 一条前置全部成为断言。
|
||||
*/
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
@ -47,6 +51,31 @@ 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));
|
||||
@ -65,44 +94,79 @@ test('★ persistedCwd 必须区分「不存在」与「读不出来」(只有
|
||||
'读失败必须抛出(让原文错误浮到调用方)—— 降级成 undefined 会让 bug '
|
||||
+ '从"报一句错"变成"静默走错分支",而日志里只剩 create 那句误导人的错。');
|
||||
|
||||
// ③ 顺序/可达性:throw 不能被无条件提前(否则"不存在"也走不到 create)
|
||||
const codeAt = body.indexOf(ABSENT);
|
||||
const throwAt = body.indexOf('throw e');
|
||||
assert.ok(codeAt >= 0 && throwAt > codeAt,
|
||||
'`throw e` 必须在 code 判断**之后** —— 反过来的话,连"确实不存在"也会抛,'
|
||||
// ③ 顺序/可达性:throw 必须在 code 判断**之后**
|
||||
assert.ok(orderHolds(SRC),
|
||||
'`throw e` 必须排在 code 判断之后 —— 反过来的话,"确实不存在"也会抛,'
|
||||
+ 'create 分支永远不可达(邮件会话再也建不出来)。');
|
||||
|
||||
// ④ 残余风险:catch 里不许出现无条件的 `return undefined`
|
||||
// (写法上最容易的错:加了 code 判断,却把 return 留在最前面)
|
||||
const catchBlock = body.slice(body.indexOf('catch'));
|
||||
assert.doesNotMatch(catchBlock, /catch\s*\([^)]*\)\s*\{\s*return undefined\s*;?\s*\}/,
|
||||
'catch 里不许无条件 `return undefined` —— 那等于把这次的修复又改回去了。');
|
||||
});
|
||||
|
||||
/*
|
||||
* 变异自检:判据本身要能红。
|
||||
* ── 变异自检:判据本身要能红 ──────────────────────────────────────
|
||||
*
|
||||
* 不复用被测源码,而是喂给同一套断言"变异后的文本"—— 判据写松了(例如只找
|
||||
* `return undefined` 就算了)这里会立刻暴露。
|
||||
* 每条都遵循同一个形状(本仓踩过的坑):
|
||||
* ① 变异必须**真的落在 persistedCwd 内** —— 用全局正则会命中文件里第一个无关的
|
||||
* `catch (e: any)`,变异没落下、判据反而全绿(自检变成恒真的假判据);
|
||||
* ② 变异后 `criterionPasses` 必须为**假**(判据能发现这个回归);
|
||||
* ③ 原样源码必须为**真**(前置那条断言保证,否则这三条全是假判据)。
|
||||
*/
|
||||
test('★ 判据自检:把修复回退成事故版本,本文件必须能发现', () => {
|
||||
// 只在 persistedCwd **这个函数内部**换掉 catch —— 用全局正则会命中文件里
|
||||
// 第一个无关的 `catch (e: any)`(本仓实测踩过:变异没落在目标上,
|
||||
// 自检于是变成恒真的假判据)。
|
||||
|
||||
/** 在 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 body = blockAt(SRC, FN);
|
||||
const bodyStart = SRC.indexOf('{', at) + 1;
|
||||
const mutated = SRC.slice(0, bodyStart)
|
||||
+ body.replace(/catch \(e: any\) \{[\s\S]*\n \}/, 'catch {\n return undefined;\n }')
|
||||
+ SRC.slice(bodyStart + body.length);
|
||||
assert.notEqual(mutated, SRC, '变异必须真的落下去(否则下面那条自检恒真,等于没写)');
|
||||
const mutatedBody = codeOnly(blockAt(mutated, FN));
|
||||
const distinguishes = body => new RegExp(`code\\s*===\\s*['"\`]${ABSENT}['"\`]`).test(body)
|
||||
&& /throw e\b/.test(body);
|
||||
assert.equal(distinguishes(mutatedBody), false,
|
||||
'把 catch 改回无条件的 `return undefined`(事故版本)后,"区分"条件必须为假 —— '
|
||||
+ '若这里仍为真,说明判据松到发现不了这个回归。');
|
||||
// 反向:原样源码必须为真(否则这条自检恒假,等于没写)
|
||||
assert.ok(distinguishes(codeOnly(blockAt(SRC, FN))),
|
||||
'未变异的源码必须满足"区分"条件(否则这条自检恒假,等于没写)');
|
||||
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 永远不可达。');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user