Files
MailUI4Agents/client/electron/test/run-all.mjs
JianFeeeee 22c9be7181 跨端: 预设色板两套(pi:这不是"观感未验"而是机制上确定不同)+ 手写色清册跨文件 + TMPDIR 按会话分家 + 提交归属可判
pi 读完 `model/Wallpaper.ts` 后指出四处,全部处理。这一提交同时改了
`client/harmony/` 与 `client/electron/`(跨端改动),所以 subject 按新约定自报家门。

## 1 预设色板不随主题 —— **类别判错了:不是"未验",是机制上确定不同**

我上一版把"深色档预设"记成"观感未验"。pi 指出:WebUI 的 `.bg-preset-*` 写的是
`rgb(var(--c-blue-100))`,而 `--c-*` 在 `.dark` 里整体换了一套(blue-100 → `30 43 67`),
所以 **WebUI 的预设自动随主题变**;这边只有浅色那套 = 深色主题下"浅色渐变垫在深色系统表面之下",
正是这一整轮在治的病。**它不需要真机就能判**(机制写在代码里)—— 我把可判的东西
记成了"未验",这跟上一轮把"没做"写成"没验"是同一类错。

选 pi 倾向的那条(跟随主题,与 WebUI 一致):

- 色板两套:`LIGHT_*` 取 CSS `:root`、`DARK_*` 取 CSS `.dark`;`paletteFor(dark)` 选一套,
  `layersFor(id, dark)` 按主题出层;
- `isDarkMode(theme, systemColorMode)` 放在纯逻辑里:选了 dark/light 就照办,
  `system` 看系统当时的 `colorMode`(锚到 SDK:`COLOR_MODE_DARK = 0` / `COLOR_MODE_LIGHT = 1`;
  读不到按浅色,与 WebUI 的 `:root` 默认一致);
- 系统深浅从 `resourceManager.getConfigurationSync().colorMode` 读
  (`Context` 基类没有 `config`;`UIAbilityContext.config` 要转型;两个枚举取值一致,都核过 SDK);
- 判据:两套值与 `:root`/`.dark` **逐个相等**;每个预设的深浅两套**必须真的不同**
  (否则"两套"是抄了两遍);网格线色也要换;`isDarkMode` 五种输入。
- **未做**:运行期间改系统深浅色不会自动重算(要重进页面)——系统侧正确做法是订阅
  `applicationContext.on('environment', …)`,记在 §7.17b。

变异:`DARK_BLUE_100` 偏一位 → 红;`paletteFor` 永远返回浅色(= 我原来那个状态)→ 红;
`isDarkMode` 把系统深浅记反 → 红;页面把深浅写死成 false → 红。

## 2 手写色清册**跨文件按类扫**(原 A2 只保护 `Theme.ets`)

`Wallpaper.ts` 也有手写色。若对照是"按名字枚举"的,第 15 个色就会逃掉 ——
与 A2 要防的是同一件事,只是换了文件。现在一份清册按类扫:全 `ets/` 树里每个
`X: string = '#RRGGBB'` 都必须登记(Theme 的品牌/业务语义色,或预设色板 ——
后者常量名必须带 `LIGHT_`/`DARK_` 前缀,值由 CSS 两段比对负责)。反向也判清册过期。

变异:`Theme.ets` 加未登记色 → 红;`Wallpaper.ts` 加未登记色 → 红;
加一个"看着合规"的 `DARK_EXTRA` → 红。

## 3 `TMPDIR` 互踩(pi 提出)

这个 worktree 可能同时有多个 agent 跑构建,而 fpm 会把 291MB 的 `linux-unpacked`
**整份复制**进 `TMPDIR` —— 撞车就是随机的产物损坏。`whoami` 区分不开(大家都是 root),
所以按**会话**分家:`TMPDIR=$PWD/.tmp/${DSH_SESSION_ID:-$(id -un)-$$}`
(进了 `npm run build:linux` 与 BUILD.md 的手敲命令;普通终端退化成"用户+PID")。

## 4 提交归属变成**跑判据就看得出来**(pi 给的形状)

新的 `test/commit-hygiene.test.mjs`:扫最近 40 条提交,**同时改两侧目录**的提交
必须在 subject 里自报家门(`跨端:`)。两条防腐:基线 = 该判据文件自己的引入提交
(**历史不改**,规则管从今往后);分类逻辑拿合成输入自检
(未标注的混合提交必须判红、标注过的不许红)——否则"解析没跑起来"时它会全绿。
变异:`COMMIT_HYGIENE_BASELINE` 指到老提交 → 历史里那两个被卷进去的提交立刻判红。

## 验证

`hvigorw assembleHap` BUILD SUCCESSFUL;`npm test` 退出码 0
(12 个判据文件全绿 + vitest 258/258;`commit-hygiene` 在本提交落地后基线生效)。
2026-09-14 14:57:18 +08:00

132 lines
6.3 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.

/**
* 判据总入口 —— **全部跑完再算退出码**。
*
* 为什么不再用 `&&` 串起来:
*
* 原先 `npm test` 是 `a && b && c …`。这种行为有个不起眼但很贵的后果 ——
* **前面红一条,后面全部不跑**。于是"只红了一条"看起来像"只有一个问题"
* 实际上后面那些判据连跑都没跑(这次就真发生了:`background` 红着,
* `packaging` 从来没跑到过,而它正是能发现"界面改了没重打包"的那条)。
* 换句话说:`&&` 链下的"全绿"是可信的,**"红"是不可信的**。
*
* 现在:每条判据都跑,红的收集起来,最后一起报、一起退出。
*
* 另外两条防"判据自己不会跑"的自检(与 process.exit 之后写判据是同一族问题):
* 1. 清单里的文件必须存在(名字写错 = 静默跳过一条判据);
* 2. `test/` 下的每个 `*.test.mjs` 都必须在清单里
* —— 这次 `cross-client-theme.test.mjs` 就是"写好了但没接进套件"
* 在它进套件之前一直是隐身状态。加了这条,**新增判据忘了接线会直接红**。
* 3. 判据规范 `test/CRITERIA.md` 要在、且要点到那几条规则
* —— 写判据的规矩本身也会被"忘了带"(形状记在某个人的脑子里等于没有)。
*
* 写判据之前先读 `test/CRITERIA.md`(判结构与行为,不判字面与邻接)。
*/
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..');
/**
* 判据清单:[文件, 额外 node 参数]。
*
* `--test` 给用 node:test 写的判据;鸿蒙那条要 `--experimental-strip-types`
* 才能直接执行 `client/harmony/.../MailGrouping.ts`(判据跑的是客户端真正引用的那份逻辑)。
*/
/*
* 自检 3判据规范在不在、有没有写到那几条关键规则。
*
* 为什么把"文档"也判:`CRITERIA.md` 里的每条都是踩出来的(窗口式判据、邻接式判据、
* 生成的清单被侵蚀、剥注释读不到理由……)。规则只在某个人的脑子里时,下一个人会重踩一遍;
* 文件被删/被搬走却没人发现,等于规则也没了。这里只断"还在 + 关键条目还在"
* 不断它的措辞 —— 那是笔记,不是接口。
*/
const CRITERIA_DOC = join(HERE, 'CRITERIA.md');
if (!existsSync(CRITERIA_DOC)) {
console.error('✗ 判据规范 test/CRITERIA.md 不见了(写判据的规矩不能只活在脑子里)');
process.exit(1);
}
const criteriaDoc = readFileSync(CRITERIA_DOC, 'utf8');
for (const must of ['配对/解析', 'allow-list', '变异验证', '剥掉注释', '按行']) {
if (!criteriaDoc.includes(must)) {
console.error(`✗ 判据规范里少了「${must}」这条 —— 规则被删掉了还是搬走了?`);
process.exit(1);
}
}
const SUITE = [
['test/markdown-xss.test.mjs', []],
['test/narrow-layout.test.mjs', []],
['test/nav-merge.test.mjs', ['--test']],
['test/theme.test.mjs', []],
['test/background.test.mjs', []],
['test/cross-client-theme.test.mjs', ['--test']],
['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings', '--test']],
['test/harmony-system-api.test.mjs', ['--test']],
// P4 外观同步:跑 model/Appearance.ts纯逻辑所以也要 strip-types
['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings', '--test']],
['test/build-stamp.test.mjs', ['--test']],
['test/packaging.test.mjs', []],
['test/commit-hygiene.test.mjs', ['--experimental-strip-types', '--no-warnings', '--test']]
];
// 自检 1清单里的文件必须真的存在写错名字 = 那条判据永远不跑)
const ghosts = SUITE.map(([f]) => f).filter((f) => !existsSync(join(ROOT, f)));
// 自检 2test/ 下每个 *.test.mjs 都要在清单里(防"写好了没接线"
const onDisk = readdirSync(join(ROOT, 'test'))
.filter((f) => f.endsWith('.test.mjs'))
.map((f) => `test/${f}`);
const unwired = onDisk.filter((f) => !SUITE.some(([s]) => s === f));
if (ghosts.length || unwired.length) {
if (ghosts.length) console.error(`清单里的判据文件不存在:${ghosts.join('、')}`);
if (unwired.length) {
console.error(`这些判据文件没接进套件(写了却不会跑):${unwired.join('、')}`);
}
process.exit(1);
}
/*
* 自检 3判据不得写在 `process.exit()` **之后**pi 提议2026-09-14
*
* 自检 1/2 管的是"文件没接线",管不到"检查写在了退出之后" —— 而那正是实际发生过的
* 第 4 例4 条玻璃判据被并发写入落到了文件末尾、`process.exit()` 后面,
* 于是**一条都不执行、也不计入通过/失败**,输出看起来完全正常。
* 这种事的成因是结构性的(并发写入总是往文件末尾追加),所以它一定会再发生,
* 而它下一次仍然不报错 —— 静态扫一遍最省事。
*/
const buried = [];
for (const [file, flags] of SUITE) {
if (flags.includes('--test')) {
continue; // node:test 那几条没有 process.exit结构上不会踩这个
}
const src = readFileSync(join(ROOT, file), 'utf8');
const exitAt = src.lastIndexOf('process.exit(');
if (exitAt >= 0 && /(^|\n)\s*check\(/.test(src.slice(exitAt))) {
buried.push(file);
}
}
if (buried.length) {
console.error(`判据写在 process.exit() 之后,永远不会跑(挪到汇总之前):${buried.join('、')}`);
process.exit(1);
}
const reds = [];
for (const [file, flags] of SUITE) {
console.log(`\n========== ${file} ==========`);
const r = spawnSync(process.execPath, [...flags, join(ROOT, file)], { stdio: 'inherit' });
// 不 break后面每条都要跑出来否则"红了几条"这个信息本身是假的
if (r.status !== 0) reds.push(`${file}(退出码 ${r.status}`);
}
console.log(`\n========== 判据汇总 ==========`);
if (reds.length === 0) {
console.log(`全部通过(${SUITE.length} 个判据文件:${SUITE.map(([f]) => f.replace('test/', '').replace('.test.mjs', '')).join('、')}`);
process.exit(0);
}
console.error(`红的判据(${reds.length}/${SUITE.length}`);
for (const r of reds) console.error(` - ${r}`);
process.exit(1);