没有同事,我一个人怎么 review

ops-agent 建造笔记第 11 篇:单人项目如何建立质量证据体系,回归、演练和发布矩阵分别能证明什么、不能证明什么。

没有同事,我一个人怎么 review

《ops-agent 建造笔记》第 11 篇 代码基线:v0.1.0-rc.1,提交 d897d2c95e70(2026-10-25 发布的候选版) 对照实现:selftest.py、tests/、docs/reviews/、.github/workflows/


0. 第二双眼睛从哪来

一个人写项目,最大的质量风险不是写不出测试,是想不到该测什么。自己选的抽象、自己命名的函数,读第二遍时很容易继续沿着第一次的思路走。

我把这个项目的质量证据分成几层,固定在这次发布上,而不是写一个会随着 main 漂移的总数:

证据 本版数量 回答的问题
selftest.py 内置自检 309 项 已有核心协议、安全边界和功能回归有没有破坏
tests/ 独立 unittest 223 项 后续复审确认的具体缺陷还能否复现,本轮新增 52 项
W8 本地演练 10 项 真实本地进程、文件、回环 HTTP 串起来后,变更和恢复链是否成立
docs/reviews/ 该 tag 保存 14 份记录 问题怎么确认、怎么改、验证到哪,哪些地方还没证据

这些数不能简单相加成“总共证明了多少件事”。不同测试会覆盖同一条链路,十个演练检查也不是十种真实环境。它们各自回答问题,不能替别的证据做担保。

历史自检的轨迹仍能在路线图里找到:

111 → W1 123 → W2 143 → W3 164 → W4 206 → W1–W4 复审 241
→ W5 261 → W5 二次复审 292 → W6 300 → W7 309

后来的复审没有继续往 selftest.py 里塞所有用例,而是把针对性回归放进 tests/。所以“自检还停在 309”不表示后面没测:本轮复审前独立回归是 171,补上 52 项后是 223。旧文章里的 168 是历史时点,不能继续当当前版本的数字。


1. 哨兵、边界和脚本接口

309 项自检使用标准库和临时状态。独立回归也不需要真实 SSH 主机、真实 provider 或通知接收人;其中一些会启动回环 HTTP 服务和子进程,所以“不接外部服务”不等于“完全不需要本地 socket 或进程能力”。

我最愿意保留的是几类会在修改时给我阻力的测试。

哨兵类。放一份假私钥、假 token、假 webhook URL,检查它们有没有进入模型请求、会话、审计和落盘输出。本轮又补了两个读出口:人工写进 memory.md 的凭据和旧事件里的秘密。只测写入时已经脱敏的新数据,会漏掉历史导入这条路。

边界类。R3 没有 force 不能发戳;批准后改实际执行环境,旧戳失效;kubectl 变更没有显式 context 被拒;未核实 unknown 不重放。这些测试把“我当初为什么要限制它”留在代码旁边。新一轮的 find -de''lete、动态选项、敏感 glob、回滚父目录改向,也都先作为具体失败被确认,再留下回归。

接口类。退出码、status、verdict、工具错误与 JSON-RPC 响应都属于接口。比如一次巡检任务可以运行成功,但健康结论为“异常”;不能只断言进程退出 0,就认定服务健康。RPC 的 id=1e999 解码成无限值时,也不能照样输出一个 Infinity 给客户端。

最后是失败后的继续工作。错误工具参数回来以后,Anthropic 下一轮还能不能发送;深层 JSON 被拒以后,RPC 的后续 ping 能不能响应;未回答的确认有没有留下可恢复的会话。我现在更在意这些后半段,而不只盯着第一次错误有没有抛出来。


2. 一次跨版本检查,把测试自己的假设也揪出来了

这轮回归里有一组深层 JSON 用例。最初很容易把某个深度当成固定分界:在一个解释器里触发递归限制,就断言另一个解释器也必须拒绝。

Python 3.9 和 3.12 的解析与递归实现并不完全一样。同样的嵌套数据,可能在一边被正常处理,在另一边受控拒绝;“没有按我预想的地方报错”不一定是产品坏了,也可能是测试把运行时细节当成了协议。

所以最后保留了两种证据:真实深层输入验证这条调用链不会以非受控异常中断;对明确要处理的 RecursionError 分支,再用定向 mock 触发,检查错误后的继续工作和无半条持久化。没有为了让用例整齐,给产品硬加一个任意的全局深度限制。

这个返工让我更愿意检查测试的结论到底是什么。我要的约束是“合法数据能处理,不能处理时受控失败”,不是“两个解释器必须在第几层用同一句错误退出”。


3. CI 验的是同一份待发布代码

.github/workflows/selftest.yml 的四个组合是 Ubuntu/macOS × Python 3.9/3.12,每格都跑编译、自检、完整独立回归和 W8 本地演练。普通分支 push、PR 会触发;tag 发布则由 Release workflow 复用这份检查,传入精确 commit,避免检查一份代码、打包另一份。

这次发布走的是工作分支合入 develop,检查成功后快进到 main,再创建 v0.1.0-rc.1 tag。Release workflow 还会核对:

  • tag 是合法版本,真实存在于 refs/tags/,提交属于 origin/main 历史;
  • VERSION、发布说明标题、CHANGELOG 版本节和 README 两个发布入口相互对应;
  • 完整矩阵通过后,再从已验证的 commit 做 git archive,生成源码包和 SHA256SUMS;
  • 发布前重新核对远端 tag object,不覆盖已有 Release;候选版标 prerelease,不顶替 stable latest。

本版 Release 运行的四格矩阵与制品发布均已成功。下载包的校验和能确认字节与公布摘要一致,版本 tag 让读者能回到同一份实现;这仍不是“来源已经经过密码学签名认证”,也不是生产可用性验收。


4. 复审记录要留下下一次的入口

复审我会换几种顺序。按功能走,会问审批是否绑定了目标、锁有没有覆盖执行;按入口走,则会看 hook、RPC、MCP、命令采集能不能绕开那条规则;按生命周期走,又会问恢复、重启、取消、截断和写盘失败之后还剩什么。

本轮“证据保存失败却仍可判正常”就属于最后一种。单测如果只给 inspect 一份正常报告,返回文本也正常,看不到问题。回归要走实际 Agent 工具调用:采集或保存失败,模型最后仍回答“正常”,再断言调度从内部 evidence_errors 得到“不完整”、退出 3。模型配合时的正确,不能替代模型忽略警告时的程序约束。

我也会借助 AI 做复审,但它指出一个可能性不等于问题已经成立。最终留下来的仍是同样的东西:可复现条件、代码修复、针对性测试,以及没覆盖到的边界。工具给我另一种检查顺序,不能给我一张没有证据的通过证书。

14 份记录不都是同一种模板,但“确认的问题、修复、验证范围”这几个位置能帮下次的我接着找。旧测试数、旧边界也保留在历史记录里,我不为了新版本好看去改写过去。


5. 绿勾之外,还剩什么

连续 7 天观察的规则写在试运行记录里:真实环境启动修复版本后连续计时,重大修复后重新开始,不能把交付日直接当观察起点。到这个 tag,本版 W8 的真实 SSH、LLM、通知渠道验收与这段观察仍未完成;候选版已经发布,受控变更正式版没有。

这不是 CI 里已经实现的“连续 N 次绿勾计数器”。CI 证明当前矩阵的测试跑过,观察记录要证明真实排期里没有未解释漏跑,通知和恢复证据对得上。我不把重跑后的成功覆盖掉先前失败的原因,也不能拿加速循环代替七个自然日的现场证据。

README 里还有一批明确边界:没有完整 shell AST;秘密识别是启发式;同 UID 下不构成恶意 Agent 沙箱;调度中断对账存在 PID 复用误判;单机宿主彻底宕机时不能自己发出通知;SSH 超时不保证远端进程退出。这些不是多加几条 happy path 就能消失的限制。

所以我当前的质量声明是有范围的:309 项自检、223 项独立回归、10 项本地演练和四组 CI 已通过,真实环境门禁仍待完成。测试数不是 KPI,哪条链一旦失效会造成什么后果,才决定我还要补什么。

下一篇收一下设计决定的账:哪些能力我暂时不做,哪些规则在这次复审后变得更具体,哪些仍得等现场验收回答。

对照源码与证据


系列总览 · 目录 · 上一篇:同一个内核,四个壳 · 下一篇:被否掉的、没底的、还没做的