Mole 安全缺陷事件目录:17 类破坏性清理缺陷的 Agent Skill 路由设计与红绿验证契约
Mole 是一款 macOS 系统清理与优化工具,其核心风险面集中在"删除"这一类破坏性操作上。本篇基于仓库中 .claude/skills/bugs/SKILL.md 这份"事件目录"型 Agent Skill,完整拆解它如何用一张 17 行的路由表把 Mole 历史上反复出现的安全缺陷归为 4 个参照家族、如何沿"发现到落盘"的完整变异生命周期审查删除链路,以及如何用红绿测试与隔离 HOME 等手段让回归测试真正具备"能够失败"的能力;读完后你可以复现这套"先路由、再取证、后验证"的缺陷审查方法,并将其应用到任何涉及删除、超时、并发与持久化派生数据的 Shell 工具中。
一、这份 SKILL 是什么:按需路由的事件目录
.claude/skills/bugs/SKILL.md 不是普通的项目文档,而是一个面向 AI 编码代理(Claude Code 等)的技能文件,其 frontmatter 自述用途:
Mole incident catalog for destructive cleanup safety, bounded Shell/macOS probes, cancellation and concurrency, dry-run/real parity, async freshness, cache and accounting consistency, Bats validity, and actionable gates.
即:一份覆盖"破坏性清理安全、有界 Shell/macOS 探测、取消与并发、dry-run 与真实执行一致性、异步新鲜度、缓存与计数一致性、Bats 测试有效性、可执行的门禁诊断"的缺陷事件目录(incident catalog)。
它在 Mole 的代理工作流中被明确定位为"按需加载的路由器",而非通用预审清单。AGENTS.md 的 Working Rules 一节写道:
Treat
.claude/skills/bugs/SKILL.mdas an on-demand router, not a universal review preflight. Load only the linked reference families signaled by deletion evidence, uncertain probes, bounded Shell/macOS work, persisted state or accounting, progress, test validity, or refusal diagnostics.
也就是说,只有在当前缺陷的证据(diff、症状、调用点)真正命中某类信号时,才加载对应的参照文件。SKILL.md 正文也划定了职责边界:通用代码审查归 check,线上故障的根因调查归 hunt,而本目录用于"已读到当前症状、diff、实现与调用点之后"的模式匹配。
二、先路由再加载:17 类复发缺陷形状的路由表
SKILL.md 的核心是一张路由表。它要求"只选择被证据触碰到的参照家族;全项目审计应先分类面,而不是加载每一条事件叙述"。完整继承如下表(原文中的 references/... 局部相对路径已转换为从仓库根目录起算的路径):
| # | 复发形状 | 第一探针 | 读取的参照 |
|---|---|---|---|
| 1 | 删除候选由弱名称信号构建 | 检查名称、bundle-id 与回退 glob | 删除证据与最终落点 |
| 2 | 存在性或空闲由单一探针决定 | 枚举所有合法位置与未知结果 | 删除证据与最终落点 |
| 3 | 保护只存在于一个分支 | 对 dry-run、真实、直接、回退、最终落点路径做差分 | 删除证据与最终落点 |
| 4 | 无界外部命令 | 数清生产者、消费者、内层循环与动作的边界 | 界限、Shell、TTY 与解析 |
| 5 | Bash 3.2、errexit 或 pipefail 陷阱 | 检查空数组、`fn | |
| 6 | TTY、stdin 或进程组被抢走 | 检查后台 worker 与可能提示的命令 | 界限、Shell、TTY 与解析 |
| 7 | 系统输出被当成稳定 API 解析 | 强制 locale 并在接受数据前校验形状 | 界限、Shell、TTY 与解析 |
| 8 | 持久化派生数据活得比算法久 | 追踪 schema、TTL、证据指纹与变异 | 状态、计数与进度 |
| 9 | 两条路径以不同方式计算同一个数 | 找到所有生产者并选定一个定义 | 状态、计数与进度 |
| 10 | 慢工作看起来像冻结 | 找到反馈窗口外约 1 秒以上的操作 | 状态、计数与进度 |
| 11 | 回归测试不可能失败 | 证明阳性对照与修复前的红色状态 | 测试有效性与拒绝诊断 |
| 12 | 门禁无法解释为什么拒绝 | 把每个原因码映射到单一成因与下一步动作 | 测试有效性与拒绝诊断 |
| 13 | 变异目标同时被接受为发现容器 | 对比递归扫描根命名空间与每个 purge 目标 basename | 删除证据与最终落点 |
| 14 | owner 元数据被当作完整原子清单 | 识别谁写它、缺失是否权威、什么锁住变异 | 删除证据与最终落点 |
| 15 | 取消停掉一个 helper 但后续工作继续 | 追踪 124 与信号状态跨循环、子 shell、worker 与章节编排 | 界限、Shell、TTY 与解析 |
| 16 | 异步或缓存数据没有代次/新鲜度契约 | 把结果绑定到请求纪元,保留每个样本的 time/stale/completeness 字段 | 状态、计数与进度 |
| 17 | 发布门禁信任含糊或预存在状态 | 要求精确 source/tag 相等、唯一生成目标与预期缺失 ref 租约 | 测试有效性与拒绝诊断 |
这张表的组织逻辑值得注意:第一列是"复发形状"(recurring shape)而非具体 bug——它描述的是代码结构层面的缺陷模式,因此可以跨 PR 复用;第二列给出"第一探针",即开始审查前 30 秒内就能执行的定向检查;第三列决定要加载哪个参照文件,把阅读成本控制在被触碰的家族内。
三、沿完整变异生命周期审查,而不是只盯报告分支
SKILL.md 要求:对 cleanup、purge、optimize、analyze 删除或 uninstall 类工作,审查完整链路而不是被报告的分支。链路如下(原文完整继承):
discover or plan
-> cheap irreversible filters
-> owner and open-handle probes
-> size or metadata work
-> final owner re-probe
-> parent and target identity rebind
-> deletion or Trash sink
-> accounting, cancellation, and user output
在每一次状态迁移处,回答八个问题:
- 存活或未知状态是否 fail closed?
- 超时与信号状态是否仍可观察,且能阻止后续变异?
- 探针与落点是否绑定到同一物理父目录与目标?
- dry-run 与真实模式是否从同一合格计划出发、且不复用过期授权?
- 廉价的"不存在、受保护、白名单、编译模型"过滤是否排在递归探针之前?
- 一个累计截止时间是否覆盖动态扫描范围,且嵌套循环中有检查点?
- 被拒、被过滤、超时或失败的条目是否留在"已清理计数"与"回收字节"之外?
- 大候选能否跳过逐项尺寸统计,同时不让报告的总数失真?
最后一条红线同样保留在原文中:不要用最终落点重绑定或 fail-closed 的 owner 检查换速度。优先优化的对象是不存在的目标、重复的发现探针、仅报告用工作与错误范围的扫描。
这条链路在 Mole 源码中有直接对应物:lib/core/file_ops.sh 中集中了四个"漏斗级"安全函数——validate_path_for_deletion、safe_remove、mole_delete、safe_find_delete。参照文件 deletion-evidence-and-final-sink.md 明确建议"优先把这些策略放在漏斗级",理由正是模式 3(保护只存在于一条路径):调用点级别的保护会被下一个调用方遗忘。
从源码结构看,validate_path_for_deletion 的实现与这份目录的多个条目一一对应:它先拒绝空路径、非绝对路径、.. 路径穿越段与控制字符(lib/core/file_ops.sh#L937-L961);随后专门处理符号链接——不仅检查叶子链接目标是否指向受保护系统路径(lib/core/file_ops.sh#L966-L990),还实现了一段"祖先符号链接守卫":如果路径的任一祖先是符号链接(例如 ~/Library/Caches 被重定向到 ~/Documents),字面路径匹配会全部落空而实际删除穿透到真实目标,于是用纯 bash 内建 [[ -L ]] 沿祖先上溯,确认存在符号链接后才付出一次 cd -P 子 shell 代价做物理规范化并复跑拒绝谓词,且明确是 deny-only(规范化路径永远不能授予字面路径没有的权限,见 lib/core/file_ops.sh#L992-L1038)。这段注释还解释了性能取舍:热路径上每次调用若都做 dirname + cd -P 约 2ms,会使验证整体增加约 23%,所以只在内建探测命中后才支付子 shell 成本——这正是"廉价过滤在前、昂贵探针在后"原则的实例。
配套的强制契约见 AGENTS.md 的 Critical Safety Rules:删除必须走 lib/core/file_ops.sh 的安全 helper;裸 rm -rf 与 find -delete 仅在同行带 # SAFE: <一句话理由> 注释时允许,这是 docs/SECURITY_DESIGN.md Layer 2 定义的契约并由 CI 强制执行。仓库的 Bats 测试也按这些家族归档,例如 tests/file_ops_mole_delete.bats、tests/file_ops_safe_remove_symlink.bats、tests/path_validation_fuzz.bats(配合 tests/fuzz_corpus/dangerous_paths.txt 语料),与目录中"验证依据"的要求形成闭环。
四、参照家族一:删除证据与最终落点
deletion-evidence-and-final-sink.md 覆盖模式 1、2、3、13、14,并给出"最终落点矩阵"。
4.1 弱名称证据不能授权删除(模式 1)
规则是:精确 bundle id 或精确 app 路径才是证据;vendor 前缀、通用词、回退通配符不是。显示名、bundle-id 前缀、TeamID 前缀或子串 glob 最终总会命中某个邻居。文件记录了四种历史形状:
find_app_files曾从 GUI 显示名推导~/.config/<name>,导致卸载 Claude.app 时删除了 Claude Code CLI 的状态;大小写不敏感的 APFS 进一步扩大了碰撞面;${bundle_id}*.plist让com.foo匹配到com.foobar.plist;- 子串拆除在卸载
Foo.app时删掉了幸存的Foo-beta.app邻居; - TeamID 前缀回退曾被合并后又回退。
文件还给出针对该缺陷类的仓库级探针命令:
command grep -rnE '\*\$\{?(app_name|bundle_id|name)\}?\*|\$\{bundle_id\}\*' lib/ bin/
对每个命中,命名"授权落点的最窄事实",并分别审查主分支与回退分支。
4.2 单一探针决定存在性或空闲(模式 2)
每个 owner 谓词必须区分 present、absent 与 could not tell 三态;超时、权限拒绝、元数据缺失或不完整的发现都属于"未知",永远不是"不存在"。历史案例包括:mdfind 漏掉 Homebrew cask 与内嵌 SMJobBless helper;command -v 加 LaunchAgents 漏掉 GUI Proton Mail Bridge 的 owner 进程,从而把 ~/.bridge 误判为孤儿;任何 UP 状态的 utun* 接口被当成 VPN,包括 iCloud Private Relay;brew list mole 能回答所有权问题却会重置用户的 sudo 时间戳,替换为 Cellar 检查消除副作用后,又因未从已装 brew 路径推导前缀而漏掉自定义前缀。
探针命令:
command grep -rn 'mdfind' lib/ bin/ | command grep -v run_with_timeout
4.3 保护只存在于一条路径(模式 3)
四个历史形状都指向同一教训——保护逻辑必须放在共享漏斗而不是调用点:
should_protect_path只在真实模式运行,导致 dry-run 承诺了真实运行会拒绝的工作;- 某调用方忘记白名单,直到白名单移入共享
find落点才补齐; - Raycast 排除条件写在了实际
find谓词之外; _safe_clean_impl只在真实模式咨询删除守卫,预览阶段就登记并计数了活跃进程守卫会拒绝的条目。
审查方法是:枚举保护 helper 的每个调用方,再枚举每个落点,对两个列表做差分;dry-run 与真实模式必须计算同一合格计划。目标特定守卫应在"不存在、受保护、白名单、编译模型"候选被过滤之后、预览登记或删除之前运行。
4.4 最终落点矩阵
对每个破坏性家族,用实时代码填写这张矩阵(原文完整表格):
| 阶段 | 必需证据 | 失败行为 |
|---|---|---|
| 发现 | 精确受支持根 + 完整扫描 | 不完整结果被丢弃或标记为部分 |
| 廉价过滤 | 不存在、受保护、白名单、编译模型 | 候选被省略,不做昂贵探针 |
| Owner 探针 | 进程与打开句柄三态判定 | 存活或未知即拒绝 |
| 尺寸/元数据 | 有界、不复用授权 | 超时可观察;不产生虚假回收字节 |
| 最终再探针 | 慢工作后的 owner 状态 | 新的存活或未知状态拒绝 |
| 身份重绑定 | 物理父目录 + 目标身份 | 重命名、替换或符号链接变化即拒绝 |
| 落点 | 共享安全 helper + 保留确认 | 无裸回退删除 |
| 计数 | 只计已完成的变异 | 被拒与失败条目保持排除 |
容器、SQLite、helper-app 与特权路径必须在落点前立即执行最终再探针与身份重绑定——发现快照不是所有权租约。信号与取消也是证据链的一部分:保留 >=128 的状态,让取消在 best-effort 调用方之间保持粘滞,并阻止取消后任何后续落点运行。
4.5 变异目标被当作发现容器(模式 13)与 owner 元数据不是删除授权(模式 14)
模式 13:递归发现不得穿过它本想提供的制品本身。若 node_modules、vendor 或 Pods 被接受为项目容器,包内清单会变成伪项目根,扫描从真实目标之下开始,嵌套 dist/build 目录进入删除列表;父制品永远无法用于嵌套目标折叠,删掉子项后包管理器会认为残缺的树已安装。文件给出的处理原则是:把目标列表本身当作"排除容器命名空间",而不是维护第二份手写拒绝表;每个扫描根入口(维护者默认、用户配置根、自动发现、发现消费者)可能有意拥有不同的信任契约;Mole 的显式 purge_paths 是"真实项目 basename 恰好撞名"的逃生舱,不能为它削弱自动发现。
模式 14 区分了三类数据:日志(journal)记录事件、注册表(registry)记录一个当前视图、缓存(cache)记录一次先前观察——除非 owner 文档化该契约并协调并发变异,否则没有任何一类是完整的删除清单。文件以编辑器扩展为例:.obsolete 是删除日志,它的空不能证明磁盘上的每个扩展都是活跃的;但反过来用目录对 profile 注册表做对账同样不安全,因为并集保留集可能漏掉未知 profile、新写入的注册、或扫描与落点之间 owner 的变异。安全的产品边界是回到"精确的 owner 撰写移除标记"。落地前必须回答五个问题:owner 称数据为清单/日志/缓存/尽力索引?能否枚举所有支持的 profile、安装与并发写者?Mole 是否共享 owner 的锁、代次或机器可读 GC 命令?保留集构建后、落点前是否可能出现新的 owner 引用?中断等价于缓存未命中,还是会留下残缺的安装/会话/创作状态?若完整性与同步性无保证,只能用 owner 撰写的移除标记、调用 owner 支持的清理命令、在恢复契约允许时提供整缓存重置,或干脆不动目标。
五、参照家族二:界限、Shell、TTY 与解析
shell-and-test-pitfalls.md 覆盖模式 4–7、15 与一批"聚焦陷阱"。这一家族在 Mole 仓库里有最直接的源码对应:lib/core/timeout.sh 的 run_with_timeout(lib/core/timeout.sh#L141)实现了三级降级链——优先 gtimeout/timeout(coreutils),回退到带专用进程组清理的 Perl helper,最后才是有已知局限(可能清不完子进程、存在竞态)的纯 shell 兜底(lib/core/timeout.sh#L17-L31)。
5.1 无界外部命令(模式 4)
du、mdfind、find、xcrun simctl、system_profiler、ioreg 与包管理工具在健康但缓慢的机器上都会卡住。文件给出的纪律包括:
- 每个嵌套循环都要有检查点,不只是每个外层根;
- 以最慢的健康场景调参——CoreSimulatorService 曾因 2 秒界限被误报不可用,需要预热重试;
- 生产者必须完整物化,非零状态时丢弃输出;"过程替换 +
|| true"不得把部分find前缀喂进删除; - 探针与动作的模式、类型、年龄、深度必须一致;
- 先分别给生产者与消费者计时再提高超时——2.3 秒的
lsregisterdump 之后每行输入一次命令替换仍是分钟级; - 已安装二进制的
--version/--help验证也要有界,坏掉的可执行文件最可能挂起; - 安装与更新按目标目录单飞(single-flight),一个进程不能验证另一个代次。
配套的仓库级核查脚本(原文完整继承):
for command_name in 'du -s' mdfind xcrun system_profiler ioreg brew; do
printf '%-16s total=%-4s wrapped=%s\n' "$command_name" \
"$(command grep -rn -- "$command_name" lib/ bin/ | wc -l | tr -d ' ')" \
"$(command grep -rn -- "$command_name" lib/ bin/ | command grep -c run_with_timeout)"
done
tests/core_timeout.bats 在 lib/ 与 bin/ 范围内用源码不变量把"每条 du -s 路径都在 run_with_timeout 之后"钉为类级契约,磁盘验证超时常量使用 MOLE_TIMEOUT_DISK_VERIFY_SEC。
5.2 Bash 3.2、errexit 与 pipefail(模式 5)
macOS 自带 Bash 3.2,且 Mole 以 nounset 运行,由此产生四条硬规则:
- 空数组展开前必须先守卫——
set -u下空数组"${arr[@]}"可以中断扫描并让 spinner 成孤儿; fn || handler会在整个fn函数体内禁用 errexit;安全关键步骤必须用显式if ! command; then return 1; fi;- 不要依赖调用方的临时
set +e窗口做优雅降级,在命令运行处捕获状态; - 可选的
[[ -n "$value" ]] && action在变量为空时返回 1,状态敏感块内必须改用if/fi。
类级探针:
command grep -rn '\$\{[a-z_]*\[@\]\}' lib/ bin/
5.3 TTY、stdin 与进程组(模式 6)
Perl 超时兜底可能把控制终端交给子进程:一个后台元数据 worker 曾以 SIGTTIN 停住前台卸载提示;BSD mv/cp 在目标不可写时也可能在 stderr 提示并读 stdin。纪律是:每个调用 run_with_timeout 的后台 worker 都用 < /dev/null 关闭 stdin;可能提示的命令使用非交互或强制选项;菜单与扫描的 trap 保存并恢复调用方的 trap——参照实现是 lib/ui/menu_paginated.sh。
5.4 系统输出被当成稳定 API(模式 7)
macOS 命令输出会被本地化、随版本漂移、在数据位置打印错误。规则包括:被解析的度量子进程强制 LC_ALL=C;信任字段前先校验形状(绝对路径、数值、预期键或精确枚举);DTSDKBuild 构建标识与 DTPlatformVersion 版本必须分开;把 PlistBuddy 的"文件不存在"散文当数据拒收;检查标志位时用 macOS 原生语义(BSD grep -Z 是 --decompress);优先退出码、plist 键与机器可读输出。注意 BSD grep 没有 GNU 的 null 输出 -Z 契约,正确姿势是 find ... -print0 枚举、grep -qF 逐文件探测。
5.5 取消是局部的,除非编排让它粘滞(模式 15)
对破坏性命令,超时 124 与信号派生的 >=128 状态取消的是剩余全部命令,而不只是当前 helper。要让它穿过每个边界:best-effort 循环在探测/登记下一个候选前必须检查挂起取消;把普通缺失报成成功的 helper 必须在进入下一家族前返回挂起取消;命令替换、子 shell、后台 worker 不会修改父 shell 变量,必须显式回传状态;并行协调器要停掉并收割同伴 worker、让取消优先于普通失败、阻止下一渲染章节启动;dry-run 使用与真实清理相同的取消契约——预览台账也是下游工作。
回归测试的形状同样被规定:第一个候选返回 124 或 130,第二个候选若被触达则会成功;断言精确的顶层状态加"第二个探针、预览登记、落点与后续章节均无阳性痕迹"。若两个候选都独立超时,测试就无法证明取消是粘滞的。也不要把已取消的安全探针藏在 || true、"警告 + return 0"或 worker 局部导出变量后面——那些形状把全局停止变成了局部跳过。
文件末尾还有一批"聚焦陷阱",其中与本文主线强相关的包括:run_with_timeout 会 exec 二进制、绕过 shell 函数 mock,外部命令测试必须用 PATH stub 目录;MOLE_TEST_MODE=1 可能让被测函数提前返回,末尾负断言在空输出上假通过,必须以 || return 1 收尾并加阳性对照;大 payload 管道喂给 grep -q 会在首匹配退出后让仍在写入的 printf 吃到 SIGPIPE,把 Broken pipe 泄漏到用户输出(应改用 here-string);数值比较前必须用 10# 归一化,否则 08/09 不是合法八进制会直接中止。
六、参照家族三:状态、计数与进度
state-accounting-and-progress.md 覆盖模式 8–10、16。
6.1 持久化派生数据活得比算法久(模式 8)
改算法却不失效缓存,等于让旧结果在源修复后继续出厂。代表形状是一次硬链接去重变更同时提升了缓存 schema 并把依赖去重的子树标记为不可缓存。对每个持久化派生值必须识别五要素:schema 版本;TTL(若年龄有意义);每个输入变异的失效;是否有调用方把值当作"不存在"的证明;验证运行是否在读上一个发布的数据。
关键论断是:TTL 只能证明条目不够旧,永远证明不了完整性。pkg_receipt_nonstandard_app_paths --require-complete 曾把一小时前仍"新鲜"的 receipt 缓存当作"没有兄弟安装"的证明——重查能删掉陈旧条目,却不能发现新安装的 owner;修复是把完整性绑定到 pkgutil --pkgs 的指纹上,新证据出现即失效条目。结论:当调用方用缓存数据授权删除时,要么绕过缓存,要么把它绑定到"任何出现都会改变结论的证据"的指纹。
6.2 两条路径以不同方式计算同一个数(模式 9)
任何被渲染两次的值终将不一致:dry-run 预览对最终摘要、条目数对原始目标数、子树尺寸对 du、十进制对二进制单位。方法是找到所有生产者、选定一个定义、优先把测量值传进落点或渲染器而不是重算,然后在回归测试里比较两个渲染面而不是钉死无关字面量。计数规则:
- 被过滤、被拒、超时、失败或消失的候选不贡献清理条目也不贡献回收字节;
- dry-run 与真实模式使用同一合格候选集,只有动作不同;
- 尺寸超时可产生显式的"未知"或"部分"总数,绝不把虚构的零当作完整呈现;
- 大候选快速路径跳过逐项尺寸统计时,输出必须声明总数是部分的或未扫描;
- 硬链接按一个被命名的策略在子树与摘要路径间保持一致。
tests/clean_core.bats 包含"预览对摘要"的既有模式;子兆字节舍入到零、逐链接硬链接计数也属于这一家族。
6.3 沉默被读作冻结(模式 10)
慢工作在 spinner 窗口外看起来就是挂死。Mole 的输出节奏被固定为四行契约:
section title
loading state
content
one trailing blank line
spinner 在"会被覆盖的输出"之前立即停止,若其后还有静默工作则重启;超时警告不能替代健康慢扫描期间的进度。性能工作需要两张收据:一条隔离变更路径的有界微基准或调用次数不变量;一条同模式同机器条件下的端到端命令计时。文件同时警告:不要用缓存目录尺寸优化——APFS 不把后代 mtime 传播到父目录;对破坏性工作,即使最终 owner 探针与身份重绑定是昂贵部分也要保留。
6.4 异步代次与样本新鲜度是一个契约(模式 16)
异步结果可能在产生时有效、到达时过期。每个请求要打单调变化的代次/探针 ID 标签,结果消息携带该标签,只应用到匹配代次;刷新或导航跳转创建新请求时递增代次,而不是视图重绘时递增。生命周期要双向测试:旧结果不得覆盖新刷新;用户在钻取时到达的匹配结果值得保留,但返回 Overview 必须调度新探针而不是把旧结果当新测呈现。
缓存指标使用平行契约:把相关字段当一个原子样本——value group + collected_at + stale + completeness。瞬态刷新失败时返回错误但保留上一次成功组(带原始采集时间与 stale=true);不得把旧值与新刷新时间拼在一起、只清一半字段、或把未测量变成测得的零;后续成功样本整体替换该组并复位 stale。每个序列化器与 fast/full/watch 路径必须保持"新鲜数据、陈旧的 last-known-good、测得的零、从未测量"四态区分。
七、参照家族四:测试有效性与拒绝诊断
test-validity-and-refusal-diagnostics.md 覆盖模式 11–12、17。
7.1 不可能失败的测试(模式 11)
多条 Mole 回归曾被"覆盖"在修复前的实现上同样通过的断言。文件列出的断言与分支陷阱包括:非末尾的裸 [[ ... ]] 在后续命令成功时被吞掉——每条有意义断言以 || return 1 收尾(内层脚本用 || exit 1);MOLE_TEST_MODE=1 可让被测函数提前返回;函数 mock 可能选到与 PATH 可执行文件不同的分支(run_with_timeout exec 二进制,超时路径必须用 PATH stub);接受状态 0 的超时测试不证明超时传播,必须断言精确状态、被丢弃的部分输出与生产分支的阳性痕迹;对从未存在的字符串做负断言毫无意义。
文件给出最小复现夹具:
cat > tests/zz_min.bats <<'EOF'
@test "non-final [[ ]] false" { [[ 1 -eq 2 ]]; [[ 1 -eq 1 ]]; }
@test "non-final [ ] false" { [ 1 -eq 2 ]; [ 1 -eq 1 ]; }
EOF
bats tests/zz_min.bats
隔离与 CI 对等一节规定了:setup_file 在同一 Bats 文件内共享 HOME,断言缺失、精确文件数、缓存新鲜度的测试要用专属子 HOME 或只清理自己创建的夹具;可变的 shell 计数器活不过命令替换(子 shell),要把调用状态持久化到测试自有的文件;通过 MOLE_TEST_NO_AUTH=1 ./scripts/test.sh 匹配 CI,窄复现需要 Bats jobs 时保留 --no-parallelize-within-files——裸 bats --jobs 6 file.bats 会改变共享状态契约;源码不变量的 grep 要跳过注释行,且在目标零命中时必须失败,否则注释会伪装成禁用调用、或改名后的目标让守卫空转变绿。
验收线是红绿:新测试先在修复前代码上跑、看到预期断言失败,再恢复修复看到通过。取消类测试多一条阳性对照要求:没有粘滞停止时,后续候选必须合格。
上述规则在 scripts/test.sh 中可见工程化落地:runner 开头即 export MOLE_TEST_NO_AUTH=1 以阻止脚本化测试触发真实 sudo/Touch ID 提示,并清扫 tests/tmp-* 下超过 60 分钟的孤儿测试 HOME 目录;启用 --jobs 时始终附加 --no-parallelize-within-files(见 scripts/test.sh#L235-L245),与 SKILL.md Working Contract 中"共享 setup_file home 不构成隔离"、"保留 --no-parallelize-within-files"两条形成互证。
7.2 无法解释拒绝的门禁(模式 12)
多个独立成因共用一条兜底消息,会迫使报告人反向工程源码。Mole 的 acquire_install_lock 遇到过不安全祖先、被拒的 sudo -n、不可用锁目录、被植入的符号链接/FIFO 锁路径、不可用锁原语与真实竞争——这些成因需要稳定原因码与不同的下一步动作。三类复发失败:消息未暴露成因导致报告人代劳分诊;新门禁跑在旧的更可操作检查之前,把"用 sudo -v 缓存凭据"降级成错误的忙锁诊断;源码不变量测试钉住了含糊的兜底字符串,把诊断回归变成硬性要求。
每个可达拒绝都要填这张映射表:
| 成因 | 稳定原因 | 用户可见解释 | 下一步命令或动作 |
|---|---|---|---|
| 精确分支条件 | 机器可读码 | 一行事实 | 一个成因特定的下一步 |
两个成因共享一条消息、且补救不同时就是缺陷;"重装"在重装会再次进入同一门禁时不是补救;新门禁前移时至少保持同等可操作度。被吞掉的 stderr 可能藏着唯一的区分证据——诊断时用受控差分探针,再把结构化结果映射到原因码,而不是永久暴露特权 stderr。相关核查命令:
command grep -c 'return 1' install.sh
command grep -c 'log_error' install.sh
command grep -rn 'sudo .*2> */dev/null' install.sh lib/
测试钉住原因码路由与下一步分支,不钉兜底散文。AGENTS.md 也把这条提升为项目级安全规则:acquire_install_lock 通过 INSTALL_LOCK_FAILURE 报告稳定原因,不安全祖先变体用 INSTALL_LOCK_UNSAFE_ANCESTOR_REASON,并保留"一条事实成因行 + 一条成因特定下一步动作"的形状。
7.3 发布门禁信任含糊或预存在状态(模式 17)
发布把解析出的源与远端名字变成不可变的公共状态,所以宽松匹配与 check-then-create 竞态必须 fail closed。三条精确契约:
- 提取一个非空源版本,要求触发 tag 精确等于
V<source-version>——前缀匹配或尽力回退可以在貌似合理的 tag 下发布错误的 commit; - 改写生成的 formula 时要求恰好一个预期顶层 source URL 与一个配对的 source 校验和;零命中说明布局漂移,多命中说明改写目标含糊;bottle 校验和不算源字段;
- 拒绝覆盖预存在发布分支,并用"预期缺失租约"(如
--force-with-lease=refs/heads/<branch>:)关闭读与推之间的竞态——只读预检不是并发控制。
门禁失败应指名不匹配的值或占用的 ref,并告诉维护者重跑前检查什么;测试对隔离夹具证伪"空、不匹配、重复、预存在"四类情形。grep 证明守卫文本存在可以作为源码不变量,但不能证明 shell 分支真的拒绝了坏状态。该模式适用于工作流与脚本中的发布安全缺陷;发布规划、版本命名、notes 与公告文案不在 bugs skill 范围内。
八、工作契约与验证底线
SKILL.md 把审查过程的纪律压缩为六条工作契约(完整继承):
- 按调用点形状扫平级代码,而不是按文件名或 helper 名;报告格式为
checked N / defective M / not applicable K; - 复发修复必须随附一条对修复前代码会失败的回归或源码不变量;
- 测试只有在证明生产 helper 真的运行后才被当作生产消费者对待;负断言需要阳性痕迹;
- 取消回归的标准形状:让下一个候选"在其他方面合格",然后证明其探针与落点从未运行;让每个候选因同一原因失败是假的粘滞取消测试;
- 对缺失敏感的测试用隔离的
HOME或夹具根;共享的setup_filehome 不构成隔离; - 尽可能用
MOLE_TEST_NO_AUTH=1 ./scripts/test.sh复现 CI;直接调用 Bats jobs 时保留--no-parallelize-within-files; - 把专家或 AI 报告当作线索——亲自读实现、调用方、回退分支与最终落点。
验证底线(Verification bar)要求:使用 AGENTS.md 中的热点命令,不要猜一个更窄的验证器。一条典型 Shell 安全变更的收尾序列(原文命令块完整继承):
./scripts/check.sh --format
MOLE_TEST_NO_AUTH=1 bats tests/<area>.bats
MOLE_TEST_NO_AUTH=1 ./scripts/test.sh
go test ./...
MOLE_TEST_NO_AUTH=1 MOLE_DRY_RUN=1 ./mole clean --dry-run
这套命令与 AGENTS.md Commands 一节列出的仓库标准命令一致(./scripts/check.sh --format、MOLE_TEST_NO_AUTH=1 ./scripts/test.sh、go test ./...、MOLE_DRY_RUN=1 ./mole clean 等),其中 Go 侧对应 cmd/analyze、cmd/status 两个 TUI 组件的单元测试。SKILL.md 的最后一句话划清了证据边界:
Never infer a production defect from a function name, comment, string, fixture, or
_test.gomatch. Confirm the live call path and verify red-green before reporting the class fixed.
即:永远不要从函数名、注释、字符串、夹具或测试文件名的匹配推断生产缺陷;确认活调用路径并在报告"该类已修复"之前完成红绿验证。
九、小结
.claude/skills/bugs/SKILL.md 及其四份参照文件构成了一套可迁移的"破坏性操作缺陷审查方法论":以 17 类复发形状做证据驱动的路由,以八阶段变异生命周期为审查骨架,以"三态 owner 判定、fail-closed、落点矩阵、红绿验收、原因码映射"为具体纪律,并逐条落到 lib/core/file_ops.sh 的漏斗级守卫、lib/core/timeout.sh 的有界执行、scripts/test.sh 的 CI 对等契约与 tests/ 下按家族归档的 Bats 回归中。对维护类似"扫描—过滤—确认—删除—计数"链路的 Shell 工具团队,这套目录的价值不在于某一条历史事故,而在于它规定了每个状态迁移处该问的八个问题,以及什么样的测试才配称为回归。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00