Files
MailUI4Agents/docs/reviews/gui-fixes-review-2026-10-03.md
jianf d94717aa95 test(ele): ★★ preload 加载闸门 —— 一句类型标注能静默废掉整条桥
## 为什么要这条(一个当场踩到、且 tsc 抓不到的坑)

上一个提交给 `preload.cjs` 加窗口动作时,第一版写了 TS 类型标注:

    onMaximizedChanged: (cb: (maximized: boolean) => void) => {   // ← .cjs 里不能有

`preload.cjs` 是纯 JS,整个文件**静默**加载失败。症状链:

    hasBridge: false  hasWin: false  base: "/api/v1"  tok: <null>  store.n: 0
    text: "用户名 | 密码 | 登录"

  · 账号读不到 ⇒ 回退到**网页版**登录分支(用户名+密码,桌面壳里注定失败)
  · 标题栏不渲染 ⇒ 自绘窗口那条顶栏整个消失

★ 而 `npm run typecheck`(`tsc --noEmit`)**不检查 .cjs**,一路全绿。
我当时就是靠它判断「没问题」的 —— 它证明不了这件事。

## 为什么这条判据要「真的加载」而不是读文件看字符串

失效形状是「**静默少给一整条桥**」:不抛错、不警告,症状出现在**别处**
(看起来像「功能没做」而不是「代码坏了」)。

与 `93697c4`(stripComments 两趟正则吃掉 137 行真代码)同一族,但更狠 ——
那条至少还会让某条判据红;这条让**所有**判据都无从察觉
(大家都不读 preload,自然没人会发现它没加载)。

⇒ 所以闸门是 `new Module(...)._compile(code(PRELOAD), PRELOAD)`,
真的解析并执行到底(stub 掉 `electron` 模块)。
不是 `node -c`、不是正则、不是 `require`。

**变异验证**:把类型标注加回去 ⇒

    失败  ★ preload.cjs 能真的加载 — 实际错误:Unexpected token ':'
    主进程安全防线:18 通过,1 失败

★ 且验证了它在 `code()` 剥掉注释后**仍然有效**(剥注释会改行号,
但这条判的是「能不能解析执行」,不依赖行号)。

## 另 4 条(都属自绘窗口这一批)

* preload 不得暴露任意 IPC 透传(只有具名动作)
* 窗口三个动作都过了窄接口(`frame:false` 之后没有系统标题栏兵底)
* 最大化状态是订阅来的(`pushMaxState`:`maximize`/`unmaximize`/进退全屏)
* `frame: false` 与 `titleBarStyle: 'hidden'` 同时在

## 自报条数 14 → 19

`run-all.mjs` 的显式编辑(§6.6)。**这条是套件自己抓到的**,
不是我改的 —— 判据报「自报 19 条 > 登记的 14 条,新加的那几条不在
"被删会红"的保护内」。

## 顺带修一条自己的判据缺陷

新写的闸门一开始用了裸 `readFileSync` 读 preload,被
`criteria-hygiene` 抓(「判据目录里不得出现裸 readFileSync」)。
改成 `code()` —— 判「有没有这个符号」时要的是代码,裸读原文会被
解释性注释骗(同坑本仓踩过两次,其中一次就是这条判据自己)。

## 两份 GUI 复查报告一并存档

`docs/reviews/gui-review-2026-10-03.md`(基线 `5e312c6`,848 行)
`docs/reviews/gui-fixes-review-2026-10-03.md`(基线 `b4610f1`,575 行)

★ 第二份里**否证了上游报告两条**,避免下一任白做工:
  ① 鸿蒙日历「格/行时间基准不一致」是**误报** —— 5 个时区实测
     `hhmmAtOffset(ts, deviceOffsetMinutes)` 恒等于 `getHours()`;
  ② `MailStore.bump()` 不发布 AppStorage 是**设计**(混用会死循环),
     `harmony-state-review.md` #3 是误读。

★ 另外查出一条比那批修复更要紧的事:HarmonyOS 客户端**当前编译不过**
(`Theme.hairline` 从未定义,48 个提交前就坏了),而修复提交的信息写着
「BUILD SUCCESSFUL」—— 路径在本机不存在。已用 `git worktree` 对照旧基线
证明**不是这批提交引入的**。已在报告 §2/§5.2 记明并给了修法选项,
**未动业务代码**(等决策)。

## 本机实测读数(装依赖 + 补齐 4 个环境变量后)

    RESULT files=39 ran=39 checks=635 pass=623 fail=2 skip=10 red=5
           broken=0 unreported=0
           变异通道 mutants=52 ran=52 on_new_criteria=37
    npx vitest run   → Test Files 16 passed / Tests 271 passed
    npm run typecheck → 通过

剩 5 个红**全是环境**,不是缺陷:
  · `build-stamp`   —— 本机没 `npm run build` 时的产物
  · `harmony-push`  —— `agconnect-services.json` 不存在(该欠账的 `due` 就是"拿到真机")
  · `--exitcode-selftest` + 因果对照 + `summary.py baseline-residue`
    —— 三者**同一个根因**:本会话 uid=1000 非 root,`runuser`/`setpriv`
       都无法降权(实测 `runuser -u nobody` 报"非 root 用户不能使用")
       ⇒ `run-all.mjs:1043` 的 `dropTo` 探测返回 null ⇒ 那 6 个需要降权的
       变异案例被跳过并计红,连带 baseline 残留检测一起失效。
       ★ 那条报错文案写着「优先按变异残留查」会**把人引向查工作区**,
         而底本在 `mkdtempSync` 造的 /tmp 迷你仓库里、与工作区无关
         —— 已在报告 §4.3 记下,建议文案补一句先看 selftest 是否先红了。
2026-10-04 11:08:57 +08:00

30 KiB
Raw Blame History

GUI 修复复查报告(2026-10-03 复核 · 基线 b4610f1)

上游:本会话早前写的 docs/reviews/gui-review-2026-10-03.md(基线 5e312c6)。 本文只做一件事:核验那份报告提的问题在 5e312c6..b4610f1 这 14 个提交里修到了什么程度, 以及修的东西本身有没有新问题。不重写架构分析。 本轮实际跑了什么:npm install + npm test(criteria 630 项 + vitest 271 项 + typecheck)、 hvigorw assembleHap、git worktree 旧基线对照编译、4 个变异注入。 ★ 与上一份报告不同 —— 本轮的每条结论都有可复现的命令,见文末「证据」。


0. 一句话结论

报告里 7 个具体条目全部动了,其中 5 个修得干净且判据真能红; 但 HarmonyOS 客户端在当前 HEAD 上编译不过(5 个错误,其中 3 个是 48 个提交前就存在的 Theme.hairline 未定义),而修复提交的信息里写着「BUILD SUCCESSFUL」。 另发现 1 处新判据有真缺口(去掉 desiredSize 不会被抓)和 1 处新引入的死锁路径(附件闸门不复位)。


1. 结算表

条目 状态 判据 变异验证
H-1 MailStore 收/发共用快照 ✅ 修得对 harmony-arkts 第 9 条 ✅ 我亲自注入 2 个变异,全被抓
H-2 壁纸 PixelMap 泄漏 ⚠️ 修得对,但判据有缺口 harmony-arkts 第 10 条 ⚠️ 2 个变异抓到、1 个漏了
E-1 PermissionPanel 重复提交 ✅ 修得对 PermissionPanel.test.tsx ✅ 我注入变异后确认变红
E-2 附件连选覆盖 ⚠️ 主 bug 修了,引入新的死锁 ❌ 无判据 —
X-1 主进程零导航拦截 ✅ 修得对 main-process-security.test.mjs(新文件,14 项) ✅ 独立跑通
X-2 无 CSP ✅ 修得对 无专门判据 —
M-1..M-6 / L-1 / G-1 / G-2 未动 — —

2. 编译:当前 HEAD 是红的,且不是这批提交引入的

cd client/harmony && hvigorw assembleHap --no-daemon
→ COMPILE RESULT:FAIL {ERROR:6 WARN:38}

5 个错误:

# 错误 位置 来源
1 Cannot find module '@luvi/lv-markdown-in' MailDetailPage.ets:9 环境(依赖未装)
2-4 Property 'hairline' does not exist on type 'typeof Theme' MainPage.ets:608 / 1028 / 1560 ★ 提交 477479a,48 个提交前
5 Markdown({...}) does not meet UI component syntax MailDetailPage.ets:1324 环境(依赖未装 ⇒ 类型退化)

归因已用 git worktree 对照证明(不是推测):

git worktree add /tmp/mui-base 5e312c6
cd /tmp/mui-base/client/harmony && hvigorw assembleHap
→ 同样 5 个错误,逐条相同(含同样的行号 608 / 1028 / 1554)

⇒ 这批修复提交没有引入编译错误,但旧基线同样是红的。

2.1 真正的源头:477479a 引入了一个从未定义的成员

git log --all -S "Theme.hairline" -- MainPage.ets   → 477479a(唯一命中)
git show 477479a~1:MainPage.ets | grep -c Theme.hairline  → 0
git show 477479a:Theme.ets | grep hairline              → 无

477479a("三页 AppHeader 顶栏避让",提交信息标了"真机实测")新增 3 处 Theme.hairline, 但 common/Theme.ets 里从头到尾没有这个字段(现在也没有)。 ⇒ 该提交把一个编译不过的版本合进了 main,并且 48 个提交里没人再碰过那 3 行。

★ 这条比本报告其余所有内容都重要,理由见 §5.2。

2.2 关于提交信息里的「BUILD SUCCESSFUL」

6a8e868 的信息写着:

/opt/huawei/command-line-tools/bin/hvigorw 6.26.2 可用, hvigorw assembleHap --no-daemon 出 BUILD SUCCESSFUL(约 26 s)。

本机实测:

  • /opt/huawei 不存在(ls: 无法访问)
  • 真实路径是 /home/jianf/command-line-tools/bin/hvigorw

⇒ 那次 BUILD SUCCESSFUL 大概率是在另一台机器上跑的(路径都不同)。 它不必然是假的 —— 但不能作为"这两条 HIGH 已验证可编译"的证据, 因为同一台机器上此刻是 FAIL。这条信息的价值被高估了。

★ 不过要给 credit:6a8e868 信息里关于 arkts-no-obj-literals-as-types 那段是真知识(ArkTS 确实不接受对象字面量当类型),且它给出的修法(先声明具名 interface) 与本仓 harmony-arkts 的既有形状一致。这类"编译期硬规则"值得留在仓库里。


3. 逐条核验

3.1 H-1 MailStore —— ✅ 修得对,且比我提的方案多修了一处

实现(MailStore.ets):新增 sentSnapshot + inboxGen/sentGen 两个代号; loadSent(:648)写 this.sentSnapshot;SentTab(MainPage.ets:1416)改读 store.sentSnapshot; clear() 清两份并把两个代号 +1 作废。

★ 比我报告里的草案多修了一处我没提的回归:拆分后若只动收件箱, 归档会话会让发件箱继续显示那个已删会话。实现把它拆成 dropSessionFromInbox / dropSessionFromSent, 且发件箱那份不用 splitByPermission(否则会把"我发出的授权请求"抹掉)。 这是对的,且判据(:780)专门钉了这一条。

判据变异验证(我实跑):

变异 结果
loadSent 的 const snap = this.sentSnapshot 改回 this.snapshot ✅ not ok 9
SentTab 的 store.sentSnapshot 改回 store.snapshot ✅ not ok 9

⇒ 这条判据有分辨力,不是摆设。

⚠️ 但 generation 守卫本身没有任何判据(全仓 grep inboxGen 在 test/ 下 0 命中)。 删掉 if (this.inboxGen !== myGen) return; 整套判据照样全绿。 ⇒ 这是"守卫没被钉住",与 §3.2 的缺口同族,但后果轻(它挡的是同栏乱序,不是跨栏覆写)。

3.2 H-2 壁纸 —— 实现对,但判据有一处真缺口

实现(AppearanceStore.ets:233-302)三件事都做了: desiredSize 取 display.getDefaultDisplaySync()(带 catch,取不到退回不限尺寸); finally 里 src.release() / next.release(); 所有权顺序正确(先 this.wallpaper = next → next = null → await old.release())。 另加 releaseWallpaper(),Logout.ets 已调用。

★ 注释里点名了"若在 finally 里释放 this.wallpaper,会把刚装上的那张释放掉"—— 这正是我草案里写的那个坑,实现避开了。

变异验证(我实跑):

变异 结果
释放的改成 this.wallpaper.release()(所有权写反) ✅ not ok 10
把 createPixelMap(decodeOptions) 改成 createPixelMap() ❌ 仍然 pass 10/10

⚠️ 这是本次唯一一个新引入的判据缺口,根因值得记

判据(harmony-arkts.test.mjs:806)只断言:

assert.match(body, /desiredSize/, '必须给 desiredSize …');

desiredSize 字面量仍然存在于方法体里(在 const decodeOptions = { desiredSize: {...} } 那一行), 即使那个对象再也没有传给 createPixelMap。⇒ 断言通过,缺陷已回来。

⇒ 判据钉的是"字符串出现过",不是"它被用上了" —— 这恰好是 CRITERIA.md §1 与 DEBTS.json 里 class-boundary-needs-real-mechanism-not-string-shape 记的同一族坑("用字符串形状冒充机制")。它在本仓已经栽过至少三次,这是第四次。

建议的修法(不要扩大窗口,照 CRITERIA.md 的取结构体形状):

// 断言 decodeOptions 真的被传进 createPixelMap,而不是只构造出来
assert.match(body, /createPixelMap\(\s*decodeOptions\s*\)/,
  '★ `decodeOptions` 必须真的传给 createPixelMap —— 只构造出来就等于没限尺寸' +
  '(26 MB 常驻那一条原样回来)');

同时建议把两个分支都钉住(现在有 if (target.width > 0 …) / else 两条路径), 否则把两个分支对调也会绿。

3.3 E-1 审批闸门 —— ✅ 修得对,判据真能红

实现(MailView.tsx:790)照 ForwardBar 的形状加 inFlight = useRef(false) + if (inFlight.current) return;。

★ 判据(PermissionPanel.test.tsx:173)值得单独说一句:它的注释(:157-172)记了 第一版假绿(两次 fireEvent.click 不放同一个 act,中间 React 会提交一次, 第二次点到的是已 disabled 的按钮 ⇒ 删掉闸门也绿), 现在把两次点击放进同一个 act。

我实跑验证(不靠信任那段注释):

删掉 inFlight 闸门 → PermissionPanel.test.tsx:
  × ★ 同一帧内的两次激活只发出一条 decidePermission
其余 23 项仍 pass

⇒ 判据有分辨力。这是本批里质量最高的一条判据。

3.4 E-2 附件闸门 —— ⚠️ 主 bug 修了,但引入了一个新的死锁

实现(Attachments.tsx:87-116):picking = useRef(false) + 进函数提前清 input.value。 两件事成对做(注释也解释了为什么必须成对),这个判断是对的。

★ 但 picking.current = false 在 :115,不在 finally 里。

const picking = useRef(false);
const handleFiles = async (files: FileList | null) => {
  if (!files || files.length === 0) return;
  if (picking.current) return;
  picking.current = true;
  …
  setUploading(null);
  if (added.length > 0) onChange([...items, ...added]);   // ← 这一行之后若抛
  picking.current = false;                               // ← 永远到不了
};

uploadAttachment 的失败被内层 catch 接住了,所以上传失败不会卡。 但 onChange(...) 之后的任何抛出(或未来加在 setUploading(null) 之后的抛错) ⇒ picking.current 永久为 true ⇒ 附件选择器此后永久失效,且不报错。

这与本仓反复防的"spinner 停了就变空列表"是同一族: 失败形态 = 静默地永久卡住。而闸门本身的失效恰好也是静默的。

修法(一行):

} finally {
  picking.current = false;
}

★ 且这条完全没有判据 —— test/components/ 下 13 个文件里没有 Attachments 测试, grep picking 在整个 test/ 下 0 命中。 ⇒ 与 §3.2 同类:闸门类改动没有判据,就等于下次重构会把它删掉。

3.5 X-1 主进程 —— ✅ 修得对

main.cjs 加了 openExternalSafely()(new URL 解析失败静默返回,只放行 http/https)、 setWindowOpenHandler 一律 { action: 'deny' }、will-navigate 放行本应用自己的加载 (dev 走 DEV_URL、prod 走 file:// 前缀)、requestSingleInstanceLock() + second-instance 把已有窗口叫到前面。

新判据 main-process-security.test.mjs(14 项)走 functionBody() 按括号配对取函数体, 不用 \{[\s\S]{0,80} 窗口 —— 与 CRITERIA.md §1 一致。 我独立跑过:14 通过 0 失败,且它额外钉了 contextIsolation/nodeIntegration/sandbox 三项"不许被放松"(=:842 的注释也说明了为什么不判"够不够安全",这个边界是对的)。

⚠️ 一个未覆盖的场景:will-navigate 里放行 url.startsWith('file://'), 而恶意链接可以写成 file:///etc/passwd 或 file:// + 本地路径 —— 那会在应用窗口里加载本地文件。当前判据只断言"放行了 file://",没断言"只放行 dist/index.html"。 不是漏洞(react-markdown 的 defaultUrlTransform 会中和不安全 scheme,且需要先有注入面), 但如果哪天有人加 rehype-raw,这就是缺口。记一笔,不建议现在改。

3.6 X-2 CSP —— ✅ 修得对

index.html:65-67 加了 CSP。注释里对每一档为什么是这个值都有交代, 特别是两个诚实说明:

  • script-src 含 'unsafe-inline' ⇒ 这挡不住注入型 XSS,只挡 javascript:/外部脚本/eval;
  • connect-src 必须宽,因为网关地址用户可填(任意 host:port)。

首帧防闪屏脚本(:31-61)保持内联、保持单引号(:54 classList.add('dark'))—— test/theme.test.mjs 逐字符断言那一段,没有被改坏(theme 判据 pass)。

3.7 未动的部分

M-1(系统返回键,仍不建议动)、M-2..M-6、L-1、G-1(无障碍)、G-2(LazyForEach 零使用) 全部未动。DEBTS.json 现有 58 条,harmony-no-lazyforeach(我上一份报告 §5.4 的草稿) 仍不在其中 —— G-2 这条"计划与实现分叉"依旧没人登记。


4. 判据套件实跑结果

npm install → 600 packages
npm run typecheck → 通过(tsc --noEmit 无输出)
npx vitest run  → Test Files 16 passed / Tests 271 passed
node test/run-all.mjs → files=39 ran=39 checks=630 pass=614 fail=6 skip=10 red=8
   变异通道:mutants=52 ran=52 on_new_criteria=37

4.1 6 个红的归因(逐条查过,不是"环境问题"一句带过)

红判据 归因 是谁的问题
harmony-system-api SDK 在 /opt/huawei,本机在 /home/jianf/command-line-tools 环境(该文件已支持 HARMONY_CLT)
harmony-appearance 同上(:164 用 HARMONY_CONFIG_CONSTANT_DTS) 环境(已支持)
harmony-nav :1011 硬编码 /opt/huawei,不走 HARMONY_CLT ★ 判据的 portability 缺陷
harmony-push agconnect-services.json 不存在(只有 .example.json) 预期内(该欠账的 due 就是"拿到真机")
build-stamp dist/assets 不存在(本机没 npm run build) 环境
commit-hygiene ★ 见下 ★ 这批提交自己引入的

补齐环境变量后(HARMONY_CLT + HARMONY_CONFIG_CONSTANT_DTS + HARMONY_ID_TABLE), 红的 8 → 7,且 harmony-appearance 转绿 ⇒ 确认前两条纯环境。

4.2 commit-hygiene 那个红是这批提交自己造的

not ok 1 - 跨端提交必须自报家门(同时改 harmony 与 electron 的提交要标 `跨端:`)
  这些提交同时改了 client/harmony/ 与 client/electron/ 却没说自己是跨端提交
  + [ '6a8e868d fix(harmony)★★: MailStore 收/发共用一份快照 + 壁纸 PixelMap 泄漏 —— 两处 HIGH' ]

6a8e868 的 subject 标 fix(harmony),但它同时改了 harmony-arkts.test.mjs / harmony-logic.test.mjs / run-all.mjs(electron 侧判据)。

⚠️ 建议不要按判据提示去改 subject。这条判据存在的理由(见其断言消息) 是"别用 git add -A 把别人的改动卷进来"。 6a8e868 的 7 个文件里 4 个是 harmony 代码、3 个是为这批改动新写的判据 —— 按路径 add 是做了的,只是 subject 的命名没覆盖"改了对方侧的判据文件"这一情形。

两个选择:

  1. 改 subject 为 跨端:(最省事,但让"跨端"这个词的含义从"改了两端的产品代码"变成"改了两端的任何文件");
  2. 在 commit-hygiene 里为"只改判据文件"开一个分支(更准确,但要动判据本身)。

★ 两个都要先想清楚再动 —— 这是判据语义问题,不是笔误。

4.3 baseline-residue 与 --exitcode-selftest 的红是同一个环境原因(不是残留)

⚠️ 本节初稿写的是「npm install 改了 package-lock.json 导致」——那是错的,已更正。 更正过程本身说明了本仓判据的一个特性,所以保留在 §6。

--exitcode-selftest 红,报「降权工具(runuser/setpriv)不可用 ⇒ rc=2 的两条分支本机覆盖不到」, 连带 baseline=0/7✗。实测根因:

$ runuser -u nobody -- true            → rc=1(非 root 用户不能使用)
$ setpriv --reuid=65534 … true          → rc=127(setresuid 失败: 不允许的操作)
$ id nobody                             → 存在(uid=65534)

⇒ 两个工具都在 PATH 上,都能执行,但本会话是 uid=1000(jianf) 非 root, 无法降权 ⇒ run-all.mjs:1043 的 dropTo 探测返回 null ⇒ 那 6 个需要降权的案例 被 continue 跳过并计红。这条报错文案是准确的(不是"工具不存在")。

而 baseline-residue 是同一个通道坏掉的副产物:变异残留检测依赖那些被跳过的案例 去构造 baseline= 四态。run-all.mjs:1078 的底本目标是 client/harmony/entry/src/main/ets/model/Target.ts,在 mkdtempSync 造的迷你仓库里, 与工作区无关 —— 所以它与我的 npm install 无关。

★ 这也是一条有用的判据卫生信息:baseline-residue 那条文案写着 「有文件既不在底本、也与 HEAD 不同 ⇒ 优先按"变异残留"查」—— 本例里底本在 /tmp,工作区当然不同 ⇒ 按文案指引去查工作区会走错方向。 文案应补一句「底本在临时目录时本条无意义,先看 --exitcode-selftest 是否先红了」。

★ 补齐 4 个环境变量后的最终读数(HARMONY_CLT / HARMONY_ID_TABLE / HARMONY_CONFIG_CONSTANT_DTS / HARMONY_COMMON_DTS):

RESULT files=39 ran=39 checks=630 pass=616 fail=4 skip=10 red=6 verdict=red
红的:build-stamp(无 dist)· harmony-push(无 agconnect-services.json)· commit-hygiene(§4.2)
自检:--exitcode-selftest(§4.3 的降权限制)· 因果对照失败(同一根因)· summary.py baseline-residue(同一根因)

⇒ 39 个文件全跑、630 项检查、0 broken、0 unreported(未装依赖时是 3 broken)。 剩下 6 个红里,只有 commit-hygiene 一个是真问题,其余是环境。


5. 结论与建议

5.1 这批修复的成色

好的地方(值得保留的做法):

  1. ★ 判据全部走"取结构体"而非固定宽度窗口,并在注释里写明为什么 (main-process-security.test.mjs 头部、harmony-arkts 新条目)。
  2. ★ E-1 的判据把踩过的假绿写进了注释(两次 fireEvent 不同 act ⇒ 中间会提交 ⇒ 删了闸门也绿), 而且实测确实能红。这是本仓判据该有的样子。
  3. ★ 6a8e868 主动记下"判据缺失导致缺陷降级"这个根因(commTab 改成只 mount 一个 tab ⇒ "必然场景"没了 ⇒ 从"每次都坏"降成"交错时坏"⇒ 没人动)。 这条洞察比代码改动本身更值钱。
  4. ★ dropSessionFromSent 不用 splitByPermission —— 避开了"发件箱一片空白"那个 09-20 修过的真 bug。

仍有问题(按建议顺序):

优先级 项 说明
1 修 477479a 的 Theme.hairline ★ 见下,这是唯一挡住"能出包"的东西
2 补 desiredSize 的判据(§3.2) 一行断言,让判据从"字符串出现过"变成"真的用上了"
3 picking.current = false 挪进 finally(§3.4) 一行
4 给 Attachments 建判据 与 §2 同族:闸门没判据就会被重构删掉
5 给 generation 守卫建判据 §3.1
6 harmony-nav.test.mjs:1011 改成走 HARMONY_CLT 与另两个文件对齐
7 commit-hygiene 那条:改 subject 还是改判据 §4.2,先想清楚再动

5.2 为什么 Theme.hairline 排第一

不是因为它本身严重(就是 3 处边框颜色),而是因为它揭示了一个流程漏洞:

  • 一个自称"真机实测"的提交,把一个从未定义的 Theme 成员合进了 main;
  • 此后 48 个提交没人碰那 3 行,没有一条判据因此变红;
  • 而后续那个修复提交的信息写着"BUILD SUCCESSFUL"—— 说明编译这一步在交付流程里是可跳过的,至少在某些环境下。

★ 也就是说:ARV 全绿、271 项测试全绿、提交信息写着 BUILD SUCCESSFUL, 但客户端编译不过。 本报告 §2 那个 5 错误的 hvigorw 输出,是这批工作里 唯一能看见这个问题的手段 —— 它没有被任何自动化接进去。

建议(按成本从低到高):

  1. 立刻:补 Theme.hairline(或把它改成 Theme.border),让编译转绿。
  2. 低成本:把 hvigorw assembleHap 接进 npm test 或 CI —— 本机 6-9 秒就能跑完(实测 BUILD FAILED in 9 s),这是全仓最便宜的一道闸。
  3. 中成本:harmony-arkts 判据里加一条 「Theme.<x> 的每个引用都必须在 Theme 类里有对应 static 成员」—— 这类"引用了不存在的成员"是纯静态可判的,且比编译快(毫秒级,不用 SDK)。 ★ 注意 run-all.mjs:810 与 harmony-nav.test.mjs:1011 现在硬编码 /opt/huawei, 写这条判据时要用 HARMONY_CLT 那种 process.env.X || 默认值 的形状。

5.3 不要动的部分(与上一份报告一致)

MailStore.bump() 不发布 AppStorage(是设计,混用会死循环)、 鸿蒙日历 hourEvents/hhmmAtOffset(5 时区实测恒等,误报)、 系统返回键 M-1(上一轮真机试过失败并撤回)、 main.cjs 的 webPreferences 三项、.pi-lens.json/biome.jsonc 的 formatter 关闭。


6. 证据(可复现)

# 编译(当前 HEAD 失败,5 个错误)
cd client/harmony && /home/jianf/command-line-tools/bin/hvigorw assembleHap --no-daemon

# 归因:旧基线同样失败 ⇒ 不是这批提交引入的
git worktree add /tmp/mui-base 5e312c6
cd /tmp/mui-base/client/harmony && /home/jianf/command-line-tools/bin/hvigorw assembleHap --no-daemon

# Theme.hairline 的源头
git log --all -S "Theme.hairline" -- client/harmony/entry/src/main/ets/pages/MainPage.ets
git show 477479a:client/harmony/entry/src/main/ets/common/Theme.ets | grep hairline   # 无输出

# 判据套件(本机需先装依赖)
cd client/electron && npm install
HARMONY_CLT=/home/jianf/command-line-tools \
HARMONY_CONFIG_CONSTANT_DTS=/home/jianf/command-line-tools/sdk/default/openharmony/ets/api/@ohos.app.ability.ConfigurationConstant.d.ts \
HARMONY_ID_TABLE=/home/jianf/command-line-tools/sdk/default/openharmony/toolchains/id_defined.json \
  node test/run-all.mjs
npx vitest run        # 16 files / 271 tests
npm run typecheck

# §3.2 的判据缺口(去掉 desiredSize 仍 pass)
cd client/harmony/entry/src/main/ets/common
sed -i 's/createPixelMap(decodeOptions)/createPixelMap()/' AppearanceStore.ets
cd ../../../../../../electron && node test/harmony-arkts.test.mjs   # 仍 # pass 10

# §3.3 的判据有分辨力
cd src/components && python3 - <<'PY'
import pathlib
p=pathlib.Path('MailView.tsx'); t=p.read_text()
p.write_text(t.replace("    if (inFlight.current) return;\n    inFlight.current = true;\n",""))
PY
cd ../../electron && npx vitest run test/components/PermissionPanel.test.tsx   # 该项变红

⚠️ 跑完变异务必还原并 git status 确认 —— §4.3 那次 baseline-residue 就是我改了 package-lock.json 留下的。

7. Electron GUI 侧专项(初稿漏了的一节)

§3 只把 Electron 的 4 个 GUI 修复逐条讲了,漏掉了本批提交里对 Electron 判据体系影响最大的两条。 这两条都不改 GUI 行为,但它们决定了"判据在把守"这个前提到底成不成立 —— 而 GUI 侧的所有结论(包括 §3.1/§3.3 的变异验证)都建立在这个前提上。

7.1 93697c4 —— 判据读取器把 137 行真代码当注释吃掉(★★★)

stripComments() 原来是两趟正则(先块 /\*[\s\S]*?\*\//g、后行)。 两趟互相看不见对方,于是有一类输入会把真代码当注释删掉:

server/cmd/server/main.go:78
  // 与 /api/v1/agent/* 完全同一份代码 —— 不存在第二套收窄或配额逻辑。
                                  ↑ 这个 /* 在 // 里面

块注释那趟跑在还没删行注释的文本上,看到这个 /* 就当块注释开头, 一路找下一个 */(在 :214)⇒ 137 行 / 37 条路由注册被抹掉, 含它正在断言的 r.Get("/auth/me", handler.Me)。

我实跑复现了这条(把旧读取器放回去):

旧读取器  → not ok 23 - ★ `/me` 不是路由:MeApi 必须调 `/auth/me`   # pass 29 / fail 1
新读取器  → # pass 30 / fail 0

⇒ 这不是假想的风险,是已经发生过一次的假红,而且假红的那条 保护的正是"管理入口对所有人永不显示"这个线上真实存在过的 bug。

★ 全仓另有 12 处同样写法(/me/*、/assets/*、plugins/*…)—— 只要落在某个块注释终点之前就触发同一形状。

失效形状值得单独记:读取器静默少给一段真代码(不抛错、不警告), 症状却出现在被测对象上 ⇒ 看起来像"服务端把路由删了"。 这与 DEBTS.json 里 criterion-silently-returns-zero、 history-rewrite-undisclosed-citations-dangle 同一族。

修法(单遍字符扫描)方向对。★ 但注意提交信息自己承认了一个刻意的残留限制: 不认字符串字面量 —— const s = "/*"; 里的块注释标记仍会被当开头。 方向是假绿(少剥注释),与 stripStrings 同性质。 组合使用 code() → stripStrings() 是安全的(顺序已在注释里写明)。

★ 这条给 GUI 侧的直接教训:Electron 与 HarmonyOS 两侧的判据共用同一个 read.mjs。 也就是说这个读取器缺陷同时影响两端判据,而我当时正在读的那份 GUI 报告 (gui-review-2026-10-03.md)里所有 grep 出来的行号都可能是被吃掉之后的。 所幸那条报告的引用是我当场用 grep -n 在原始文件上量的,不是过 read.mjs 的。

7.2 6df3c35 —— 判据套件 24 小时一条都没跑过(★★★)

现象:inbox-fallback-poll / sse-credentials / web-comment-only 三个文件没接进 SUITE。

为什么严重(三层放大,提交信息已列出,我复核成立):

  1. 自检 2「每个 *.test.mjs 都要在清单里」在跑任何判据之前 exit(1) ⇒ 实测 RESULT 行数 = 0。 ★ 我实跑复现:从 SUITE 里删掉第一项 → rc=1、RESULT 行数 = 0。
  2. npm test = run-all && vitest && typecheck ⇒ vitest(271 格)与 typecheck 一起不跑 —— 而两者单独跑都是绿的。
  3. 失败信息只有一行中文 stderr、不含"红"字样 ⇒ 看起来像"环境问题"。

⇒ 后果是:下一个人依据上一份报告继续推断"判据在把守", 而这个前提当时是假的。提交信息把这叫「报告的证据等级被系统性高估」。

★ 这条直接命中我上一份 GUI 报告的方法论:我在那份报告 §7 写了 「我没运行任何测试,所以所有判据建议都只是建议」—— 那个自我限制恰好是当时唯一没被这条击穿的防线。 若我当时因为"判据全绿"就采信某些结论,会直接踩空。

同时它也是好消息:CRITERIA.md 新增了 §6.0「怎么读判据的数」, 补上了"原有 17 节全在讲怎么写,缺的就是怎么读"这一半。 §6.0.1 接线守卫的失效形状是「全停」不是「那一条不跑」;§6.0.5「当你就是改工具的人, 第一假设是我弄坏了它」。

7.3 §6.0.2 那条陷阱 —— 我在本轮当场踩了,记下来

§6.0.2:「退出码只能来自不接管道的运行(cmd >file 2>&1; echo $?)」, 并记着作者自己「本会话连踩 5 次 cmd | tail -N ⇒ 报的是 tail 的码」。

★ 我这次也踩了。本轮我一度用:

node test/run-all.mjs 2>&1 | tail -3   →  $? = 0

而同一时刻套件实际是红的(commit-hygiene)。$?=0 是 tail 的码。 改用重定向取真码后 rc=1。

⚠️ 更隐蔽的一层:run-all.mjs 自己就是靠退出码汇报的 (UPSTREAM_RC[...] 表 + summary.py 的 rc)。 所以「管道/重定向搞错退出码」这件事在本仓会直接污染判据结论, 不只是"看日志方便一点"的问题。

7.4 Electron 侧本批修复的最终读数

npm install            → 600 packages
npm run typecheck      → 通过(tsc --noEmit 无输出)
npx vitest run         → Test Files 16 passed / Tests 271 passed
node test/run-all.mjs  → files=39 ran=39 checks=630 pass=616 fail=4
                          skip=10(设备不在,不是通过)red=6 broken=0 unreported=0
                          变异通道:mutants=52 ran=52 on_new_criteria=37

★ broken=0 / unreported=0 是装完依赖 + 补齐 4 个环境变量之后的状态 —— 未装依赖时是 broken=3(theme / background 等因缺 tailwindcss / postcss 跑不起来)。 这正是 §7.2 那条判据的形状:broken 不是 red,但它证明不了任何东西。

7.5 Electron 侧我上轮提、至今未动的两条(数字复核)

条目 上轮读数 现读数 状态
G-1 无障碍 aria-label 22 / <button 139 / tabIndex 0 完全没变 ★ 未动
G-2 列表虚拟化 MailList 两层 .map() :99 / :225 仍全量 ★ 未动
  • G-1:test/components/ 下 13 个测试文件里没有 a11y; aria-label 只在 narrow-layout 与 5 个 manual/ 脚本里出现过,不是判据。
  • G-2:我上轮 §5.4 给的 harmony-no-lazyforeach 欠账草稿 —— grep -c harmony-no-lazyforeach docs/DEBTS.json = 0, 仍未登记(DEBTS.json 现有 58 条)。

★ 这两条都不是"忘了修",是"记下了但没排期"。按本仓的纪律, G-2 这种"计划与实现分叉"本该先登记再排期,否则下一次上下文切换就彻底丢了。 我上轮给了可直接用的 JSON 草稿(含已核实的 kind 无 schema 约束、 只有 static-criteria 有机器镜像这两个口径),没被采用。