适用工具:Claude Code · Codex · 所有读 AGENTS.md 的工具 · 验证于 2026-09
前八章解决的是第一条轴:agent 能否读得懂这个仓库。本章开始第二条轴:agent 能否自己验得动——跑一条命令、读到确定的结果、拿到能定位问题的失败输出。04 讲的是用 agent 时的验证工作流(agent-TDD、独立评审、diff 对账),本章讲的是仓库基建:单命令验证回路、确定性、结构化失败输出、flaky 治理、Feature Map。基建缺位时 04 的每个手段都会退化成你本人重跑一遍。本章的检查项可以用 verify-doctor 直接跑出来。
场景:新 agent(或新同事)进入仓库,要知道”怎么起环境”“怎么证明改动没坏东西”。当前答案散在 README、CI workflow 和某人的 shell history 里。
做法:
只暴露两个入口,用仓库既有的任务运行器(package.json scripts / Makefile / justfile),不新增第二套:
dev: # 幂等:重复执行不重起已在跑的依赖
docker compose up -d --wait # --wait 等健康检查通过,不写 sleep
make migrate seed
npm run dev
check: typecheck lint test # 本地与 CI 是同一条命令
typecheck: ; tsc --noEmit
lint: ; eslint . --max-warnings=0
test: ; vitest run --reporter=json --outputFile=.artifacts/test.json
CI 里只写这一条,禁止在 workflow 里重新拼一遍命令:
- run: make check # 与本地共用同一入口;分叉出来的就是 CI-only 失败
make lint、make test),agent 才能局部迭代,不必为一处类型错误等全量。依据:--max-warnings 是 ESLint 官方定义的”告警数触发非零退出码”开关,0 即告警等于失败(ESLint CLI);docker compose up --wait 的官方语义是”等到服务 running/healthy 才返回”,可替掉固定睡眠(docker compose up);Vitest 官方指定用 --reporter=json --outputFile= 产出可编程消费的报告(Reporters)。
边界:monorepo 用运行器自带的过滤参数收窄范围(make check PKG=web),而不是给每个包各造一套入口。已有成熟运行器的仓库直接沿用;为了”统一”再套一层 wrapper 只会多一个分叉点。
反模式:README 写”跑测试前先 export 这几个变量、起 docker、再执行三条命令”——这三步里任何一步 agent 猜错,后面所有验证结论都无效。
场景:同一 commit 跑两次结果不同。此时 agent 无法区分”我改坏了”和”环境抖了”。
做法:
按来源逐类改,不靠加重试掩盖:
| 不确定源 | 修法 |
|---|---|
硬等待 sleep(500) |
换条件等待:vi.waitFor(() => cond)、Playwright 的自动等待断言 |
| 共享状态、DB 残留 | 每个测试独立事务并回滚,或独立 schema / 独立临时目录 |
| 真实时钟、时区 | 注入固定时钟 vi.setSystemTime(...),命令里写 TZ=UTC |
| 随机数、随机顺序 | 固定 seed,并显式开随机顺序跑一次以暴露顺序依赖 |
| 外部网络 | 全量 mock 或 record/replay,CI 断网跑一次证明没漏 |
| 端口冲突 | 监听端口 0 让内核分配,或按 worker id 派发 |
自检命令挂 nightly,不挂 PR 门(慢且噪音大):
TZ=UTC make check # 时区固定后仍应全绿
npx vitest run --sequence.shuffle --sequence.seed=1000 # 暴露顺序依赖
docker run --network=none $IMAGE make test # 断网证明无外网访问
随机顺序要固定并打印 seed,失败才能原样重放;否则”偶尔红一次”没有下一步。
依据:vi.setSystemTime 与 vi.waitFor 是 Vitest 官方 API(Vi);sequence.shuffle / sequence.seed 的官方用途正是”追查意外依赖前一个测试执行结果的测试”(sequence);TZ 环境变量对 Node 进程时区的作用见 Node.js CLI;Playwright 在每个动作前执行 visible / stable / enabled 等 actionability 检查并自动等待,官方以此取代固定睡眠(Auto-waiting);把网络行为收敛成一层独立 mock 见 Mock Service Worker;pytest 官方也把”依赖测试顺序”“上一个测试没清理干净”列为 flaky 的首要根因(Flaky tests)。
边界:封死不确定源会牺牲一部分真实性——全 mock 的网络测不到真实契约漂移,固定时钟测不到夏令时。补法是另开一档真实环境的慢速测试跑 nightly,而不是让 PR 门上的测试自己去连外网。
反模式:给抖动的测试加 retries: 3 当修复。绿灯从此意味着”三次里蒙对一次”,agent 再也拿不到可信信号。
场景:agent 修一个 CI 失败。它唯一的输入是失败输出,不是你脑子里的上下文——修复质量约等于失败信息质量。
做法:
每个失败给齐四件:
.artifacts/让 runner 直接产出机器可读报告和 trace,而不是靠人转述:
vitest run --reporter=json --outputFile=.artifacts/test.json
playwright test --trace=on-first-retry --output=.artifacts/e2e
CI 无论成败都上传证据,否则 agent 只能看日志尾巴猜:
- uses: actions/upload-artifact@v4
if: $
with:
name: evidence
path: .artifacts/
在 AGENTS.md 里写明 artifact 落在哪、怎么读,agent 才会去看 trace 而不是重跑一遍复现。
依据:Vitest 的 json reporter 配 outputFile 是官方指定的程序化消费方式(Reporters);Playwright 官方推荐 CI 上设 trace: 'on-first-retry',为重试的失败测试留下可逐动作回放的 trace.zip(Trace viewer);上传用 actions/upload-artifact(README),GitHub 官方文档说明 always() 连 job 被取消时也会执行、可能把流水线挂到超时,推荐用 if: $ 表达”无论成败都跑”(Evaluate expressions);产物的下载入口见 Downloading workflow artifacts。
边界:artifact 有保留期与体积成本(GitHub 默认存 90 天、可按仓库改),trace 常开会显著拖慢 e2e——官方明确 trace: 'on' 不推荐。只给失败和首次重试留证据。
反模式:把 2000 行原始日志整段贴给 agent。它会挑一个看起来相关的报错去改,改到日志变短为止。
场景:主干上有几个”偶尔红”的测试,团队习惯重跑一次就过。
做法:
先认清人与 agent 的行为差异——这是 flaky 从卫生问题升级为 P0 的原因:
| 人遇到 flaky | agent 遇到 flaky | |
|---|---|---|
| 判断 | “又抖了” | 认为是自己的改动破坏了它 |
| 行为 | 重跑 | 改代码或改测试让它绿 |
| 结果 | 浪费两分钟 | 引入真 bug 或删掉有效断言,PR 全绿 |
先要数据再谈治理。同一 commit 重复跑,20 次里出现过一次红就入名单:
for i in $(seq 1 20); do npx vitest run --reporter=json \
--outputFile=".artifacts/run-$i.json" || true; done
npx playwright test --repeat-each=20 # e2e 侧
pytest --count=20 # pytest-repeat
go test -count=20 -race ./...
修不了的先关笼子,不是删掉。标记格式做成机械可校验的 FLAKY: #<issue> @<owner> due <YYYY-MM-DD>:
// FLAKY: #4213 @alice due 2026-09-30 — websocket 重连时序,等 infra 升级
test.skip('checkout retries on 502', async () => { /* ... */ });
配套三条硬规则,否则隔离区会变垃圾场:隔离测试不参与合并门但每天单独跑一次并出报表;隔离清单条目数只允许下降,新增需显式批准;每条有到期时间,过期未修则删测试并同时收回该功能路径的自动合入权限——那条路径已无可信验证。
CI 扫描跳过标记与 .only:缺格式或已过期直接失败,.only 计数必须为 0(它会静默跳过同文件其余测试)。Playwright 自带开关:
npx playwright test --forbid-only --fail-on-flaky-tests
验收标准写成一句可核对的话:同一 commit 连续 20 次运行,非隔离测试集 100% 绿;隔离清单条目数单调不增;.only 计数为 0。
依据:pytest 官方 Flaky tests 一页指出”测试结果不再是可信信号时,开发者会开始不信任测试结果,从而漏掉真实失败”,并把 xfail 式手工隔离称为长期使用相当危险(Flaky tests);重复跑与门禁开关都有官方出处:Playwright 的 --repeat-each、--forbid-only、--fail-on-flaky-tests(Command line)、pytest-repeat 的 --count(pytest-repeat)、Go 的 -count n(go command);随机顺序暴露顺序依赖用 pytest-randomly,需要关掉时官方给出 -p no:randomly(pytest-randomly、How to install and use plugins);Playwright 把”首跑失败、重试通过”单独归类为 flaky,可直接用于统计(Retries)。
边界:20 次是起步阈值不是定理——秒级单测可以跑 100 次,十分钟的 e2e 只在 nightly 跑一轮,再按测试名聚合历史通过率补足样本。存量 flaky 很多的仓库先冻结增量(新测试不许进隔离区)再逐条清,不要停下功能开发做一次性大扫除。
反模式:把 flaky 测试直接删掉。它覆盖的行为随之失去保护,而且没有任何记录说明它曾经存在。
场景:agent 要验证一个 UI 行为,但不知道功能入口在哪、用哪个选择器、什么算通过。
做法:
每个关键功能只写四行,放进对应的 skill 或 AGENTS.md:
## 消息列表
路径:登录 → 侧栏「消息」→ `/inbox`
选择器:`[data-testid="inbox-list"]`,行 `[data-testid="inbox-row"]`
夹具:`make seed SCENARIO=inbox-unread-3`
判定通过:列表出现 3 条未读,控制台无 error,请求命中 `/api/inbox?unread=1`
机械保护:写进 Feature Map 的每个 data-testid 必须被至少一条冒烟测试引用。用一条测试遍历 Feature Map 断言选择器存在,文档一漂移就红:
for (const id of featureMapTestIds()) {
await expect(page.getByTestId(id)).toBeAttached();
}
定位统一走测试专用属性,不用 CSS 类名或界面文案——前者会被重构改掉,后者会被文案改掉,两者都不会让测试红,只会让 agent 的坐标失效。
依据:Playwright 官方称按 test id 定位是”最有韧性的测试方式”,getByTestId() 默认读 data-testid,属性名可用 testIdAttribute 配置(Locators)。
边界:Feature Map 只写”入口 + 选择器 + 夹具 + 判定通过”这四行,写成功能说明书就没人维护了。没有冒烟测试覆盖的条目立刻删——一个失效的坐标比没有坐标更贵,agent 会拿它当事实反复尝试。
反模式:在 skill 里粘贴整页 UI 描述,且没有任何测试引用其中的选择器。半年后它变成一份自信的幻觉地图。
场景:验证基建齐了,团队开始讨论”人只审证据”甚至自动合入。
做法:
PR 模板做成三段,缺失即 CI 失败(用 body 检查脚本或 label 门):
## 复现命令
<!-- 可原样粘贴,验证者不需要额外上下文 -->
## 证据
<!-- 前后对比截图或 trace 链接,CI artifact 链接亦可 -->
## 影响面
<!-- 涉及模块 / 是否带 flag / 回滚方式 -->
依据:GitHub 官方 auto-merge 的语义是”所有必需评审与状态检查通过后自动合并”——合并决策整体委托给检查结果,因此检查的可信度就是合并的可信度(Automatically merging a pull request);而检查一旦不可信,pytest 官方对 flaky 的判断是开发者会转而忽略真实失败(Flaky tests)。
边界:证据是 review 的输入,不是替代。缺了上面四条中的任何一条,”只看证据”等于看 agent 自己拍的照片——它选了拍什么,也选了不拍什么。安全相关改动、数据迁移、权限边界一律逐行读,无论证据多漂亮(见 08 章)。
反模式:先上自动合入,再说”回头把 flaky 清一清”。顺序反了:验证不可信时,自动化只是把错误合并得更快。