← 全部极客知识库

主流 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

这里证据的强弱顺序很重要:

  1. 固定边界测试直接覆盖已知根因;
  2. 完整 5 项测试证明非目标行为没有回归;
  3. 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 篇不会再引入新项目,而会把全课程常见阻塞整理成按症状查找的最短诊断链。

官方参考:Subagents、Commands、Common workflows。