Files
MailUI4Agents/client/electron/test/harmony-presets.test.mjs
JianFeeeee f5c4f56682 跨端: feat(预设): 静默兜底留一条可观测痕迹;"只写不读"在鸿蒙侧也补上判据(照 LEGACY_BACKUP_KEY 照搬);§10 出处落地
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。
2026-09-14 17:17:37 +08:00

134 lines
6.8 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.

/*
* 预设的**行为**判据:每一档都真的画得出来。
*
* 为什么需要它(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)})是已知档,不该留下"替换过"的痕迹(痕迹变成噪声就等于没有)`);
}
}
});