diff --git a/client/electron/test/criteria-hygiene.test.mjs b/client/electron/test/criteria-hygiene.test.mjs index 0138c18..e3f57b2 100644 --- a/client/electron/test/criteria-hygiene.test.mjs +++ b/client/electron/test/criteria-hygiene.test.mjs @@ -24,7 +24,7 @@ import assert from 'node:assert/strict'; import { readdirSync, unlinkSync, writeFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; -import { basename, dirname, join, relative, resolve } from 'node:path'; +import { dirname, join, relative, resolve } from 'node:path'; import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; @@ -756,13 +756,34 @@ test('★ 非门禁工具(deploy/*.sh)必须有发现路径(至少一处 * 判据红/绿的原因变成了**它自己怎么写这段说明**,而不是被测对象的状态。 * ⚠️ 泛化:**任何"扫全仓找引用"的判据都必须排除观察者本身**, * 否则"描述缺陷"与"存在引用"不可区分(这条与 `stripComments` 那条同源)。 + * + * ★★★ 还有**第二个**排除,它常被误认成同一个(pi `661416f3` 就认错了,我实测反驳): + * 这里有两处 `continue`,**职责不同**: + * · `SCAN` 那一层(上面 `.filter(resolve(p) !== …import.meta.url)`)排除的是**观察者** + * —— 本判据自己,理由见上。 + * · 这一层(`resolve(join(DEPLOY, t))`)排除的是**被测工具自己的文件** —— + * 每个工具的头注释都写自己的名字(实测 5/5 各 1 处),若不排除, + * **"自名"就会被算成"发现路径"** ⇒ 每个孤儿自己证明自己可达 ⇒ 永久假绿。 + * ⇒ ★ 所以这一处**不能**改成"排除观察者",那是**删掉**本排除(实测: 真孤儿立刻漏报)。 + * ★ 但"按名字(basename)"确实不够 —— 那是 pi 那封里**唯一成立**的部分: + * 若别处出现与工具同 basename 的文件(如 `docs/prune-demo.sh`), + * `basename(p) === t` 会**把它也跳过** ⇒ 那处引用白算 ⇒ **假红**(实测: 唯一引用在 + * 同名他文件里时,按名字的版本误报孤儿;按身份的版本判绿 ✓)。 + * ⇒ 结论(比 pi 的建议严一格,且不丢本排除): **按身份比对被测工具自己的路径**。 */ + const SELF_FILE = resolve(fileURLToPath(import.meta.url)); + /** 反空真: 两处排除必须真的指向不同对象,合并了就会退化成"删掉一个"。 */ + assert.notEqual( + resolve(join(DEPLOY, tools[0])), SELF_FILE, + '两处排除指向了同一个文件 ⇒ 本判据的排除逻辑坏了(观察者 == 被测工具?)。', + ); const orphans = []; for (const t of tools) { let refs = 0; for (const p of SCAN) { - if (basename(p) === t) continue; // 排除自身 + // 排除**工具自己的文件**(按身份,不按名字 —— 按名字会漏掉同名他文件里的引用) + if (resolve(p) === resolve(join(DEPLOY, t))) continue; if (prose(p).includes(t)) refs++; } if (refs === 0) orphans.push(t);