应用部分 5. 规范的变异测试
状态:前沿。 规范的变异测试(mutation testing)和免疫度量向量(immunity score)—— 这些实践尚未标准化。「一个变异体对应一个预期失败」的理念属于建议性质。变异算子集合和阈值本身需要根据项目自行配置。
对于教学通关,只需运行 examples/stress-mutator/ 并观察到一个变异体产生一个预期失败。算子选择、阈值调整和 CI 闸门属于完整的生产级流程。
让我们引入基本概念。变异测试 —— 是一种将基准工件可控地「破坏」,而测试回路必须捕获该缺陷的技术。免疫度量 —— 是验证器鲁棒性的向量度量,由三个分量组成:
strict_reject_rate—— 在预期步骤被严格拒绝的案例比例;depth_of_diagnostics—— 失败前有用诊断深度;recovery_time—— 返回稳定判定结果所需时间。
「验证器疫苗接种」这一形象化名称指的是普通的规范变异测试。验证器接收可控损坏的输入,并必须在预期步骤将其拒绝。
与相邻机制的边界如下。在第 2 章中,你创建一个手动缺陷以学习读取症状。在本章中,你创建一系列机器变异体以测量验证器的鲁棒性。在第 4 章中,验证器寻找规则的最小反例,而非遍历变异算子目录。在第 8 章中,此类检查的结果可以成为判定的证据,但文件仲裁本身不能替代变异体生成器。
本章依赖第一卷第 9 部分的事实纪律。没有它,变异就没有意义。变异体检验的正是 Given/When/Then 预期步骤上的失败事实。这一纪律的最简单示例已在教学 AgentClinic 中出现过:来自第 12 部分的空评论文本必须被拒绝。此处相同的逻辑被推广到一组变异算子,这些算子与第 20 部分「SDD 反模式」中的经典错误目录相关联。
阅读前
- 第一卷的依赖基础:第 9 部分引入验证事实,第 20 部分引入流程错误类别。
- 本地教学案例:
appointment_latency_spike(最小事件负载,可运行示例中的base/base_spec.json基于此构建)。 capstone/的痕迹:种子、算子列表、三个免疫度量和high_memory_usage的validation.md中的判定字符串。- 第一遍的主要术语:变异测试(章节入口)和免疫度量(出口 —— 向量的三个分量)。其余 —— 变异算子、变异工厂、「验证器疫苗接种」—— 属于参考性质,仅在配置 CI 闸门时展开。
- 可推迟的内容:算子选择、阈值校准和变异 CI 闸门。
目标
阅读本章后,读者将为自动事件管理项目组装退化规范生成器,并配置验证器回路以完成三件事:以精确诊断丢弃荒谬案例、在 SDD 中保存证据链、在合并前计算免疫度量。验证器不再是语法看守,而成为解剖诊断工具:展示失败事实、字段、Given/When/Then 步骤、JSON Schema 规则、失败路径和回归风险。这与「规范优先」(spec-first)方法一致 —— 契约先于代码规划和实现(GitHub Spec Kit)。
最小教学场景
教学案例
生产事件 appointment_latency_spike(源自教学功能 /agents,见 book/part-11-second-feature-phase.md):SLA 10 分钟,从 appointments_oncall 升级到 sre_lead。Nullify 变异将 severity 置空。预期 —— 验证器在 When:evaluate_sla_window 之前以 EMPTY_REQUIRED_FIELD 代码停止,早于 SLA 计算和所有者选择。
准备
book2/examples/stress-mutator/base/base_spec.json—— 正确的原始文件。book2/examples/stress-mutator/expected/expected_failures.json——by_operator下的预期(diagnostic_code, halt_before)和thresholds中的免疫阈值。book2/examples/stress-mutator/scripts/mutate_specs.py、fake_validator.py、immunity_score.py。book2/examples/stress-mutator/manifest.example.json—— 确定性基准。
步骤
cd book2/examples/stress-mutator。预期:你在示例目录中,无需额外依赖。python3 scripts/mutate_specs.py --base base/base_spec.json --seed 20260517 --operators Nullify,FutureTime,EscalationCycle,PriorityContradiction --out out/mutations。*预期:创建out/mutations/manifest.json和每个变异体一个 JSON 文件。*- 确定性控制 —— 重复步骤 2。*预期:
mutation_id列表和顺序与上次运行一致。*
不好: 只运行一次而不重复 —— 无法区分确定性生成器和随机噪声。 好: 连续两次运行,mutation_id 顺序相同,回归基准可复现。
- 通过
diff比较out/mutations/manifest.json与manifest.example.json。预期:0 行差异。 python3 scripts/fake_validator.py --mutations out/mutations --out out/validator_results.json。*预期:每个mutation_id的结果中都有diagnostic_code+halt_before对。*python3 scripts/immunity_score.py --validator-results out/validator_results.json --expected expected/expected_failures.json。*预期:strict_reject_rate >= 0.98,depth_of_diagnostics >= 3,recovery_time_p95_ms <= 1200。*- 对于教学最小值,到此为止:可运行示例证明了变异体的确定性、预期失败和免疫计算。
如果你已安装 Qwen Code 并希望获得额外解释,请执行单独的可选步骤:
qwen -p "阅读 @out/validator_results.json 和 @expected/expected_failures.json。哪些变异体未在预期步骤被拒绝?不要修改文件。" --approval-mode plan
此请求不能替代可运行检查。其结果可用作评审评论,但不能作为就绪的唯一事实。
完整的生产流程会添加单独的 CI 闸门。在你的项目中,这通常是 python3 scripts/ci_gate.py --strict-reject-min 0.98 --diag-depth-min 3 --recover-ms-p95 1200 --fail-on-regression —— 三个阈值,任何违规都会阻止合并。教材中没有专门针对 stress-mutator 的可运行对应物;类似理念的 examples/goodhart-validator/scripts/ci_gate.py 在第 10 部分展示。
控制事实
步骤 6 中的三个度量同时满足阈值。manifest.json 与 manifest.example.json 逐位相同。如果执行了可选的 Qwen 请求,其输出不应与可运行事实矛盾。没有确定性、预期失败和绿色免疫度量,教学流水线不被视为绿色。
如何进入 capstone/
仅将 smoke 运行的最终结果转移到 capstone/validation.md 或简短的 capstone/README.md:种子、算子、三个免疫度量和判定。不要转移 out/mutations 目录:它应保持为可复现的本地痕迹,而非评审工件。
最小片段:
stress_run:
seed: 20260517
operators: [Nullify, FutureTime, EscalationCycle, PriorityContradiction]
strict_reject_rate: "1.0 >= 0.98"
depth_of_diagnostics: "4.0 >= 3"
recovery_time_p95_ms: "850 <= 1200"
verdict: PASS
可评审痕迹
out/ 目录是本地运行结果,在 book2/examples/.gitignore 中被忽略。不要将其作为教学工件提交,也不要为了标记而提交。对于第一遍,capstone/validation.md 中的一行已足够:种子、算子、三个度量和 verdict。
在你自己的生产仓库中,可以存储简短的报告 outputs/immunity.last-run.json,如果它由 CI 创建并参与评审。在教学路线中,真相来源仍然是可复现的命令和上面的最小 capstone 片段。
核心思想
将事件流程的退化场景分为四类。空字段 —— 不仅仅是 null:还包括空字符串、空所有者数组、缺失的 severity、service_id 或 runbook_ref —— 任何没有它就无法选择安全操作的空值。时间异常 形式上看起来正确:存在 ISO 时间戳,但 response_timestamp 早于 event_received_at 或晚于约定的 now。可逆升级循环 和 递归依赖 比普通遗漏更危险 —— 它们可能将执行回路送入所有者、优先级或下一步操作的无限重定义。
再引入一个概念。变异工厂 —— 不是随机噪声生成器,而是基于正确 base_spec.json 的确定性变异器。基础规范被解析为具有显式 Given/When/Then 节点、SLA 矩阵、升级规则和 JSON Schema 片段的抽象语法树(AST)。然后对其应用算子:
Nullify—— 字段置空;FutureTime—— 时间戳向未来偏移;EscalationCycle—— 在升级图中添加反向边;
PriorityContradiction—— 引入相互矛盾的优先级规则。
未来扩展将添加 RecursiveDependency 用于计算字段之间的间接递归。
「一个变异体对应一个预期失败」是工厂的主要规则。让我们展示对比。
不好:
> 一个变异体同时置空 service_id、反转升级图并反转优先级;未设置 expected_failure。
问题:失败时无法定位原因。验证器可能在三个缺陷中的任何一个处停止,回归与组合工件绑定。
好:
> 一个 Nullify 变异器仅置空 severity;expected_failure.code = EMPTY_REQUIRED_FIELD,halt_before = When:evaluate_sla_window。
每次运行获得固定种子(seed)。相同输入产生相同 mutation_id 列表的稳定顺序。这对于验证器与实现者的对决至关重要:争议案例可以复现,交给两个角色,并检查谁违反了契约。
> [可运行] —— 此接口的最小实现在 examples/stress-mutator/README.md 中。
cd book2/examples/stress-mutator
python3 scripts/mutate_specs.py \
--base base/base_spec.json \
--seed 20260517 \
--operators Nullify,FutureTime,EscalationCycle,PriorityContradiction \
--out out/mutations
python3 scripts/fake_validator.py \
--mutations out/mutations \
--out out/validator_results.json
#### 控制:用相同种子重复运行应产生相同的 mutation_id 列表和相同顺序
组合爆炸在深度 2-3 时已经出现。为生成器设置选择策略,而非完全枚举:每个类别至少一个变异体(必填字段、时间窗口、升级图、递归依赖、优先级冲突)。将算子优先级与事件历史关联:如果事后分析更常显示错误的时间窗口,则给 FutureTime 和 NegativeLag 在队列中更高权重。定向模糊测试检查历史上契约的脆弱点,而非将令牌预算浪费在均匀混乱上。
flowchart TD A[文件 base_spec.json] --> B[AST 规范化器] B --> C[变异工厂] C --> C1[Nullify] C --> C2[FutureTime] C --> C3[EscalationCycle] C --> C4[PriorityContradiction] C1 --> D[验证器/实现者对决,绑定 Given/When/Then 步骤] C2 --> D C3 --> D C4 --> D D --> E[诊断和堆栈路径] E --> F[mutation_id 和 validation.md] F --> G[CI 闸门]
将每个变异体绑定到特定的 Given/When/Then 步骤和特定的 JSON Schema 规则。否则诊断将过于笼统而无法修复。绑定必须明确:Nullify(service_id) 变异属于 Given:incident_received 和 required.service_id 规则,FutureTime(response_timestamp) 变异属于 When:evaluate_sla_window 和 format + maximum(now) 约束。
如果变异体破坏 Then:notify_primary_owner,报告必须展示问题本质。问题不在于通知作为操作。问题在于损坏路径后无法计算有效所有者。这种追踪减少手动调试:工程师看到卡住点,而非仅看到最终的 VALIDATION_FAILED。
{
"mutation_id": "m_20260517_0009",
"operator": "EscalationCycle",
"target_step": "When:route_escalation",
"json_schema_rule": "$defs.escalation_graph.no_cycles",
"failed_step": "Verifier::GraphCheck::Escalation",
"stack_route": [
"schema.normalize",
"step.when.prepare",
"graph.build",
"graph.detect_cycle",
"halt"
]
}
循环诊断需要单独的图遍历。原因是 JSON Schema 擅长检查数据形状,但不一定能表达路径的拓扑行为。对于 EscalationCycle,验证器构建所有者或队列的有向图并运行深度优先搜索(DFS),状态为 white/gray/black。检测到 gray 节点返回最小循环,例如 primary_oncall → sre_lead → primary_oncall。
对于可逆的优先级转换使用类似的控制。如果规则 1 将 P1 降级为 P2,然后规则 2 在没有决胜规则(tie_breaker)的情况下将 P2 恢复为 P1,验证器必须在执行阶段前停止。诊断代码必须区分 CYCLE_ESCALATION 和 PRIORITY_REVERSAL。前者通过路径图修复。后者通过冲突解决策略修复。
在时间异常之前检查路由。错误时间扭曲 SLA、严重性和响应通道选择。给验证器至少三个锚点 —— event_detected_at、event_received_at、来自受控时间源的约定 now —— 以及 max_reaction_lag 策略。相应地,失败获得三个代码之一:INVALID_TIME_ANCHOR(如果 response_timestamp 在未来 —— 输入负载问题),NEGATIVE_RESPONSE_LAG(负响应延迟 —— 时间规范化问题)或 STALE_INCIDENT_WINDOW(事件超过允许窗口 —— SLA 规则问题)。不同代码对 SDD 日志很重要:它们显示契约在哪里被削弱。
递归依赖与循环的区别在于可能不像图中的短环那样明显。典型链:owner 从 priority 计算,priority 依赖 blast_radius,blast_radius 查询 owner_group,而 owner_group 再次要求已计算的 owner。
对此类情况设置展开限制,例如 max_resolution_depth = 8。保存依赖解析尝试的轨迹。如果超过限制,验证器返回 RECURSION_LIMIT 及字段链,而非将问题掩盖为超时。这保护 LLM 执行器免受无限条件细化,并使失败级联可观察。
现在关于免疫度量(向量分量 —— 见章节开头)。将其作为向量引入,而非单一总评分。如果 strict_reject_rate 上升但 depth_of_diagnostics 下降到 1,回路变得更严格但更盲目。如果 recovery_time_p95_ms 超出限制,即使正确的验证器也会拖慢 CI 并诱发绕过实践。
在 CI 中基于免疫阈值和与上次运行的回归比较构建阻塞。对于教学回路,从以下值开始:
strict_reject_rate >= 0.98,depth_of_diagnostics >= 3,recovery_time_p95_ms <= 1200。
然后根据实际负载和变异体数量校准值。
如果新更改导致以下至少一项,则合并被阻塞:
- 遗漏旧
mutation_id, - 降低诊断深度,
- 超出恢复时间限制。
此类闸门不仅保护 JSON Schema,还保护整个验证器回路:规范化器、图检查、Given/When/Then 规则和报告格式。
> [可运行] —— 以下命令对应 book2/examples/stress-mutator。
cd book2/examples/stress-mutator
python3 scripts/immunity_score.py \
--validator-results out/validator_results.json \
--expected expected/expected_failures.json
在你的项目中,此闸门通常看起来像 python3 scripts/ci_gate.py --strict-reject-min 0.98 --diag-depth-min 3 --recover-ms-p95 1200 --fail-on-regression。教材中没有专门针对 stress-mutator 的现成脚本;「一个未通过阈值 = 阻塞」的理念在形式相近的 examples/goodhart-validator/scripts/ci_gate.py 中保留(第 10 部分)。
将运行结果作为证据链固定在 SDD 中,而非作为一次性测试日志:mutation_id、规范差异、原始和变异片段、拒绝日志、诊断代码、stack_route、JSON Schema 规则引用和 validation.md 中的最终记录。对于评审,特别有用的存储是 expected_failure 和 actual_failure:如果它们分歧,验证器可能是偶然或太晚拒绝案例。这种结构将变异目录转化为先例目录,其中每个新规则与特定盲区和可验证基础关联。
完整流程:阈值校准
strict_reject_rate、depth_of_diagnostics、recovery_time_p95_ms 和每类变异体数量的「低/默认/高」表、阈值偏移练习和重审信号已移至 附录 D,D.1 节。第一遍不需要该部分。
示例和应用
示例:正确规范描述事件 appointment_latency_spike。SLA 要求 10 分钟内响应。升级路径从 appointments_oncall 到 sre_lead。
变异器创建 m_20260517_nullify_855e4297f7。其中 severity 字段被替换为空字符串。变异体绑定到 Given:incident_received 和 severity.minLength 规则。预期失败 —— EMPTY_REQUIRED_FIELD。流水线必须在 When:evaluate_sla_window 之前停止,早于 SLA 计算和所有者选择。
如果验证器反而到达 Then:notify_owner,意味着空 severity 字段渗透太深,可能产生关于未分类事件的虚假通知。
{
"mutation_id": "m_20260517_nullify_855e4297f7",
"base_case": "appointment_latency_spike",
"operator": "Nullify",
"target_step": "Given:incident_received",
"json_schema_rule": "$.properties.severity.minLength",
"diff_spec": {
"before": { "severity": "P1" },
"after": { "severity": "" }
},
"expected_failure": {
"code": "EMPTY_REQUIRED_FIELD",
"halt_before": "When:evaluate_sla_window"
}
}
第二个示例检查事件 cdn_error_budget_burn 的升级图。所有者 edge_oncall 将 P1 传递给 traffic_sre。变异器添加反向边 traffic_sre → edge_oncall。
验证器应该做什么。返回 CYCLE_ESCALATION,展示最小循环,并将失败绑定到 When:route_escalation。实现者不应提出诸如「从列表中选择第一个所有者」之类的绕过方案。在 JSON Schema 或额外图规则中修复后,相同的 mutation_id 重新运行,以证明补丁确实关闭了发现的缺陷。
validation.md 中的记录必须包含差异(diff)、判定、恢复时间和 CI 运行引用。否则在下次路径更改时将无法验证决策。
总结
压力规范生成器将验证器检查转化为受控的工程循环:它分类退化场景,创建可复现变异,将每次破坏绑定到 Given/When/Then 步骤和 JSON Schema 规则,通过向量三个分量测量免疫,并通过 mutation_id、规范差异、拒绝日志和 validation.md 在 SDD 中保存证据。这种回路将荒谬案例转化为针对未来有毒需求和隐藏失败级联的回归集。下一章转向影子规范拍卖。
工件和就绪标准
| 工件 | 就绪条件 |
|---|---|
base/base_spec.json | 描述将基于此构建变异的正确事件场景 |
本地 out/mutations/(4 个变异体) | 用相同 seed 重复运行产生相同 mutation_id 顺序;目录不提交 |
out/validator_results.json | 每个变异体绑定 Given/When/Then 步骤和 JSON Schema 规则;有 diagnostic_code、halt_before、深度(depth) |
| 最小免疫报告 | 填充向量三个分量 —— strict_reject_rate、depth_of_diagnostics、recovery_time_p95_ms;可运行示例通过 smoke 测试 |
完整流程添加 expected/expected_failures.json 作为 CI 回归基准、简短可评审报告或 validation.md 记录,以及将新运行与旧 mutation_id 比较的 CI 闸门。当验证器在执行阶段前停止循环和时间异常,且 CI 至少阻塞一个旧 mutation_id 的回归时,视为就绪。
实践
cd book2/examples/stress-mutator && python3 scripts/mutate_specs.py --base base/base_spec.json --seed 20260517 --out out/mutations—— *预期:out/mutations/中恰好 4 个文件,mutation_id为m_20260517_nullify_855e4297f7、m_20260517_futuretime_…、m_20260517_escalationcycle_…、m_20260517_prioritycontradiction_…;diff out/mutations/manifest.json manifest.example.json产生 0 行差异。*python3 scripts/fake_validator.py --mutations out/mutations --out out/validator_results.json && python3 scripts/immunity_score.py --validator-results out/validator_results.json --expected expected/expected_failures.json --out out/immunity.json—— *预期:strict_reject_rate >= 0.98,depth_of_diagnostics >= 3,recovery_time_p95_ms <= 1200。*- 将一行转移到
capstone/validation.md:「免疫(seed=20260517):<n>/4变异体在预期步骤被拒绝;失败 ——<mutation_id>,需要额外 guard」。*预期:下次回归时比较针对固定seed,而非「全部绿色」。*
自测问题
- 为什么 JSON Schema 不足以检查循环和递归依赖?
strict_reject_rate展示什么,又隐藏什么?- 验证器严格性的增长何时变得有害?
- 验证器通过 50 个变异体的 smoke 测试,显示
strict_reject_rate=0.95,depth_of_diagnostics=2.4,recovery_time_p95_ms=900。三个标量都在默认阈值内。请说出至少一个应视为此测试失败的场景,以及需要检查 manifest.json 的哪些额外字段以使此类失败对下一位评审者可见。