程序员技能专题:用系统化调试、TDD 和完成前验证修复一个真实 Bug

面向开发人员,介绍系统化调试、测试驱动开发、完成前验证与编码纪律如何配合;用一个隔离的 Node.js Bug 保存真实红灯、根因、最小修复和绿灯证据,并附可复制提示词。

TDD 代码知识图谱 测试 程序员 调试
浏览 316
程序员技能专题:用系统化调试、TDD 和完成前验证修复一个真实 Bug封面

开发人员最容易被 AI 放大的坏习惯,不是不会写代码,而是“看到报错,马上改一行;测试绿了,就宣布完成”。这条路径可能偶尔奏效,却无法说明改动修的是根因,还是恰好躲开了当前样例。

这篇教程介绍一组适合开发人员组合使用的 Skills:superpowers:systematic-debuggingsuperpowers:test-driven-developmentsuperpowers:verification-before-completionkarpathy-coding-discipline。它们不是四种写代码的花样,而是一条顺序严格的工程工作流:先取证,再写红灯测试,然后做最小修复,最后用新鲜证据决定能否说“完成”。

为避免碰到网站业务代码,本篇使用一个隔离的 Node.js 示例。它模拟安全配置里的常见输入契约 Bug:函数应同时接收逗号字符串和字符串数组,却错误地把所有输入都当成字符串。

最终成果包括:

  • 一次真实可复现的失败输出;
  • 根因假设与反例;
  • 先红后绿的测试证据;
  • 最小修复后的测试、语法检查、退出码;
  • 可以复制给其他编码 Agent 的安全任务块。

四项常用开发技能:职责不能互相替代

技能解决什么问题何时使用可靠输出不该做什么
superpowers:systematic-debugging从症状追到根因,避免猜测式修复遇到 Bug、测试失败、构建异常或行为不符合预期时复现步骤、证据、单一根因假设未完成调查就给出“可能修复”
superpowers:test-driven-development先定义应有行为并确认它失败,再写最小代码修 Bug、加功能、改行为前真正会失败的测试、最小实现、绿灯先写实现再补一个一直通过的测试
superpowers:verification-before-completion要求用当前运行结果支撑“已修复 / 已通过”准备说完成、关闭任务、提交代码前命令、退出码、完整关键结果用“应该可以”“看起来没问题”代替证据
karpathy-coding-discipline控制改动范围,避免修一处顺手重构十处所有代码修改、排障、重构和代码审查最小差异、明确假设、分层验证与剩余风险擅自加框架、抽象、依赖或无关清理

技能获取与安装

技能点击获取安装/使用说明
superpowers:systematic-debugging下载 Superpowers · 技能源码在 Codex 的 Plugins 中搜索并安装 Superpowers;遇到 Bug 时先用它调查。
superpowers:test-driven-development下载 Superpowers · 技能源码同一 Superpowers 插件内提供;在写生产代码前使用。
superpowers:verification-before-completion下载 Superpowers · 技能源码同一 Superpowers 插件内提供;准备声明完成前使用。
karpathy-coding-discipline下载 Karpathy 技能包 · 源码当前会话的 Codex 版为项目适配封装;链接指向其公开上游来源,安装或覆盖项目规则前必须确认。
点击链接可查看并获取公开技能。不要把“能下载”理解成“可直接覆盖项目 AGENTS.md、全局规则或现有技能”;任何安装、覆盖、执行第三方安装脚本和大依赖变更都应先确认。

顺序不能颠倒:调试先于修复;红灯测试先于生产代码;验证先于完成声明。编码纪律贯穿全程,防止“把一个 Bug 修成一轮重构”。

真实隔离示例:allowlist 规范化为什么崩溃

示例文件位于本篇草稿的 examples/ 目录,不引用网站业务代码、数据库、网络或账号:

examples/allowlist-normalizer.mjs
examples/allowlist-normalizer.test.mjs

业务约定很小:normalizeAllowlist 可以接受下面两种输入,并返回清理过的小写域名数组。

normalizeAllowlist('Docs.Example.com, api.example.com')
normalizeAllowlist(['Docs.Example.com ', 'api.example.com'])

初始实现却直接调用 input.split(',')。这意味着字符串能工作,数组会崩溃。它不是“字符串处理不够健壮”的模糊问题,而是函数声明支持的输入契约与实现假设不一致

第一步:用 Systematic Debugging 收集证据

systematic-debugging 的铁律是:没有根因调查,就不要提修复。先读错误,稳定复现,比较输入路径,再写一个单一假设。

本轮实际复现

examples/ 目录中,先写期望行为的测试,再运行:

node --test allowlist-normalizer.test.mjs

实际结果:退出码 1,Node 内置测试报告 1 个失败;关键错误如下:

TypeError: input.split is not a function
at normalizeAllowlist (.../allowlist-normalizer.mjs:3:6)

复现输入为 ['Docs.Example.com ', 'api.example.com']。错误指向 split,而数组没有这个方法;这已经把调查范围收敛到输入类型处理,而不是域名清理、大小写、测试框架或网络配置。

可复制提示词

请使用 superpowers:systematic-debugging 调查以下 Bug。此阶段禁止修改生产代码。

症状:[粘贴完整错误、堆栈、退出码]
稳定复现:[命令、输入、预期、实际]
相关文件:[列出函数、调用点、测试文件]
已知约束:[输入契约、兼容性、安全边界]

请按四阶段输出:
1. 已确认事实与仍未知事实;
2. 可复现步骤和最小证据;
3. 与正常路径的差异;
4. 一个单一根因假设,格式为“我认为 X 是根因,因为 Y”;
5. 验证该假设所需的最小测试。

禁止:提修复、同时猜多个原因、修改文件、把推测写成结论。

根因假设

我认为 normalizeAllowliststring[] 当成逗号字符串处理是根因,因为数组输入稳定在 .split 抛出 TypeError,而函数应接受两种输入形式。

这是一条可反驳的假设:如果数组并未进入此函数,或输入契约只允许字符串,它就不成立。程序员的任务是先证明或推翻它,不是先写 try/catch 把错误吞掉。

第二步:用 TDD 写红灯,证明测试真的在抓 Bug

test-driven-development 要求测试先写、先失败,而且必须因为目标行为尚未被实现而失败。测试先绿不代表成功,往往说明你只测到了现有行为,或测试写错了。

本例的测试如下:

import assert from 'node:assert/strict';
import test from 'node:test';
import { normalizeAllowlist } from './allowlist-normalizer.mjs';

test('normalizes an array of allowed hostnames', () => {
  assert.deepEqual(
    normalizeAllowlist(['Docs.Example.com ', 'api.example.com']),
    ['docs.example.com', 'api.example.com']
  );
});

它只验证一个行为:数组输入是否被规范化。实际运行的红灯正是上节的 TypeError,因此确认测试不是因为拼写、导入或框架配置而失败。

可复制提示词

请使用 superpowers:test-driven-development 为以下 Bug 写最小回归测试。

已确认根因假设:[粘贴假设]
现有行为:[粘贴当前失败输出]
期望行为:[用一条可断言的句子描述]
测试框架与命令:[粘贴项目真实测试方式]

先只输出测试代码和运行命令;不要给实现代码。
测试必须:只覆盖一个行为、使用真实代码、清楚说明预期。
随后请让我先运行并确认红灯;只有我提供失败证据后,才能提出最小实现。
禁止:修改断言让它通过、先写生产代码、用宽泛 mock 掩盖真实输入。

错误修法为什么不成立

把测试输入改回 'Docs.Example.com, api.example.com' 会让旧实现通过,但它删除了数组调用方的真实需求;这不是修复,是把回归用例改没。另一种错误修法是给 .splittry/catch 并返回空数组,它掩盖配置错误,可能把本应允许的域名全部丢掉。

两者都没有解决“函数承诺与输入类型不一致”的根因,因此不应合入。

第三步:用 Karpathy Coding Discipline 做最小修复

karpathy-coding-discipline 提醒我们:每一行改动都应能追溯到请求或修复本身。这里不需要新配置层、泛型框架、依赖或全局重命名,只需要根据输入类型选择正确的分支。

export function normalizeAllowlist(input) {
  const entries = Array.isArray(input) ? input : input.split(',');

  return entries
    .map((entry) => entry.trim().toLowerCase())
    .filter(Boolean);
}

这个修改保留原来的字符串行为,并为数组输入补上正确入口。它没有修改调用方、没有改变返回类型、没有引入依赖,也没有“顺手”重构域名校验逻辑。

可复制提示词

请遵循 karpathy-coding-discipline,对已确认根因做最小修复。

根因:[粘贴单一根因]
红灯测试:[粘贴测试文件和失败输出]
允许改动:[精确文件/函数]
禁止改动:[数据结构、调用方、依赖、无关格式化、重构范围]
成功条件:[测试通过、指定验证通过]

先说明计划修改的行和原因;然后只实现通过该测试所需的最小代码。
完成后列出:修改了什么、刻意没有改什么、仍存在什么风险。
不要声明“已修复”,直到我提供新鲜验证结果。

第四步:用 Verification Before Completion 跑绿灯,再说完成

verification-before-completion 要求先找出什么命令能证明你的结论,再运行它、阅读完整输出和退出码。测试绿灯不能代替构建,语法检查不能代替回归测试;根据实际风险选择最窄但足够的组合。

本轮实际绿灯验证

修复后实际运行:

node --test allowlist-normalizer.test.mjs
node --check allowlist-normalizer.mjs

实际结果:

# tests 1
# pass 1
# fail 0
TEST_EXIT=0
CHECK_EXIT=0

这证明了两件事:数组输入的回归测试现在通过,修复后的模块也通过 Node 的语法检查。它证明一个真实业务系统全部通过,也不替代该系统自身的全量测试、构建、lint、类型检查或浏览器回归。

可复制提示词

请在准备宣称“修复完成”前使用 superpowers:verification-before-completion。

变更:[列出改动文件与行为]
原始 Bug:[粘贴复现命令和失败症状]
最小回归测试:[命令]
项目完整验证:[真实的 test / lint / typecheck / build / 浏览器检查命令]

请按顺序:
1. 说明每个命令要证明什么;
2. 实际运行命令;
3. 记录退出码、失败数和关键输出;
4. 区分“已验证”和“未执行”;
5. 只有所有必要检查有新鲜证据时,才能写完成结论。

禁止:引用旧日志、把局部测试说成全量通过、用“应该没问题”代替结果。

交给其他编码 Agent 的安全任务块

任务:修复 [模块/函数] 的 [Bug 名称],使用系统化调试、TDD 和完成前验证。

复现:
- 命令:[真实命令]
- 输入:[最小输入]
- 预期:[可断言结果]
- 实际:[完整关键错误与退出码]

调查边界:
- 先阅读错误、调用路径、输入契约和相关测试;本阶段禁止改代码。
- 输出已确认事实、未知事实、一个根因假设和最小验证测试。

实现边界:
- 先写并运行会失败的回归测试;确认红灯原因正确后才改代码。
- 只修改 [允许文件/函数];不改调用方、数据模型、依赖、配置或无关代码。
- 若两次假设都失败,回到证据调查;若已有三次修复失败,停止并讨论架构。

验证:
- 运行原始回归测试、受影响模块的完整测试、项目规定的 lint/typecheck/build 和必要的界面检查。
- 记录命令、退出码、失败数和关键结果;未执行项必须写“未执行”。

停止条件:
- 需要权限、真实数据、生产配置、外部依赖安装、大范围重构或第三次失败时,停止并请求确认。

开发人员最常见的失败方式

  1. 先改代码再找原因。 这会把原始证据毁掉,并让下一次失败更难解释。
  2. 先实现再补测试。 一上来就通过的测试无法证明它能抓住 Bug。
  3. 一次改五处。 即使测试变绿,也无法知道哪一处真正解决了问题。
  4. 局部测试绿了就说项目好了。 单测、lint、类型检查、构建和真实交互证明的是不同问题。
  5. 借修复做重构。 修复范围扩大时,评审、回退和根因判断都会失控。

完成前检查

  • [x] 真实隔离示例先复现了失败,并记录退出码与关键错误。
  • [x] 回归测试在修复前实际红灯,修复后实际绿灯。
  • [x] 修复只处理输入契约,不涉及网站业务代码或外部依赖。
  • [x] 已执行 Node 测试与语法检查,退出码均为 0。
  • [ ] 未执行任何真实业务项目的全量测试、构建、lint、类型检查或浏览器回归;不可把本例外推为项目级验证。
  • [x] 每项技能都有用途、使用时机、可复制提示词、输出、验证或停止条件。

好的开发技能专题不该教人“更快改代码”,而应让人学会:什么时候该停下、证据够不够、测试有没有真的抓到行为,以及凭什么说修复已经成立。

316