跳转到正文

调试与验证

铁律:在没有失败测试的前提下,不得声明任何事情已修复。


调试的四阶段流程

Superpowers 调试遵循严格的四阶段流程。跳过任何阶段都会增加误诊的风险,并引入新的 bug。

阶段 1:根本原因调查

在触碰任何代码之前,先理解问题所在。

调试会话的第一阶段完全是调查性的。不做任何更改。只是观察和理解。

根本原因调查清单:

□ 完整地阅读错误信息——不要总结,要逐字阅读
□ 识别哪行代码触发了错误
□ 识别该行代码为何被执行(调用栈)
□ 识别哪些数据产生了这种结果(输入)
□ 识别预期的数据应该是什么(预期输出)
□ 确认你理解了差异

根本原因陈述:

在进入阶段 2 之前,你必须能够完整填写这句话:

"这个 bug 是由 [具体事物] 引起的,因为 [证据]。"

如果你无法填写这句话,你还没有完成阶段 1。


阶段 2:模式分析

这是一次性的 bug,还是系统性问题的症状?

在修复特定 bug 之前,检查它是否属于更大的模式:

  • 代码库中是否还有其他地方存在相同的模式?
  • 这个 bug 是否表明了关于数据流的错误假设——如果是,这个假设在其他地方也成立吗?
  • 这是否是一个边缘情况,其他类似函数也可能以同样的方式失败?

模式分析防止你修复一个症状,结果两周后在不同位置又遇到相同的根本 bug。


阶段 3:假设与测试

在实现之前先建立假设,然后测试假设,而不是修复。

阶段 3 是 TDD 的调试等价物:

  1. 形成假设: "我认为这个 bug 是由 [X] 引起的"
  2. 写一个捕获该 bug 的失败测试
  3. 运行该测试并确认它失败(这证明了测试是真实的)
  4. 现在进入阶段 4

如果你无法为这个 bug 写一个失败测试,就暂停——这是一个信号,要么你还没有完全理解根本原因,要么这个问题处于需要不同方法的类别(集成问题、环境问题等)。


阶段 4:实现

只在前三个阶段完成后才进行修复。

阶段 4 清单:

□ 我有一个失败的测试能够重现这个 bug
□ 我理解根本原因(能够填写"是由...引起的,因为...")
□ 我检查了是否存在类似模式
□ 现在我实现修复
□ 测试通过
□ 所有其他测试仍然通过
□ 代码已提交,并附有描述根本原因的提交信息

3 次失败规则

如果你在三次尝试后仍未能修复一个 bug,停止,重新开始根本原因调查。

三次失败的修复尝试几乎总是意味着以下两种情况之一:

  1. 你误诊了根本原因。 你在修复症状,而不是病因。
  2. 问题比你想的更大。 这不是一个局部 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 要求工程,而不是运气。