diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index b4a22ac..b50012b 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -350,8 +350,8 @@ pi 2026-09-14 提的形状:**不是无条件 fail-closed** —— 缺字段可 它的注释写明"省略或为空 = **不受限**",因为 17 个存量清单都没有这个字段, 若把空声明当最小权限,它们会全部静默失去 IO 注入与记忆读写。 -| **未知的预设 id**(`layersFor`/`normalizePreset`) | **静默替换为 `aurora`** | **宽(静默)** | 产品决定:背景不该因未知 id 变成空白。**这是"静默给别人的档",不是"给一个安全的空值"** —— 用户不会看到错误,但会看到**别人的档** | -| 只写不读的字段:鸿蒙 `bgBlur` | **写进去、存下来、同步它,但没有任何消费点** | 无(当前无效果) | 核实于 2026-09-14:`Appearance.ts` clamp 存入、`AppearanceStore` 同步,**无页面读它**(材质档位是固定枚举)。⇒ 钉"px ↔ 档位映射表"会是**假判据**(钉一张不存在的表);将来开始消费时**必须**补映射判据 | +| **未知的预设 id**(`layersFor`/`normalizePreset`) | **静默替换为 `aurora`** | **宽(静默)** | **无人类批准**:这是实现时的默认行为(`client/harmony/entry/src/main/ets/model/Wallpaper.ts` 随该文件一起引入,`git log --diff-filter=A` 可查出处),**本行是补登记**(pi 2026-09-14:写成"产品决定"就是事后追认)。意图(写在这里,不冒充批准):背景不该因未知 id 变成空白。**这是"静默给别人的档",不是"给一个安全的空值"** —— 用户不会看到错误,但会看到**别人的档**;替换发生时会留下痕迹(`BackgroundPlan.presetSubstitutedFrom`,页面据此打日志,判据见 `harmony-presets.test.mjs`) | +| 只写不读的字段:鸿蒙 `bgBlur` | **写进去、存下来、同步它,但没有任何消费点** | 无(当前无效果) | 出处:`model/Appearance.ts:39,52,88,104,133`(clamp + 合并)+`common/AppearanceStore.ets:142-195`(同步);**消费侧 0 处**。⇒ 钉"px ↔ 档位映射表"会是**假判据**(钉一张不存在的表);将来开始消费时**必须**补映射判据。**该侧现在也有判据**(`harmony-appearance.test.mjs`:消费侧出现次数必须为 0),与 WebUI `LEGACY_BACKUP_KEY` 那条同形 | | 只写不读的字段:WebUI `LEGACY_BACKUP_KEY` | 删旧值前**另存一份**,没有任何代码读它 | 无(有意为之) | `backgroundStore.ts:200` 的注释写明目的(否则它会变成新的继承源)。**登记在此是为了审计能一次找全** | | 邮件的 `permission_mode` | **不写、不改档**(不是写默认档) | 窄(fail-closed) | 本仓 2026-09-14:缺字段被 `\|\| 'workspace'` 兜成窄档,等于"一个 bug 以正常形态活着";守卫见 `plugins/*/lib/permission-mode.js` 的 `modeForStateWrite` | | HomeAgent `plugin.json` 的 `sdk` | **内核不读**(字段只对人有效) | 无(不是语义,是文档) | 内核 `internal/plugin/manifest.go` 的结构体里没有该字段;`registry.go` 的 `loadOne` 只用 `NameZh/NameEn` | diff --git a/client/electron/test/harmony-appearance.test.mjs b/client/electron/test/harmony-appearance.test.mjs index a0c1b42..fa78515 100644 --- a/client/electron/test/harmony-appearance.test.mjs +++ b/client/electron/test/harmony-appearance.test.mjs @@ -755,3 +755,55 @@ test('★ 遮盖色方向:`mask_*` 两套主题下都是**深色**(模态遮 assert.match(settings, /Theme\.overlay/, '自绘弹层的遮罩仍然用 mask(那里的语义是压暗背后)'); assert.ok(!/wallpaperScrim/.test(settings), '弹层不该用壁纸遮盖色(两个语义别混)'); }); + +/** + * ★ `bgBlur` 是"只写不读"的字段:**消费侧出现次数必须为 0**(pi 2026-09-14 指出同一形状只有一边有判据)。 + * + * WebUI 的 `LEGACY_BACKUP_KEY` 早就钉着"只写不读,否则它会变成新的继承源"; + * 而鸿蒙侧 `bgBlur`(`Appearance.ts` clamp 存入、`AppearanceStore` 同步)**只有文档**。 + * 同一个形状只有一边有判据 ⇒ 这一边补上,不必等一张新的字段清册: + * **登记表本身就是清册**(与 `server/internal/repo/mail_status_readers_test.go` 的 + * "文件 + 出现次数"同一个模板,只是这里的登记值是 0)。 + * + * 消费侧一旦出现(有人按 px 选档位/设模糊半径)本条就红 —— 那是**必须停下来**的时刻: + * 那时要补的是"px ↔ 材质档位"的**映射判据**(见 CRITERIA.md §10 与计划文档 §7.12), + * 而不是把次数从 0 改成 1 了事。 + */ +test('★ bgBlur 只写不读:消费侧出现次数必须为 0(要消费就得先补映射判据)', () => { + const files = []; + const walk = dir => { + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, e.name); + if (e.isDirectory()) { walk(p); continue; } + if (!/\.(ets|ts)$/.test(e.name)) continue; + files.push(p); + } + }; + walk(HARMONY_ETS); + + /* + * 豁免**按文件登记 + 写理由**(不是"凡是这几个目录都放行"):这三处是**搬运/传输**, + * 不是消费 —— 判据要挡的是"有人拿这个值去决定画什么"。 + * 与 `server/internal/repo/mail_status_readers_test.go` 的豁免同一个形状。 + */ + const plumbing = new Map([ + ['model/Appearance.ts', '域模型:clamp(0,40) 后存下 bgBlur(搬运)'], + ['common/AppearanceStore.ets', '状态同步:与快照互转(搬运)'], + ['api/AppearanceApi.ets:17', '线上 DTO 声明 bg_blur(传输格式,不是消费)'] + ]); + + const consumers = []; + for (const f of files) { + const src = prose(f); + const rel = f.slice(HARMONY_ETS.length + 1); + src.split('\n').forEach((line, i) => { + if (!/\bbgBlur\b|\bbg_blur\b/.test(line)) return; + const where = rel + ':' + (i + 1); + if (plumbing.has(rel) || plumbing.has(where)) return; + consumers.push(`${where} ${line.trim()}`); + }); + } + assert.deepEqual(consumers, [], + `有人在消费 bgBlur 了 —— 停下:那时**必须**先补"px ↔ 材质档位"的映射判据` + + `(CRITERIA.md §10 / 计划文档 §7.12),而不是把登记值从 0 改成 1:\n ${consumers.join('\n ')}`); +}); diff --git a/client/electron/test/harmony-presets.test.mjs b/client/electron/test/harmony-presets.test.mjs index 41cb2e3..dd8270e 100644 --- a/client/electron/test/harmony-presets.test.mjs +++ b/client/electron/test/harmony-presets.test.mjs @@ -105,3 +105,29 @@ test('深色档必须与浅色档不同("深色没换色" → 红)', () => { `${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)})是已知档,不该留下"替换过"的痕迹(痕迹变成噪声就等于没有)`); + } + } +}); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 0de7067..14402af 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -66,11 +66,11 @@ const SUITE = [ ['test/cross-client-theme.test.mjs', [], 15], // 预设的**行为**判据:每一档都真的画得出来(能真跑,不需要设备 ⇒ 不进 static 欠账)。 // 与 appearance-defaults 那条「清单 id/顺序相等」配对:值判据管清单,行为判据管渲染器。 - ['test/harmony-presets.test.mjs', ['--experimental-strip-types', '--no-warnings'], 4], + ['test/harmony-presets.test.mjs', ['--experimental-strip-types', '--no-warnings'], 5], ['test/harmony-logic.test.mjs', ['--experimental-strip-types', '--no-warnings'], 28], ['test/harmony-system-api.test.mjs', [], 5], // P4 外观同步:跑 model/Appearance.ts(纯逻辑),所以也要 strip-types - ['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 24], + ['test/harmony-appearance.test.mjs', ['--experimental-strip-types', '--no-warnings'], 25], // P5 悬浮玻璃导航:点击配对 / index 决定挂载 / 命中区 ≥44vp / 悬浮与让位 ['test/harmony-nav.test.mjs', ['--experimental-strip-types', '--no-warnings'], 6], // 外观契约:默认值去 Go 源码里读(服务端 DefaultAppearance 是权威)+ 缓存键按账号 diff --git a/client/harmony/entry/src/main/ets/model/Wallpaper.ts b/client/harmony/entry/src/main/ets/model/Wallpaper.ts index d4af7f3..52b3d4b 100644 --- a/client/harmony/entry/src/main/ets/model/Wallpaper.ts +++ b/client/harmony/entry/src/main/ets/model/Wallpaper.ts @@ -220,6 +220,23 @@ export class BackgroundPlan { /** 用的是深色那套色板吗(跟着主题走,不是"未验") */ dark: boolean = false; presetId: string = ''; + /** + * **静默兜底的痕迹**(pi 2026-09-14 裁定):服务端/WebUI 先加了新档、而这台设备是旧构建时, + * `normalizePreset` 会把认不出的 id **静默换成 aurora** —— 用户以为选的是新档。 + * 这条缺省语义已登记在 `CRITERIA.md` §10,但登记只防"被误报成 bug", + * **防不住"没人知道它正在发生"**(真实环境里不会留下任何痕迹)。 + * + * 所以把"发生过替换"记在这里:换过就是**原来那个 id**(非空),没换过就是空串。 + * 页面拿到非空值时**打一行日志**(`hilog`/`console.info` 皆可)—— + * 于是这个后果从"可能发生"变成"**可数**":出问题时能立刻确认是版本偏移,而不是猜。 + * + * ⚠️ 为什么痕迹记在**返回值**里、而不在这一层直接打日志:这一层是**纯逻辑** + * (无 `@ohos` 依赖 ⇒ 判据能用 node `--experimental-strip-types` 直接跑它, + * 见 `harmony-presets.test.mjs`)。为了打一行日志而引入 `@ohos.hilog`, + * 会把"能真跑的行为判据"换成"只能读源码的形态判据" —— 那是不划算的交易。 + * 记事实在这一层(可判),打日志在页面层(可观测)。 + */ + presetSubstitutedFrom: string = ''; layers: PresetLayer[] = []; /** * 遮盖浓度(0~1):**两档都用**(预设档也压),用系统遮罩色刷一层。 @@ -251,6 +268,8 @@ export function resolveBackground(bgKind: string, presetId: string, scrim: numbe if (bgKind === 'preset') { plan.kind = 'preset'; plan.presetId = normalizePreset(presetId); + // 留痕:认不出的 id 被静默替换时,把**原来那个 id**带出去(页面据此打一行日志) + plan.presetSubstitutedFrom = plan.presetId === presetId ? '' : presetId; plan.layers = layersFor(plan.presetId, dark); // 预设档**也**套遮罩:与 WebUI 的 `--bg-dim` 一致(它不区分档位),服务的是可读性 plan.scrim = scrim;