test(预设): 补行为判据 —— "清单里有、渲染器没有"必须红(值判据只挡住一半)

pi 2026-09-14 指出:我那条"两端预设清单 id/顺序逐项相等"只挡得住**一半** ——
它挡"清单漏一档",挡不住"清单有、渲染器没有"(第 7 档两边都加、忘了给
`layersFor` 加分支 ⇒ 值判据全绿,用户看到的却是 aurora)。症状与"信息对等缺口"
一模一样,只是路径换了。核对:他说得对,`layersFor` 的兜底就是 `aurora`,
所以丢分支是**静默**的。

新增 `test/harmony-presets.test.mjs`(4 条,`--experimental-strip-types` 直接执行
纯逻辑 `model/Wallpaper.ts` —— 与 `harmony-logic` 同一先例,**能真跑**):

1. 六档 × 两主题:不抛异常、非空、每层 kind/色标/网格字段有效;
2. ★ **清单里有、渲染器没有 → 红**:手法是拿一个不存在的 id 调一次当作"兜底长相",
   任何一档丢了分支就会与兜底逐字节相同 ⇒ 红。**并写明已知边界**:`aurora` 自己
   就是兜底档,它"丢分支"在行为上不可观测(由值判据 + 第 3 条兜住);
3. 六档两两不同(复制分支没改参数 → 红);
4. 深色档必须与浅色档不同("深色没换色" → 红)。

**不需要设备 ⇒ 按 pi 的提醒不登记进 `static` 欠账**(那会虚增余额)。
两条变异都验过:删 `mesh` 分支 → 行为判据报"画出来的和兜底档一模一样";
清单加第 7 档不加分支 → **值判据与行为判据同时红**(两条一起才闭合)。
安装相位 14/14 绿。

另按 pi 要求处理两处登记:
· §7.12 那行"px ↔ 档位"改成**核实后的真相**:`bg_blur` 在鸿蒙侧只有搬运没有消费
  (`Appearance.ts` clamp 存下、`AppearanceStore` 同步,**没有任何页面读它**),
  所以不存在映射表可钉 —— 钉一张不存在的表是假判据;并写明"若将来开始消费它,
  那时必须补映射判据",作为"人工约定、当前无判据"的登记处。
· 预设 id/顺序那条**标清钉的是产品要求**(顺序=用户看到的选项次序),理由写进判据注释。
This commit is contained in:
2026-09-14 17:05:15 +08:00
parent 863b583838
commit 5e74e0312e
3 changed files with 125 additions and 1 deletions

View File

@ -0,0 +1,107 @@
/*
* 预设的**行为**判据:每一档都真的画得出来。
*
* 为什么需要它(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} 的深色档与浅色档完全一样 ⇒ 深色模式下这一档没有换色`);
}
});

View File

@ -64,6 +64,9 @@ const SUITE = [
['test/theme.test.mjs', [], 30],
['test/background.test.mjs', [], 42],
['test/cross-client-theme.test.mjs', [], 15],
// 预设的**行为**判据:每一档都真的画得出来(能真跑,不需要设备 ⇒ 不进 static 欠账)。
// 与 appearance-defaults 那条「清单 id/顺序相等」配对:值判据管清单,行为判据管渲染器。
['test/harmony-presets.test.mjs', ['--experimental-strip-types', '--no-warnings'], 4],
['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings'], 28],
['test/harmony-system-api.test.mjs', [], 5],
// P4 外观同步:跑 model/Appearance.ts(纯逻辑),所以也要 strip-types

View File

@ -101,6 +101,20 @@ WebUI 侧踩过这个坑,见 `gateway/handler/permission.go` 的 Note 传递
| 第 2 步 | **圆角 + 能裁剪 + 给对边** | ✅ **可验收,且不需要设备** —— 只读 `.ets` 的层结构。**不许登记成 `static` 欠账**(那会虚增余额,而它本来就能判) |
| 第 3 步 | 交互:点某天/翻页/滑动阈值/ics | ⚠️ **必然进 `static` 欠账**(要设备:能装、能点)。到期前提见套件的探针表 |
**预设清单那条判据钉的是"集合"还是"产品要求"(pi §2 要求标清)**:
钉的是**产品要求** —— WebUI 的 `PRESETS` 顺序就是预设选择器里用户看到的顺序(`backgroundStore.ts`
用它渲染列表),鸿蒙若顺序不同,用户在两端看到的选项次序不一致、会以为"选错了"。
所以"逐项一致(含顺序)"要**保留**,理由写在判据注释里;将来无害重排也必须**两边同时改**
(两边都改时判据仍绿 —— 它比的是两份清单,不是某一份的历史)。
**行为判据(pi 2026-09-14 指出"清单相等只挡住一半")**:清单相等是"能画出来"的**必要**条件,
挡不住"**清单里有、渲染器没有**"(第 7 档两边都加、忘了给 `layersFor` 加分支 ⇒ 值判据全绿,
用户看到的却是 aurora)。已补可跑的行为判据 `client/electron/test/harmony-presets.test.mjs`
(4 条,`--experimental-strip-types` 直接执行纯逻辑 `model/Wallpaper.ts`,**不需要设备 ⇒ 不进欠账**):
每档两主题都非空且字段有效 / **每一档必须与"兜底档"不同**(丢分支 ⇒ 红)/ 六档两两不同 / 深色档必须与浅色档不同。
两条变异都验过:删 `mesh` 分支 → 行为判据命中"清单里有、渲染器没有";清单加第 7 档不加分支 →
**值判据与行为判据同时红**(这就是"两条一起才闭合")。
**手势阈值(pi §2:先证明那个值在 WebUI 侧被钉着,否则"一致"只是当下的巧合)**:
核实结果 —— **WebUI 侧没有被钉**:`client/electron/src/components/CalendarView.tsx:204`
是裸字面量 `Math.abs(dx) < 40 || Math.abs(dx) < Math.abs(dy) * 1.5 || !fast`(`fast` = `<600ms`),
@ -537,7 +551,7 @@ deb 也不必从 targets 里摘。已写进 `client/electron/BUILD.md`(含排
|---|---|---|---|
| 圆角 | 自声明 `--radius-card: 0.875rem` | 系统 `sys.float.ohos_id_corner_radius_card/button` | 系统圆角会随设备/主题/无障碍设置变;跟着系统才是"系统方案" |
| 材质(玻璃) | 自声明 `--nav-bg: 255 255 255 / 0.72` + `backdrop-filter` | 系统 `backgroundBlurStyle(BlurStyle.COMPONENT_THICK)` | 系统材质自带深浅两套颜色与模糊半径,手写 alpha 跟不了深色 |
| **壁纸模糊度** | 用户的 `bg_blur`(**像素半径**,初值 4px)作用在壁纸图层上 | **没有对应物**:栏上的系统**材质档位**(`BlurStyle.*`)是唯一一次模糊,壁纸层不糊 | **不是同一个物理量**:`bg_blur=8px` 与"材质档位"对不上**不是 bug**,是映射 —— 不写清就会有人当 bug 报(pi 2026-09-14 指出) |
| **壁纸模糊度** | 用户的 `bg_blur`(**像素半径**,初值 4px)作用在壁纸图层上 | **没有被消费**:栏上的系统**材质档位**(`BlurStyle.COMPONENT_THICK`)是唯一一次模糊,壁纸层不糊 | **不是同一个物理量,而且这边根本没有映射表**。2026-09-14 核实:`bg_blur` 在鸿蒙侧只有"搬运"没有"消费" —— `Appearance.ts` 把它 clamp 到 0~40 存进 `bgBlur`、`AppearanceStore` 同步它,但**没有任何页面/组件读它**(材质档位是固定枚举)。所以不存在"px ↔ 档位"的值映射可钉,也就**没有可判的性质**(pi 问的"映射表有没有判据":没有,且当前不该有 —— 钉一张不存在的表是假判据)。**改一端的含义**:改 WebUI 的 `bg_blur` 语义**不影响鸿蒙**;若将来鸿蒙开始消费它(例如按 px 选不同 `BlurStyle`),**那时**必须补一条映射判据,并更新本行 —— 这是"人工约定、当前无判据"的登记处(CRITERIA.md §12 的同一族)。 |
| 动效 | 自定义 transition/时长 | `animateTo` + 系统 `curves` | 动效曲线应跟随系统设置(含"减弱动效") |
| 遮罩 | 自声明 `--bg-scrim` + `--bg-dim` 两段式 | 系统 `sys.color.ohos_id_color_mask_regular` | 遮罩要随主题换向(浅色洗白/深色压黑),这件事系统已经做了 |
| **品牌色** | `--c-blue-600: 37 99 235` | `Theme.accent = '#2563EB'` | **不允许差异** —— 两个客户端是同一个产品 |