修复: 判据注释说"这个值来自 Go 源",实际**一个字节没读** —— 服务端上限真漂移 29 pass/0 fail 一个字不变

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**;
凡"必须与别处一致"的判据先问:它真读了别处,还是抄了一份?
This commit is contained in:
2026-09-19 12:42:32 +08:00
parent 36ef15a8f2
commit a0e950109a
3 changed files with 142 additions and 4 deletions

View File

@ -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 <n> << <m>` 兜底值' };
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("…", <n> << <m>)`' };
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);