Bug 通常表现在调用栈深处(在错误目录执行 git init、在错误位置创建文件、用错误路径打开数据库)。你的本能是在错误出现的地方修复,但那只是治标。
核心原则: 沿着调用链反向追踪,直到找到最初的触发点,然后在源头修复。
digraph when_to_use {
"Bug 出现在调用栈深处?" [shape=diamond];
"能反向追踪吗?" [shape=diamond];
"在症状处修复" [shape=box];
"追踪到最初的触发点" [shape=box];
"更好的做法:同时添加纵深防御" [shape=box];
"Bug 出现在调用栈深处?" -> "能反向追踪吗?" [label="是"];
"能反向追踪吗?" -> "追踪到最初的触发点" [label="是"];
"能反向追踪吗?" -> "在症状处修复" [label="否——死胡同"];
"追踪到最初的触发点" -> "更好的做法:同时添加纵深防御";
}
适用场景:
Error: git init failed in /Users/jesse/project/packages/core
哪段代码直接导致了这个错误?
await execFileAsync('git', ['init'], { cwd: projectDir });
WorktreeManager.createSessionWorktree(projectDir, sessionId)
→ 被 Session.initializeWorkspace() 调用
→ 被 Session.create() 调用
→ 被测试中的 Project.create() 调用
传入了什么值?
projectDir = ''(空字符串!)cwd 会解析为 process.cwd()空字符串从哪里来的?
const context = setupCoreTest(); // 返回 { tempDir: '' }
Project.create('name', context.tempDir); // 在 beforeEach 之前就访问了!
当无法手动追踪时,添加诊断埋点:
// 在有问题的操作之前
async function gitInit(directory: string) {
const stack = new Error().stack;
console.error('DEBUG git init:', {
directory,
cwd: process.cwd(),
nodeEnv: process.env.NODE_ENV,
stack,
});
await execFileAsync('git', ['init'], { cwd: directory });
}
重要: 在测试中使用 console.error()(而非 logger——可能不会显示)
运行并捕获:
npm test 2>&1 | grep 'DEBUG git init'
分析堆栈跟踪:
如果某些现象在测试期间出现,但你不知道是哪个测试造成的:
使用本目录下的二分查找脚本 find-polluter.sh:
./find-polluter.sh '.git' 'src/**/*.test.ts'
逐个运行测试,在第一个"污染者"处停止。详见脚本中的使用说明。
症状: .git 被创建在 packages/core/(源代码目录)中
追踪链:
git init 在 process.cwd() 中执行 ← cwd 参数为空context.tempDir{ tempDir: '' }根本原因: 顶层变量初始化时访问了空值
修复: 将 tempDir 改为 getter,在 beforeEach 之前访问时抛出异常
同时添加了纵深防御:
digraph principle {
"找到了直接原因" [shape=ellipse];
"能向上追踪一层吗?" [shape=diamond];
"反向追踪" [shape=box];
"这就是源头吗?" [shape=diamond];
"在源头修复" [shape=box];
"在每一层添加校验" [shape=box];
"Bug 不可能再发生" [shape=doublecircle];
"绝不只修症状" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
"找到了直接原因" -> "能向上追踪一层吗?";
"能向上追踪一层吗?" -> "反向追踪" [label="是"];
"能向上追踪一层吗?" -> "绝不只修症状" [label="否"];
"反向追踪" -> "这就是源头吗?";
"这就是源头吗?" -> "反向追踪" [label="否——继续追踪"];
"这就是源头吗?" -> "在源头修复" [label="是"];
"在源头修复" -> "在每一层添加校验";
"在每一层添加校验" -> "Bug 不可能再发生";
}
绝不只在错误出现的地方修复。 反向追踪,找到最初的触发点。
在测试中: 使用 console.error() 而非 logger——logger 可能被抑制
操作之前: 在危险操作之前记录日志,而不是在失败之后
包含上下文: 目录、cwd、环境变量、时间戳
捕获堆栈: new Error().stack 能显示完整的调用链
来自调试实践(2025-10-03):