修复: 判据注释说"这个值来自 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:
@ -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):一条判据通过变异验证之后,只能说"我注进去的那一条会被抓"。
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user