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

576 lines
30 KiB
Markdown
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.

# 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`)只断言:
```js
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` 的取结构体形状):
```js
// 断言 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` 里。**
```ts
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 停了就变空列表"是**同一族**:
*失败形态 = 静默地永久卡住*。而**闸门本身的失效恰好也是静默的**。
**修法**(一行):
```ts
} 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. 证据(可复现)
```bash
# 编译(当前 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` 有机器镜像这两个口径),**没被采用**。