From a0e950109ae9a92ca0bd34b44a68ce2f2535c281 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 19 Sep 2026 12:42:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E5=88=A4=E6=8D=AE?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E8=AF=B4"=E8=BF=99=E4=B8=AA=E5=80=BC?= =?UTF-8?q?=E6=9D=A5=E8=87=AA=20Go=20=E6=BA=90"=EF=BC=8C=E5=AE=9E=E9=99=85?= =?UTF-8?q?**=E4=B8=80=E4=B8=AA=E5=AD=97=E8=8A=82=E6=B2=A1=E8=AF=BB**=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E6=9C=8D=E5=8A=A1=E7=AB=AF=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E7=9C=9F=E6=BC=82=E7=A7=BB=2029=20pass/0=20fail=20?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E5=AD=97=E4=B8=8D=E5=8F=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 报的那格(4b 的 if(false))已由并发会话的自检 4c 补上,我变异验证通过(三种恒假写法都红)。 这轮顺着"第 2 列现在每次运行都可见"去读那 6 条理由,挖出**同族的另一条缝**: `harmony-imageprep` 的 /** 服务端壁纸上限(appearance.go 的 appearanceMaxBytes() 默认值) */ const SERVER_LIMIT = 4 << 20; 注释说它来自 Go 源,而它一个字节的 Go 源都没读。实测(同刻 A/B): 把 Go 里那个默认值改成 8<<20(真漂移)⇒ 本文件 **29 pass / 0 fail 一个字都没变** ⇒ 这条判据存在的全部理由(客户端上限要留在服务端那道门之内,否则必然 413 / 白扔分辨率)在服务端那道门真动了时**不会红**。危险处在于注释让读者以为已对齐。 修法(三件): ① 从**真源头**解析,且**两处都读、要求相等** —— ⚠️ 我第一版只读了 appearance.go 的 `return 4<<20`,那是**兜底分支**; 生产里 config.C 非 nil ⇒ 生效值来自 config.go 的 `MaxAppearanceBytes: getEnvInt64("AGENTMAIL_MAX_APPEARANCE_BYTES", 4<<20)`。 "读了源"还不够,还得问"读的是不是生效的那一处"(同族缝的下一层)。 ② 解析失败必须红,**不许静默回退到硬编码**(回退 = 把"我读不到"变成"值是对的")。 ③ 新增一条判据钉住"真的读出来了":两处都无 err、生效值等于 config 那处、等于兜底那处、且 >0。 并**如实标出标签范围**:这证明"与默认值对齐",**不证明**"与运行值对齐" —— AGENTMAIL_MAX_APPEARANCE_BYTES 可覆盖;别把本条读成"413 已不可能发生"。 变异验证(每个只动一处):config.go 默认值→8MB ⇒ fail=2; appearance.go 兜底→8MB(两处不一致)⇒ fail=1(恰为"两处相等"那条, 余量那条**故意不红**,因为生效值没变 ⇒ 所以"两处相等"必须单独存在); config.go 那行删掉 ⇒ fail=2(不许静默)。基线 30 pass / 0 fail。 另:`STATIC_ONLY` 第 2 列里那句「+ 服务端,三样本机都没有」是**假话** —— `server/internal/handler/attachments.go` 在本机、其 Go 测试 `-run Attach` 跑得通、 且本判据根本没连服务端。已改成"欠的只有设备侧那一半"(列每次运行都播报,假话会被读出来)。 注册条数 29→30 已同步。全套:files=31 checks=396 pass=384 fail=12 red=9 broken=3(跑在隔离 worktree)。 `CRITERIA.md §16.4`:通用规则 —— **注释里写"这个值来自 X"不构成读 X**; 凡"必须与别处一致"的判据先问:它真读了别处,还是抄了一份? --- client/electron/test/CRITERIA.md | 60 ++++++++++++++++ .../electron/test/harmony-imageprep.test.mjs | 72 ++++++++++++++++++- client/electron/test/run-all.mjs | 14 +++- 3 files changed, 142 insertions(+), 4 deletions(-) diff --git a/client/electron/test/CRITERIA.md b/client/electron/test/CRITERIA.md index 98277fb..64c0d91 100644 --- a/client/electron/test/CRITERIA.md +++ b/client/electron/test/CRITERIA.md @@ -990,6 +990,66 @@ if (!staticBroadcastSeen.some(t => STATIC_ONLY.every(([f, why]) => t.includes(f) > 若它不该被任何人读,**删掉它** —— **余额里读不出来的东西等于不存在**。 > 只有在"这个字段本来就有真假"时,才轮得到加判据 —— 而且**必须先拿现有数据验一遍**。 +### 16.4 ★★ **注释说是从某处读的,实际一个字节没读** —— 硬编码冒充"读源"(dsh 2026-09-19) + +§16.3 把第 2 列变成每次运行都可见之后,我做的第一件事就是**去读它** —— 于是读到一句假话, +顺着它又挖出本仓一条**同族**的缝。**这条缝的形状**: + +```js +/** 服务端壁纸上限(`server/internal/handler/appearance.go` 的 `appearanceMaxBytes()` 默认值) */ +const SERVER_LIMIT = 4 << 20; // ← 注释说它来自 Go 源,而它一个字节的 Go 源都没读 +``` + +**实测**(dsh,同刻 A/B):把 `appearance.go` 里 `appearanceMaxBytes()` 的默认值 +`4 << 20` 改成 `8 << 20`(**真漂移**)⇒ `harmony-imageprep` **29 pass / 0 fail,一个字都没变**。 +⇒ 这条判据存在的全部理由就是"客户端上限要留在服务端那道门之内(否则必然 413 / 白扔分辨率)", +而**服务端那道门真动了,它不会红**。 + +★ **它为什么危险**:注释让读者**以为**这里已经对齐了服务端 —— +**"我以为它在读源头"正是没人再去读源头的原因**。 +这与 §16.3 那条母规则同一族:**一个字段/一个判据的可信度,来自它真的读了那个东西, +不来自它说自己读了。** + +**修法**(三件): + +1. **从真源头解析** —— ⚠️ **而"真源头"是哪一处,我第一版也读错了**,如实记下: + 我最初去读 `appearance.go` 的 `return 4 << 20`,那是**兜底分支** + (`if config.C != nil && config.C.MaxAppearanceBytes > 0` 之后才轮到它)。 + 生产里 `config.C` 非 nil ⇒ **真正生效的值来自 `config.go`**: + `MaxAppearanceBytes: getEnvInt64("AGENTMAIL_MAX_APPEARANCE_BYTES", 4<<20)`。 + ⇒ **只读兜底分支的修法,在"有人动了 config 默认值"时会漏** —— 这是同一族缝的下一层: + **"读了源"还不够,还得问"读的是不是生效的那一处"。** + 现在**两处都读,并要求它们相等**(不相等 ⇒ config 未注入时会走另一个门)。 +2. **解析失败必须红**,**不许静默回退到硬编码** —— 回退等于把"我读不到"变成"值是对的" + (这几轮反复消的那条缝)。 +3. **加一条判据钉住"真的读出来了"**:两处都无 `err`、`SERVER_LIMIT === LIMIT_CONFIG.value`、 + **`SERVER_LIMIT === LIMIT_FALLBACK.value`**、且 `> 0`(防解析出 `0` 让"小于上限"变成恒真)。 + +**变异验证**(dsh,每个变异只动一处、其余不变): + +| 变异 | 期望 | 实测 | +|---|---|---| +| `config.go` env 默认值 `4<<20` → `8<<20`(**生产生效那一处**) | 红 | `fail=2`(第 9 条"两处都读到" + 第 11 条余量)✓ | +| `appearance.go` 兜底 `4<<20` → `8<<20`(**两处不一致**) | 红 | `fail=1`(恰为第 9 条"两处相等")✓ | +| `config.go` 那一行整个删掉(**读不到**) | 红,且**不许静默** | `fail=2`(含"读不出生效上限")✓ | +| 基线 | 全绿 | `30 pass / 0 fail` ✓ | + +★ 注意 B 只红第 9 条、**不红第 11 条**:因为生效值取 config 那处、而它没变 —— +**这是对的**(余量断言针对生效值),也说明"两处相等"那条**必须单独存在**, +否则"兜底分支漂了"会完全无声。 + +⚠️ **本节的标签范围**:它证明的是"客户端上限与**源码里那个默认值**对齐", +**不是**"与服务端运行时实际生效的上限对齐" —— 那个值是 `config.C.MaxAppearanceBytes` **可覆盖**的 +(`appearanceMaxBytes()` 先看配置、配置没有才用它)。配置一旦调过,**静态读源码仍然对不上运行值**。 +要覆盖后者得真起服务端读它的行为 —— 那属于"设备/服务端在场"的判据,本机判不了。 +**别把这条读成"413 已经不可能发生”。** + +★ **通用规则**(与 §16.1/§16.3 并列): + +> **注释里写"这个值来自 X",不构成读 X。** +> 凡"某个数必须与别处一致"的判据,先问:**它真的读了别处吗,还是抄了一份?** +> 抄一份的判据**在别处改变时不会红** —— 而它的注释会让你以为它会。 + ## 17. 变异只证明「注入的样本被抓」,**不证明完备性**;判据的**标签必须等于断言范围** **规则**(pi 2026-09-14):一条判据通过变异验证之后,只能说"我注进去的那一条会被抓"。 diff --git a/client/electron/test/harmony-imageprep.test.mjs b/client/electron/test/harmony-imageprep.test.mjs index 173c3e9..0cc044e 100644 --- a/client/electron/test/harmony-imageprep.test.mjs +++ b/client/electron/test/harmony-imageprep.test.mjs @@ -39,8 +39,57 @@ const SETTINGS_PAGE = join(ETS, 'pages/SettingsPage.ets'); const P = await import(pathToFileURL(PREP_TS).href); -/** 服务端壁纸上限(`server/internal/handler/appearance.go` 的 `appearanceMaxBytes()` 默认值) */ -const SERVER_LIMIT = 4 << 20; +/* + * 服务端壁纸上限 —— **从 Go 源里读出来,不许硬编码**。 + * + * ★ 原来这里是 `const SERVER_LIMIT = 4 << 20;`,注释还写着"(`appearance.go` 的 + * `appearanceMaxBytes()` 默认值)"—— 但**它一个字节的 Go 源都没读**。 + * 实测(dsh 2026-09-19):把 Go 里那个默认值改成 `8 << 20`,本文件 + * **29 pass / 0 fail 一个字都没变** ⇒ 真漂移了它不会红。 + * + * ★★ 而**我第一版修法读错了地方**(自查抓出,如实记下): + * 我去读 `appearance.go` 的 `return 4 << 20` —— 那是**兜底分支** + * (`if config.C != nil && config.C.MaxAppearanceBytes > 0` 之后才轮到它)。 + * 生产里 `config.C` 非 nil ⇒ **真正生效的值来自 `config.go` 里 + * `MaxAppearanceBytes: getEnvInt64("AGENTMAIL_MAX_APPEARANCE_BYTES", 4<<20)`**。 + * ⇒ 只读兜底分支的修法,**在"有人分了 config 默认值"时会漏**。 + * + * ⇒ 现在**两处都读,并要求它们相等**: + * · `config.go` 的 env 默认值 = **运行时真正生效的那一处**(生产); + * · `appearance.go` 的 `return` = 兜底分支; + * 两者不一致本身就报红(那意味着"config 没设时"与"config 设了但为 0"走不同的门)。 + * ★ **仍然读不到运行期实际值**:`AGENTMAIL_MAX_APPEARANCE_BYTES` 可以被环境变量覆盖 ⇒ + * 静态读源码永远只能说"与**默认值**对齐"。要判运行值得真起服务端。别把本条读成 + * "413 已经不可能发生"(见 `CRITERIA.md §16.4` 的标签范围)。 + */ +const APPEARANCE_GO = join(ROOT, 'server/internal/handler/appearance.go'); +const CONFIG_GO = join(ROOT, 'server/internal/config/config.go'); + +/** 从 `appearance.go` 的 `appearanceMaxBytes()` **兜底分支**取默认值 */ +function fallbackLimit() { + const src = code(APPEARANCE_GO); + const fn = /func appearanceMaxBytes\(\)[^{]*\{([\s\S]*?)\n\}/.exec(src); + if (!fn) return { err: `在 appearance.go 里找不到 func appearanceMaxBytes()` }; + // 取函数体内**最后**一个 return(前面的 return 是配置分支) + const all = [...fn[1].matchAll(/return\s+(\d+)\s*<<\s*(\d+)/g)]; + if (!all.length) return { err: 'appearanceMaxBytes() 里没有 `return << ` 兜底值' }; + const m = all[all.length - 1]; + return { value: Number(m[1]) * 2 ** Number(m[2]), raw: `(${m[1]} << ${m[2]})` }; +} + +/** 从 `config.go` 的 `MaxAppearanceBytes:` 取 env 默认值 —— **这才是生产里生效的那一处** */ +function configLimit() { + const src = code(CONFIG_GO); + const m = /MaxAppearanceBytes:\s*getEnvInt64\(\s*"[^"]*"\s*,\s*(\d+)\s*<<\s*(\d+)\s*\)/.exec(src); + if (!m) return { err: '在 config.go 里找不到 `MaxAppearanceBytes: getEnvInt64("…", << )`' }; + return { value: Number(m[1]) * 2 ** Number(m[2]), raw: `(${m[1]} << ${m[2]})` }; +} + +const LIMIT_FALLBACK = fallbackLimit(); +const LIMIT_CONFIG = configLimit(); +// 生效值以 config 那处为准(生产里 config.C 非 nil,走的是它) +const SERVER_LIMIT_SRC = LIMIT_CONFIG; +const SERVER_LIMIT = SERVER_LIMIT_SRC.value; /* ────────────────────── ① 缩放 ────────────────────── */ @@ -130,6 +179,25 @@ test('估算:随像素数与质量**单调**增(否则"要不要退档"的 assert.ok(P.estimateJpegBytes(1000, 1000, 0.5) < base, '质量更低,估算该更小'); }); +test('★ 服务端上限**真的从 Go 源读出来了** —— 两处都读到、且两处相等(读不到必须红,不许静默回退)', () => { + assert.ok(!SERVER_LIMIT_SRC.err, + `★ 读不出生效上限(config.go):${SERVER_LIMIT_SRC.err} ⇒ 下面两条"余量"断言就退化成** + 用我猜的数去比客户端**(那正是这次修掉的那条缝:注释说是服务端的值,实际一个字节没读)`); + assert.ok(!LIMIT_FALLBACK.err, + `★ 读不出兜底上限(appearance.go):${LIMIT_FALLBACK.err} ⇒ 兜底分支没人守` + + '(config 没设/为 0 时走的是它,与生产走的是**不同的门**)'); + assert.equal(SERVER_LIMIT, SERVER_LIMIT_SRC.value, + '客户端用的上限与从 Go 源解析出来的值不一致 ⇒ 解析没接上'); + // ★ 两处必须相等:不相等意味着"config 为 nil 时"与"config 设了"走不同的门 + assert.equal(SERVER_LIMIT, LIMIT_FALLBACK.value, + `★ 两处默认值不一致:config.go ${SERVER_LIMIT} ≠ appearance.go 兜底 ${LIMIT_FALLBACK.value}` + + ' ⇒ config 未注入时服务端会用另一个上限(客户端余量就是按错的那个算的)'); + // 上限本身要是个合理的正数(防止解析出 0 让"小于上限"变成恒真) + assert.ok(SERVER_LIMIT > 0, `★ 解析出的服务端上限是 ${SERVER_LIMIT} ⇒ 不是有效上限`); + // ⚠️ 标签范围:这只证明"与**默认值**对齐",不证明"与运行值对齐"—— + // AGENTMAIL_MAX_APPEARANCE_BYTES 可覆盖(见 §16.4) +}); + test('估算:**不超过**服务端那道门的那一档,估算值要落在上传上限之内', () => { // 2560×1920 是 4000×3000 缩到首档后的实际尺寸 —— 正常照片走的就是这一档 const est = P.estimateJpegBytes(2560, 1920, 0.85); diff --git a/client/electron/test/run-all.mjs b/client/electron/test/run-all.mjs index ee58cf1..7f13964 100644 --- a/client/electron/test/run-all.mjs +++ b/client/electron/test/run-all.mjs @@ -112,7 +112,7 @@ const SUITE = [ ['test/harmony-admin.test.mjs', ['--experimental-strip-types', '--no-warnings'], 22], // P4c 图片上传:阈值与两档策略 / 失败必带原因 / 退档判定只有一处 / // release 都 await / 解码按目标尺寸 / multipart 字段名 / 上传后重新同步 - ['test/harmony-imageprep.test.mjs', ['--experimental-strip-types', '--no-warnings'], 29], + ['test/harmony-imageprep.test.mjs', ['--experimental-strip-types', '--no-warnings'], 30], // ArkTS **编译期**硬规则(纯文本可判、不需要设备)。这一条是构建撞出来的: // 我把常量表插在了既有 import 之前 ⇒ arkts-no-misplaced-imports,而当时没有任何判据会跑它。 ['test/harmony-arkts.test.mjs', [], 5], @@ -1289,7 +1289,17 @@ const STATIC_ONLY = [ ['test/appearance-defaults.test.mjs', '默认值契约里 `.ets` 那半:运行时行为要设备', 'device'], // P4c 同批的两条:判的是 `.ets` 里的页面/组件,本机没有设备也没有模拟器 ['test/harmony-admin.test.mjs', '用户管理页:`.ets` 页面要 hvigorw 才能编译、要设备才能点(本机两者都没有)', 'device'], - ['test/harmony-imageprep.test.mjs', '图片上传链:要 `@ohos.multimedia.image` + 相册 + 服务端,三样本机都没有', 'device'] + /* + * ★ 第 2 列的**事实修正**(dsh 2026-09-19,把这一列变成每次运行都可见之后的第一件事 + * 就是去读它,结果读到一句假话): + * 原文是「要 `@ohos.multimedia.image` + 相册 + **服务端**,三样本机都没有」—— + * **"服务端"这一项不成立**:`server/internal/handler/attachments.go` 在, + * 且它的 Go 测试在本机能跑通(`go test ./internal/handler/ -run Attach` ⇒ ok)。 + * 而且这条判据**根本没连服务端**:它读的是 `appearance.go` 里 + * `appearanceMaxBytes()` 的默认值(本笔刚把它从硬编码改成**真读 Go 源**)。 + * ⇒ 服务端那一半由本判据**静态覆盖**,欠的只有**设备侧的相册/解码真跑**。 + */ + ['test/harmony-imageprep.test.mjs', '上传链的设备侧:`@ohos.multimedia.image` + 相册要设备才能真跑(服务端那半本判据直接读 `appearance.go` 的源码,已覆盖)', 'device'] ]; for (const [file, why, probe] of STATIC_ONLY) {