名称: 根因追踪 描述: 系统地追踪bug向后通过调用栈以找到原始触发点 使用时机: 当错误在执行深处发生且需要追踪回找到原始触发点时 版本: 1.1.0 语言: 所有
根因追踪
概述
Bug经常在调用栈深处显现(例如,git init在错误目录,文件创建在错误位置,数据库用错误路径打开)。你的直觉是在错误出现的地方修复,但那只是治标。
核心原则: 通过调用链向后追踪,直到找到原始触发点,然后在源头上修复。
使用时机
digraph when_to_use {
"bug出现在调用栈深处?" [shape=diamond];
"可以向后追踪?" [shape=diamond];
"在症状点修复" [shape=box];
"追踪到原始触发点" [shape=box];
"更好:同时添加纵深防御" [shape=box];
"bug出现在调用栈深处?" -> "可以向后追踪?" [label="是"];
"可以向后追踪?" -> "追踪到原始触发点" [label="是"];
"可以向后追踪?" -> "在症状点修复" [label="否 - 死胡同"];
"追踪到原始触发点" -> "更好:同时添加纵深防御";
}
使用时机:
- 错误发生在执行深处(非入口点)
- 堆栈跟踪显示长调用链
- 不清楚无效数据源自何处
- 需要找到哪个测试/代码触发问题
追踪过程
1. 观察症状
错误:git init在 /Users/jesse/project/packages/core 失败
2. 找到直接原因
什么代码直接导致这个?
await execFileAsync('git', ['init'], { cwd: projectDir });
3. 询问:什么调用了这个?
WorktreeManager.createSessionWorktree(projectDir, sessionId)
→ 由 Session.initializeWorkspace() 调用
→ 由 Session.create() 调用
→ 由测试在 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'
逐个运行测试,在第一个污染者处停止。参见脚本用法。
真实示例:空项目目录
症状: .git 创建在 packages/core/(源代码)中
追踪链:
git init在process.cwd()中运行 ← 空 cwd 参数- WorktreeManager 以空 projectDir 调用
- Session.create() 传递空字符串
- 测试在 beforeEach 之前访问
context.tempDir - setupCoreTest() 最初返回
{ tempDir: '' }
根因: 顶层变量初始化访问空值
修复: 使 tempDir 为 getter,如果在 beforeEach 之前访问则抛出
同时添加纵深防御:
- 层 1: Project.create() 验证目录
- 层 2: WorkspaceManager 验证非空
- 层 3: NODE_ENV 防护拒绝在 tmpdir 外进行 git init
- 层 4: 在 git init 之前堆栈跟踪日志
关键原则
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):
- 通过 5 级追踪找到根因
- 在源头修复(getter 验证)
- 添加 4 层防御
- 1847 个测试通过,零污染