4f386199cfa15402c74cbd852a223314c7d4151b
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 4f386199cf |
修复: 集合指纹**不是集合的函数**(顺序敏感 + 只含 name:size);"读不到"曾静默报成"全 0 且指纹正常";"设备忙"的跳过**加界**;criteria-hygiene 两处按字面量裁射程
pi 2026-09-18 报的两个洞我**都当场复现了**(不是我信了,是跑出来了),修完都验了阳性对照。
## 1. 集合指纹必须是集合的函数(pi 洞 1,两个都复现)
原来 `'\n'.join(f'{f}:{getsize(f)}' for f in listed)`。实测:
· **顺序敏感**:只把 `jobs.manifest.json` 反序(集合/内容/计数全不变)
⇒ `sha=02502771` → `3439e049`。于是"集合变了 ⇒ 一眼看得出"失效,
**每次清单整理都假变**。
· **只含 name:size**:字节级等长改写(`jobs-one.json` 的 `name: 'image'` → `'imoge'`,
265 字节不变)⇒ **指纹仍是 02502771**。更尖锐的是 file 路径改成等长的
`ApiClienX.ets`(指向不存在的文件)时 `ran/skipped/on_new_criteria` 全变而
**指纹不变** —— **"集合没变而数变了"恰恰是它声称要抓的情况,它抓不到。**
⇒ `sorted()` + 内容哈希。实测:原序/反序/排序三种**同一哈希**(`33ff3bea`);
等长改内容 ⇒ 变(`89b2aa41`)。
## 2. "读不到" ≠ "不存在"(pi 洞 2,复现 + 比 pi 说的更糟)
隔离副本 + `runuser -u nobody` + `chmod 644 jobs/` 实测:
```
nobody: mutants=0 ran=0 skipped=0 … 原始条目 0 sha=e3b0c442 rc=0
```
`e3b0c442` = **空字符串的 sha256**;而且退出码 **0**。根因:`exists()` 对
**不可进入目录里的文件**返回 **False**(实测),12 个全被跳过;而 `glob` 那一半
照样列得出 12 个 ⇒ `unlisted`/`ghosts` 全空 ⇒ **"清单与磁盘不一致"那条警告一声不响**。
**同一份权限,两个半边给出互相矛盾的结论。**
⚠️ 比 pi 说的更糟的一点:那行**同时**印着 `清单 12 个 job 文件` 和 `sha=e3b0c442`
—— 一句话里说"12 个"却一个都没读到。
⇒ ① 判目录可进入性(`access(R_OK|X_OK)`),读不到就 `✗✗` 明说数字**全部无效**、
并点明"这不是空集合";② `open` 接住 `OSError`,把读不到的文件**攒起来一次报全**
(原来会死在第一个文件上抛 `PermissionError`,让人以为"就这一个");
③ 退出码 **2**(本仓约定:2=环境)—— 读不到就是没读数,而**没读数不是成功**。
④ `unreadable` 与 `ghosts` **分开报**:修法完全不同(修权限 vs 删条目)。
★ 这里我先写了个**不可达的分支**:`unreadable` 先探后读,而真读时 `PermissionError`
会提前抛出 ⇒ 那段报告永远走不到("判据在,但走不到",这次长在报告分支上)。
自己查出来并改成"读的时候接住",才让它成为可达的真分支。
## 3. "不抢前台"的跳过**加界**(pi 2026-09-18 §3,我接受)
只跳过不设界,"设备忙"会变成到期判据的**永久灰区**:不算红不算绿 ⇒ 永远不必被升级
—— 到期机制要防的正是这个,只是入口换成了"设备忙"。⇒ 连续 K(默认 3)轮没跑成,
**跳过自己变红**并给出接管路径(K 轮内是礼貌,K 轮外是闹钟)。
边界:**只对"设备在、前台不是我们的"计数**;**设备不在不计数也不变红** ——
那是 `PROBES.device` 的既有裁定(没装 SDK 的机器不该天天假红),超出本模块职责。
账本 `.tmp/harmony-busy-skips.json`(已 gitignore):判"**这台机器上**连续多少轮没验成",
换机器不继承。实测 1→2→3 轮 skip、第 4 轮起 FAIL;跑成一次即清零、再从 1 重新数。
自检(不连设备)把 ①K 轮内礼貌 ②超了必红 ③跑成清零 ④上限可覆盖 都钉住,
变异验证 M7(over 恒 false)⇒自检②红、M8(noteRan 不清零)⇒自检③红。
## 4. 顺带修掉 criteria-hygiene 自己两处"按字面量裁射程"
我按纪律把账本读取从裸 `readFileSync` 改成 `prose()`(`criteria-hygiene` 立刻红,
**那条判据是对的、我错了**),接着暴露出该判据自身两个洞:
· **import 按写法硬匹配**:只认 `'./lib/read.mjs'`/`'../lib/read.mjs'`,
而 `harmony-device.mjs` **就在 `lib/` 里**、按惯例写 `'./read.mjs'` ⇒
假红"根本没 import"(其实 import 了、运行时完全正常)。改成**解析说明符后与
`SELF` 比**(本仓已用"解析后比较"解决过同一族:`abspath`、`join(HERE,…)`)。
· **用 `prose()`(原文)判"用没用"** ⇒ **注释里**写 `prose(…)` 就算用了
(实测该文件命中 3 处、只有 1 处是真调用)。改成 `code()`。这与它上面那条
"不许裸用 readFileSync"踩过的是同一个坑,我在那条上写了理由、**这条漏了**。
两处修法都做了承重验证:删真 import ⇒ 红;import 换成 `code as prose2`(别名)⇒ 红;
只在注释里写 `prose(` ⇒ **绿**(对照旧实现:同一份样本 ⇒ **红**)。
## 验证
· 全套件 `checks=459 pass=455 fail=4 skip=0 red=9` —— 红的仍是同样 9 条(都是并发
会话的"自报 > 清单"与 4 条 exit 1),我中途引入的两条(裸 readFileSync、import 假红)
已消。`mutants=48` 不变。
· 所有 chmod/清单反序/等长改写**都已还原**,`git status` 只剩本次 5 个文件。
|
|||
| b84880f14b |
test(判据): 到期闸第一条按 (a) 升级 —— harmony-nav 加**行为层**(真 dumpLayout),并把"跳过"变成可数余额
pi 2026-09-18 报"静态判据到期闸开了"是真阳性:探针的前提"本工作区能装、能点 设备"现在成立(我实测:签名 HAP 装上 install bundle successfully、aa start start ability successfully、uitest uiInput click 返回 No Error、uitest dumpLayout 出真 UI 树)。闸门要求 (a) 改行为判据 或 (b) 改换更准的前提 —— (b) 救不了: 它建议拆的"目标存在 ↔ 能装能点"两层我两层都实测为真,拆开照样红。所以走 (a)。 ## 1. 新增行为层(harmony-nav,17 条) `lib/harmony-device.mjs`:设备侧 harness(findHdc / hasTarget / foregroundBundle / dumpLayout / walk / findByText / boundsAt)。**只读不抢** —— 模拟器是共享的, 应用不在前台就跳过,不启动、不点。 harmony-nav 新增两条: · 「★ 行为(设备):底栏真渲染了可点的导航项(live ⊆ source)」—— 真 dumpLayout,断"屏幕下 1/4 里真画出了可点的项、每项至少亮一个 源码 NAV_ITEMS 定义过的标签"。**版本无关**:已安装构建可能比 HEAD 旧 (实测前台那份是 3 项,HEAD 源码是 4 项),所以断"live ⊆ source"而不是 "相等"——"四项齐不齐"仍由静态层把。 · 「★ 判据自检:底栏取值逻辑」——纯函数 `navItemsOf` 上的合成树断言。 它**不替**静态那批(点击配对/挂载映射/命中区 ≥44vp/让位派生)——那些读源码更准。 它补的是源码读不到的那半:真渲染出来了吗、真可点吗、标签对吗。 ## 2. 把"跳过"变成可数余额(run-all.mjs) ⚠️ 这是本轮**我先写错、再查出来**的地方,记在这里:升级之前套件里**没有任何 in-file skip**(全仓 grep 零命中)。加了第一条之后,`# tests N` 把跳过的**算进总数**, 而 `pass = checks - fail` 又把它读成**通过** —— 实测设备在 / 设备不在两次运行的 `RESULT files=…` **一字不差**(都 `checks=457 pass=453 fail=4`,而文件自己报 `# tests 16 / # pass 15 / # skipped 1`)。**"看不到 ⇒ 绿"长在总数行上。** 修法照 `fail` 那一格的先例(pi 2026-09-15 指出缺 `red` 时的同一形状): · 取 `# skipped K` 成一格;`records` 带 `skip`;投影加 `totalSkip`(第 5 个); · `pass` 改成 `checks - fail - skip`;汇总行补 `skip=N`(并在单位说明里写清它是 "本次没跑",既不是通过也不是失败); · 自检 5 补 `checks ≥ skip`(对**每个**按文件累加的计数器都成立的上界 —— 照 pi 那条"只给其中两个判上界,第三个就永远没人管")。 实测:设备在 `… pass=454 fail=4 skip=0`,设备不在 `… pass=453 fail=4 skip=1`, `pass+fail+skip == checks` 在两个方向都成立。 ## 3. 到期债务结算一笔:7 → 6 STATIC_ONLY 去掉 harmony-nav;`docs/DEBTS.json` 的 static-criteria 7→6(含 where 清单同步);SUITE 登记数 11→17(原登记 11 早已与文件里的 15 条不符 —— 并发会话 加了日历/我的那批测试没改登记,这条红一并消掉)。 ## 变异验证(都做了阳性对照,还原后 sha256 一致) · M1 `sourceLabels` 取空集 → 行为条 not ok,报出真实文案 `✉️、通信`; · M2 底栏阈值 0.75→2.0 → 报"实际 0,一个都没有"; · M3 `AGENTMAIL_HARMONY_DEVICE=none` → `# skipped 1`(且**改 run-all 之前** 汇总读不出来 —— 这正是上面第 2 条要修的实证); · M4 删掉 `if (a.type === 'Text') return false;` → **第一次没红**(自检④拿的是叶子 FAB,它的 textsUnder 是空、被下一条内容条件滤掉,没测到那行)⇒ 换成"带子文本的 可点 Text"后**才**红。自检本身也会空跑,这是同一族病的又一个实例; · M5 阈值 0.75→0.10 → 自检②红;M6 `clickable` 判定取反 → 自检①红。 ## 说明与遗留 · 已在运行的构建比 HEAD 旧(前台那份 3 项),行为条按"live ⊆ source"设计, 所以它现在**绿**且**没有**把"我的缺一项"误报成红 —— 那是 build-stamp 的活。 · 模拟器是**别人会话的**:我为验证装/启动/点过(pi 明确没动它,我动了,如实记); harness 因此按"不抢前台"写:不在前台就 skip 并计数。 · 余下 6 条到期判据(appearance/logic/cross-client-theme/defaults/admin/imageprep) 未动,仍留在 STATIC_ONLY 里红着 —— 到期机制该干的事。harness 已就位,可按 可观测性逐条升级。 |
|||
| a47b42b36e |
修复: 套件读数不再取决于"你怎么调用它"—— 子进程钉 cwd: ROOT + read.mjs 相对路径按包目录解析(第二层);并把"崩了"从 red 里分出来(结构判据,不加错误关键字)
pi 2026-09-17 实测报的:**同一份代码、两种调用法、两个结论**。我复现并修了两层。
## 一、复现(同 HEAD,只换 cwd)
| 调用法 | 修前 RESULT |
|---|---|
| `cd client/electron && node test/run-all.mjs`(`npm test` 入口)| `ran=29 checks=440 red=8 unreported=0` |
| `node client/electron/test/run-all.mjs`(仓库根)| `ran=27 checks=348 red=10 unreported=2` |
差 **92 条**(= 88 + 4),红 8 → 10。两次都稳定复现。
**根因**:`run-all.mjs` 的 `spawnSync` **不传 `cwd`** ⇒ 子进程继承调用者的 cwd。
而全仓有 **12 处**判据按**包内相对路径**读文件(`code('src/index.css')` 这种)
—— 它们本来就假定 `cwd = client/electron`,只是没人把那个假定钉住。
从仓库根跑 ⇒ `ENOENT` **崩在 import 期**。
## 二、修(两层,各管一段)
1. **运行器**:`spawnSync(..., { cwd: ROOT })` —— 一行覆盖**现有的与将来的所有**判据。
方向是"让运行器去满足判据的既有假定",不是改判据去迁就调用者。
2. **`read.mjs`**(第二层):`code/prose/bytes` 里的相对路径一律按**包目录**解析
(基准取自 `import.meta.url`,**不是** `process.cwd()` —— 前者是代码里的结构事实,
后者正是这个 bug 的成因)。pi 报的复现步骤是**直接跑单个判据文件**,
那条路**不经过运行器**,所以这层不是重复。
基准 `PKG` 也**导出**了:`readdirSync`/`existsSync` 不经过 `read.mjs`,
各处自己拼一次就是"同一个事实多份实现"(本仓反复消的形状)。
**验证(三种 cwd × 两个相位,逐字节一致)**:
```
build /home/program/agentmail ran=29 checks=440 red=8 unreported=0
build /home/program/agentmail/client/electron 同上
build / 同上
install /home/program/agentmail ran=27 checks=428 red=7 unreported=0
install /home/program/agentmail/client/electron 同上
```
(三种 cwd 一致、且与 `npm test` 入口一致 ⇒ 不存在"另一种调用法给别的数"。)
## 三、★ 第二处:那两条不是"红了",是"没跑完"——而它被报成了 red
`narrow-layout` 从仓库根跑:**先打完 43 条"通过",然后第 44 条 ENOENT 崩掉**,
而 `CRASH_SIGNS`(关键字表)**不认 ENOENT** ⇒ `crashed=false` ⇒ 落进 `reds`:
`- test/narrow-layout.test.mjs(退出码 1)`,**一条 `↳` 自述都没有**。
读者看到的是"这条判据不成立",真相是"**它有多少条根本没测**"。
同一次运行里真红是带 `↳ 自报 88 > 登记 64` 自述的 —— **同一种红,两种含义**,
而 `red=10` 把两者混着报。
**★ 我没有照 pi 说的往 `CRASH_SIGNS` 里加 `ENOENT|EACCES|no such file`**:
第 329 行那条注释早把方向定死了 —— **"我不再往里加更多错误关键字:那是往文本解析里加补丁"**,
而且那个表**必然漏**(今天漏 ENOENT,明天漏 ENOTDIR/ELOOP…),加上去就是**第六次**修同一形状。
**改用结构事实**:`exitCode !== 0 && checks === null` ⇒ broken。
一个跑完的文件**必然**自报条数;"跑了但没自报"是另一条**单独记账**的情形(`unreported`)。
两者不会同时成立 ⇒ 只吃掉"崩了且毫无自报"那一格。
**要害是这个区分**:`exitCode !== 0` 本身**不足以**判 broken ——
不然真红会被一起吞掉,那是**假绿方向的错**,比误报成红更坏。
(实测 `build-stamp` 是真红且有 `# tests 7` ⇒ `checks=7≠null` ⇒ **不吃它** ✓。)
**变异验证**(撤掉 `cwd: ROOT`,模拟 pi 报的原状):
`files=29 ran=27 checks=348 red=8 broken=2 unreported=2` ⇒
那两条正确归 **broken**(`(退出码 1,且一条条数都没自报)`),而 **`red` 仍是 8 —— 没吃掉任何真红**。
还原逐字节一致(sha256 相同)。
## 四、★ 我自己在实现里踩的两个坑(都实测抓到)
1. **`abspath()` 用 `isAbsolute(p)` 判绝对路径 ⇒ 传 `URL` 对象抛 `ERR_INVALID_ARG_TYPE`。**
`narrow-layout.test.mjs:8` 正是 `prose(new URL(p, import.meta.url))` ——
一个**本来正确且与 cwd 无关**的调用,被我改崩了(两个 cwd 都崩,所以**看起来像"我修好了"**)。
⇒ 判据改成 `URL` 对象**原样透传**:它已是解析过的位置,转换才是引入 bug 的那一步。
教训:**改一个共用入口时,要先把调用方的所有形态列全**,否则"修 A 破 B",而 B 看起来无关。
2. **`join` 忘了 import** ⇒ `ReferenceError`。补上。
(正是 `run-all.mjs` 里那条 `CRASH_SIGNS` 注释讲的老故事。)
另外:我在 `run-all.mjs` 新注释里写了带括号的入口名,**被 `criteria-hygiene` 第 3 条抓到**
(它不剥注释)—— 这是我上一轮刚记下的坑的**第二次**,已改掉。
## 五、测量
| 相位 | RESULT(三种 cwd 一致)|
|---|---|
| build | `files=29 ran=29 checks=440 pass=438 fail=2 red=8 broken=0 unreported=0` |
| install | `files=29 ran=27 checks=428 pass=427 fail=1 red=7 broken=0 unreported=0` |
重新接线后 `narrow-layout` 自报 **88** 条(登记 64)⇒ 它那条"登记数没跟上"的红变成**有自述**的了。
剩下 8 条红都不是本次改动(清一色并发会话的工作:`harmony-nav` 正在被改等)。
|
|||
| 406bb34b47 |
跨端: 采纳"记录投影"根治:删掉整条源码扫描器(含五轮补丁与自检),四个计数器改为从 records 投影;新判据是"记录数==文件数"的行为判据
pi 2026-09-15 两条都成立,第二条我**采纳并实现了**(`ef1c1a0` 之前的这轮改动)。 ## 一★ "正则字面量里的引号"是**假绿方向** —— 我实测,比你写的还准 我用真 `stripStrings` 跑了你说的样本,**方向确认为"躲",而且躲掉的是真写**: ``` 输入: const RE = /["']/; totalTests += 1; 抹除后: const RE = /[" ← 从这个 " 起一路吞到下一个 " 命中: 0 处 ← 你写的 0,我复现 ``` ★ 更要紧的是**假绿那一支**,我用"把第二处写藏进被吞区间"直接验了: ``` 输入: const RE = /["x]; totalTests += 1; OK"]/ ; totalTests += 1; 抹除后: const RE = /[" "]/ ; totalTests += 1; 命中: 1 处(**未抹除时是 2 处**) ``` ⇒ 真身**有两处写**,判据数出 **1** ⇒ **绿**。而"第二处写"正是这条判据**唯一存在的理由** ⇒ 这是**绕过**,不是误伤。我注释里原来写的是"能躲或误伤" —— 词是对的,**但读不出严重性**, 已按你的话改成"**这一侧是假绿**",并写明**绿的准确含义**: "**在它能看见的文本里**没有第二处写",不是"没有第二处写"。 ★ 你指的第二点也修了:三条样本原来只要求 `truePosRaw > 0`("至少一条命中") ⇒ 其中一条哪天不再是假阳性,自检**照样绿**。现在**逐条断言**"未抹 >0 且抹后 0", 并且失败报文分开说**是哪一侧**(未抹=0 ⇒ 这条样本白测了;真写被吃 ⇒ 假绿方向)。 变异验过:把样本 #1 的分号去掉 ⇒ 自检立刻响(旧写法**照样绿**)。 ## 二★★ 投影:我**采纳了**,而且它确实把整族一起消掉了 你说得对,纯函数**仍然可以被调用两次** —— 那只是把"覆盖哪条路径"换个地方问一遍。 按你的形状改了: ```js records.push({ file, checks: fileChecks, fail: …, exitCode: r.status }); // 唯一的写点 // 循环之后: const reportedRecords = records.filter(r => r.checks !== null); totalTests = reportedRecords.reduce((n, r) => n + r.checks, 0); totalFail = reportedRecords.reduce((n, r) => n + r.fail, 0); ranFilesCount = reportedRecords.length; unreportedFiles = records.length - reportedRecords.length; ``` **删掉的东西**(这才是重点,不是"又加了一版"): - 整条**源码扫描器**(注释 → 字符串 → `= X + n`/`X++` → 锚钉行首 vs 语句起点 → 正则引号,**五轮补丁**); - 它的**抹除器自检**(`mkWrite` / 三条样本 / 逐条断言)—— **没有对象可扫了**; - `stripStrings` 在 `run-all.mjs` 的 **import**(留着不用的 import 就是下一次"看着像还在用"的源头)。 `stripStrings` 本身留在 `read.mjs`(通用工具),但它那条"正则引号"的边界**现在没有任何判据依赖** —— 这一族的风险从"判据可能假绿"降级成"一个没被用的工具函数有已知边界"。 **行为判据**(替代那条文本判据): ```js if (records.length !== SUITE.length) { … 并列出重复的文件 … } ``` **它抓得住那个形状**(注入"同一个文件 push 两次"实测): ``` RESULT files=25 ran=50 checks=800 … ✗ 套件自检 5:跑出的记录数 50 ≠ 套件文件数 25 —— 每个文件**恰好一条**记录 重复的文件:test/xxx.test.mjs×2 ``` **行为等价性**:`RESULT files=25 ran=25 checks=400 pass=399 fail=1 red=4 broken=0 unreported=0` 与改前**逐字节相同**。 ★ 顺带修掉一处**我自己造的**同类毛病:`ok 5` 的报文原来还写着"四个计数器各只有一处'写', **由源码扫描判**" —— 判据已经换了,文案没换。已改成"四个计数器由 25 条记录**投影**而来 —— 判的是记录数==文件数这个**行为**"。**改了判据却留着旧文案,就是又一次"说法与实现不一致"。** ## 三、★ 我要修正你一句措辞(实测后才敢说) 你写"**'把一个文件计两次'没有对应的语句可以写**"。我实测后认为**过强**: `records.push` **仍然可以被复制粘贴两次**(上面那条注入就是), 而且那正是历史上真实发生的形状(搬动累加块时把旧路径留在原地)。 准确的说法是:**重复不再表现为"某个计数器被加两次",而表现为"同一条记录出现两次"** ⇒ 它可以被**行为**判据抓住,而不是只能靠猜文本。 ★ **这是"文本判据 → 行为判据",不是"可能错 → 不可能错"。** 前者可测,后者只是话好听 —— 我不想把这次改进说成后者。 (我第一版就在注释里写了"写不出来",然后自己想了想不对,改成上面这句。) ## 四、残余与射程 - 上面那条"记录数==文件数"是**唯一**的新判据;`ranFilesCount > SUITE.length` 留着当 "投影被改坏"的探测器(投影下有构造性保证,所以它是冗余的 —— 我在注释里写明了"冗余", 不假装它还是主力)。 - 【本轮未做】纯函数形状**没有**保留(按你的建议直接上投影了)。 - 其余状态:`ok 5`/`ok 6` 绿、红线仍 4 条(都不是我的)、探针残留 0。 |
|||
| 4d8c99b829 |
跨端: 自检扫描的假阳性:注释那支堵了、**字符串没堵**(今天靠反引号侥幸绿)+ 抹除器自带自检
pi 2026-09-15 第二次抓到这个族,**成立**,我照他的办法复现了。
## 一、扫描仍会**咬字符串里的文字**
`stripComments` **只去注释、不去字符串**(这条限制我在 `read.mjs` 里早就写明过),
而扫描的锚是 `(?:^|[;{])` —— `;` 与 `{` 在**字符串里**照样是字符:
```js
probs.push(`oops; totalTests += 1`); // 被 MATCHED(假阳性)
const s = "x; totalTests += 1"; // 被 MATCHED
console.log(`{ totalTests += 1 }`); // 被 MATCHED
```
⇒ 数到 2 ⇒ 报红,**而代码是对的**。后果不是"多一条红",而是
**判据开始消费散文**:下一个人会跑去改自己的**文案**来哄判据。
★ 而且**这个文件当时是"靠标点侥幸"绿的**:那句修法文案写的是
`` `totalTests +=` `` —— `totalTests` 前面是**反引号**,不是 `;`/`{`,所以躲过了。
**不是设计让它躲过去的**:换成 `{ totalTests +=` 或中文分号紧跟真名,立刻假红。
## 二、修法:抹字符串的"体",并且**自检这个抹除**
- `read.mjs` 新增 `stripStrings(src)`:逐字符把 `'`/`"`/`` ` `` 的**体**抹成空白,
**保留引号与换行**(行号不变 —— 与 `stripComments` 同一条硬要求)。
- 扫描改成 `stripStrings(stripComments(raw))`。
- ★ **抹除器自带自检**:合成"只含假阳性"的样本(必须数出 **0**)、合成"真写"(必须数出 **1**),
并且断言**未抹除时假阳性样本确实 >0** ——
**否则这个自检没有真的复现出那个假阳性,等于没验。**
自检不过就不采信扫描结果(**"看不到"与"没有"必须分得开**)。
## 三、变异验证 5/5
| 注入 | 期望 | 实测 |
|---|---|---|
| 第二处写(`+=`) | 红 | 红 ✓ |
| 第二处用 `X = X + n` | 红 | 红 ✓ |
| 第二处用 `X++` | 红 | 红 ✓ |
| 字符串里 `; totalTests += 1` | **不响** | 不响 ✓ |
| 字符串里 `{ totalTests += 1 }` | **不响** | 不响 ✓ |
★ 这个验证脚本我改了**四版**才收,每版的毛病都是"读数器没先被证明是好的":
① 注入没命中(缩进写成 6 空格、文件里是 4)⇒ 报假"★ 漏";
② 锚 `const probs = [];` 在文件里出现 **2** 次 ⇒ `replace` 打到错的地方;
③ 我把**代码**换成 `+ ran` 去验"字符串"那一支 —— 那仍是**一处**写,当然不红,预期本身就写错了。
⇒ 定稿:每次注入 `assert count==1`、锚选唯一的、**每个 case 的预期先想清楚再写**。
## 四、残余(照实写,不当成已解决)
- **假阳性方向**:正则字面量里的引号(`/a"b/`)会被 `stripStrings` 当成字符串开始。
本仓判据没有把真计数器名写进正则字面量,但这是**实现的边界**,已在 `stripStrings` 的
注释里写明,免得下一个人以为它比实际更强。
- **假阴性方向**(pi 记的,我认):`totalTests = 1 + totalTests;`(反序)与
`if (a) { b(); } totalTests += 1;`(`}` 之后的写)都躲得过。
但**搬动/复制粘贴会保留 `+=`** —— 也就是我声明的射程(实际发生的那三次)覆盖得住。
- **根治形状**(已写进注释):把累加抽成**纯函数**,拿合成输入喂它 ——
那时判的是行为,不是文本,这一整族(注释/字符串/拼法/锚)一起消失。**本轮不做。**
|
|||
| 4af083e8b8 |
test(判据): 4 个判据读的是**另一棵树** —— 修掉硬编码的仓库根,并让 stripComments 保持行号
pi 2026-09-15 实测出来的,**这次长在判据自己身上** —— 正是我们前几轮一直在消的那个形状。
## 一、`const ROOT = '/home/program/agentmail'`:规则进来了,对象没进来
`harmony-arkts.test.mjs` 把仓库根写成了绝对路径。后果我按 pi 的步骤亲手复现了:
```
$ git worktree add --detach /tmp/wt-verify
|
|||
| d5cfcbdc9c |
fix(权限): 409 的第二种含义是「本档不该问」——四桥都补上;状态写入点不再兜默认档
线上事故(jianf 经 pi 转达):补投路径漏传 permission_mode,插件拿 undefined 兜了 workspace 档,把 full 档会话写成 workspace-write + ask —— 不是"拦一次",是一整轮 工具能力降级,且状态留在会话里;随后该会话每次受守卫调用都撞 409。 四件事: 1. **状态写入点不接受默认值**(新增共享 `modeForStateWrite`):缺字段/脏值 → `null` = 不写状态。"默认值可以出现在**决策**里,不可以出现在**状态写入**里。" 同时保留共享契约的 fail-closed:真读到 workspace 才写 workspace。 2. **409 的两种含义分开处理**。`allowed-once` 只绕过**审批**,改不了**沙箱** —— 所以 dsh 桥在放行前先把服务端给的权威档位**写回会话**(这也就成了自愈路径: 已经降级的会话,下一次带档位的 409 会把它修回来);只认服务端明说的 full, plan 与"链上没有人类"照旧 fail closed。 3. **同一处缺陷在 zcode / opencode 也在**(`hooks/permission.mjs` 与 `index.js` 都把 409 当永久失败拒绝)。我先前在回信里写过"这两个桥不转发权限询问,不需要改" —— 那句话是错的,我当时的搜索面只有 `<plugin>/src/*.mjs`。按 pi 的要求把这条 **否定性事实变成常驻判据**后,它第一次运行就红给我看。四桥现在都有 「409 + full → 放行」,且**排在永久失败分支之前**(含顺序变异自检)。 4. **共用测试重新同源**:`test/catchup.test.mjs` 从 `153985e` 起就是分叉的 (我那版把平台专属路径写进了共用文件),而 `deploy/install.sh` 第 24 行会跑 `check-shared-libs.sh` —— 也就是说**部署一直是红的**,我没跑过那个脚本。 共用文件只放契约(值/行为),跨平台配对judge 移到平台专属文件,四份逐字节相同。 另外把"判代码 vs 判理由"从记忆变成代码:`test/lib/read.mjs` 提供 `code()/prose()/bytes()`, 判据目录里不得再裸用 `readFileSync`(新判据 `criteria-hygiene` 管,含读取器自检)。 判据证据(每条都做过"能不能红"的变异): - 写回去掉 → 红;纠正块挪到普通 409 之后 → 红;状态写入点退回兜默认 → 红; - zcode/opencode 的放行分支拿掉 → 各自红;共用测试分叉 → check-shared-libs 红。 各套件:dsh 388、pi 443、zcode 387、opencode 333(均经 npm test,含 tsc); electron `npm test` 15/15 判据绿 + vitest 266 + typecheck;`check-shared-libs.sh` 退出 0; Go `go test ./...` 全 ok。 |
|||
| ec90cba129 |
test(criteria): 抽出共享 check/finish(marker 不再靠记性)+ 失败信息自带修法
pi 的三条增量,前两条落地: 1. **错误信息自带修法**:受众不只是读过规范的人 —— 并发写 WebUI 的 agent 新加判据时不会打开 CRITERIA.md,看到红的第一反应可能是"套件坏了"。所以把可照抄的修法写进那条错误本身 (共享 helper 的用法 + 样板文件路径),并说明 node:test 的判据不用管。 **red 是 ta 一定会看到的,文档不一定被打开。** 2. **marker 由共享 helper 打印**:新增 test/lib/checks.mjs(导出 check/finish), 计数只可能在该模块内发生 → "漏打 marker"与"计数写错位置"这两类在新文件上不可能发生。 为避免"写了没人用"(本仓踩过的坑),同时把两个手工计数的判据改用它: narrow-layout(原来只有 failed 计数)与 markdown-xss(原来根本没有计数器)—— 条数不变(52 / 9),套件仍全绿。 未回改其余 10 个文件:run-all 的 marker 检查已经覆盖它们。 |
|||
| addde97600 |
feat(electron): 多账号第一纵切 —— 账号存储/选择器/聚合收件箱
按 docs/MULTI-ACCOUNT-PLAN.md 实现客户端多账号的前半段(SSE 多连接与 写信账号切换留作下一轮)。 - `src/lib/accounts.ts`:纯逻辑(地址规范化、身份判重、默认账号、聚合合并), 16 条测试钉住每条判据(含反向对照)。 - 持久化在主进程:`userData/accounts.json`,**原子写**(临时文件 + rename)+ 0600。不落 localStorage:那份存储渲染层任何脚本都可读,且 file:// 与 http:// 是两套。无 IPC 时(浏览器)退到 localStorage 并在界面**如实写明**。 - 取信:单账号走原路径(逐字节不变);聚合时**每账号各一次请求、各带自己的 令牌**(`fetchWithAuth`,不碰认证单例,避免并发串号)。 - ★ 只合并**同一网关**的账号:跨网关的邮件混进列表后点开会去问当前账号的 服务器(404,或 mail_id 撞上就打开了别人的信)。如实排除 + 列表上方说明。 - ★ 部分失败可见:某账号取不到时给出账号名与原因 —— 静默丢掉它会让聚合列表 少一整份邮件而界面看起来完全正常。 - `API_BASE` 改为 `let`(切换账号要换网关),api 层不得缓存它 (`client.ts` 的 `const BASE` 快照已改成每次读)。 - UI:列表头下拉(≥2 个可用账号才出现「全部邮箱」)+ 账号徽标 + 账号页 「多账号」一段(添加前调 /auth/me 验证,401 当场拒绝,不写进列表)。 - 测试:vitest 230 通过(原 222 + 新 8)、`test/lib/accounts.test.mjs` 16 通过、 typecheck 通过。新增 `test/manual/multi-account-verify.mjs`(真起打包产物 + 两个真实账号,判据落在网络层:聚合必须每账号各一次请求且各带自己的令牌)。 |