调试与验证
铁律:在没有失败测试的前提下,不得声明任何事情已修复。
调试的四阶段流程
Superpowers 调试遵循严格的四阶段流程。跳过任何阶段都会增加误诊的风险,并引入新的 bug。
阶段 1:根本原因调查
在触碰任何代码之前,先理解问题所在。
调试会话的第一阶段完全是调查性的。不做任何更改。只是观察和理解。
根本原因调查清单:
□ 完整地阅读错误信息——不要总结,要逐字阅读
□ 识别哪行代码触发了错误
□ 识别该行代码为何被执行(调用栈)
□ 识别哪些数据产生了这种结果(输入)
□ 识别预期的数据应该是什么(预期输出)
□ 确认你理解了差异
根本原因陈述:
在进入阶段 2 之前,你必须能够完整填写这句话:
"这个 bug 是由 [具体事物] 引起的,因为 [证据]。"
如果你无法填写这句话,你还没有完成阶段 1。
阶段 2:模式分析
这是一次性的 bug,还是系统性问题的症状?
在修复特定 bug 之前,检查它是否属于更大的模式:
- 代码库中是否还有其他地方存在相同的模式?
- 这个 bug 是否表明了关于数据流的错误假设——如果是,这个假设在其他地方也成立吗?
- 这是否是一个边缘情况,其他类似函数也可能以同样的方式失败?
模式分析防止你修复一个症状,结果两周后在不同位置又遇到相同的根本 bug。
阶段 3:假设与测试
在实现之前先建立假设,然后测试假设,而不是修复。
阶段 3 是 TDD 的调试等价物:
- 形成假设: "我认为这个 bug 是由 [X] 引起的"
- 写一个捕获该 bug 的失败测试
- 运行该测试并确认它失败(这证明了测试是真实的)
- 现在进入阶段 4
如果你无法为这个 bug 写一个失败测试,就暂停——这是一个信号,要么你还没有完全理解根本原因,要么这个问题处于需要不同方法的类别(集成问题、环境问题等)。
阶段 4:实现
只在前三个阶段完成后才进行修复。
阶段 4 清单:
□ 我有一个失败的测试能够重现这个 bug
□ 我理解根本原因(能够填写"是由...引起的,因为...")
□ 我检查了是否存在类似模式
□ 现在我实现修复
□ 测试通过
□ 所有其他测试仍然通过
□ 代码已提交,并附有描述根本原因的提交信息
3 次失败规则
如果你在三次尝试后仍未能修复一个 bug,停止,重新开始根本原因调查。
三次失败的修复尝试几乎总是意味着以下两种情况之一:
- 你误诊了根本原因。 你在修复症状,而不是病因。
- 问题比你想的更大。 这不是一个局部 bug——这是一个架构问题或错误的假设,已经深入到多个层次。
3 次失败规则是硬性停止信号。不是"再试一次"。停止。重新从阶段 1 开始。
单一修复规则
每次调试会话只做一个更改。
这条规则对经验丰富的开发者来说感觉违反直觉,因为当你能看到多个问题时,同时修复它们似乎更有效率。但它不是:
- 当你同时做了三个更改而测试通过时,你不知道哪个更改解决了问题。
- 当你同时做了三个更改而测试失败时,你不知道哪个更改引入了新问题。
- 当你同时做了三个更改时,你减少了能够确定地说"这个更改是安全的"的更改数量。
一次做一个更改。测试每个更改。如果它有效,提交它,然后继续下一个。
5 步验证关卡
在声明任何 bug 已修复之前,应用 5 步验证关卡:
┌──────────────────────────────────────────────────────────────────┐
│ 验证关卡 │
│ │
│ 第 1 步:IDENTIFY — 列出验证此修复所需的每个测试 │
│ 第 2 步:RUN — 执行测试套件(不要假设——运行它) │
│ 第 3 步:READ — 逐行阅读实际输出 │
│ 第 4 步:VERIFY — 在输出中确认每个测试都通过了 │
│ 第 5 步:CLAIM — 只有在此之后才可以声明 bug 已修复 │
└──────────────────────────────────────────────────────────────────┘
"测试应该能通过"不是完成第 5 步。只有实际的测试输出才能完成第 5 步。
调试红色警报
如果在调试会话中出现以下任何情况,立即停止并重新评估:
| 红色警报 | 含义 |
|---|---|
| 测试通过了,但行为仍然错误 | 测试测试了错误的东西 |
| 修复了 bug,但其他地方出现了新 bug | 根本原因理解不完整 |
| 相同的 bug 在不同位置再次出现 | 修复的是症状,而不是根本原因 |
| 3 次修复尝试均失败 | 停止;重新从阶段 1 开始 |
| AI 声称 bug 已修复但没有显示测试输出 | 不可接受——要求实际输出 |
| 修复使代码更复杂 | 你可能在解决错误的问题 |
| 修复使其他 10 个测试失败 | 修复了症状,破坏了架构 |
完整的调试会话示例
以下是四阶段流程在实践中的样子:
场景: 用户登录以 401 失败
阶段 1:根本原因调查
错误:401 Unauthorized
位置:POST /api/login
栈:authMiddleware → validateToken → jwt.verify
逐字阅读错误信息:
"JsonWebTokenError: invalid signature"
这告诉我什么:
- 令牌存在(否则会是"无令牌"错误)
- 令牌格式正确(否则会是"malformed")
- 签名验证失败
为什么签名无效?
- 用于验证的密钥与用于签名的密钥不匹配
- 令牌在创建后被修改
- 环境变量 JWT_SECRET 在两个服务之间不一致
检查 JWT_SECRET:
- 认证服务:JWT_SECRET = "dev-secret-local"
- API 服务:JWT_SECRET = 未设置(回退到 undefined)
根本原因陈述:
"这个 bug 是由 API 服务中 JWT_SECRET 未设置引起的,因为
jwt.verify 使用了 undefined 作为密钥,而认证服务
使用了 'dev-secret-local'——签名不匹配。"
阶段 2:模式分析
这是否是一个更大的模式?
- 检查所有使用 JWT_SECRET 的服务
- 发现:3 个服务在本地和 CI 环境中都没有设置 JWT_SECRET
- 这不是一次性的——这是一个配置管理问题
阶段 3:假设与测试
// 捕获根本原因的失败测试
test('rejects token when JWT_SECRET is not configured', () => {
delete process.env.JWT_SECRET;
const token = createTokenWithSecret('dev-secret-local');
expect(() => validateToken(token)).toThrow('JWT_SECRET not configured');
});
运行该测试。它失败了(validateToken 没有检查缺失的密钥)。好——测试是真实的。
阶段 4:实现
function validateToken(token: string): TokenPayload {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error('JWT_SECRET not configured');
}
return jwt.verify(token, secret) as TokenPayload;
}
测试通过。所有其他测试仍然通过。提交:
git commit -m "fix: throw when JWT_SECRET not configured in validateToken
Root cause: JWT_SECRET was not set in the API service, causing
jwt.verify to use undefined as the secret key. Token validation
now throws a clear error when JWT_SECRET is absent rather than
producing an invalid signature error."
调试不是猜测。它是一个系统性的调查流程。 每次你跳过根本原因调查而直接进行修复时,你就是在猜测。有时猜测是对的。但猜测不是工程——它是运气。Superpowers 要求工程,而不是运气。