主流 Agent 入门教程 / Claude Code
17. 实战三排查跨平台偶发测试
原文备份 · 第 19 / 28 篇 · 查看极客时间原知识库 ↗
本篇目录 · 17 节
来源:Claude Code
预计用时:65~85 分钟
本篇目标:从三份脱敏 CI 日志中提出两个互斥假设,用两个只读 Subagent 分别找证据,再由主 Agent 运行固定实验和确定性边界测试裁决;最后只改一个文件,完成回归、重复验证和有边界的事故报告。
“Windows 偶尔失败,macOS 和 Linux 通过”只是现象,不是根因。路径分隔符、大小写、换行、编码、时区都听起来合理;如果看到平台名就直接改路径,往往只是把猜测写进代码。
本篇使用一个个人开源小工具 TrailSnap。它把当天的徒步笔记导出成 JSON,文件名日期和 JSON 中的 export_date 都应使用用户本地日历日。素材包含真实可运行代码、5 项测试、三份脱敏平台日志、两个项目 Agent 和固定实验,不需要读者自己编造 CI 输出。
开始状态
本练习独立于 MealCraft、博客和旅行笔记仓库。需要:
- Claude Code、Git 和 Python 3.11+ 可用;
~/claude-code-course/flaky-test-lab尚不存在;- 知道修订版教程目录的实际位置;
- 接受调用两个 Subagent 会产生额外模型请求并计入用量。
练习不会连接真实 CI,不会声称已经在三种真实操作系统上运行修复版。Windows、macOS 和 Linux 日志是固定的脱敏历史证据;本机验证通过显式时区输入模拟关键环境差异。
一、创建固定故障仓库
macOS、Linux 或 WSL2:
export COURSE_SOURCE="/替换为实际路径/claude-code-tutorial-v0.2"
export FLAKY_SOURCE="$COURSE_SOURCE/exercise-assets/flaky-test-lab"
test -f "$FLAKY_SOURCE/scripts/verify_flaky_lab.py"
cd ~/claude-code-course
test ! -e flaky-test-lab
mkdir flaky-test-lab
cp -R "$FLAKY_SOURCE/." flaky-test-lab/
cd flaky-test-lab
git init
Windows PowerShell:
$CourseSource = "C:\替换为实际路径\claude-code-tutorial-v0.2"
$FlakySource = "$CourseSource\exercise-assets\flaky-test-lab"
if (-not (Test-Path "$FlakySource\scripts\verify_flaky_lab.py")) { throw "偶发测试素材路径错误" }
Set-Location "$HOME\claude-code-course"
if (Test-Path flaky-test-lab) { throw "flaky-test-lab 已存在,请先检查旧目录" }
New-Item -ItemType Directory flaky-test-lab | Out-Null
Copy-Item "$FlakySource\*" -Destination flaky-test-lab -Recurse -Force
Copy-Item "$FlakySource\.claude" -Destination flaky-test-lab -Recurse -Force
Copy-Item "$FlakySource\.gitignore" -Destination flaky-test-lab -Force
Copy-Item "$FlakySource\.gitattributes" -Destination flaky-test-lab -Force
Set-Location flaky-test-lab
git init
先验证固定证据与项目 Agent:
macOS、Linux 或 WSL2:
python3 -B scripts/verify_flaky_lab.py --expect-bug
claude plugin validate .claude/agents --strict
Windows PowerShell:
py -B scripts\verify_flaky_lab.py --expect-bug
claude plugin validate .claude\agents --strict
预期:
PASS fixed evidence: logs, tests, agents, and experiments
PASS bug state: 5 tests, 1 deterministic boundary failure
✔ Validation passed
检查器会锁住日志、测试、事故简报、Agent 和实验脚本的 SHA-256,并真实运行 5 项测试。只有 trailsnap/exporter.py 是后续允许修改的业务文件。
提交缺陷基线:
git add .
git diff --cached --check
git commit -m "create deterministic TrailSnap boundary failure lab"
git status --short
预期工作区为空。项目结构如下:
flaky-test-lab/
├── .claude/agents/
│ ├── path-hypothesis.md
│ └── time-hypothesis.md
├── logs/
│ ├── macos-pass.txt
│ ├── linux-pass.txt
│ └── windows-fail.txt
├── trailsnap/exporter.py # 唯一允许修复的文件
├── tests/
│ ├── test_exporter.py
│ └── test_boundary.py
├── scripts/
│ ├── evaluate_hypotheses.py
│ ├── repeat_boundary_test.py
│ └── verify_*.py
├── baseline/exporter.py
└── INCIDENT_BRIEF.md
二、先读三份日志,不把平台相关性当因果
打开或让 Claude 读取 logs/ 后,应能提取:
| 样本 | 本地时间 | 同一时刻的 UTC | 结果 |
|---|---|---|---|
| macOS | 2026-03-15 12:00 +00:00 | 2026-03-15 12:00 | 通过 |
| Linux | 2026-03-15 09:00 +01:00 | 2026-03-15 08:00 | 通过 |
| Windows | 2026-03-14 23:30 -07:00 | 2026-03-15 06:30 | 失败 |
Windows 日志已经成功创建:
C:\Temp\trailsnap\export\trail-night-walk-2026-03-14.json
失败断言比较的是文件名中的 2026-03-14 与 JSON 的 export_date=2026-03-15。这给出两个可被证伪的候选:
- 路径假设:路径拼接或分隔符让 Windows 生成了错误目标;
- 时间假设:文件名使用本地日期,JSON 却使用同一时刻的 UTC 日期,跨日界线时两者不同。
它们不能同时是这个断言的直接根因。用两个窄 Agent 分开调查,能减少“一个 Agent 一开始偏向某个答案,随后只找支持材料”的风险。
三、确认确定性失败,而不是反复等偶发
先在普通终端运行完整测试:
python3 -B -m unittest discover -s tests -v
Windows 使用 py。稳定结果是 5 项中恰好一个失败:
FAIL: test_local_day_remains_consistent_across_utc_boundary
AssertionError: '2026-03-15' != '2026-03-14'
Ran 5 tests
FAILED (failures=1)
测试直接构造有时区的 2026-03-14T23:30:00-07:00,不依赖当前电脑的操作系统、真实时钟或等待午夜。它把“偶发时间窗口”变成每次都相同的输入。
此时不需要再跑 100 次随机 CI 来证明问题存在:一个与日志完全对应的确定性失败,信息量更高。
四、显式调用两个只读 Subagent
项目 Agent 已在启动前写入 .claude/agents/。从仓库根目录启动新会话:
claude
先发送路径假设:
@agent-path-hypothesis 调查固定 TrailSnap case 的路径假设。严格遵守 Agent 的范围和 JSON 输出契约;不要修改文件。
紧接着发送时间假设:
@agent-time-hypothesis 调查固定 TrailSnap case 的本地日期与 UTC 日期假设。严格遵守 Agent 的范围和 JSON 输出契约;不要修改文件。
两个 Agent 的 background: true,可以重叠运行。输入 /tasks 查看它们是否真的启动、使用了什么模型以及是否完成。展开工具记录时,只应看到 Read、Grep 或 Glob;它们没有 Bash、PowerShell、Write、Edit、MCP、Skill 或 Agent 工具。
每个 Agent 都是一次独立模型调用。并行不会自动减少总用量,也不保证更快;这里使用两个,是因为两个假设边界清楚且互斥,不是为了展示“越多 Agent 越好”。
五、保存并校验 Agent 报告
两个 Agent 应分别返回一个无 Markdown 围栏的 JSON 对象。让主会话保存,注意写入动作来自主 Agent:
不要再委派。把 path-hypothesis 返回的原始 JSON 保存为 reports/path.json,把 time-hypothesis 返回的原始 JSON 保存为 reports/time.json。不要修改其他文件,保存后只报告两个路径。
退出 Claude Code,在普通终端验证:
macOS、Linux 或 WSL2:
python3 -B scripts/verify_hypothesis_reports.py \
--path-report reports/path.json \
--time-report reports/time.json
Windows PowerShell:
py -B scripts\verify_hypothesis_reports.py `
--path-report reports\path.json `
--time-report reports\time.json
预期:
PASS hypothesis reports: path=REFUTED time=SUPPORTED evidence=4
校验器要求路径报告给出 PATH-01、PATH-02 两条证据并判定 REFUTED;时间报告给出 TIME-01、TIME-02 并判定 SUPPORTED。两个报告都必须声明没有改文件,并给出下一步固定实验命令。
如果 Agent 输出不符合 JSON 契约,不要手工改出一个“正确答案”。在同一主会话要求对应 Agent 基于已有证据重返合法 JSON,再保存并重验。
六、主 Agent 必须亲自复核,而不是投票
Subagent 的结论只是待验证材料。由主会话或你自己运行两个实验:
macOS、Linux 或 WSL2:
python3 -B scripts/evaluate_hypotheses.py --hypothesis path
python3 -B scripts/evaluate_hypotheses.py --hypothesis time
Windows PowerShell:
py -B scripts\evaluate_hypotheses.py --hypothesis path
py -B scripts\evaluate_hypotheses.py --hypothesis time
稳定输出:
PATH result=REFUTED windows_name=trail-night-walk-2026-03-14.json posix_name=trail-night-walk-2026-03-14.json pathlib_join=true
TIME result=SUPPORTED ... local_day=2026-03-14 utc_day=2026-03-15
再对照代码:pathlib 已正确构造目标,Windows 日志也证明文件创建成功;差异发生在 JSON 字段。exporter.py 一边计算 local_now.date(),另一边把同一时刻转换到 UTC 再取日期。跨 UTC 日界线时,两者自然可能相差一天。
因此准确结论是:根因是混用了本地日历日与 UTC 日历日,Windows 只是这次日志中恰好处于负时区且接近午夜的运行环境。 同样的输入在任何操作系统上都会失败。
七、只修复日期语义,不特殊判断 Windows
回到 Claude Code 主会话,先让它给出两三行计划:负责编码的函数、需要统一的语义、不可修改范围和验证命令。确认后发送:
实施最小修复,只修改 trailsnap/exporter.py。
要求:
- 文件名与 JSON export_date 都使用传入 aware local_now 的本地日历日;
- 不增加 Windows/macOS/Linux 分支;
- 不修改日志、测试、Agent、baseline、检查器或事故简报;
- 不增加重试或读取真实系统时钟;
- 删除修复后不再需要的 import。
修改后先运行边界测试,再运行完整 5 项测试;不要提交 Git。
权限提示必须只涉及 trailsnap/exporter.py。一个合格的最小修复会让 export_date 复用已经算出的 local_day,并移除不再需要的 timezone import;它不需要识别操作系统。
八、独立验收与重复验证
退出 Claude Code,先检查范围:
git diff --name-only
git diff --check
git diff -- trailsnap/exporter.py
git diff --name-only 必须只列出:
trailsnap/exporter.py
运行固定验收:
macOS、Linux 或 WSL2:
python3 -B scripts/verify_flaky_lab.py --expect-fixed
python3 -B scripts/repeat_boundary_test.py --runs 20
Windows PowerShell:
py -B scripts\verify_flaky_lab.py --expect-fixed
py -B scripts\repeat_boundary_test.py --runs 20
预期:
PASS fixed evidence: logs, tests, agents, and experiments
PASS fixed state: 5 tests, local-day contract holds
PASS repeated boundary test runs=20
这里证据的强弱顺序很重要:
- 固定边界测试直接覆盖已知根因;
- 完整 5 项测试证明非目标行为没有回归;
- 20 次重复运行补充检查状态泄漏或残留不稳定性。
20 次通过不能数学上证明“永不失败”,也不能替代三平台真实 CI。反过来,如果只重复原来大多数时间都会通过的普通测试,即使跑 1000 次,也可能一次都没经过问题时间窗。
提交修复:
git add trailsnap/exporter.py
git commit -m "use local calendar day for TrailSnap exports"
git status --short
reports/ 被忽略,所以 Agent 中间结果不会污染提交;固定日志仍保留为事故证据。
九、生成不夸大的事故报告
让 Claude 只创建 INCIDENT_REPORT.md:
只创建 INCIDENT_REPORT.md,不修改其他文件。报告必须包含:
## 现象
描述三份脱敏日志和 Windows 样本的失败,不把平台相关性写成因果。
## 日志差异
逐字包含:
- path hypothesis: REFUTED
- time hypothesis: SUPPORTED
## 根因
逐字包含 local calendar day、UTC calendar day 和 not Windows itself,并用代码与日志证据解释。
## 稳定复现
包含 test_local_day_remains_consistent_across_utc_boundary 与确定输入。
## 修复
说明只修改 trailsnap/exporter.py,统一使用本地日历日。
## 验证
逐字包含 5 tests 和 20 repeated runs,并说明重复通过不是数学证明。
## 未验证
明确本课没有触发真实 macOS、Linux 或 Windows CI,不能写“已在三平台验证”。
退出后检查:
macOS、Linux 或 WSL2:
python3 -B scripts/verify_incident_report.py --report INCIDENT_REPORT.md
git diff --check
git add INCIDENT_REPORT.md
git commit -m "document TrailSnap boundary incident"
git status --short
Windows 把 python3 换成 py。预期报告检查通过,最终工作区为空。
十、关键排错
Agent 没有被发现
确认会话启动前文件已经位于 .claude/agents/,并运行:
claude plugin validate .claude/agents --strict
若会话在复制素材前已经启动,退出后从项目根目录新开会话。当前 /agents 不再是创建向导;不要依赖旧教程里的交互界面。
Agent 报告支持了两个假设
回到原始断言:Windows 输出路径已经生成,文件名日期正确,失败值在 JSON 中。让路径 Agent 明确区分“平台差异存在”和“它直接解释了这个断言”。最终裁决仍以两个固定实验和代码数据流为准,不按 Agent 票数决定。
基线不是恰好一个失败
运行:
git status --short
python3 -B scripts/reset_flaky_lab.py
python3 -B scripts/verify_flaky_lab.py --expect-bug
Windows 使用 py。重置脚本只覆盖练习中的 trailsnap/exporter.py。若固定哈希失败,日志、测试或 Agent 被改过,应查看 Diff 并重新复制素材,而不是修改检查器。
修复后边界测试仍失败
检查是否仍对 local_now 调用了 astimezone(timezone.utc) 后再取日期。需求是用户本地日历日,不是统一成 UTC。不要通过改期望值为 15 日让测试“通过”。
本地全绿,但真实 CI 仍偶发
保留新测试与修复,收集新的脱敏失败日志,比较是否仍是同一断言、时间点和代码版本。新日志可能揭示第二个独立问题;不能把一次已修根因扩展成“所有跨平台问题都解决”。
文章级验收
- 固定证据检查与两个 Agent strict validation 通过;
- 缺陷态固定为 5 项测试、1 个指定边界失败;
- 能从日志区分平台相关性与根因证据;
- 路径与时间两个只读 Agent 均被真实调用,并在
/tasks可见; - 两份 JSON 报告通过结构和证据 ID 校验;
- 主 Agent 亲自运行 PATH refuted、TIME supported 两个实验;
- 最终只修改
trailsnap/exporter.py,没有平台特判或重试; - 修复态 5 项全绿,确定边界测试重复 20 次通过;
- 能解释重复通过为什么不是数学证明;
- 报告明确没有真实三平台验证,最终 Git 工作区为空。
本篇完成了三个迁移实战中的最后一个:固定输入、先证伪假设、把偶发窗口变成确定测试、再做最小修复。第 18 篇不会再引入新项目,而会把全课程常见阻塞整理成按症状查找的最短诊断链。