pi 2026-09-15 实测出来的,**这次长在判据自己身上** —— 正是我们前几轮一直在消的那个形状。
## 一、`const ROOT = '/home/program/agentmail'`:规则进来了,对象没进来
`harmony-arkts.test.mjs` 把仓库根写成了绝对路径。后果我按 pi 的步骤亲手复现了:
```
$ git worktree add --detach /tmp/wt-verify 7f4fa26 # 那个检出里 import 顺序**确实**违规
(核对:最后 import 在第 80 行,而第 63 行已是 `const NAV_MATERIAL_OF…`)
$ cd /tmp/wt-verify/client/electron && node --test test/harmony-arkts.test.mjs
ok 1 / ok 2 / ok 3 # pass 3 # fail 0 ← **在一个明显违规的检出上 3/3 全绿**
```
因为它读的不是 `/tmp/wt-verify`,是 `/home/program/agentmail`(那份早已修好)。
两层后果,第二层最糟:
① 它**永远无法验证任何别的 checkout / CI / 镜像** —— 换目录不是"红",是 `readdirSync` 直接抛;
② 在本机做 worktree 复核时,它**静默读另一棵树并报绿**。
**判据的逻辑是对的、对象是错的** —— 这比"判据写错了"更难发现,因为它在原地永远是绿的。
同一个毛病在 4 个文件里,**恰好全是最近这几笔新写的**(另 10 个鸿蒙判据写法是对的):
```
harmony-admin / harmony-imageprep / harmony-presets / harmony-arkts → const ROOT = '/home/program/agentmail';
其余 10 个 → const ROOT = join(HERE, '..', '..', '..');
```
已全部照邻居改掉。**修好之后在同一个违规检出上:`# fail 1`** —— 它终于会红了。
## 二、修这条时又牵出一个:`stripComments` **改变了行号**
修好路径后,判据报出"最后一个 import 在第 64 行、第 47 行已是语句",
而**真实文件里是第 80 / 63 行**。成因:`stripComments` 把块注释整块抹成 `''`,
而块注释**自带换行** ⇒ 它之后所有行号整体前移。
这不是小节:全仓判据都用 `文件:行号` 定位(`grep -n`、编辑器跳转、`git show` 核对),
**报出来的行号必须能直接用**,否则读者第一步得先猜"这是剥过的还是没剥的"。
改成"块注释里的每个换行换成等量空行"。修完报的就是 **80 / 63**,与文件逐字对上。
## 三、新增两条判据,让这两个形状不能再回来
1. **`★ 判据不许把仓库根硬编码成绝对路径`** —— 扫判据目录里**真代码**
(`code()` 剥注释,否则本文件自己的说明文字就会误报),找
`const X = '/绝对路径'` 且**看着像仓库内**的声明。
**例外按名字放行**(含 `TOOLCHAIN`/`SDK`/`HDC` 的常量)—— 工具链本来就不在仓库里、推不出来;
按**值**做白名单会逼着下一个人为了过判据去改那个路径的写法。
2. **`★ stripComments 必须保持行号`** —— 造含多行块注释的样本,断言剥完
**行数不变**、且第 N 行仍是原来的第 N 行;**同时**断言注释内容确实被去掉了
(别为了保行号把注释留下)。
两条都做了**变异验证**:
- 把 `harmony-admin` 的 ROOT 改回硬编码 ⇒ 新判据**红**,并点名那个文件;还原后绿。
- 在 `MainPage.ets` **import 之前**插一条语句 ⇒ `harmony-arkts` **红**
(第 79 行 vs 第 1 行);还原后绿。**这条同时证明了"读的是自己那棵树"** ——
同样这个变异,在修路径**之前**是绿的。
## 四、未做 / 未验
- 到期闸门那 7 条**没动**(要真装真点,是另一件活)。
- **"把 build 做成一条判据"我探了,两个硬障碍**(详见给 pi 的回信):
① `client/harmony/oh_modules` 被 `.gitignore` 排除且未入库 ⇒ **全新检出没有它**,
构建会先死在装依赖上;② 本沙箱**拒写 `/root/.hvigor`**(`mkdir` Permission denied),
`hvigorw` 在 worktree 里直接 `EACCES: mkdir '/root/.hvigor/project_caches/…'`。
所以它在本仓能编过、在干净检出编不过 —— 作为判据它现在会**假红**。
178 lines
9.6 KiB
JavaScript
178 lines
9.6 KiB
JavaScript
/*
|
||
* 预设的**行为**判据:每一档都真的画得出来。
|
||
*
|
||
* 为什么需要它(pi 2026-09-14 指出):`appearance-defaults.test.mjs` 那条
|
||
* "鸿蒙 `PRESET_IDS` 与 WebUI `PRESETS` 的 id 与顺序逐项一致" 只挡得住**一半** ——
|
||
* 它挡"清单漏一档",**挡不住"清单有、渲染器没有"**:将来两边都加了第 7 档、
|
||
* 却忘了给 `layersFor` 加分支,那条判据**全绿**,而用户看到的是"无背景"
|
||
* ——正是"信息对等缺口"的同一个症状,只是路径换了。
|
||
*
|
||
* 所以这里是**两条配对**:值判据管"清单对不对",行为判据管"每一档真的画得出来"。
|
||
* 这与命中区那处(数值在 `NavItems.ts`、判据直接 import)是同一个配对方式。
|
||
*
|
||
* **能真跑**,不是形态判据:`model/Wallpaper.ts` 是纯逻辑(无 `@ohos` 依赖、
|
||
* 类型可擦除),用 node 的 `--experimental-strip-types` 直接执行它 —— 与
|
||
* `harmony-logic.test.mjs` 同一个先例。因此它**不需要设备**,
|
||
* 也**不该**登记进 `static` 欠账(那会虚增余额)。
|
||
*/
|
||
import { dirname, join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { pathToFileURL } from 'node:url';
|
||
|
||
/*
|
||
* ★ 仓库根必须**从本文件的位置推**,不许硬编码绝对路径。
|
||
*
|
||
* 原来这里写的是 `const ROOT = '/home/program/agentmail';` —— pi 2026-09-15 实测出后果:
|
||
* 把带违规的提交检出到**别的目录**再跑,判据**读的仍是 `/home/program/agentmail`**,
|
||
* 于是"在一个 import 顺序明显违规的检出上 3/3 全绿"。
|
||
* 两层后果,第二层最糟:
|
||
* ① 它**永远无法验证任何别的 checkout / CI / 镜像**(换目录不是"红",是 readdirSync 直接抛);
|
||
* ② 在本机做 worktree 复核时,它会**静默读另一棵树并报绿** —— 正是我们这几轮在消的形状,
|
||
* 这次长在判据自己身上。**"规则进来了,对象没进来"**。
|
||
* 修法照邻居(10 个鸿蒙判据都是 `join(HERE, '..', '..', '..')`)。
|
||
*/
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = join(HERE, '..', '..', '..');
|
||
const WALLPAPER_TS = join(ROOT, 'client/harmony/entry/src/main/ets/model/Wallpaper.ts');
|
||
|
||
// 直接执行 `.ets` 树里的**纯逻辑** `.ts`(有 `@ohos` 依赖的文件不能这样跑)
|
||
const W = await import(pathToFileURL(WALLPAPER_TS).href);
|
||
|
||
const IDS = W.PRESET_IDS;
|
||
const THEMES = [false, true];
|
||
const themeName = d => (d ? '深色' : '浅色');
|
||
|
||
/** 层栈的稳定序列化(只取有意义字段,便于比较与报错) */
|
||
const shapeOf = layers => JSON.stringify(layers.map(l => ({
|
||
kind: l.kind,
|
||
cx: l.cx, cy: l.cy, radius: l.radius, angle: l.angle,
|
||
colors: l.colors, stops: l.stops,
|
||
lineColor: l.lineColor, lineAlpha: l.lineAlpha, step: l.step
|
||
})));
|
||
|
||
test('六档 × 两种主题:都画得出来(不抛异常、非空、每层字段有效)', () => {
|
||
assert.ok(Array.isArray(IDS) && IDS.length >= 6, `预设清单要能取到(实际 ${IDS?.length})`);
|
||
for (const id of IDS) {
|
||
for (const dark of THEMES) {
|
||
let layers;
|
||
assert.doesNotThrow(() => { layers = W.layersFor(id, dark); },
|
||
`${id}(${themeName(dark)})调用就抛了 —— 用户看到的是"无背景"`);
|
||
assert.ok(Array.isArray(layers) && layers.length >= 1,
|
||
`${id}(${themeName(dark)})返回空层栈 ⇒ 这一档画出来是空白`);
|
||
for (const [i, l] of layers.entries()) {
|
||
assert.ok(['radial', 'linear', 'grid'].includes(l.kind),
|
||
`${id}(${themeName(dark)})第 ${i} 层的 kind 认不出:${l.kind}`);
|
||
if (l.kind === 'grid') {
|
||
assert.ok(typeof l.step === 'number' && l.step > 0, `${id} 网格档缺 step`);
|
||
assert.ok(typeof l.lineColor === 'string' && l.lineColor.length > 0, `${id} 网格档缺线色`);
|
||
} else {
|
||
assert.ok(Array.isArray(l.colors) && l.colors.length >= 2 && l.colors.every(c => typeof c === 'string' && c.length > 0),
|
||
`${id}(${themeName(dark)})第 ${i} 层的色标不完整:${JSON.stringify(l.colors)}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
/*
|
||
* ★ 这条是 pi 那半边的直接对应物:**"清单里有、渲染器里没有"必须红。**
|
||
*
|
||
* 手法:拿一个**不存在的 id** 调一次 —— `normalizePreset` 会把它落到默认档
|
||
* (aurora),于是"兜底长什么样"就有了可比较的对象。任何一档如果**丢了分支**,
|
||
* 它会静默落到同一个兜底上 ⇒ 与兜底逐字节相同 ⇒ 红。
|
||
*
|
||
* **已知边界(写出来,不假装全覆盖)**:`aurora` 自己**就是**那个兜底档,
|
||
* 所以它"丢了分支"在行为上不可观测(结果一样)。这条判据能测到的是
|
||
* **另外五档**;aurora 的存在性由值判据(清单相等)与下面那条"六档两两不同"兜住。
|
||
*/
|
||
test('★ 清单里有、渲染器里没有 → 红(每一档都必须与"兜底档"不同)', () => {
|
||
for (const dark of THEMES) {
|
||
const fallback = shapeOf(W.layersFor('__这个 id 不存在__', dark));
|
||
const aurora = shapeOf(W.layersFor('aurora', dark));
|
||
assert.equal(fallback, aurora, `自检:认不出的 id 应当落到默认档 aurora(${themeName(dark)})`);
|
||
for (const id of IDS) {
|
||
if (id === 'aurora') continue; // 见上面的"已知边界"
|
||
assert.notEqual(shapeOf(W.layersFor(id, dark)), fallback,
|
||
`${id}(${themeName(dark)})画出来的和"兜底档"一模一样 ⇒ 它没有自己的渲染分支` +
|
||
`(清单里有、渲染器没有:用户看到的是 aurora,不是他选的那档)`);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('六档两两不同(复制粘贴分支没改参数 → 红)', () => {
|
||
for (const dark of THEMES) {
|
||
const seen = new Map();
|
||
for (const id of IDS) {
|
||
const s = shapeOf(W.layersFor(id, dark));
|
||
assert.ok(!seen.has(s),
|
||
`${id} 与 ${seen.get(s)} 在${themeName(dark)}下画出来完全相同 —— 复制分支后忘了改参数`);
|
||
seen.set(s, id);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('深色档必须与浅色档不同("深色没换色" → 红)', () => {
|
||
for (const id of IDS) {
|
||
assert.notEqual(shapeOf(W.layersFor(id, true)), shapeOf(W.layersFor(id, false)),
|
||
`${id} 的深色档与浅色档完全一样 ⇒ 深色模式下这一档没有换色`);
|
||
}
|
||
});
|
||
|
||
/*
|
||
* ★ 静默兜底必须留下**可观测的痕迹**(pi 2026-09-14)。
|
||
*
|
||
* §10 登记了"未知 id 静默换成 aurora"这条缺省语义,但**登记只防"被误报成 bug"**,
|
||
* 防不住"没人知道它正在发生" —— 真实环境里它不留任何痕迹。所以返回值里带
|
||
* `presetSubstitutedFrom`:换过就是原来那个 id,没换过是空串,页面据此打一行日志。
|
||
*
|
||
* 这条判据同时钉住两件事:① 替换**必须**留痕;② 没替换时**不许**留痕
|
||
* (否则痕迹会退化成噪声,等于没有)。
|
||
*/
|
||
test('★ 静默兜底留痕:换过要带出原 id,没换过必须是空串', () => {
|
||
const unknown = 'ocean-v2-这个档还不存在';
|
||
const bad = W.resolveBackground('preset', unknown, 0.2, false, false);
|
||
assert.equal(bad.presetId, 'aurora', `自检:未知 id 应当被换成默认档(实际 ${bad.presetId})`);
|
||
assert.equal(bad.presetSubstitutedFrom, unknown,
|
||
'认不出的 id 被静默替换了,却没留下痕迹 ⇒ 版本偏移时无法确认它在发生(只能猜)');
|
||
|
||
for (const id of IDS) {
|
||
for (const dark of THEMES) {
|
||
const plan = W.resolveBackground('preset', id, 0.2, false, dark);
|
||
assert.equal(plan.presetSubstitutedFrom, '',
|
||
`${id}(${themeName(dark)})是已知档,不该留下"替换过"的痕迹(痕迹变成噪声就等于没有)`);
|
||
}
|
||
}
|
||
});
|
||
|
||
/* ── 模糊档的搬运与归一(P4c 起壁纸真的会糊了) ── */
|
||
|
||
test('模糊档:计划**搬得动**这个值,且 0~40 归一(边界与 Appearance.ts 的 clamp 同值)', () => {
|
||
/*
|
||
* ★ 这条是变异测试补上的:把 `plan.blurPx = normalizeBlur(blurPx)` 改成 `= 0`
|
||
* (计划根本不搬运模糊值)时,appearance 那几条**全绿** ——
|
||
* 它们判的是"页面把值传进来了"和"页面拿它去糊了",中间这一段没人判。
|
||
*/
|
||
assert.equal(typeof W.normalizeBlur, 'function', 'Wallpaper 要提供 normalizeBlur');
|
||
assert.equal(W.MAX_BLUR_PX, 40, '上限 40(与 model/Appearance.ts 的 clampNumber(bgBlur,0,40,4) 同值)');
|
||
|
||
for (const [input, want] of [[0, 0], [4, 4], [40, 40], [41, 40], [999, 40], [-5, 0], [-0.4, 0]]) {
|
||
assert.equal(W.normalizeBlur(input), want, `normalizeBlur(${input}) 应当是 ${want}`);
|
||
}
|
||
assert.equal(W.normalizeBlur(4.6), 5, '小数要四舍五入到整数 px');
|
||
assert.equal(W.normalizeBlur(NaN), 0, 'NaN 要归 0(不能是 NaN,否则喂给 blur() 是未定义行为)');
|
||
|
||
// 搬运:三档都要带上,而且值就是传进去的那个
|
||
for (const kind of ['preset', 'image', 'none']) {
|
||
const plan = W.resolveBackground(kind, 'aurora', 0.2, kind === 'image', false, 12);
|
||
assert.equal(plan.blurPx, 12, `\`${kind}\` 档没有把模糊值搬进计划(计划里是 ${plan.blurPx})`);
|
||
}
|
||
// 缺省入参:老调用点(不传第 6 个参数)语义不变 ⇒ 0
|
||
const legacy = W.resolveBackground('preset', 'aurora', 0.2, false, false);
|
||
assert.equal(legacy.blurPx, 0, '不传模糊档时应当是 0(老调用点语义不变)');
|
||
// 越界要**归一**再搬,而不是把 999 原样带出去
|
||
assert.equal(W.resolveBackground('image', 'aurora', 0.2, true, false, 999).blurPx, 40,
|
||
'越界的模糊值要在搬运时就归一(否则页面会把 999 直接喂给 blur())');
|
||
});
|