pi 2026-09-14 的三小条。 1. **§3 静默兜底要留可观测痕迹**:`normalizePreset` 把认不出的 id 换成 aurora 这件事, 原先在真实环境里**不留任何痕迹** —— §10 的登记只防"被误报成 bug",防不住 "没人知道它正在发生"。现在 `BackgroundPlan.presetSubstitutedFrom` 带出**原来那个 id** (换过非空、没换过空串),页面据此打一行日志 ⇒ 后果从"可能发生"变成"**可数**"。 **痕迹记在返回值里而不是在这一层直接打日志**,理由写进代码:这一层是**纯逻辑** (无 `@ohos` 依赖 ⇒ 判据能用 node strip-types 直接跑它);为打一行日志引入 `@ohos.hilog`, 等于把"能真跑的行为判据"换成"只能读源码的形态判据"——不划算的交易。 **判据两条方向都钉**:替换必须留痕(且带出原 id);**没替换时必须为空** (痕迹退化成噪声就等于没有)。变异与正反例都在(5 条全绿)。 2. **§2 §10 的"谁批准"落成出处**:那一格原写"产品决定" —— 按 pi 的话这是**事后追认**。 现在写的是 **「无人类批准:这是实现时的默认行为(随 `model/Wallpaper.ts` 引入, `git log --diff-filter=A` 可查出处,2e42aac),本行是补登记」**,并把**意图**与**批准**分开写 (不拿意图冒充批准)。另两行也补了出处(`Appearance.ts:39,52,88,104,133` / `AppearanceStore.ets:142-195` / `backgroundStore.ts:200`)。 3. **§1 "可扫的字段清册"不必先造 —— 同形状已有一边是判据**:他说得对。 WebUI 的 `LEGACY_BACKUP_KEY` 早就钉着"只写不读",鸿蒙的 `bgBlur` 只有文档 ⇒ 差的是**同一个形状只有一边有判据**。已照搬:`harmony-appearance.test.mjs` 新增 **"消费侧出现次数必须为 0"**;豁免**按文件登记 + 写理由**(域模型 / 状态同步 / 线上 DTO 三处是搬运与传输,不是消费)——与 `mail_status_readers_test.go` 的豁免同一形状, 登记表本身就是清册,不必另造一张。 变异验证:让 MainPage 读一次 `bgBlur` → 判据红,红的信息写着"**停下:那时必须先补 px ↔ 材质档位的映射判据**,而不是把登记值从 0 改成 1"。 SUITE 计数同步:harmony-presets 4→5、harmony-appearance 24→25。
134 lines
6.8 KiB
JavaScript
134 lines
6.8 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 { join } from 'node:path';
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { pathToFileURL } from 'node:url';
|
||
|
||
const ROOT = '/home/program/agentmail';
|
||
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)})是已知档,不该留下"替换过"的痕迹(痕迹变成噪声就等于没有)`);
|
||
}
|
||
}
|
||
});
|