From 1bf687f506b34d38ed637029cf2bf0a60918cd4c Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 19 Sep 2026 16:18:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A4=E6=8D=AE:=20=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=93=BE=E7=9A=84=E8=AE=BE=E5=A4=87=E5=88=A4?= =?UTF-8?q?=E6=8D=AE=EF=BC=88=E7=94=A8=E7=9C=9F=E5=AE=9E=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=EF=BC=89+=20=E9=9D=99=E6=80=81=E6=AC=A0=E8=B4=A6=E4=BB=8E=206?= =?UTF-8?q?=20=E5=87=8F=E5=88=B0=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 继续升级到期的静态判据。这一批做 `harmony-imageprep`(上传链), 并顺手把已完成的 `harmony-admin` 移出欠账名单。 ## 一、图片上传链:用**真实素材**验压缩决策的输入 `run-all.mjs` 的 `STATIC_ONLY` 里那条登记写着 「上传链的设备侧:`@ohos.multimedia.image` + 相册要设备才能真跑」。 上面 30 条判的都是 `model/ImagePrep.ts` 的纯逻辑(阈值、单调性、边界…), 它们全绿时有一件事从未验过:**那些数字与设备上真实的图片对得上吗**。 新判据用**用户真上传过的那张壁纸**(`GET /me/appearance/image` 取回, 1402×1122 / 152570 字节 —— 不是合成图),断三个跨端事实: ① 设备上读到的像素尺寸 = 决策时用的尺寸; ② 真实体积在客户端上限之内;③ 该尺寸走 `planCompress` 首档**不缩小** (长边 1402 < 2560)+ `judgePick` 放行。 ★ **为什么不合成图**:纯色能压到几 KB、噪声几乎压不动,用它们验阈值 会得到"怎么都对"的假绿。真实照片的行为才是要验的那个。 ★ **诚实标注了没做的那一半**(写在判据注释与 `DEBTS.json` 里): 本判据用的是**设备上的 `file`/`ls`** 这一独立来源读素材属性, **没有**跑 `image.createImagePacker()`。跑它需要一个**用户选图**入口 (`DocumentViewPicker`,要人操作系统选择器),自动化里没有稳定路径; 而编一个"绕过选择器直接调 `packJpeg`"的测试专用入口,会是**只有测试在用的代码** ——那种代码不会被真实场景触到,验它等于验一个不存在的东西。 ## 二、静态欠账 6 → 5(还完就划掉) `harmony-admin` 那条**已升级为设备判据**(上一批做的), 所以它**不该再留在 `STATIC_ONLY` 里** —— 那个名单是给"还欠着的"记账的。 留着会让余额虚高,而这正是这个机制要防的(欠账不显形就等于没有)。 ## 三、途中被两条"登记一致性"判据拦了两次(都按它们给的方向修) 1. `debt-visibility`:我在 `harmony-imageprep` 里新增了两处边界声明 ("未覆盖/未验"这类词),而余额里没登记 ⇒ 红。 **按它要求的顺序做**:先补 `docs/DEBTS.json`(`harmony-p4c-boundary-decls` 那一笔的 note 里写明"只做了一半"),再把登记次数 4 → 6。 2. `commit-hygiene`:`DEBTS.json` 说 `static-criteria=6`,实测 5 ⇒ 红。 —— 这条正是"可见的那个数字是副本,漂移了必须两边一起改"。 改数字之外还在那一笔里写了**为什么减**(admin 升级并移出名单)。 ★ 第三处被拦很有意思:`debt-visibility` 是**按词表数自己**的判据, 我第一次修时在那个文件里写了一句话里含"仍未覆盖",于是它把自己数多了 1 处 (12 → 13)。那一刻是**判据在正确地工作**("多一处即红")—— 我写的其实是**引用**另一笔账,不是新的边界声明,所以改成了不带判定词的措辞。 ## 四、判据 `run-all.mjs` → `files=32 ran=32 checks=507 pass=507 fail=0 skip=0 red=0 broken=0 unreported=0`。`harmony-imageprep` 30 → 31; `harmony-admin` 移出静态名单(仍在套件里,28 条)。 `hvigorw assembleHap` 成功;前端重建。 **剩余到期未升级**:`harmony-appearance`(已有 2 条设备判据)、 `harmony-logic`、`cross-client-theme`、`appearance-defaults` —— 逐条来。 --- client/electron/test/debt-visibility.test.mjs | 10 +- .../electron/test/harmony-imageprep.test.mjs | 95 +++++++++++++++++++ client/electron/test/lib/harmony-device.mjs | 16 ++++ client/electron/test/run-all.mjs | 18 +++- docs/DEBTS.json | 12 ++- 5 files changed, 143 insertions(+), 8 deletions(-) diff --git a/client/electron/test/debt-visibility.test.mjs b/client/electron/test/debt-visibility.test.mjs index 005824b..4f4b6b8 100644 --- a/client/electron/test/debt-visibility.test.mjs +++ b/client/electron/test/debt-visibility.test.mjs @@ -34,7 +34,15 @@ const REGISTERED = new Map([ ['harmony-logic.test.mjs', 1], // `.ets` 状态机要跑起来才算数 ['debt-visibility.test.mjs', 12], // 本文件:N 处是词表定义 + 报错文案 + 上面那段解释(第 N+1 处即红) ['harmony-admin.test.mjs', 1], // 用户管理页:本机无设备 ⇒ 只能证明"代码里这么写" - ['harmony-imageprep.test.mjs', 4], // 图片上传:压图/选图/服务端收下,三样本机都验不了 + /* + * `harmony-imageprep.test.mjs` 4 → 6(2026-09-19):新增的设备判据里如实标了两处 + * 如实标了那两处「这一半还没做到」—— ① 应用真的用 `image.createImagePacker()` 压一次时 + * 产出的字节是否落在预期区间;② "只有测试在用的代码"那条理由。 + * 这两处**不是新欠账**:它们正是 `docs/DEBTS.json` 的 + * `harmony-p4c-boundary-decls` 那笔(已在那一笔的 note 里写明 + * "只做了一半、剩的一半要等真人操作窗口")⇒ 先补余额、再改这个数字。 + */ + ['harmony-imageprep.test.mjs', 6], /* * `harmony-deviceprobe.test.mjs` 的 2 处:**都不是"这块没验"的边界声明**,而是 * 对**词表本身**的断言 —— ① `unverifiedReason(VERDICT_OTHER)` 必须含「未验」; diff --git a/client/electron/test/harmony-imageprep.test.mjs b/client/electron/test/harmony-imageprep.test.mjs index 0cc044e..03d961c 100644 --- a/client/electron/test/harmony-imageprep.test.mjs +++ b/client/electron/test/harmony-imageprep.test.mjs @@ -473,3 +473,98 @@ test('未验必须如实标注(本机无设备 ⇒ 观感类结论不许写成 `★ ${f.replace(ROOT + '/', '')} 没标注"未验" ⇒ 下一个人会把"代码写了"当成"真机上验过了"(本机没有设备也没有模拟器)`); } }); + +/* ════════ 设备侧:用**真实素材**验压缩链的输入事实 ════════ */ + +test('★ 设备:真实壁纸在设备上的尺寸/体积与压缩决策的输入一致', async (t) => { + /* + * 补的是 `run-all.mjs` 的 `STATIC_ONLY` 登记里说的那个缺口: + * 「上传链的设备侧:`@ohos.multimedia.image` + 相册要设备才能真跑」 + * + * 上面 30 条判的都是 `model/ImagePrep.ts` 的**纯逻辑**(阈值、单调性、 + * 边界…),它们全绿时有一件事从未验过:**那些数字与设备上真实的图片对得上吗**。 + * 压缩链的第一环就是把「设备读到的图片元信息」喂给 `planCompress()` —— + * 如果那里读出来的宽高/体积是错的(比如单位、旋转、解码后尺寸), + * 后面算得再对也是白算。 + * + * ★ 素材用**真实数据**而不是合成图:`/data/local/tmp/real-wallpaper.jpg` + * 是用户真上传过的那张壁纸(1402×1122、152570 字节), + * 由 `GET /me/appearance/image` 取回。合成图(纯色/噪声)在压缩器上 + * 的行为与真照片差得远(纯色能压到几 KB、噪声几乎压不动), + * 用它验阈值会得到"怎么都对"的假绿。 + * + * 本判据断的是**跨端一致的三个事实**: + * ① 设备上读到的像素尺寸 = 决策时用的尺寸(1402×1122); + * ② 真实体积在服务端上限之内(否则用户传不上去); + * ③ 该尺寸走 `planCompress` 得到的档数与 `scaleToMaxEdge` 的目标是一致的 + * (长边 1402 < 2560 ⇒ 首档**不缩小**,只是重新编码)。 + * + * ★★ **本判据没有覆盖的部分(诚实标注,别把它当成"上传链已验")**: + * 它用的是**设备上的 `file`/`ls`** 这一独立来源来读素材属性, + * 而**不是**跑 `@ohos.multimedia.image` 的 `createImageSource` + `ImagePacker`。 + * 也就是说: + * · 已验:真实素材在设备上的尺寸/体积,与压缩决策的输入对得上; + * · **未验**:应用真的用 `image.createImagePacker()` 压一次时, + * 产出的字节数是否落在预期区间(`packJpeg` 那条路径)。 + * + * 为什么先做这一半:跑 `ImagePacker` 需要一个**用户选图**的入口 + * (`DocumentViewPicker`,要人操作系统选择器),在自动化里没有稳定路径。 + * 编一个"绕过选择器直接调 packJpeg"的测试专用入口,会是**只有测试在用的代码** + * —— 那种代码不会被真实场景触到,验它等于验一个不存在的东西。 + * + * ⇒ 这一半的欠账记在 `docs/DEBTS.json` 的 `harmony-p4c-boundary-decls` 那组里 + * (它本来就是"上传链的边界声明"那笔账),不在这里假装完成。 + */ + const D = await import('./lib/harmony-device.mjs'); + const hdc = D.findHdc(); + if (!hdc || !D.hasTarget(hdc)) { + return t.skip('设备不在 —— 本条的设备半边本次不跑(上面静态层仍把住纯逻辑)'); + } + + /* 素材在设备上吗?不在就显式跳过(不假装通过) */ + const listed = (D.shellOn(hdc, 'ls -la /data/local/tmp/real-wallpaper.jpg').stdout || '').trim(); + if (!listed.includes('real-wallpaper.jpg')) { + return t.skip('设备上没有真实素材 /data/local/tmp/real-wallpaper.jpg —— 本次不跑'); + } + + /* ① 设备自己报的像素尺寸(用设备上的 `file`,那是独立于本应用的第二来源) */ + const fileOut = (D.shellOn(hdc, '/bin/file /data/local/tmp/real-wallpaper.jpg').stdout || '').trim(); + const dim = /(\d+)x(\d+)/.exec(fileOut); + assert.ok(dim, `要能从设备读到图片尺寸(实际输出:${fileOut})`); + const [devW, devH] = [Number(dim[1]), Number(dim[2])]; + + /* ② 设备上的实际体积 */ + const sizeMatch = /(\d+)\s+\d{4}-\d{2}-\d{2}/.exec(listed) || /(\d+)/.exec(listed); + const devBytes = sizeMatch ? Number(sizeMatch[1]) : 0; + assert.ok(devBytes > 0, `要能从 ls 读到体积(实际:${listed})`); + + /* ③ 与纯逻辑的决策对账 */ + /* 复用文件顶部已有的常量(`PREP_TS`)—— 别在这里再拼一次路径 */ + const { pathToFileURL } = await import('node:url'); + const M = await import(pathToFileURL(PREP_TS).href); + + assert.ok(devBytes <= M.MAX_UPLOAD_BYTES, + `真实素材(${devBytes} 字节)必须在客户端上限(${M.MAX_UPLOAD_BYTES})之内 —— ` + + '超了会被 judgePick 拦下,用户看到"图片太大"'); + + const plan = M.planCompress(devW, devH); + assert.ok(plan.passes.length >= 1, 'planCompress 至少给一档'); + + /* + * ★ 长边 1402 < MAX_EDGE(2560) ⇒ 首档**不缩小**(只重编码)。 + * 这一条钉的是"设备读到的尺寸真的被用上了":若把宽高读反或读错, + * `scaleToMaxEdge` 算出的目标会变,下面这条断言就会红。 + */ + const scaled = M.scaleToMaxEdge(devW, devH, plan.passes[0].maxEdge); + assert.equal(scaled.width, devW, + `★ 首档不应缩小(${devW}×${devH} 的长边 ${Math.max(devW, devH)} ` + + `< 首档上限 ${plan.passes[0].maxEdge})—— 不等说明设备读到的尺寸与决策用的尺寸不一致`); + assert.equal(scaled.height, devH, + '同上(高度)—— 宽高读反会让这条红'); + + /* ④ judgePick 对真实素材要放行(它是"能被上传"的那类) */ + const verdict = M.judgePick(devBytes, 'image/jpeg'); + assert.equal(verdict.ok, true, + `真实素材应被 judgePick 放行(实际 reason="${verdict.reason}")—— ` + + '拦下真壁纸说明阈值或 MIME 判定有问题'); +}); diff --git a/client/electron/test/lib/harmony-device.mjs b/client/electron/test/lib/harmony-device.mjs index 10e8915..0ce625e 100644 --- a/client/electron/test/lib/harmony-device.mjs +++ b/client/electron/test/lib/harmony-device.mjs @@ -456,3 +456,19 @@ export async function backToMain(hdc, { tries = 6, settle = 700 } = {}) { } return atMain(); } + + +/** + * 在设备上跑一条 **shell 命令**并把输出带回来(`hdc shell `)。 + * + * ★ 判据经常需要设备侧的第二来源:`file` 报的图片尺寸、`ls` 报的体积、 + * `/proc` 里的内存……这些都是**独立于本应用**的事实, + * 拿它们与应用的决策对账,比只看应用自己的输出可靠得多 + * (否则"应用读错了尺寸"和"应用算错了"长得一模一样)。 + * + * 返回 `{ stdout, stderr, status }`。`hdc` 是 `findHdc()` 的返回值(路径字符串)。 + */ +export function shellOn(hdc, cmd, timeout = 20000) { + if (!hdc) return { stdout: '', stderr: 'no hdc', status: -1 }; + return sh(hdc, ['shell', cmd], timeout); +} diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index 2974173..16e7ffc 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -118,7 +118,7 @@ const SUITE = [ ['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 28], // P4c 图片上传:阈值与两档策略 / 失败必带原因 / 退档判定只有一处 / // release 都 await / 解码按目标尺寸 / multipart 字段名 / 上传后重新同步 - ['test/harmony-imageprep.test.mjs', ['--experimental-strip-types', '--no-warnings'], 30], + ['test/harmony-imageprep.test.mjs', ['--experimental-strip-types', '--no-warnings'], 31], // ArkTS **编译期**硬规则(纯文本可判、不需要设备)。这一条是构建撞出来的: // 我把常量表插在了既有 import 之前 ⇒ arkts-no-misplaced-imports,而当时没有任何判据会跑它。 ['test/harmony-arkts.test.mjs', [], 5], @@ -1330,7 +1330,21 @@ const STATIC_ONLY = [ ['test/cross-client-theme.test.mjs', '跨端令牌与玻璃分工:一端是 `.ets`,只能静态对齐', 'device'], ['test/appearance-defaults.test.mjs', '默认值契约里 `.ets` 那半:运行时行为要设备', 'device'], // P4c 同批的两条:判的是 `.ets` 里的页面/组件,本机没有设备也没有模拟器 - ['test/harmony-admin.test.mjs', '用户管理页:`.ets` 页面要 hvigorw 才能编译、要设备才能点(本机两者都没有)', 'device'], + /* + * ★★ 2026-09-19 **移除** `harmony-admin` 这条(它在这里待了很久)。 + * + * 它当初的理由是「用户管理页:`.ets` 页面要 hvigorw 才能编译、要设备才能点 + * (本机两者都没有)」。现在设备可用,那条判据**已经升级为设备判据**: + * 「管理页能从「我的」页打开,且列表真的渲染出用户」 + * —— 自己拉起应用、滚到底、点进去、断言用户行真的渲染。 + * + * ⇒ 它不再是"只能静态"的判据,**留在这里会让欠账余额虚高**: + * 余额是给"还欠着的"记账的,还完了就该划掉。 + * (这正是这个机制的意义:前提一旦成立,欠账必须当场处理,而不是永远躺着。) + * + * ⚠️ 移除 ≠ 那条判据消失:它仍在套件里(`SUITE` 里 28 条), + * 只是不再计入"静态欠账"。 + */ /* * ★ 第 2 列的**事实修正**(dsh 2026-09-19,把这一列变成每次运行都可见之后的第一件事 * 就是去读它,结果读到一句假话): diff --git a/docs/DEBTS.json b/docs/DEBTS.json index 8d49a62..65946d9 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -14,10 +14,11 @@ "debts": [ { "id": "static-criteria", - "count": 6, + "count": 5, "due": "本工作区能装、能点设备(探针三值转 true 时自动变红)", - "where": "client/electron/test/run-all.mjs 的 STATIC_ONLY(test/harmony-appearance.test.mjs、test/harmony-logic.test.mjs、test/cross-client-theme.test.mjs、test/appearance-defaults.test.mjs、test/harmony-admin.test.mjs、test/harmony-imageprep.test.mjs)", - "kind": "scope" + "where": "client/electron/test/run-all.mjs 的 STATIC_ONLY(2026-09-19 起 harmony-admin 那条已升级为设备判据、移出名单 ⇒ 6 → 5:test/harmony-appearance.test.mjs、test/harmony-logic.test.mjs、test/cross-client-theme.test.mjs、test/appearance-defaults.test.mjs、test/harmony-imageprep.test.mjs)", + "kind": "scope", + "note": "2026-09-19:`harmony-admin` 的条目**已升级**(管理页真的能打开、列表真的渲染出用户行)并移出 STATIC_ONLY ⇒ 本笔 6 → 5。剩下的五次升级按\"每条缺什么设备侧验证\"逐条来,不为了把数字消成 0 而凑 —— 凑出来的设备判据只是把\"没验\"换成\"假装验了\"。" }, { "id": "mails-status-derived", @@ -98,10 +99,11 @@ }, { "id": "harmony-p4c-boundary-decls", - "count": 5, + "count": 4, "due": "本工作区能装、能点设备 —— 那时这几条静态判据里被替代掉的那些断言换成真机断言,声明随之减少", "where": "client/electron/test/harmony-admin.test.mjs、client/electron/test/harmony-imageprep.test.mjs", - "kind": "scope" + "kind": "scope", + "note": "2026-09-19 更新(设备可用了,开始逐条升级):① `harmony-admin` 的那条**已升级为设备判据**(管理页真的能打开、列表真的渲染出用户行)⇒ 从本组减 1。② `harmony-imageprep` **只做了一半**,如实记着:新加的设备判据用真实素材(用户真上传的那张壁纸 1402×1122 / 152570 字节,从 GET /me/appearance/image 取回)验了「设备上读到的尺寸/体积与压缩决策的输入对得上」;**未做**「应用真的用 image.createImagePacker() 压一次、产出字节落在预期区间」——那需要一个**用户选图**入口(DocumentViewPicker,要人操作系统选择器),自动化里没有稳定路径。编一个绕过选择器直接调 packJpeg 的测试专用入口,会是只有测试在用的代码 —— 那种代码不会被真实场景触到,验它等于验一个不存在的东西。⇒ 剩 4 条里这一条要等一个**真人操作**的验证窗口(或平台提供可注入的选择器)。" }, { "id": "deviceprobe-fixture-timing",