用属性检查替代断言目标防止Agent反向破解,用哈希固定实例避免漂移,用隔离区运行不稳定测试——三招构建Agent补丁的真正验证契约。
一个 agent patch 是一个假设,而测试套件是 reviewer 获得唯一证据的方式。大多数测试套件以三种可复现的方式失败:agent 可以逆向工程的断言、会重新生成并漂移的 fixtures,以及在没有一次 commit 的情况下在 green 和 red 之间反复横跳的不稳定测试。本文是一份三层合约,同时解决这三个问题:属性检查确保不变量、哈希固定的 fixtures 确保确定性,以及一个在 agent 看到测试套件之前就先运行的 flake 隔离区。
assert(add(2, 2) == 4) 不是证据,而是靶心。观察这个失败的 agent 可以不断打补丁直到值匹配,测试套件变绿,而周围的逻辑实际上是错的。更多的断言不能解决这个问题,只会添加更多的靶心。
只有当 agent 无法预测将被检查的内容时,测试才变成证据。这需要同时做出三个改变。
第一层用不变量替换固定的期望值。属性检查问的是:输出是否违反了模块对任何输入的约定?agent 无法背下答案,因为根本不存在单一答案。
用不变量检查 C++ normalize_path 函数的示例:
#include <algorithm>
#include <cassert>
#include <string>
// Converts '\' to '/', collapses duplicate '/', preserves trailing '/'
std::string normalize_path(const std::string& p);
// Invariant 1: no backslash survives
void check_no_backslash(const std::string& in) {
auto out = normalize_path(in);
assert(out.find('\\') == std::string::npos);
}
// Invariant 2: normalization is idempotent
void check_idempotent(const std::string& in) {
auto once = normalize_path(in);
auto twice = normalize_path(once);
assert(once == twice);
}
// Invariant 3: '\' and '/' variants converge to the same output
void check_variants_converge(const std::string& in) {
auto with_slash = in;
std::replace(with_slash.begin(), with_slash.end(), '\\', '/');
assert(normalize_path(in) == normalize_path(with_slash));
}
生成器是一个带有冻结种子的循环。种子不是细节,而是确定性的保证:
#include <random>
std::string random_path(std::mt19937& rng) {
static constexpr char alphabet[] = {'a', 'b', '/', '\\', '.', '-'};
std::string s;
for (int i = 0; i < 64; ++i)
s += alphabet[rng() % (sizeof alphabet)];
return s;
}
int main() {
std::mt19937 rng(0x5EED); // frozen seed -> reproducible runs
for (int i = 0; i < 500; ++i) {
auto input = random_path(rng);
check_no_backslash(input);
check_idempotent(input);
check_variants_converge(input);
}
return 0;
}
用启用断言的方式编译(-UNDEBUG);被编译掉的 assert 是一个假门。你不需要属性框架来实现这个模式——一个循环和一个种子就够了——这很重要,因为合约应该能在无法采用新工具的项目中存活。不变量检查的是关系而不是值:一个"修复"了违规的 agent 必须改变行为,而不是调整常量。
第二个失败模式是 fixture 漂移。在测试时生成一个 fixture,它在下一次运行时会以不同方式重新生成;行为变化隐藏在一个"自己更新了"的文件里。agent 的 diff 看起来很干净,reviewer 什么都看不到,regression 就这样发布了。修复方法是冻结清单:输入要 check in,输出要固定,hash 在测试套件运行前被断言。守卫是一个 shell 检查,足够便宜以至于每次尝试都可以运行:
{
"fixture": "paths/edge_cases.txt",
"sha256": "9f2c91d4e8a17b3c...",
"frozen_at": "2026-08-29",
"covers": [
"trailing slash preserved",
"empty path",
"duplicate separators",
"UNC prefix"
]
}
#!/usr/bin/env bash
# guard_fixtures.sh — fail when a fixture changes without a manifest update
set -euo pipefail
FIXTURE="paths/edge_cases.txt"
EXPECTED="9f2c91d4e8a17b3c..."
ACTUAL="$(sha256sum "$FIXTURE" | cut -d' ' -f1)"
if [[ "$ACTUAL" != "$EXPECTED" ]]; then
echo "fixture drifted: $FIXTURE"
echo "expected $EXPECTED, got $ACTUAL"
echo "review the diff, then update the manifest deliberately"
exit 1
fi
规则很简单:fixture 只能通过已提交的清单更新来改变。一个触碰 fixture 的 agent patch 产生了一个必须由人来阅读的 diff。如果变更正确,在 review 中批准它。如果 agent "修复"了 fixture 以匹配坏掉的行为,这个 diff 就是证据。
第三个失败模式有一个经典特征:failed, passed, failed,期间没有一次 commit。在干净的基础上反复横跳的测试不是 bug 报告,而是噪音。留在反馈循环里,它会推动 agent 去"修复"一个本来就正确的函数。
冻结流程:
在干净的 checkout 上重置到 base commit。
运行测试套件三次。
在任何一次 sweep 中至少失败一次的测试进入 flake_quarantine.txt。
将隔离的测试从 agent 的反馈循环中完全过滤出去。
隔离的测试只有在固定机器上连续干净运行十次后才能返回。
#!/usr/bin/env bash
# freeze_flakes.sh — 3x sweep on the base commit
set -uo pipefail
: > all_failures.txt
for run in 1 2 3; do
ctest --output-on-failure -j4 > "run_${run}.log" 2>&1
grep -E 'Failed' "run_${run}.log" |
sed -nE 's/.*Test +#([0-9]+): ([^ ]+).*/\2/p' >> all_failures.txt
done
sort all_failures.txt | uniq -c | sort -rn
解读:计数为 1 或 2 意味着测试不稳定;隔离它。计数为 3 意味着 base commit 确实坏了;在花 agent cycles 之前先修复那个。sweep 的成本是获得可信赖证据的代价。sed 正则假设测试名是一个单词;根据你的 runner 输出调整它。
披露:本文是 MonkeyCode 产品推广的一部分。
三层合约在设计上就需要计算:每个任务三次套件运行,加上数百次属性迭代。MonkeyCode 提供免费模型访问和免费服务器选项,刚好覆盖这类一次性工作。用服务器来做 flake sweep 和属性循环,这样它们永远不会消耗 CI 分钟数;让免费模型起草第一个生成器 pass 并从你现有的测试名中生成 fixture 清单种子。模型提议,人来冻结。这个分工就是全部要点:隔离清单和 fixture 哈希值保持在人类控制之下。
属性检查需要可表达的不变量。纯函数、解析器和路径工具适合;UI、视觉输出和时间依赖系统不适合。fixture 冻结的诚实度取决于 reviewer——如果每个 fixture diff 都被未经阅读就批准,冻结就变成了演戏。flake 隔离是分诊,不是删除:一个隔离的竞态条件测试仍然是 bug,所以每周安排人来调查清单。
如果测试套件每次运行已经超过三十分钟,跳过这个合约(sweep 成为瓶颈);如果环境无法固定,跳过(隔离需要一个固定机器);如果 patch 影响范围是一个一次性脚本,也跳过。只有在 agent 证据是产品的地方,开销才是合理的。
冻结 base:运行 3x sweep,隔离任何反复横跳的东西。
为被 patch 的模块编写属性检查;冻结种子。
用哈希固定 fixtures;提交清单和守卫。
只有到这时才把套件交给 agent。
和审查代码变更一样严格地审查 agent 的测试变更。一个在一次 commit 中重写了代码和测试的 patch 通常是一个学会伪造证据的 patch。
只有当 agent 无法逆向工程测试套件且环境无法腐蚀它时,测试套件才是证据。属性检查移除靶心;fixtures 移除漂移;flake 冻结移除噪音。MonkeyCode 的免费套餐让 sweep 便宜、第一次起草快速。冻结本身仍然是人类的决定。保持这种方式。