程序员技能专题:用系统化调试、TDD 和完成前验证修复一个真实 Bug
面向开发人员,介绍系统化调试、测试驱动开发、完成前验证与编码纪律如何配合;用一个隔离的 Node.js Bug 保存真实红灯、根因、最小修复和绿灯证据,并附可复制提示词。
开发人员最容易被 AI 放大的坏习惯,不是不会写代码,而是“看到报错,马上改一行;测试绿了,就宣布完成”。这条路径可能偶尔奏效,却无法说明改动修的是根因,还是恰好躲开了当前样例。
这篇教程介绍一组适合开发人员组合使用的 Skills:superpowers:systematic-debugging、superpowers:test-driven-development、superpowers:verification-before-completion 和 karpathy-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. 验证该假设所需的最小测试。
禁止:提修复、同时猜多个原因、修改文件、把推测写成结论。
根因假设
我认为normalizeAllowlist把string[]当成逗号字符串处理是根因,因为数组输入稳定在.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' 会让旧实现通过,但它删除了数组调用方的真实需求;这不是修复,是把回归用例改没。另一种错误修法是给 .split 套 try/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 和必要的界面检查。
- 记录命令、退出码、失败数和关键结果;未执行项必须写“未执行”。
停止条件:
- 需要权限、真实数据、生产配置、外部依赖安装、大范围重构或第三次失败时,停止并请求确认。
开发人员最常见的失败方式
- 先改代码再找原因。 这会把原始证据毁掉,并让下一次失败更难解释。
- 先实现再补测试。 一上来就通过的测试无法证明它能抓住 Bug。
- 一次改五处。 即使测试变绿,也无法知道哪一处真正解决了问题。
- 局部测试绿了就说项目好了。 单测、lint、类型检查、构建和真实交互证明的是不同问题。
- 借修复做重构。 修复范围扩大时,评审、回退和根因判断都会失控。
完成前检查
- [x] 真实隔离示例先复现了失败,并记录退出码与关键错误。
- [x] 回归测试在修复前实际红灯,修复后实际绿灯。
- [x] 修复只处理输入契约,不涉及网站业务代码或外部依赖。
- [x] 已执行 Node 测试与语法检查,退出码均为 0。
- [ ] 未执行任何真实业务项目的全量测试、构建、lint、类型检查或浏览器回归;不可把本例外推为项目级验证。
- [x] 每项技能都有用途、使用时机、可复制提示词、输出、验证或停止条件。
好的开发技能专题不该教人“更快改代码”,而应让人学会:什么时候该停下、证据够不够、测试有没有真的抓到行为,以及凭什么说修复已经成立。
用 AI Agent 做可持续迭代的网页原型:React、SQLite 与项目规范入门
面向刚开始使用编码 Agent 的产品、设计与项目人员:从一个真实长期维护原型的经验和教训出发,用 React、Express 与 SQLite 做出可运行的需求评审台账,并用事实源文档、项目级技能和验证清单约束后续修改。
让 AI Agent 先读懂代码再动手:CodeGraph 与代码知识图谱实战
从 Token 消耗、调用链和影响范围出发,拆解 CodeGraph 的本地图谱原理,对比 Serena、Graphify 等工具,并以 Windows + Codex 为主线说明跨平台、多 Agent 的安全接入、验证、测量与风险控制流程。
用 AI Agent 从零生成网页原型:静态 HTML、Vue、React 怎么选,才能少返工、少烧 Token
先分清页面组件化与数据边界两条轴线,再比较静态 HTML、Vue、React、Mock、MSW 与 SQLite 的首次和长期 Token 成本,按原型生命周期选择更少返工的架构。