name: 根因追溯 description: 当错误在深层次执行中出现时,需要追溯回找到原始触发点 - 通过调用栈系统地反向追踪bug,必要时添加仪器,以识别无效数据或错误行为的来源
根因追溯
概述
bug通常出现在调用栈的深层次(例如,在错误目录中执行git init,文件创建在错误位置,数据库以错误路径打开)。你的本能是修复错误出现的地方,但这只是治标不治本。
核心原则: 通过调用链向后追踪,直到找到原始触发点,然后在源头进行修复。
何时使用
digraph when_to_use {
"Bug appears deep in stack?" [shape=diamond];
"Can trace backwards?" [shape=diamond];
"Fix at symptom point" [shape=box];
"Trace to original trigger" [shape=box];
"BETTER: Also add defense-in-depth" [shape=box];
"Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"];
"Can trace backwards?" -> "Trace to original trigger" [label="yes"];
"Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"];
"Trace to original trigger" -> "BETTER: Also add defense-in-depth";
}
使用时机:
- 错误发生在深层次执行中(非入口点)
- 堆栈跟踪显示长调用链
- 不清楚无效数据来源于何处
- 需要找到哪个测试/代码触发了问题
追踪过程
1. 观察症状
Error: git init failed in /Users/jesse/project/packages/core
2. 找到直接原因
什么代码直接导致了这个?
await execFileAsync('git', ['init'], { cwd: projectDir });
3. 询问:什么调用了这个?
WorktreeManager.createSessionWorktree(projectDir, sessionId)
→ called by Session.initializeWorkspace()
→ called by Session.create()
→ called by test at Project.create()
4. 继续向上追踪
传递了什么值?
projectDir = ''(空字符串!)- 空字符串作为
cwd解析为process.cwd() - 那是源代码目录!
5. 找到原始触发点
空字符串从哪里来?
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'
逐个运行测试,在第一个污染者处停止。查看脚本用法。
真实示例:空projectDir
症状: .git 创建在 packages/core/(源代码)
追踪链:
git init在process.cwd()中运行 ← 空的cwd参数- WorktreeManager 以空projectDir调用
- Session.create() 传递空字符串
- 测试在beforeEach之前访问
context.tempDir - setupCoreTest() 最初返回
{ tempDir: '' }
根因: 顶级变量初始化访问了空值
修复: 使tempDir成为一个getter,如果在beforeEach之前访问则抛出错误
还添加了深度防御:
- 第一层:Project.create() 验证目录
- 第二层:WorkspaceManager 验证非空
- 第三层:NODE_ENV 保护拒绝在tmpdir外部进行git init
- 第四层:在git init之前添加堆栈跟踪日志
关键原则
digraph principle {
"Found immediate cause" [shape=ellipse];
"Can trace one level up?" [shape=diamond];
"Trace backwards" [shape=box];
"Is this the source?" [shape=diamond];
"Fix at source" [shape=box];
"Add validation at each layer" [shape=box];
"Bug impossible" [shape=doublecircle];
"NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
"Found immediate cause" -> "Can trace one level up?";
"Can trace one level up?" -> "Trace backwards" [label="yes"];
"Can trace one level up?" -> "NEVER fix just the symptom" [label="no"];
"Trace backwards" -> "Is this the source?";
"Is this the source?" -> "Trace backwards" [label="no - keeps going"];
"Is this the source?" -> "Fix at source" [label="yes"];
"Fix at source" -> "Add validation at each layer";
"Add validation at each layer" -> "Bug impossible";
}
永远不要仅仅修复错误出现的地方。 追溯回找到原始触发点。
堆栈跟踪提示
在测试中: 使用 console.error() 而不是logger - logger可能被抑制
在操作之前: 在危险操作之前记录,而不是在失败之后
包括上下文: 目录、cwd、环境变量、时间戳
捕获堆栈: new Error().stack 显示完整的调用链
真实世界影响
从调试会话(2025-10-03):
- 通过5级追踪找到根因
- 在源头修复(getter验证)
- 添加了4层防御
- 1847个测试通过,零污染