Files
MailUI4Agents/client/electron/test/align-refs.test.mjs
JianFeeeee b806a05bfa 跨端: harmony 管理页(用户管理)+ P4c 壁纸上传入口 —— 「功能做全再交付」的两块
pi 的交付清单里缺的两块(`docs/GUI-PLAN-HARMONY.md` 原先把管理后台划在首版之外,
用户明确要求「功能做全再给我」之后收进来)。

标 `跨端:` 是因为判据落在 `client/electron/test/`(鸿蒙的判据目录一向量在那里),
代码本体全在 `client/harmony/`。

## 管理页(用户管理)

- `pages/AdminUsersPage.ets`:新建 / 编辑(显示名·角色·白名单)/ 启停 / 重置密码。
  排布照 `AdminUsersPage.tsx`,包括「受限」徽标口径(普通用户且白名单非空才显示)、
  最后登录缺席与空串都显示「从未登录」、管理员对白名单两项忽略。
- 入口在设置页底部,**仅管理员可见**(`role === 'admin'` 严格相等,与 `App.tsx` 同口径)。
  读不到身份时**不**显示也不报错(乐观放行会让每个普通用户看到点进去 403 的入口)。
- `api/AdminApi.ets` + `model/AdminUsers.ts`(纯逻辑,零 import ⇒ 判据能真跑)。
- 启停**只发 status 一个字段** —— 服务端是部分更新,多发字段会把显示名与白名单一起改掉。
- `model/Models.ets` 补管理端 DTO;`main_pages.json` 注册路由。

## P4c 壁纸上传

- `model/ImagePrep.ts`:阈值与两档策略(2560/0.85 → 1280/0.78,入口 20MB,压后上限 3.5MiB)。
  **一处有意不对齐 WebUI** 并写明理由:WebUI 卡 data-URL 长度(含 base64 膨胀),
  鸿蒙内存直传 ArrayBuffer,卡的是字节数。
- `common/BackgroundPicker.ets`:不设 / 预设 / 自定义图片 + 浓度与模糊滑杆。
  上传链:picker → 判可不可以 → 逐档按 desiredSize 解码压缩 → 上传 → **请页面以服务端为准重新同步**。
  失败**必带原因**(服务端 415/413 文案原样透出)。用户取消选图**不算失败**。
- `ApiClient.uploadBytes`:MultiFormData.data 收 ArrayBuffer(核了 SDK,since 11;本工程 23)
  ⇒ 内存直传,不需要 base64 也不需要临时文件。

## 两处真 bug(变异测试逼出来的,不是"新写坏的")

1. **压缩循环的第二档此前是死代码**:循环里的 break 与循环外那句 shouldRetryWithActual
   互相抵消 —— 把循环里那处改成 `if (true)`(永远只压一档)整套判据照样全绿。
   收成一处判定(overLimit),循环外只读结论,并加结构性判据(该函数在这条链上只许调用一次)。
2. **壁纸的模糊档一直是「只写不读」**(计划文档 §7.12 登记过):滑杆能拖、值能存、
   blurStyleFor 也写了,就是**没有调用点**,壁纸一点没糊。
   本次补上的调用点分两层:壁纸层 `.blur(px)` = **图片内容模糊**
   (与 WebUI 的 `filter: blur(var(--bg-blur))` 同一个量、同一个数,所以不需要映射表);
   而那张**材质档**映射表 `blurStyleFor` 也终于有了调用点(`navMaterialFor` 内部复用它)。
   `docs/HARMONY-ALIGN-PLAN.md` 的 §7.12 两行(材质 / 壁纸模糊度)已一并改准、不再互相矛盾。

## pi 复核后**改回来的**(这一笔里我自己犯的两处,都由 pi 抓出)

1. **导航条材质一度绑定到 `bg_blur`,`bg_blur=0` 时整个消失。**
   我把 `NavBar` 从固定档改成 `blurStyleFor(bgPlan.blurPx)`,而滑杆 `min: 0` 可达、
   `blurStyleFor(0) === 'NONE'` ⇒ 用户把壁纸调清晰时**导航条一点材质都没有**。
   而且它与本笔自己的论证**相反**:刚论证完"图片内容模糊"与"面板材质"是两个物理量,
   转头把面板材质接到壁纸模糊这个输入上。
   现在**分层**:`blurStyleFor` 是通用映射(**允许** NONE —— "0 px 不模糊"是它的正确语义);
   `navMaterialFor` 是**导航条专用、有下限**的入口(0 px ⇒ 最薄档)。
   判据钉**可达性**(滑杆 0..40 每个整数 + 界外值都不许 NONE,且三档都要出现 ——
   否则"恒定最薄档"会让滑杆成为死控件)。
2. **`Theme.navMaterial` 被我弄成了死令牌**,而看着它的判据**照样绿**
   (那条只断言"声明存在且不是 NONE" —— 守的是声明,坏的是活的调用路径)。
   现在导航条真的用它;并把同文件里**只覆盖 `Theme.overlay` 一个令牌**的死令牌规则
   **铺到 Theme 的全部 35 个令牌**(量**外部引用数**:只被 Theme 内部方法读、
   而那个方法自己有外部调用点 ⇒ 不算死 —— `chipSpentBg` 是这种;`navMaterial` 当时
   唯一的消费者是一张可整体删掉的局部表,所以必须被抓)。

## pi 复核后**补上的**(这一笔漏掉的接线,都是我造成的)

- **`test/run-all.mjs` 的 SUITE 没接两个新判据文件** ⇒ HEAD 上 `npm test`
  **一条判据都不跑、直接 exit 1**(套件自检 2 就是为这件事写的)。已接入,
  并把两条登记进 `STATIC_ONLY`(`.ets` 要设备 ⇒ 静态欠账)。
- **`debt-visibility` 是我自伤**:那两个新文件里有 5 处"边界声明"但一次都没登记。
  我当时报"2 条失败是改动前就红" —— **只对一半**:这条在父提交上是**绿的**。
  我那次 `git stash push -u -- client/harmony` 的对照是**无效对照**
  (`-- client/harmony` 把 `client/electron/test/` 整个排除在外,新判据文件根本没被 stash),
  所以两次跑都红、看着像"既有"。已按 pi 的建议改用 `git worktree` 到父提交做对照。
  现在两处都登记进 `docs/DEBTS.json`(含 `static-criteria` 5→7,Go 侧同一份登记同步改)。

## 一并修正的旧判据(都是"太宽/太窄/钉错东西",不是放宽标准)

- 「模糊归属」:原文「壁纸层不许有**任何**模糊调用」把**图片内容模糊**与**面板材质**
  混为一谈(WebUI 侧核实:`.app-backdrop` 的 filter 与它之上那层的 backdrop-filter
  是两个不同的量)⇒ 改成按两种模糊分别钉。
- 「bgBlur 只写不读,消费侧必须为 0」:值不再成立,**形状保留**(逐文件登记 + 计数 + 理由),
  标题与断言里的假话一并改掉。
- isDarkMode 那条 `/dark\s*\)/` 断的是**参数顺序**(加一个入参就误红)⇒ 改成"dark 在实参里"。
- 三条钉 `backgroundBlurStyle` **整条字面表达式**的断言 ⇒ 改成钉语义
  ("用系统材质 + 材质有下限"),不再匹配那一行的字符。

## 判据

新增 `harmony-admin.test.mjs`(22 条)、`harmony-imageprep.test.mjs`(29 条);
`harmony-presets.test.mjs` 加 1 条(模糊档搬运与归一,含 `-0` 那个洞:
`Math.round(-0.4)` 是 `-0` 而 `-0 < 0` 为 false ⇒ 改成判 `!(r > 0)`)。

**`node test/run-all.mjs`:22 个判据文件全部跑起来**,红的只有 1 个:
`build-stamp`(`dist` 是 `a5fc86b` 上构建的,`gitRev` 对不上当前 HEAD)。
这条**不是我的代码造成的**(可证:`a5fc86b..HEAD` 之间,`srcHash` 覆盖的那批文件
——`client/electron/src` 等——**一个都没动过**,所以 `srcHash` 没变,差的是 `gitRev`),
但也**不是"改动前就红"**:任何推进 HEAD 的提交都会让它变红,正确修法是重构建。

## 未验(如实标注)

- **本机无设备/无模拟器 ⇒ 全部观感未验**:管理页排版与卡片观感、滑杆手感、
  模糊在真机上的实际档位观感、系统材质在自绘悬浮条上的实际效果。代码齐 ≠ 真机验过。
- 预设档**没有**上模糊(壁纸在预设档下是一叠自绘矩形,系统材质对它不生效)——
  这是我**主动收的范围**,不是漏,真机看一眼再决定要不要补。
- **Go 侧的 `debt_registry_test.go` 我没能跑**(沙箱里没有 Go 模块缓存,`go test` 起不来),
  只做了 `gofmt` 校验;那处改动是一行 `Count: 5 → 7`。
2026-09-15 11:17:23 +08:00

88 lines
5.7 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.

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { prose } from './lib/read.mjs';
/*
★ 对齐参照物必须有**版本号**(pi 2026-09-14 §1)。
WebUI 的 `CalendarView.tsx` 可能同时在动(gui-lab 有未合入的改动)。照工作副本画完之后,
参照物一变,这一版就成了"照一份没人认领的草案对齐的" —— 而且**没人能判它对不对,
因为参照物没有版本号**。所以把参照物的 blob 哈希登记在 `docs/ALIGN-REFS.json`,
**它一变就红**,逼一次"重新核对再更新登记"。
报错按 §14 写"正确修法 + 最常见的错误修法":正确修法是**重新核对对齐**(读差异、改骨架、再更新登记),
**不是**把新哈希抄进去 —— 那是把这条判据降级成"记录当前状态",它就不再是闸门了。
*/
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..', '..');
test('★ 对齐参照物的版本必须与登记一致(变了就重新核对,不是抄新哈希)', () => {
const reg = JSON.parse(prose(join(ROOT, 'docs', 'ALIGN-REFS.json')));
assert.ok(Array.isArray(reg.refs) && reg.refs.length > 0, '登记里至少要有一个参照物');
const drifted = [];
for (const r of reg.refs) {
const res = spawnSync('git', ['hash-object', r.path], { cwd: ROOT, encoding: 'utf8' });
assert.equal(res.status, 0, `要能算 ${r.path} 的 blob 哈希:${res.stderr}`);
const now = res.stdout.trim();
if (now !== r.blob) drifted.push({ id: r.id, path: r.path, registered: r.blob.slice(0, 12), now: now.slice(0, 12), at: r.atCommit });
}
assert.deepEqual(drifted, [],
`对齐参照物变了,登记还是旧的:\n ${drifted.map(d => `${d.id}(${d.path}):登记 ${d.registered}(@${d.at})→ 现在 ${d.now}`).join('\n ')}\n` +
`**正确修法**:读一遍参照物的差异 → 判断骨架要不要跟着改(并说明改了哪里)→ 再把登记的 blob/commit 更新。\n` +
`**最常见的错误修法**:把新哈希抄进去就完事 —— 那是把这条判据降级成"记录当前状态",\n` +
`它就不再是闸门,"照一份没人认领的草案对齐"这件事会静默发生。`);
// 每一条都要有出处(哪个 commit)与它对齐的是什么,否则"版本号"只是个数字
for (const r of reg.refs) {
assert.ok(r.atCommit && r.atCommitSubject, `参照物 ${r.id} 必须写明登记于哪个 commit(否则无人能判断对齐目标)`);
assert.ok(r.note && r.note.trim(), `参照物 ${r.id} 必须写明"以哪次为准"`);
}
});
/*
★ 圆角也是"裸值"那一族:从 `index.css` 抄一个数字进 `.ets`,今天看起来一样,
等 WebUI 改一次半径,鸿蒙这边**没有任何东西会红**(与当初 14 处 Material 调色板同一形态,
量纲从颜色换成长度)。所以:**鸿蒙侧圆角必须来自 Theme.ets 令牌**,
且语义配对登记在 ALIGN-REFS.json 里(数值来源不同属于形态差异,另行登记)。
*/
test('★ 圆角走令牌:鸿蒙侧必须有 radius 令牌,且每一条语义都在登记里配了对', () => {
const reg = JSON.parse(prose(join(ROOT, 'docs', 'ALIGN-REFS.json')));
const theme = prose(join(ROOT, 'client', 'harmony', 'entry', 'src', 'main', 'ets', 'common', 'Theme.ets'));
assert.match(theme, /radiusCard/, 'Theme.ets 必须有卡片圆角令牌(不许在页面里写裸数字)');
assert.match(theme, /radiusControl/, 'Theme.ets 必须有控件圆角令牌');
assert.ok(Array.isArray(reg.radius) && reg.radius.length >= 2, '圆角的语义配对要登记');
for (const r of reg.radius) {
assert.ok(r.semantic && r.webui && r.harmony, `圆角条目必须写明语义、WebUI 侧与鸿蒙侧出处`);
assert.match(r.webui, /--radius-/, `WebUI 侧要指向变量(不是抄来的数字):${r.semantic}`);
assert.match(r.harmony, /Theme\.ets/, `鸿蒙侧要指向令牌出处:${r.semantic}`);
assert.ok(r.judgement && r.judgement.includes('语义'), `${r.semantic} 必须写明按语义对齐还是按数值对齐(否则下一个人会去比数字)`);
}
});
/*
★ 包名必须与 AGC 下发的配置**完全一致**(pi 2026-09-14):
不一致时 Push Kit 推不到设备 —— 这是**功能性约束**,不是风格问题。
AGC 实测拒绝 `com.agentmail.harmony`(`harmony` 是包名保留字),用户定为 `com.jianf.agentmail`。
这条把"两处必须一致"变成可判的,免得改一处忘另一处(那种错只有在真机上表现为"收不到推送")。
*/
test('★ bundleName 必须与 agconnect-services.json 的 package_name 一致(否则推送送不到)', () => {
const reg = JSON.parse(prose(join(ROOT, 'docs', 'ALIGN-REFS.json')));
const app = prose(join(ROOT, 'client', 'harmony', 'AppScope', 'app.json5'));
const m = /"bundleName"\s*:\s*"([^"]+)"/.exec(app);
assert.ok(m, '要能从 AppScope/app.json5 取到 bundleName');
const bundle = m[1];
assert.ok(reg.agc && reg.agc.packageName, 'ALIGN-REFS.json 里要登记 AGC 的 package_name');
assert.equal(bundle, reg.agc.packageName,
`bundleName(${bundle})与 AGC 的 package_name(${reg.agc.packageName})不一致 —— ` +
`**正确修法**:改 AppScope/app.json5 让它与 AGC 一致(或按新包名在 AGC 重建应用)。` +
`**最常见的错误修法**:只改这一处断言里的期望值 —— 那会让"收不到推送"变成一个测不出来的状态。`);
assert.ok(!/harmony/i.test(bundle),
`bundleName 里带 harmony 是 AGC 保留字(实测被拒):${bundle}`);
});