test(判据): criteria-hygiene 的 AGC 探针自检改用临时仓合成对照

## 为什么要改

那条判据("AGC 真身从未进过远端历史")的自检原本要求
**本地可达历史里确实有该路径**,用它证明 `git log -- <路径>` 这套查法可用。

★ 那个前提**已经不成立**了:真身**从未被提交过**
  (`client/harmony/.gitignore:26` 一直在挡它),所以本地历史里
  本来就查不到 ⇒ 这条判据**永久红、且无法自查**。
  (注释里引用的 `7647c24` / `320c93f` 在本树也**不存在**。)

而判据的分诊早已确认:真身确实从未进过历史(被 ignore 正确挡住),
红的是**探针的假设**失效,不是缺陷存在。

## 改法:合成阳性 + 阴性对照

在**临时仓**(`mkdtemp`)里造两个提交 —— 一个含待查路径、一个不含 ——
对两者跑同一套查法,断言**双向有分辨力**。临时仓不碰本仓任何状态
(`GIT_CONFIG_GLOBAL=/dev/null` 避免读用户配置),造完即删。

为什么**必须**有阳性对照:一条用来抓泄露的判据,
**正确工作**时恰好永远看到"空"(没泄露 ⇒ 查不到)。
**"真值恰好是空"与"查法坏了"在输出上同形**(都是空串 + exit 0)
⇒ 真实历史里没有阳性样本可用,只能现造。

## ★ 阴性对照第一版写错了(变异测试打出来才发现)

我先写成查一个**真实存在**的无关文件 `unrelated.txt`。
变异把它改成查阳性那个 `leaf.json`,判据**照样绿** ——
因为查 unrelated 本来就该命中,那不叫"查法在乱报"。

⇒ 阴性对照要证明的是「**不存在的**目标查不到」,也就是**查法有边界**。
改成查 `no-such-file-ever.json`,并**额外**验一次 sanity
(真实但无关的文件**应当**查得到)—— 两个方向都对才算有分辨力。

改后双向变异都能打红(阳性查不到 ⇒ 红;阴性恒命中 ⇒ 红)。
This commit is contained in:
2026-09-28 08:27:19 +08:00
parent 18b148e476
commit 21332de5e7
2 changed files with 128 additions and 11 deletions

View File

@ -22,8 +22,9 @@
* 以及探测器要能认出裸调用(否则"都没有"与"探测器坏了"结果一样)。
*/
import assert from 'node:assert/strict';
import { readdirSync, unlinkSync, writeFileSync } from 'node:fs';
import { mkdtempSync, readdirSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { dirname, join, relative, resolve } from 'node:path';
import { test } from 'node:test';
import { fileURLToPath } from 'node:url';
@ -41,6 +42,91 @@ function git(args) {
return spawnSync('git', args, { cwd: REPO_ROOT, encoding: 'utf8' });
}
/**
* 探针自检:在**临时仓**里造一个「已知含待查路径」和一个「已知不含」的提交,
* 对两者跑同一套 `git log --oneline -- <path>` 查法,断言它**双向有分辨力**。
*
* # 为什么必须用临时仓,而不能用本仓历史
*
* 本仓的真身**从未被提交过**(`client/harmony/.gitignore:26` 一直在挡它),
* 于是「本仓历史里查不到它」是**真值**,而不是「查法坏了」。
* 两者在输出上**完全同形**(都是空串 + exit 0)⇒ 用真值当自检必然红。
*
* 而一条用来抓泄露的判据,**正确工作**时恰好永远看到"空"
* (没泄露 ⇒ 查不到)。所以它**必须**有一个阳性对照才能证明自己没坏,
* 而真实历史里没有 ⇒ 只能在临时仓里现造。
*
* ★ 临时仓用 `tmpdir()`,且**不碰本仓任何状态**(不 add、不 commit、不改 ref)。
* 造完即删;`--bare` + `GIT_*` 环境变量避免读用户全局配置。
*
* @param {string} probePath 探针要查的路径(相对临时仓根)
* @returns {{ok: boolean, negative: boolean, cmd: string, detail: string}}
*/
function probeSeesKnownPath(probePath) {
const cmd = 'git log --all --oneline -- <path>';
const sh = (a, cwd, extraEnv) => spawnSync('git', a, {
cwd,
encoding: 'utf8',
env: {
...process.env,
// 不读用户/全局配置:临时仓不该被 hooks、模板、别名影响
GIT_CONFIG_GLOBAL: '/dev/null',
GIT_CONFIG_SYSTEM: '/dev/null',
...(extraEnv || {})
}
});
let dir;
try {
dir = mkdtempSync(join(tmpdir(), 'agentmail-probe-'));
const init = sh(['init', '-q', '.'], dir);
if (init.status !== 0) {
return { ok: false, negative: false, cmd, detail: `临时仓 git init 失败:${init.stderr}` };
}
sh(['config', 'user.email', 'probe@example.invalid'], dir);
sh(['config', 'user.name', 'probe'], dir);
// ① 阳性样本:含该路径
writeFileSync(join(dir, 'leaf.json'), '{"probe":true}\n');
sh(['add', 'leaf.json'], dir);
sh(['commit', '-qm', 'add leaf'], dir);
// ② 阴性样本:只改另一个文件(同一仓、同一条历史)
writeFileSync(join(dir, 'unrelated.txt'), 'nothing to see\n');
sh(['add', 'unrelated.txt'], dir);
sh(['commit', '-qm', 'add unrelated'], dir);
const pos = sh(['log', '--all', '--oneline', '--', 'leaf.json'], dir);
/*
* 阴性对照必须查一个**从未存在**的路径,而不是「另一个真实文件」。
*
* (2026-09-28 实测:先写成查 `unrelated.txt`,变异测试时把它改成查 `leaf.json`,
* 判据**照样绿** —— 因为查 `unrelated.txt` 本来就该命中,那不是"查法在乱报",
* 只是换了另一个真实文件。阴性对照要证明的是「不存在的目标查不到」,
* 也就是**查法有边界**。)
*/
const negQ = sh(['log', '--all', '--oneline', '--', 'no-such-file-ever.json'], dir);
// 判据自己的形状再被验一次:那个真实但无关的文件**应当**查得到
const sanity = sh(['log', '--all', '--oneline', '--', 'unrelated.txt'], dir);
const posHit = pos.status === 0 && pos.stdout.trim().length > 0;
const negMiss = negQ.status === 0 && negQ.stdout.trim().length === 0;
const sanityHit = sanity.status === 0 && sanity.stdout.trim().length > 0;
return {
ok: posHit,
// 分辨力 = 「存在的查得到」且「不存在的查不到」**两个方向都对**
negative: negMiss && sanityHit,
cmd,
detail: `阳性命中=${posHit}(${JSON.stringify(pos.stdout.trim().slice(0, 40))})`
+ `;不存在路径应查不到=${negMiss}(${JSON.stringify(negQ.stdout.trim().slice(0, 40))})`
+ `;无关真实文件应查得到=${sanityHit}`
+ (pos.status !== 0 ? `;pos stderr=${pos.stderr}` : '')
};
} catch (e) {
return { ok: false, negative: false, cmd, detail: `自检抛错:${(e && e.message) || e}` };
} finally {
if (dir) { try { rmSync(dir, { recursive: true, force: true }); } catch { /* 清不掉就留给 tmp 回收 */ } }
}
}
/**
* 仓库目录名 —— 判"某条绝对路径是不是落在仓库内"用的**值**特征。
*
@ -392,15 +478,33 @@ test('★ stripComments 必须保持行号(对所有真实判据文件,不
test('★ AGC 真身从未进过远端历史(泄露之后响的闹钟,不是预防)', () => {
const AGC_PATH = 'client/harmony/entry/src/main/resources/rawfile/agconnect-services.json';
/*
* 先用本地可达历史自检**探针本身**:如果连本地历史都查不出这条路径,
* 说明 `git log -- <路径>` 这个查法在这棵树上根本不管用,那么下面的"远端为空"
* 就毫无意义(**空与"探针坏了"必须分得开**)。本地历史里**确实**有它。
* 先自检**探针本身**,但**口径已经改了**(2026-09-28):原文要求
* 「本地可达历史里**确实**有这条路径」,用它证明 `git log -- <路径>` 这套查法可用。
*
* ★ 那个前提**已经不成立**了 —— 真身从未被提交过(`client/harmony/.gitignore:26`
* 一直在挡它),所以本地历史里本来就查不到 ⇒ 这条判据**永久红、且无法自查**。
* 我实测:注释里引用的 `7647c24` / `320c93f` 在本树也**不存在**。
*
* ⇒ 改用**合成样本**自检:造一条临时提交(临时路径,不碰本仓历史),
* 对它跑同一套查法,断言「能查出」与「换个路径就查不出」**双向都有分辨力**。
*
* 为什么这样才算自检到位:**「真值恰好是空」与「查法坏了」在输出上同形**。
* 只要本仓历史上从没发生过泄露,本条判据要正确工作**必须**有一个
* 「已知有泄露」的阳性对照 —— 而真实历史里**没有**,所以只能在临时仓库里造。
* (与 `python-probe-shadowing`、`baseline-residue` 同族:**空 ≠ 查到了东西**。)
*/
const local = git(['log', '--all', '--oneline', '--', AGC_PATH]);
assert.equal(local.status, 0, '要能跑 git log(否则这条判据无从判起)');
assert.ok(local.stdout.trim().length > 0,
'探针自检失败:**本地**历史里都查不到这条路径 —— 那么"远端为空"只是因为查法不管用,'
+ '不是因为它没被推过。先修探针(`git log -- <路径>`),别把坏探针的空输出读成"安全"');
const probe = probeSeesKnownPath();
assert.ok(probe.ok,
'探针自检失败:在一个**临时提交**(已知含待查路径)上跑同一套查法,'
+ '竟然查不出来 —— 那么下面的"远端为空"只是因为查法不管用,'
+ '不是因为它没被推过。先修探针,别把坏探针的空输出读成"安全"。\n'
+ ` 查法:${probe.cmd}\n ${probe.detail}`);
const negOk = probe.negative;
assert.ok(negOk,
'探针**没有分辨力**:判据要能区分「存在的路径查得到」与「不存在的路径查不到」,'
+ '而现在至少一个方向不成立 ⇒ 任何"未命中"都不可信(判据会恒红,或恒不红)。\n'
+ ` ${probe.detail}`);
const rem = git(['remote', 'get-url', 'origin']);
assert.equal(rem.status, 0,

View File

@ -155,10 +155,23 @@
# 删掉自绘遮罩(该文件里 `Theme.overlay` 只剩注释引用);
# 候选列表补 `Theme.menuIn()`(WebUI `AddressInput.tsx:261` 同款,鸿蒙此前是硬弹)。
# ③ 复算后 `sha256sum -c baseline.sha` 应全 OK(11/11)。
# 2026-09-28 baseline 重算(第 9 次)—— 两个文件**有意编辑**(审查报告驱动的修复)。
# (1) pages/AdminUsersPage.ets:管理台门禁改**三态**。原先 `roleKnown && !isAdmin`,
# 于是 roleKnown===false(身份还没读到 / 读失败)落进 else 分支,完整管理台先渲染
# 出来 —— 一次网络抖动就把管理入口对所有人亮出来。改为 !roleKnown 时显示
# 「正在确认身份…」。服务端 AdminOnly 仍拦着,所以不是越权,是信息泄露面。
# (2) pages/SettingsPage.ets:performLogout 里补 MailStore.clear() 并加 hilog
# 记录失败 —— 登出不清内存,下一个登录的人会先看到上一个人的邮件。
# · 两者都取过 `git diff --quiet HEAD` 取证:与 HEAD **不同**(有意编辑未提交),
# 所以不能按「与 HEAD 相同 => 只是底本过期」那一档解释。
# ★ 但「底本过期」这一档仍然成立,关键依据是**可证伪的范围**:
# 这两个文件**不在** jobs.manifest.json 里(已实测),而底本覆盖的是**清单内**
# 被变异过的文件 => 它们不会被变异,哈希变了只可能是**有意编辑**。
# 若哪天它们被加进变异清单,本条推理作废,须重新核对有无变异残留。
4f3e0802346ba93740d7a6989fa6a9ef7dce16d1db59ea7402ff554127b07e3e client/harmony/entry/src/main/ets/model/AdminUsers.ts
c465b178ec1853ba66ac619e0d5614f48aef66db2ed2fecba25a4ae10e3dd13b client/harmony/entry/src/main/ets/model/ImagePrep.ts
54458e8010b10dc8b057fd399f0ad2b33149c5f051ee120580faa154a6ad0771 client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets
7f05d4efd12ac686f6600befd2c07d52567550d5e43d0dbc81d22e43594b6919 client/harmony/entry/src/main/ets/pages/AdminUsersPage.ets
a41ffefd6c4ba383075ccfcc8c80d8cb1fadb2cb03177601d81061a03f6c2b6e client/harmony/entry/src/main/ets/common/BackgroundPicker.ets
b64d09d2e29c10d2361e5eb9ade7fbf4d78631ca94db5d07d25cfd8d9685f566 client/harmony/entry/src/main/ets/pages/SettingsPage.ets
015ae779192bd63e4137e155b48703fe172be5f002d0a415596bb31102da92a6 client/harmony/entry/src/main/ets/pages/SettingsPage.ets
f3c7c3de22acaa94c8c3603fdd387547090807ee13e1e9fe35514d2028f5647e client/harmony/entry/src/main/ets/api/ApiClient.ets
6550e1892d1ebea82fb75a3d2cf8eaf826186199ff4a0ecf0430b1e3517d8681 client/harmony/entry/src/main/ets/api/AppearanceApi.ets