Files
MailUI4Agents/client/electron/test/debt-visibility.test.mjs
JianFeeeee 6df3c356d6 test(判据): ★★★ 套件从 10-02 10:46 起一条都没跑过 —— 补接线 + 结算登记数
**零读数 24 小时,仓库里没有任何记录**(DEBTS.json 58 条里 grep「没接进」0 命中)。

漏接线的三个(都来自 aeb1f41 / 2f17f62,都比 run-all.mjs 最后一次改动晚):
    test/inbox-fallback-poll.test.mjs   ← SSE 兜底轮询(探测 total 变化)
    test/sse-credentials.test.mjs      ← SSE 订阅跟账号凭证走
    test/web-comment-only.test.mjs

**为什么严重(三层放大)**:
1. 自检 2「每个 *.test.mjs 都要在清单里」在跑任何判据**之前** exit(1)
   ⇒ 实测 RESULT 行数 = **0**
2. npm test = run-all && vitest && typecheck ⇒ vitest(270 格)与
   typecheck **一起不跑** —— 而两者单独跑都是绿的
3. 失败信息只有一行中文 stderr,**不含"红"字样**,看起来像"环境问题"
⇒ 下一个人据上一份报告继续推断"判据在把守" ⇒ **报告的证据等级被系统性高估**。
守卫本身**不删**(漏接线绝不静默是真价值),代价靠「新增即接线」这条义务兜。

**顺带结算的登记数漂移**(都在 clean HEAD 上就红,非本次引入):
· harmony-window 9→10、appearance-defaults 7→8、harmony-2in1 23→24
  ("自报条数 > 登记条数"是显式的编辑义务:只判下界时多出来的条数删掉不红)
· harmony-appearance 28→29 —— 配合工作树里别人新增的那条设备判据
· static-criteria 5→4 —— appearance-defaults 已上设备并移出 STATIC_ONLY,
  **移出名单时忘了回头改这笔登记**,由 commit-hygiene 的机器镜像抓住
· debt-visibility REGISTERED['harmony-appearance'] 4→6 —— 两处都是
  **散文**(一处引用文件既有句子、一处在报错文案里),按该文件既有先例登记
  并注明;⚠️ 写那段说明时不能引用词表里的词,否则本文件自己数超(实测 12→14 即红)

**commit-hygiene 基线推进**:7d081095 与 477479a37 两条 `fix(harmony):` 改了
"鸿蒙源码 + 另一侧判据",按本仓口径该标 `跨端:`。二者**已推送到 origin 与
origin-https**(merge-base --is-ancestor 实测为真)⇒ 不能 amend;也不往 MARKERS
放行 `fix(harmony):`(那等于永久允许"说单端、实际改两端")。唯一正确处置是
推进基线 + 具名记下。已验证:COMMIT_HYGIENE_BASELINE 覆盖后 pass=4 fail=0。

**CRITERIA.md 新增 §6.0「怎么读判据的数」** —— 原有 17 节全在讲「怎么写」,
缺的就是这一半,而今天两起独立事件都出在它:
· 6.0.1 接线守卫的失效形状是「全停」不是「那一条不跑」;ran 必须 == SUITE 条数
· 6.0.2 退出码只能来自不接管道的运行(`cmd >file 2>&1; echo $?`)。
  **本会话我连踩 5 次** `cmd | tail -N` ⇒ 报的是 tail 的码。实例:
  npm test|tail-80(真实:0 条判据跑过)、npm test|tail-30(真实:vitest 根本没跑)、
  tsc --noEmit|tail-20(**碰巧**也是 0 —— 事实为真但**当时无根据**,仍须重取证)
· 6.0.3 && 链里「全绿」要问**真跑到那一环了吗**(red 之后的东西根本没跑,
  而日志里「有 RESULT 行」与「无下游输出」可以同时出现)
· 6.0.4 判据变红先问「判据用的工具本身可信吗」(读取器的缺陷是**静默**的)
· 6.0.5 **当你就是改工具的人**:第一假设是「我弄坏了它」不是「代码回归」——
  「判据过期了」这个反应本身就错,它默认了「我改的是正确的东西」。
  附本次三次改错的下游依赖表,以及"下游依赖是**行为依赖**,
  codegraph 那类符号图看不见"。

★ 顺带记一条取证教训:本机每条命令都吐一行 libpcre 的
  `no version information` 噪声 ⇒ 某次 grep 的输出被它吞掉,
  我把"命令返回空"当成了"没有匹配"。**空输出要连退出码一起看**,
  这与 6.0.2 是同一族,只是这次发生在我自己的取证上。
2026-10-03 10:56:47 +08:00

131 lines
8.1 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` —— 2026-10-03 恢复成 4。
*
* ★ 我一度把它改成 6,那是**错的**:多出的两处来自**工作树里别人未提交的**
* 那条设备判据(「运行期换肤」),而本笔登记必须只描述**本提交树里**的真实值。
* 实测(`git show HEAD:…`):HEAD 该文件的词表命中数是 **4**。
* 把登记绑到未提交的工作上 ⇒ **任何人检出这个提交都会红**,
* 而那看起来像"别人的判据坏了",实际是我的数字错了。
* ⇒ 口诀:**登记数只能取自本提交树**(`git show <rev>:<file>`),
* 不能取自工作树 —— 工作树含别人未提交的改动。
*/
['harmony-appearance.test.mjs', 4],
['harmony-logic.test.mjs', 1], // `.ets` 状态机要跑起来才算数
['cross-client-theme.test.mjs', 1], // 设备条里那句「声明层全绿时,渲染那一层没人看过」的出处说明——
// 它正是本轮补上的那个缺口(渲染 vs 声明),留着当出处
['debt-visibility.test.mjs', 12], // 本文件:N 处是词表定义 + 报错文案 + 上面那段解释(第 N+1 处即红)
['harmony-admin.test.mjs', 1], // 用户管理页:本机无设备 ⇒ 只能证明"代码里这么写"
/*
* `harmony-imageprep.test.mjs` 4 → 6(2026-09-19):新增的设备判据里如实标了两处
* 如实标了那两处「这一半还没做到」—— ① 应用真的用 `image.createImagePacker()` 压一次时
* 产出的字节是否落在预期区间;② "只有测试在用的代码"那条理由。
* 这两处**不是新欠账**:它们正是 `docs/DEBTS.json` 的
* `harmony-p4c-boundary-decls` 那笔(已在那一笔的 note 里写明
* "只做了一半、剩的一半要等真人操作窗口")⇒ 先补余额、再改这个数字。
*/
['harmony-imageprep.test.mjs', 6],
/*
* `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 的理由同此)`);
}
});