From 21332de5e74e0bb752f503a9ba944fd86b2bfb88 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 28 Sep 2026 08:27:19 +0800 Subject: [PATCH] =?UTF-8?q?test(=E5=88=A4=E6=8D=AE):=20criteria-hygiene=20?= =?UTF-8?q?=E7=9A=84=20AGC=20=E6=8E=A2=E9=92=88=E8=87=AA=E6=A3=80=E6=94=B9?= =?UTF-8?q?=E7=94=A8=E4=B8=B4=E6=97=B6=E4=BB=93=E5=90=88=E6=88=90=E5=AF=B9?= =?UTF-8?q?=E7=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 为什么要改 那条判据("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 (真实但无关的文件**应当**查得到)—— 两个方向都对才算有分辨力。 改后双向变异都能打红(阳性查不到 ⇒ 红;阴性恒命中 ⇒ 红)。 --- .../electron/test/criteria-hygiene.test.mjs | 122 ++++++++++++++++-- client/electron/test/mutants/baseline.sha | 17 ++- 2 files changed, 128 insertions(+), 11 deletions(-) diff --git a/client/electron/test/criteria-hygiene.test.mjs b/client/electron/test/criteria-hygiene.test.mjs index 089fb4a..a0e0536 100644 --- a/client/electron/test/criteria-hygiene.test.mjs +++ b/client/electron/test/criteria-hygiene.test.mjs @@ -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 -- ` 查法,断言它**双向有分辨力**。 + * + * # 为什么必须用临时仓,而不能用本仓历史 + * + * 本仓的真身**从未被提交过**(`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 -- '; + 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, diff --git a/client/electron/test/mutants/baseline.sha b/client/electron/test/mutants/baseline.sha index a33fce6..a8d8a22 100644 --- a/client/electron/test/mutants/baseline.sha +++ b/client/electron/test/mutants/baseline.sha @@ -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