避坑实验室 · run-002 · 样本量 n = 1
同一个隐形字符 bug,三个 AI 都修对了——差在谁的 diff 更干净
如果你用 Windows 记事本保存过 .env,第一行配置前面可能会多一个看不见的字符;程序不报错,但 API_KEY 就是查不到。
我们让 Codex、Claude Code、Gemini CLI 同题修复,外加它们看不到的隐藏验收。
上期看谁修得快,这期看谁收尾干净。
先讲人话:这个 bug 是什么
你在 Windows 记事本里编辑过 .env 配置文件吗?保存时它可能悄悄在文件最前面塞了 3 个字节——
一个叫 BOM 的标记,你可以把它理解成某些编辑器写在文件开头的编码提示。它完全不可见,但程序读配置时,第一个变量名就从
API_KEY 变成了 \ufeffAPI_KEY——前面那个 \ufeff 就是隐形字符的学名。
于是:不报错、不崩溃,但你的程序死活读不到 API_KEY。新手能被这种问题卡一整天。
红色那 3 个字节就是 BOM——肉眼在编辑器里根本看不到它。这是常见 Python 第三方配置库 python-dotenv 的真实 bug(上游 PR #640)。
实验怎么做
同一起点
固定在官方修复之前的 commitfa4e6a90
同一段指令
三家收到逐字相同的任务描述
隐藏验收
它们看不到的 5 项隐藏验收,修完才揭晓
记分牌
下面只记录这一次任务里的交付差异,不代表三家工具综合能力。
Codex
隐藏验收 ✓API 层回归测试
环境里没装测试工具,它自己找到替代方案跑通,最后还把过程中生成的锁文件删干净了。
Claude Code
隐藏验收 ✓解析器层回归测试
只动了该动的两个文件,测试直接加进现有用例表,还写了注释说明为什么。
Gemini CLI
隐藏验收 ✓新建独立测试文件×3用例
修对了,测试覆盖也最广,但 diff 里混进了一个无关的依赖锁文件和多余空格。
用时为操作者实测的本地 CLI 墙钟时间,非标准化算力基准。Gemini 含一次「429 无可用容量」平台重试。
有意思的地方:三家改的是同一行代码
核心修复一模一样——读文件时,开头如果是那个隐形字符,掐掉它:
class Reader:
def __init__(self, stream: IO[str]) -> None:
self.string = stream.read()
+ if self.string.startswith("\ufeff"):
+ self.string = self.string[1:]
self.position = Position.start() 真正拉开差距的,是 diff 里的收尾动作。
Claude Code 把回归测试加进现有用例表、附注释;Codex 在 API 层测、顺手清理了临时文件;
Gemini 单开一个测试文件覆盖 3 种用法——但把一个无关的 uv.lock 也提交了进来。
| 收尾检查 | Codex | Claude Code | Gemini CLI |
|---|---|---|---|
| 核心修复正确 | ✅ | ✅ | ✅ |
| 带回归测试 | ✅ | ✅ | ✅ 覆盖 3 种入口 |
| diff 只含相关改动 | ✅ | ✅ | ⚠️ 混入 uv.lock |
| 无格式噪音 | ✅ | ✅ | ⚠️ 尾随空格 |
| 临时文件清理 | ✅ 主动删掉 | —(没产生) | ❌ 留下了 |
隐藏验收长什么样
修复完成后,我们做了 5 项隐藏验收。先看最容易理解的 3 个 BOM 行为用例:
- 带 BOM 的单变量文件 —— 必须解析出干净的
FIRST,禁止出现带隐形前缀的键 - 带 BOM 的多变量文件 —— 第二行之后的变量不能受影响
- 不带 BOM 的正常文件 —— 防止「修过头」破坏原有行为
另外两项是 Unicode 值保留和 parser 测试套件——防止修复误删正常 Unicode 字符,或破坏现有解析行为。
三家全过、5 项全过。这轮没有翻车故事——但正因为都修对了,「干净程度」才成了真正的分水岭。
这期的结论
代码能跑 ≠ 能直接合进项目。
三个 AI 都能把活干对,但「干不干净」决定了你敢不敢不看一眼就合并。 如果你只记一件事:AI 说"修好了"之后,看一眼它的 diff 里有没有混进无关文件。 发布前多看一眼 diff,通常比事后回滚便宜得多。
证据与复现
- 真实仓库:theskumar/python-dotenv,起点 pin
fa4e6a90b45428212452afc6ee0d5c8103b9301d(官方修复 PR #640 之前) - 三家完整 diff、隐藏验收用例、运行时间线:见上文与 方法论 v1
- 原始终端日志已完整存档,复现或索取请走 GitHub issue
单任务 n=1,本文不构成三家工具的综合排名。每期任务都会换,结论只对本任务负责。上一期:run-001 · 同一个 bug,三个 agent,一个隐藏测试