Files
MailUI4Agents/client/electron/test/debt-visibility.test.mjs
JianFeeeee 77aa42623a 跨端: debt-visibility 补登记(新判据文件不会自动跑守卫)+ blurStyleFor 删除后的注释真相
## 一、`debt-visibility` 那条红:**新文件不会自动跑一遍守卫**

```
这些文件里有"边界声明",但一次都没登记:
  harmony-deviceprobe.test.mjs(2 处)      ← e917b87/4880c31 新加的判据文件
```

**这是同一个洞在新文件上的复发**:上一轮我刚修完 `harmony-admin` / `harmony-imageprep`,
下一个**新建的**判据文件又踩了同一个坑。pi 之所以看见,只是因为他跑了整个套件 ——
**缺的不是"记得登记",是"新建判据文件"这个动作没有守卫**。这条形状与"写了判据忘了接线"同族,
只是这次忘的是**登记边界**。

处理:**按次数登记(2),不整文件放行** —— 整文件放行的话,将来在这个文件里写一句
真实的「这里没判 / 已知缺口」就**不会红**。那 2 处本身也不是"这块没验",
而是对**词表本身**的断言(`unverifiedReason(...)` 必须含「未验」)。

同时在 `docs/DEBTS.json` 补一笔 `deviceprobe-fixture-timing`(`where` 指向该文件)——
`debt-visibility` 的第二条要求"声明必须有对应的一笔",两处各写各的会让审计只找到一处。
这笔的**到期前提是"两份 fixture 变成当场采集而不是人工存文件"**。

⚠️ **Go 侧未能本机验证**:`go test ./internal/repo/` 在本机报
`module cache not found: neither GOMODCACHE nor GOPATH is set`。我读了
`TestDebtLedgerMatchesMeasurement`:它只校验"每笔都有 due/where"+"三笔必须同处登记",
**没有"所有 id 必须在 Go 侧也列出"的断言**,所以新增一笔不需要改 Go。
但这是**读代码得出的结论,不是跑出来的** —— 如实标成未验。

## 二、`blurStyleFor` 删除后:生产代码里 5 处注释在说一个**不存在的函数**

函数已按 pi 的裁定删除(别的会话的 `9a10ab2` 落的)。但删除后
`Wallpaper.ts`(4 处)与 `MainPage.ets`(1 处)还在用现在时提它:

```
Wallpaper.ts:242  "由 `model/Appearance.ts` 的 `blurStyleFor` 映射成系统材质档"
Wallpaper.ts:248  "页面拿它去问 `blurStyleFor`"
Wallpaper.ts:251  "`blurStyleFor` 也写了、就是没有任何调用点"
Wallpaper.ts:311  "`blurStyleFor` 里面也有一次 clamp,那是它自己的防线"
MainPage.ets:1711 "(`blurStyleFor` 那张表服务的是**材质档**…)"
```

**这正是本会话反复在消的"注释描述一份不存在的代码"**,而它现在比之前更危险:
下一个人读注释会去找一个**已经被有意删掉**的函数,找不到就会**重新实现它** ——
而"它为什么不该回来"恰恰是那次删除唯一值钱的东西。

已全部改成**过去时 + 它已删除**,并在 `Appearance.ts` 原处留了碑文(函数没了,理由不能没)。
`:251` 那处尤其要改:原文说"`blurStyleFor` 也写了、就是没有任何调用点"——
函数已不存在,这句会让读者以为**还差一个调用点没补**,而事实是**连函数都不该有**。

## 三、这条碑文判据我做了变异验证

`harmony-appearance.test.mjs` 里那条「碑文不许回来」的判据**确实在校验**(不是摆着好看):
把 `Wallpaper.ts` 那段碑文抹掉 ⇒ **红**;还原 ⇒ **绿**。
顺带核了它的**指向**:碑文现在的主要落点是 `Appearance.ts`(函数原来所在处),
而判据的正则锚的是 `Wallpaper.ts` —— 两处都有内容才过,我保留了 `Wallpaper.ts` 里的引用
(它说明"这里的 px 不是材质档"),所以判据成立。

## 四、未做

- 到期闸门那 **7 条**(pi 更正过我:`STATIC_ONLY` 是 7 不是 8,我上封记串了)**仍然没动**。
- `PROBE_DEVICE=none` 下**剩 5 条红**,都是**别的会话**新加判据但没更新登记数
  (`narrow-layout` 88>64、`nav-merge` 9>8、`harmony-presets` 6>5、`commit-hygiene` 3>2)
  加 `build-stamp`(`dist` 没重构建,与本次改动无因果)。**我没有替他们改**。
2026-09-15 12:22:23 +08:00

110 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { prose } from './lib/read.mjs';
/*
★ 边界声明必须**同时进余额**(默认路径可见)—— 把 §16 本身变成判据(pi 2026-09-14)。
§16 原文是「"要提醒人的"输出必须走默认路径」。它一直是**靠记得问**的一条纪律 ——
而这轮我们已经确认过四次同一形状:**"我知道一个边界" → 写进注释/标签/信里 →
那个位置在默认路径上等于不存在**(余额打在 TestMain、权威源写在信里、overlay 边界写在注释里、
两条未覆盖的路写在判据标签里)。第五次它落在了**规则自己**身上:写在 CRITERIA.md 里的规则,
没有任何东西在判它被执行。
所以这条判据就是 §16 的可判形态:**凡在判据目录里声明"未覆盖 / 未验 / 已知缺口"的地方,
必须在 `docs/DEBTS.json` 里有一笔**(那里是默认路径可见的余额)。
登记方式与其它清册同形:**按文件 + 出现次数**(次数写死在这里,多一处即红),
且该文件必须被余额里的某一笔 `where` 引用到。
报错照 §14 写"正确修法 + 最常见的错误修法":正确修法是**把边界登记成一笔欠账**
(余额里可见、有到期前提),**不是**把下面这个数字 +1。
*/
/** 会被当作"边界声明"的措辞(新增措辞要连同它一起登记,别偷偷放行) */
const MARKERS = ['未覆盖', '未验', '已知缺口'];
/** 登记值:文件 → 该文件里边界声明的**出现次数上限**(超一处即红) */
const REGISTERED = new Map([
['background.test.mjs', 3], // 两条反向断言的未覆盖路(route B / route C)+ 说明
['harmony-appearance.test.mjs', 4], // bgBlur 消费侧/映射、运行期形态类边界
['harmony-logic.test.mjs', 1], // `.ets` 状态机要跑起来才算数
['debt-visibility.test.mjs', 12], // 本文件:N 处是词表定义 + 报错文案 + 上面那段解释(第 N+1 处即红)
['harmony-admin.test.mjs', 1], // 用户管理页:本机无设备 ⇒ 只能证明"代码里这么写"
['harmony-imageprep.test.mjs', 4], // 图片上传:压图/选图/服务端收下,三样本机都验不了
/*
* `harmony-deviceprobe.test.mjs` 的 2 处:**都不是"这块没验"的边界声明**,而是
* 对**词表本身**的断言 —— ① `unverifiedReason(VERDICT_OTHER)` 必须含「未验」;
* ② `mayAssertOn(…)` 那条的说明文字("失效方式是永远未验")。
* 但**按次数登记、不整文件放行**(π 2026-09-15 指出的形状):整文件放行的话,
* 将来在这个文件里写一句真实的「这里没判 / 已知缺口」就**不会红** ——
* 那正是"新文件不会自动跑一遍守卫"这个洞。2 就是上限,第 3 处即红。
*/
['harmony-deviceprobe.test.mjs', 2]
]);
const HERE = dirname(fileURLToPath(import.meta.url));
test('★ 用**已登记词表**声明边界的文件,必须在余额里有对应的一笔(词表外的说法不在本判据范围内)', () => {
const ledger = JSON.parse(prose(join(HERE, '..', '..', '..', 'docs', 'DEBTS.json')));
const wheres = ledger.debts.map(d => `${d.where || ''}`);
/*
* 本文件**也**在受判之列 —— 而且**按次数**登记,不整文件放行(pi 2026-09-14)。
*
* 我第一版给它的是"整文件豁免",理由是"用自己定义的词表数自己无意义"。这句话是对的,
* 但它推出的是"**这 N 处**无意义",不是"**这个文件**无意义":整文件放行的后果很具体 ——
* 将来有人在这个文件里写一句真实的「这里没判 / 已知缺口」,①不会红。
* 那就是 `migrate.go` 那个形状换了个落点,这次落在判据自己身上。
*
* 所以现在把 N 当成普通登记值:N 处是**词表定义与报错文案**(它们必须提到这些词),
* 第 N+1 处就是新的边界声明 ⇒ 红。
*
* **词表是采样、不是完备**(同一个词表键控的盲区):换 `TODO` / 「这里没判」/「跳过」
* 等同义说法声明同一个边界,本判据**抓不到** —— 这条盲区已登记在 `docs/DEBTS.json`
* 的 `boundary-vocabulary-incomplete`,所以在默认路径的余额里看得见,不靠记得。
*/
const files = readdirSync(HERE).filter(f => f.endsWith('.test.mjs') || f.endsWith('.mjs'));
const findings = [];
for (const f of files) {
const src = prose(join(HERE, f));
let n = 0;
for (const m of MARKERS) n += src.split(m).length - 1;
if (n === 0) continue;
findings.push({ file: f, count: n });
}
// ① 出现次数必须与登记一致(多一处即红:那是**新**的边界声明,还没进余额)
const unregistered = [];
const overCount = [];
for (const { file, count } of findings) {
if (!REGISTERED.has(file)) { unregistered.push({ file, count }); continue; }
if (count > REGISTERED.get(file)) overCount.push({ file, count, max: REGISTERED.get(file) });
}
assert.deepEqual(unregistered, [],
`这些文件里有"边界声明",但一次都没登记:\n ${unregistered.map(x => `${x.file}(${x.count} 处)`).join('\n ')}\n` +
`**正确修法**:把该边界登记成 docs/DEBTS.json 里的一笔(这样它出现在 RESULT 行的余额里、带到期前提),` +
`再把本文件 REGISTERED 里的次数补上。\n` +
`**最常见的错误修法**:只把下面这个数字调大/把声明删掉 —— 那是把这条判据废掉(§14),` +
`边界会回到"只有读过源码的人才知道"。`);
assert.deepEqual(overCount, [],
`边界声明比登记的多(新增的还没进余额):\n ${overCount.map(x => `${x.file}:${x.count} 处 > 登记 ${x.max}`).join('\n ')}\n` +
`**正确修法**:先在 docs/DEBTS.json 里补一笔,再改这里的次数。**别只改数字**。`);
// ② 每一笔余额都必须指向一个**存在**的位置,且带到期前提
for (const d of ledger.debts) {
assert.ok((d.due || '').trim().length > 0, `欠账 ${d.id} 没写到期前提 —— 那不是欠账,是"我们知道"`);
assert.ok((d.where || '').trim().length > 0, `欠账 ${d.id} 没写判据位置`);
}
// ③ 有边界声明的文件,必须被余额里某一笔 where 引用到(两处挂钩,不是各写各的)
for (const { file } of findings) {
assert.ok(wheres.some(w => w.includes(file)),
`${file} 里有边界声明,但没有任何一笔欠账的 where 指向它 —— ` +
`两处各写各的,审计时只会找到一处(§10.1 的理由同此)`);
}
});