你跳过的那条测试

先讲这个模块的由来。我当初测 Agent 稳定性,手动 kill 了一个进程。Agent 自动复活了,还继续干之前没干完的活。我当时挺兴奋——但紧接着想了一句话:

This was just a manual test. Can I automate it?

于是我造了 Arena——一个唯一目的就是搞破坏的模块。它不实现自己的 Runtime、DAG 或恢复逻辑,就只是故意去调那些危险 API(StopAgentRemoveNodeRemoveEdge),然后等着看系统自己修好自己。我把这叫 「Edo Tensei Verification」(秽土转生验证)——在 Dashboard 上点一个按钮,暗杀一个正在干活的 Agent,看它能不能自己爬起来。

这个念头本身,来自一次真实的生产事故:我手动 kill,Agent 复活、续上任务——那一刻我才意识到,大多数人根本不会写这种测试,因为我们要测的是「它活着时能干活」,不是「它死了会怎样」

Problem: ARES 的 Arena 能一键「谋杀」运行中的 Agent,看它能否自愈(第九篇)。这测试大多数人根本不会写——因为我们要测的是「它活着时能干活」,不是「它死了会怎样」。哪些测试你明知道该写却跳过了?

测试文化的盲区

团队写测试,默认在测快乐路径:输入正确,输出正确。这是「它能干活」的证明。

但系统真正的生死,不在「干活时」,在「出事时」。ARES 第九篇讲得很直白:作者一开始是手动 kill 进程,发现 Agent 自动复活并继续未完成的任务,然后想——能不能把它做成一键按钮?

把「谋杀 Agent 看它能否自愈」做成一键按钮,这测试稀有的地方在于:它测的不是功能,是系统的尊严——崩溃后还能不能爬起来。

Arena 的杀手锏是 internal/arena/injector.go 里的 KillLeader

func (in *Injector) KillLeader(ctx context.Context) (string, error) {
    leaderID := ""
    for _, info := range in.runtime.ListAgents() {
        if info.Type == "leader" {
            leaderID = info.ID
            break
        }
    }
    if leaderID == "" {
        return "", ErrLeaderNotFound
    }
    if err := in.runtime.StopAgent(ctx, leaderID); err != nil {
        return "", fmt.Errorf("arena: kill leader %s: %w", leaderID, err)
    }
    return leaderID, nil
}

因果链是:Arena 调 StopAgent("leader-1") → Runtime 标记停止 → goroutine 退出 → NotifyAgentDead 被调用 → LeaderSupervisor 发现 Leader 缺席 → 触发 failover(选举 → checkpoint 恢复 → 事件重放)→ 新 Leader 几秒内被选出并跑起来。一次「暗杀」,同时证明了三件事:选举、状态恢复、事件重放。

我给一个朋友演示过:打开 Dashboard,点「Assassinate Leader」,Agent 死了……1.4 秒后它自动复活了。他说:「Holy shit, it does that?」

我想的是:对,这就是我花那么多时间造这玩意儿的原因。

你跳过的是哪几条

对照已有系列,几个反复出现、却常被测试遗漏的维度:

跳过的测试真实出处你测了什么,却没测什么
复活测试ARES 第七、九篇测了正常流程,没测「死过一次之后」状态能不能恢复、未完成的任务能不能续上
并发下的自身安全memscope-rs 篇,每秒数十万事件测了正确性,没测「高负载下它自己先死」
边界守卫的失败模式OmniScope 篇,跨语言 FFI测了「对的调用」,没测「C 端乱来、Rust 端接不住时是优雅降级还是直接 UB」
错误的传播「错误是设计的镜子」篇测了成功路径,没测「Leader 崩溃、评审者故意说错话时,系统怎么对待失败」

Arena 自己定义了 13 种混沌动作(kill_leader / kill_agent / remove_node / remove_edge / slow_agent / network_partition / tool_timeout / memory_corrupt / mcp_disconnect / llm_failure……),外加一个持续随机注入故障的 survival 模式。它用三维弹性评分来量化「抗揍能力」:

维度权重
Availability(故障成功比)40%
Recovery(恢复率 70% + 恢复速度 30%)30%
Consistency(数据一致性率)30%

survival 模式的实时输出长这样:

Elapsed: 12s    Actions: 1    Score: 100.0 (A+)
Elapsed: 22s    Actions: 2    Score: 97.3 (A+)

13 种故障随机选目标,Ctrl+C 停止并打印最终报告。这东西把 ares arena run cascading_storm.yaml 从一个测试命令,变成了一句宣言:「看我的系统有多能扛」。

为什么跳过

不是不知道该测,是跳过的成本不在当下。快乐路径的测试今天就能给你绿灯,让你心安理得地合并。复活测试、混沌测试要搭额外的基础设施(Arena、Compactor、重放),收益却要等某次生产事故才兑现——而那次事故发生时,你早忘了当初为啥没写。

这是典型的跨期套利:用未来的痛苦,贴现成现在的安逸。

ARES 的 Arena 值得所有框架学的地方,不是混沌工程多酷,而是它把「我们跳过的那条测试」变成了一个按钮。当验证自愈只要点一下,你就没有借口再跳过了。Arena 自己也很诚实:它不实现 Runtime/DAG/Recovery——只是调已有的危险 API,然后等系统自己修好。这种「薄一层 chaos 接口」的设计,恰恰是它能被真的用起来的原因。

我当时怎么想的,又差点错在哪

我差点没造 Arena。第一反应跟大多数人一样——「造个专门搞破坏的模块?听起来像个玩具」。是那次「手动 kill → 自动复活」的真实冲击,把我从「这有啥用」拽到了「我得把它变成一键的」。

我错在把「测试」默认等同于「测功能」。直到 Agent 真的在生产里静默死亡、而我的测试全绿——我才承认:那些全绿的测试,测的恰恰是最不怕出事的路径,跳过的才是要命的。

没写的测试不会消失,它们只是排着队,等在生产环境里一次性收了你的债。

结论

测试覆盖率高不代表你测对了。你测的往往是最不怕出事的路径,跳过的恰恰是最要命的。

ARES 的 Arena 值得所有框架学的地方,不是混沌工程多酷,而是它把「我们跳过的那条测试」变成了一个按钮。当验证自愈只要点一下,你就没有借口再跳过了。大多数系统的差距,不在于懂不懂该测什么,而在于有没有把该测的变成不费力的动作

写测试的本质问题从来不是「测什么」,是「你怎么对待那些你明知道该测、却总想拖到以后的东西」。拖着拖着,它们就变成了事故报告里的第一行。


Next: 离开 Web3 的那天 -- 从 ethermint 的共识代码,到 memscope-rs 的内存工具:我离开 Web3,是重写自己的问题域。