diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index 83db880..f28b2fc 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -213,6 +213,54 @@ const ALLOW = [ /* { name, replacement, why } */ ]; ★★ 而它们共同的教训是:**每根轴的"底"看起来都像整体的底。** "到底了"永远只对**当前那一根轴**成立 —— 这条我等了三轮才学会,写在这里给下一个人。 +### 6.1.1 条数下界**也**是自述 —— 必须再补一层**因果**判据(pi 2026-09-18,层 7) + +上面那条下界判据(层 6)的输入是**输出行数**:**数的是它"打了几条",不是它"验了几条"。** +所以把函数体换成"循环打满 13 条空 `ok`、`return 0`"就**恰好绕过**它: + +``` +基线: red=9 … verdict=red +假 ok×13: red=9 … verdict=red ← 逐字相同、下界相关 RED 0 条(13 ≥ 下界 12) +假 ok×13 + UPSTREAM_RC['baseline-residue']=99 ⇒ 0 条 RED、0 次提及 UPSTREAM +对照(只改值、不掏空)⇒ red=9→10,真报「与真跑出来的 rc=1 不符」 +``` + +⇒ 根因一句话:**下界判据的输入是"自我报告"** —— 判据自己说它做了多少,而它可以撒谎。 +**锚点不能落在"被守对象的自述"上,必须落在它管不着的东西上。** + +★ 三种"看起来更硬"的替代都被否掉了,理由都是它们仍在自述这一侧: +· **黄金输出**(逐字节比对期望输出)= 又一份**手写数据**(本文件刚证过那族可被同步改); +· **扫源码找关键词**("源码里有那个断言")= "**提到 vs 就是**"(本文件反复证过它脆); +· **毒化环境变量**(该模式下每条自检必须报红)= 把"必须红"的断言写在**自检函数体内** + ⇒ **假体不打它**。实测:模拟毒化后假体 `red=9`、下界红 0 —— 与层 7 同一问题。 + +★★ **本仓采用的锚点是"外部因果":改坏被守的生产符号 → 那条自检必须红。** + +``` +· 假体(空 `ok`)在符号被改坏时仍然打 ok ⇒ 它不红 ⇒ **红**(判据抓它); +· 真自检读了那个符号 ⇒ 符号坏 ⇒ 它报 RED ⇒ 绿。 +⇒ 这是"自检真的读了那个符号"的**行为**证据,不是它的自述。 +``` + +实现要点(`run-all.mjs` 的 `CAUSAL` 表 + 快入口): + +- 快入口 `--only-selftest=<名>`:实测 **52~696ms/条** + (对比 `--X-selftest` 要跑整套 suite 的 4.3~4.9s); +- 隔离用 `mkdtempSync` + `cpSync(join(HERE), …)`(**只拷 `test/`,1.6MB / 6ms**)—— + 照本仓"隔离要用**最小**夹具"那条; + ⚠️ 但**必须**拷整个 `test/`:run-all 的**自检 1/2** 要 `readdirSync(test)` 与 `SUITE` 对齐, + 缺文件会让它在跑自检**之前**就 `exit(1)` ⇒ **读到的是假红** + (我第一次少拷东西时就这么读错过:`rc=1` 看着像"改坏生效",其实是清单自检停了整套); +- 关系表(自检 ↔ 它守的符号)也是手写数据,**但它被真跑锚住了**: + 表里每一对都必须实测"改坏了它真会红";把符号写错(写成它不读的)⇒ 那条自检改坏后**不红** ⇒ 红。 + ⇒ 表自己也被因果判据守着,这是它与"下界数字"的**关键区别**。 +- ⚠️ 锚文本的唯一性必须在**除去 `CAUSAL` 表本身**的源码里数, + 否则会**匹配到表里那一行字面量**(我第一版就踩了:probe/verdict 立刻报"不唯一", + 真因是表自己,不是生产里有两处)。★ 又是那句:"提到"与"就是"在文本上长得一样。 + +★ 层级表(**每层都是一根轴**):写法(4) → 存在性(5,"它还在吗") → 行为(6,"它做了吗") +→ **自述(7,"它说的话可信吗")**。 + ## 6.5 涉及**运行时行为**的结论,必须实测过才能写进规范/判据(pi 2026-09-14) 同一个错在本仓犯了两次,方向相反但错法相同:**从观察推断机制、没跑**。 diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 9262c56..14172ad 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -304,7 +304,9 @@ const brokens = []; const phaseWillRun = SUITE.map(([f]) => f) .filter(f => !(PHASE === 'install' && sideOf(f) === SIDE.SOURCE)); const skippedByPhase = []; +const ONLY_SELFTEST = (process.argv.find(a => a.startsWith('--only-selftest=')) || '').split('=')[1] || ''; for (const [file, flags, expected] of SUITE) { + if (ONLY_SELFTEST) { skippedByPhase.push(file); continue; } if (PHASE === 'install' && sideOf(file) === SIDE.SOURCE) { skippedByPhase.push(file); continue; @@ -2292,6 +2294,14 @@ if (process.argv.includes('--mutants-line-selftest')) process.exit(mutantsLineSe * ⇒ 任何"调用自检"的代码都必须排在**所有** `const` 表与自检函数定义之后。 * (所以这里紧贴生产区段入口:`let mutantsLine` 之前。) */ +if (ONLY_SELFTEST) { + const T = { 'probe-selftest': probeSelfTest, 'exitcode-selftest': exitcodeSelfTest, + 'skip-selftest': skipSelfTest, 'verdict-selftest': verdictSelfTest, + 'mutants-line-selftest': mutantsLineSelfTest }; + const fn = T[ONLY_SELFTEST]; + if (!fn) { console.error(`✗ 未知自检 ${ONLY_SELFTEST}`); process.exit(2); } + process.exit(fn() > 0 ? 1 : 0); +} let selfCheckRuns = 0; { const SELFTESTS = [ @@ -2623,6 +2633,94 @@ let selfCheckRuns = 0; + ' —— **存在性不等于在做事情**'); } } + /* + * ★★★ **因果锚点**:把被守的**生产符号**真的改坏,那条自检**必须红** + * (pi 2026-09-18 报的层 7)。 + * + * 层 6 我给的是"比条数下界",而条数是从**输出**里数的 —— + * **数的是它"打了几条",不是它"验了几条"**。实测: + * + * 把 `exitcodeSelfTest` 的体换成"循环打 13 条空 `ok`、`return 0`"(不打任何真断言) + * ⇒ `checks=459 pass=455 fail=4 skip=0 red=9 broken=0 unreported=0 verdict=red` + * 与基线**逐字相同**、下界相关 RED **0 条**(条数 13 ≥ 下界 12 ⇒ 相等,无从对比)。 + * 最尖形式(假 ok×13 + `UPSTREAM_RC['baseline-residue']=99`)⇒ 0 条 RED、0 次提及 UPSTREAM; + * **对照**(不掏空、只改值)⇒ `red=9→10` 且真报"与真跑出来的 rc=1 不符"。 + * + * ⇒ 根因一句话:**下界判据的输入是"自我报告"** —— 判据自己说它做了多少,而它可以撒谎。 + * 这与这条线是同一句:**锚点不能落在"被守对象的自述"上,必须落在它管不着的东西上。** + * + * ★ 我**没有**采用 pi 提的三种替代(黄金输出 / 扫源码关键词 / 毒化环境变量): + * · 黄金输出 = 又一份手写数据(刚被证过那族); + * · 扫源码关键词 = "提到 vs 就是"(本文件反复证过它脆); + * · 毒化模式 = 把"必须报红"的断言写在**自检函数体内** ⇒ **假体不打它**。 + * 我实测过:模拟毒化后假体 `red=9`、下界红 0 —— 与层 7 同一问题(仍是自述)。 + * ⇒ 三种都仍在"被守对象自己说了算"这一侧。所以锚点改成**外部因果**: + * + * **改坏生产符号 → 那条自检必须红。** + * · 假体(空 `ok`)在符号被改坏时**仍然打 ok** ⇒ 它不红 ⇒ **红**(判据抓它); + * · 真自检读了那个符号 ⇒ 符号坏 ⇒ 它报 RED ⇒ 绿。 + * ⇒ 这是"**自检真的读了那个符号**"的**行为**证据,不是它的自述。 + * + * ★ 成本:实测单条自检的**快入口** `--only-selftest=<名>` 是 **52~696ms** + * (对比 `--X-selftest` 要跑整套 suite 的 4.3~4.9s),`cp -a test` 是 6ms。 + * 隔离用 `mkdtempSync` + `cpSync`(**只拷 `test/`,1.6MB**)—— + * 照本仓那条"隔离要用最小夹具"(整棵复制会换掉判据的输入)。 + * ⚠️ 但这里**必须**拷 `test/` 整份:run-all.mjs 的**自检 1/2** 要 `readdirSync(test)` + * 与 `SUITE` 对齐,缺文件会让它在跑自检**之前**就 exit(1) ⇒ 读到的是假红。 + * (我第一次少拷东西时就这么读错过:rc=1 看着像"改坏生效",其实是清单自检停了整套。) + * + * ★ 关系表(自检 ↔ 它守的生产符号)也是手写数据 —— **但它现在被真跑锚住了**: + * 表里每一对都必须实测"改坏了它真会红";写错了(比如把符号写成它不读的) + * ⇒ 那一条自检在改坏后**不会红** ⇒ **红**。⇒ 表自己也被因果判据守着。 + */ + const CAUSAL = [ + ['probe-selftest', "const probeIsDue = v => v === true || v === 'unknown';", + 'const probeIsDue = v => v === true;'], + ['skip-selftest', 'return [...(out || \'\').matchAll(/^(?:ok|not ok) \\d+ - (.+?) # SKIP(.*)$/gm)]', + 'return [...(out || \'\').matchAll(/^(?:ok|not ok) \\d+ - (.+?) # SKIPPED(.*)$/gm)]'], + ['verdict-selftest', "? 'green' : 'red';", "? 'red' : 'red';"], + /* + * ★ 这一条是 pi 点名的**最尖形式**:`UPSTREAM_RC` 是"真脚本真跑"那个锚点的表。 + * 把 `baseline-residue` 的值改坏 ⇒ `--exitcode-selftest` **必须红** + * (它真跑 `summary.py` 拿 rc 来比)。假体不读表 ⇒ 仍然全绿 ⇒ 被这条抓住。 + */ + ['exitcode-selftest', " 'baseline-residue': 1,", " 'baseline-residue': 99,"], + ]; + for (const [name, from, to] of CAUSAL) { + const tmp = mkdtempSync(join(tmpdir(), 'am-causal-')); + try { + cpSync(join(HERE), join(tmp, 'test'), { recursive: true }); + const target = join(tmp, 'test', 'run-all.mjs'); + const src = prose(target); + /* + * ⚠️ 必须在**除去这张因果表本身**的源码里数唯一性 —— + * 否则锚文本会**匹配到它自己那一行**(我第一版就踩了:probe/verdict 两条 + * 立刻报"锚文本不唯一",而真因是**表里的字面量**,不是生产里有两处)。 + * ★ 又是本文件那句:"提到"与"就是"在文本上长得一样。 + */ + const CAUSAL_BLOCK_RE = /const CAUSAL = \[[\s\S]*?\n \];/; + const searchable = src.replace(CAUSAL_BLOCK_RE, (m) => m.replace(/[^\n]/g, ' ')); + if (searchable.split(from).length - 1 !== 1) { + console.log(`RED 因果判据的锚文本在源码里不唯一(${name})⇒ 这条判据没在测它以为的东西`); + reds.push(`(自检)因果锚文本不唯一:${name}`); + continue; + } + writeFileSync(target, src.replace(from, to)); + const got = spawnSync(process.execPath, [target, `--only-selftest=${name}`], + { encoding: 'utf8', cwd: ROOT }); + if (got.status === 0) { + console.log(`RED 把 \`${name}\` 守的生产符号改坏之后,它**仍然全绿**` + + ' ⇒ 它没在读那个符号(或它是个空壳)'); + reds.push(`(自检)**因果判据**:改坏生产符号后 \`${name}\` 不红 —— 它没在读它`); + } else if (got.status !== 1) { + console.log(`RED 因果判据跑 \`${name}\` 时退出码是 ${got.status}(不是 1)` + + ' ⇒ 它不是"报红",是崩了/环境不对,这个读数无效'); + reds.push(`(自检)因果判据里 \`${name}\` 退出码异常:${got.status}`); + } + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + } const SAMPLE_FAKE_NAMES = ['fooSelfTest']; const allNames = [...new Set([...selfSource.matchAll(/\b(\w+SelfTest)\b/g)].map(m => m[1]))]; const nameUnwired = allNames.filter(n => !wired.has(n) && !SAMPLE_FAKE_NAMES.includes(n));