From 73065664ddda1e8546e3242179726f5a8fe45978 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 25 Sep 2026 04:45:44 +0800 Subject: [PATCH] =?UTF-8?q?test(=E6=A1=A5):=20=E5=88=A4=E6=8D=AE=E4=BB=8E?= =?UTF-8?q?=E3=80=8C=E7=89=87=E6=AE=B5=E5=AD=98=E5=9C=A8=E3=80=8D=E6=94=B6?= =?UTF-8?q?=E7=B4=A7=E5=88=B0=E3=80=8C=E7=BB=93=E6=9E=84=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E3=80=8D=E2=80=94=E2=80=94=20=E5=AF=B9=E6=8A=97=E6=80=A7?= =?UTF-8?q?=E5=8F=98=E5=BC=82=E2=91=A3=E6=89=BE=E5=87=BA=E6=88=91=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E7=9A=84=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 复跑变异①报 `pass=1 fail=4`(我报 0/5)。两个数**都对**,但说的是**两种不同变异**: · 变体A(我上封跑的)整段替换成 `catch { return undefined }` ⇒ 0/6 · 变体B(pi 跑的)保留 `catch (e: any)` 外形、只删 code 判断 ⇒ 1/5 两者的差别正是"catch 外形" —— 我上一封没把变异**写清楚**,这是我的表述问题。 事故版本的原文(`1de93fe^`)是 `} catch {\n return undefined;`,即**变体A**。 ★ 更重要:顺着 pi 的复跑做**对抗性变异**,我找到了自己判据的一个真洞 —— } catch (e: any) { if (e?.code === ABSENT) { /* 什么都不做 */ } ← 片段"在" return undefined; ← 读失败被降级 = **事故本身** throw e; ← 不可达 } 旧的 `distinguishes() && orderHolds()` 对**全绿**:code 比较在、`throw e` 在、 且 ABSENT 排在 throw 之前 —— 三条"**片段存在**"判据全过,而语义已经是事故。 ⇒ "片段在不在"与"结构对不对"是**两种性质**(与 orderHolds 同族),必须分开表达。 本次收紧: - 新增 `catchInner()`(按花括号取 catch 内层正文,不会误取函数体); - 新增 `absentBranchReturnsUndefined()`:「不存在」那支必须**真的** `return undefined` (空转守卫 `{}` 不算); - 新增 `unreachableThrowFree()`:`throw e` 必须**可达**(它前面与"不存在"之前 不许有无条件的 `return undefined`); - `criterionPasses` = 区分 && 顺序 && 上面两条结构判据; - 新增测试 ④,并**先断言它骗得过旧判据**(`distinguishes` 真、`orderHolds` 真)—— 否则这条自检就不证明那个洞存在。 验证(对**真源码**改、逐字节还原): 基线 6/6 | 变体A 0/6 | 变体B 1/5 | **变体C(洞)1/5**(旧判据下全绿)| 还原 6/6 门禁 `npx tsc && npm test`:403/403。 --- .../test/persisted-cwd-not-found.test.mjs | 104 +++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/plugins/dsh-mail-bridge/test/persisted-cwd-not-found.test.mjs b/plugins/dsh-mail-bridge/test/persisted-cwd-not-found.test.mjs index 03c73a9..023b4c4 100644 --- a/plugins/dsh-mail-bridge/test/persisted-cwd-not-found.test.mjs +++ b/plugins/dsh-mail-bridge/test/persisted-cwd-not-found.test.mjs @@ -73,8 +73,64 @@ function orderHolds(src) { return at >= 0 && th > at; } -/** 判据整体:既要区分,也要顺序对 */ -const criterionPasses = src => distinguishes(src) && orderHolds(src); +/** + * 取 `catch (...) { … }` 的**内层正文**(花括号配对,不会误取到函数体)。 + * + * ★ 2026-09-25 新增:`distinguishes()` 那套是「**这些片段在不在**」, + * 而它能被一个**语义空转**的变体骗过 —— + * + * if (e?.code === ABSENT) { /* 什么都不做 *\/ } + * return undefined; ← 读失败也降级了(= 事故本身) + * throw e; ← 不可达 + * + * 这个变体里 `code === ABSENT` 在、`throw e` 在、且 ABSENT 还排在 throw 前面 + * ⇒ 旧判据**全绿**,而代码已经是"读失败静默变不存在"。这条洞是我自己在 + * 对抗性变异里试出来的(不是被别人的例子提醒的),所以下面把判据从 + * **"片段存在"** 收紧成 **"结构正确"**:守卫必须**真的**返回、"抛"必须**可达**。 + */ +function catchInner(src) { + const body = codeOnly(blockAt(src, FN)); + const ci = body.indexOf('catch'); + if (ci < 0) return ''; + const ob = body.indexOf('{', ci); + let depth = 0; + for (let i = ob; i < body.length; i++) { + if (body[i] === '{') depth++; + else if (body[i] === '}') { depth--; if (depth === 0) return body.slice(ob + 1, i); } + } + return ''; +} + +/** + * 「不存在」那一支必须**真的**返回 undefined。 + * + * 允许 `if (c) return undefined;` 与 `if (c) { return undefined; }` 两种写法, + * 但**不接受**空转守卫(`{ }`)或不带 return 的守卫 —— 那正是上面那个骗过旧判据的变体。 + */ +const absentGuard = () => + new RegExp(`if\\s*\\(\\s*e\\?\\.code\\s*===\\s*['"\`]${ABSENT}['"\`]\\s*\\)\\s*(?:\\{\\s*)?return undefined\\s*;(?:\\s*\\})?`); + +const absentBranchReturnsUndefined = src => absentGuard().test(catchInner(src)); + +/** + * 读失败必须**可达地**抛出:把「不存在」那一支摘掉之后,catch 里仍要有 `throw e`, + * 且它**前面不能有无条件的 `return undefined`**(那会让 throw 永不可达 ⇒ 又回到事故)。 + */ +function unreachableThrowFree(src) { + const inner = catchInner(src); + const g = absentGuard().exec(inner); + if (!g) return false; + const before = inner.slice(0, g.index); + const after = inner.slice(g.index + g[0].length); + const th = after.indexOf('throw e'); + if (th < 0) return false; + const between = after.slice(0, th); + return !/return\s+undefined/.test(before) && !/return\s+undefined/.test(between); +} + +/** 判据整体:区分(片段)+ 顺序 + **结构**(守卫真的返回、抛真的可达) */ +const criterionPasses = src => distinguishes(src) && orderHolds(src) + && absentBranchReturnsUndefined(src) && unreachableThrowFree(src); test('★ persistedCwd 必须区分「不存在」与「读不出来」(只有前者能 create)', () => { assert.ok(SRC.indexOf(FN) >= 0, '找不到 persistedCwd —— 结构变了,判据要跟着改'); @@ -103,6 +159,16 @@ test('★ persistedCwd 必须区分「不存在」与「读不出来」(只有 const catchBlock = body.slice(body.indexOf('catch')); assert.doesNotMatch(catchBlock, /catch\s*\([^)]*\)\s*\{\s*return undefined\s*;?\s*\}/, 'catch 里不许无条件 `return undefined` —— 那等于把这次的修复又改回去了。'); + + // ⑤ ★ 结构判据(2026-09-25 补):光有"片段"不够,守卫必须**真的**返回、抛必须**可达**。 + // 否则 `if (c) { /* 空转 */ } return undefined; throw e;` 这种变体会全绿而过 —— + // 它语义上**就是事故本身**(读失败静默变"不存在")。这条是我用对抗性变异自己试出来的。 + assert.ok(absentBranchReturnsUndefined(SRC), + `「${ABSENT}」那一支必须**真的** ` + '`return undefined`(不能是空转守卫)——' + + '否则"不存在"不再返回 undefined,调用方永远走不到 create。'); + assert.ok(unreachableThrowFree(SRC), + '`throw e` 必须**可达**:它前面(以及"不存在"那支之前)不许有无条件的 `return undefined`。' + + '一旦不可达,读失败就又静默降级成"不存在"了 —— 这正是事故的形状。'); }); /* @@ -170,3 +236,37 @@ test('★ 变异③:throw 提到 code 判断之前("不存在"也抛 ⇒ cre assert.equal(criterionPasses(mutated), false, '判据整体必须为假 —— 反过来的话,连"确实不存在"也会抛,create 永远不可达。'); }); + +test('★ 变异④(对抗性,我自己试出来的洞):守卫**空转** + 无条件 return undefined + 不可达 throw ⇒ 必须红', () => { + // 2026-09-25:pi 复跑后我顺手拿"语义等价但形状不像"的变体做对抗测试,发现旧判据有洞: + // + // } catch (e: any) { + // if (e?.code === ABSENT) { /* 什么都不做 */ } ← 片段"在" + // return undefined; ← 读失败被降级(= 事故本身) + // throw e; ← 不可达 + // } + // + // 旧判据(distinguishes && orderHolds)对它**全绿**:code 比较在、throw e 在、 + // ABSENT 还排在 throw 前面 —— 三条"片段存在"判据全过,而语义已经是事故。 + // ⇒ "片段在不在"与"结构对不对"是两种性质,必须分开表达(与 orderHolds 同族的教训)。 + let body = mutateInside(blockAt(SRC, FN), + /if \(e\?\.code === 'SESSION_QUERY_SESSION_NOT_FOUND'\) return undefined;/, + "if (e?.code === 'SESSION_QUERY_SESSION_NOT_FOUND') { /* 空转:什么都不做 */ }"); + body = mutateInside(body, /^(\s*)throw e;$/m, '$1return undefined;\n$1throw e;'); + const mutated = spliceBody(body); + + // 前提:这个变体**确实**骗得过旧的两条判据(否则它没测到那个洞) + assert.equal(distinguishes(mutated), true, + '(前提)变异④ 保留了 code 比较与 `throw e` 片段 ⇒ `distinguishes()` 仍为真。'); + assert.equal(orderHolds(mutated), true, + '(前提)ABSENT 仍排在 `throw e` 之前 ⇒ `orderHolds()` 也为真。' + + '正因如此,旧判据全绿 —— 这就是那个洞。'); + + // 新判据必须抓住它 + assert.equal(absentBranchReturnsUndefined(mutated), false, + '守卫空转(`{ }` 而非 `return undefined`)时,结构判据必须为假。'); + assert.equal(unreachableThrowFree(mutated), false, + '`throw e` 前出现无条件 `return undefined`(=> 不可达)时,结构判据必须为假。'); + assert.equal(criterionPasses(mutated), false, + '判据整体必须为假 —— 否则"读失败静默变不存在"(事故本身)可以全绿通过。'); +});