Skip to main content

用途

修 Bug 不是把报错文字藏起来,而是完成一条证据链:固定现象,收集日志,写出修复前会失败的测试,定位根因,做最小修复,跑回归测试,最后审查 diff。 本页只用一个案例贯穿:用户会话过期后刷新页面,POST /api/session/refresh 返回 500。产品约定的正确行为是:有效刷新令牌返回 200;过期或撤销令牌返回 401 和 SESSION_EXPIRED;客户端收到该响应后清理会话并跳转登录页;不改变公开路由、有效响应,也不把令牌写入日志。 本文假定项目使用 Node.js、npm、Vitest 和 Git。实际框架、目录和脚本以仓库为准;先看 package.json,不要为了照抄示例而新增测试框架或创建平行实现。

完成标准

一次合格的修复要能回答:
  1. 谁在什么环境下遇到了什么现象?
  2. 哪些确定步骤可以重新触发?
  3. 日志哪一行提供了调用位置,哪一行只是症状?
  4. 哪个测试在修复前失败,并表达了用户可观察的契约?
  5. 哪个状态或数据假设被违反,才是根因?
  6. 为什么这组改动是最小修复?
  7. 原失败测试、相关测试和工程检查是否通过?
  8. diff 是否只有目标实现、测试和必要文件?
没有证据的结论标记为“未验证”,不要写成“已解决”。

案例上下文

用户报告:登录后放置超过一小时,再打开订单页,Network 面板显示:
脱敏后的服务端日志:
探索后可能找到的相关文件如下:
这些路径只是案例上下文。让 Codex 报告你仓库中的实际路径,不要按示例强行创建文件。

1. 先建立安全检查点

检查目录、分支和工具

在项目根目录执行:
PowerShell 可执行:
预期:路径、分支和工具版本都符合任务环境。若 git status 已有他人或其他任务的改动,记录文件名,不要使用 git reset --hard 或 git restore . 清理它们。 工作区干净且团队流程允许时,可以先打检查点:
不要把别人的未提交改动一起提交。工作区不干净时,用补丁或独立分支保存状态。

先只读调查

启动 Codex:
输入:
预期动作:Codex 读取真实文件并引用符号,报告调用链和命令,不创建文件。若它在调查阶段改了文件,先停止,查看 git status、git diff,让它撤回调查改动或切到只读模式。

2. 把现象写成复现配方

“登录后有时失败”不是复现步骤。复现配方必须包含初始数据、操作顺序、实际输出和期望输出。时间相关问题使用固定时钟或固定过期时间,不要在测试中真的等待一小时。 先查看脚本,不要盲目安装依赖:
在确认仓库脚本后,让 Codex 只运行相关测试或服务。把脱敏事实补充给它:
预期结果是:请求进入 refresh 路由,过期分支可被触发,在 session-service.ts 抛出异常,当前错误处理器把它包装成 500。无法复现时不能直接修,转到“环境与数据问题”。

3. 用日志取证

日志要帮助确认:是否进入正确路由;令牌被判断为有效、过期、撤销还是未知;数据库返回 null、undefined 还是连接异常;错误发生在哪个调用点;错误处理器如何映射;是否泄露秘密。 让 Codex 区分证据和假设:
本案例中,日志证明 refreshSession 发生了 TypeError,但还不能单独证明查询为空的原因。候选原因包括过期令牌、用户被删除、数据库连接故障或 schema 不一致。堆栈行号是坐标,不是根因。 若日志出现下列内容,先遮盖后再交给 Codex:
提示:
检查日志代码也要限定范围:

4. 先写修复前失败的测试

没有失败测试,Codex 可能只让 500 消失。先让它阅读已有测试的 fixture、请求工具、时钟和断言风格:
然后输入:
修复前应看到类似失败:
如果测试直接通过,不要宣布 Bug 已修复。检查:过期时间是否真正生效;mock 是否绕过了生产分支;请求是否命中真实路由;工作区是否已经有未提交修复。用 git diff 和测试日志确认。 测试应表达契约,而不是实现行号:
语法以仓库风格为准。至少保留一条路由或服务边界测试,不能只 mock 一个函数然后断言它抛错。

5. 根据调用链定位根因

让 Codex 对每一步列文件、函数、输入、输出和异常:
案例中的调用链可能是:
用下面的输入要求根因证明:
合理结论是:不可用 session 查询结果被当成成功结果继续使用,访问 session.id 抛出通用 TypeError,统一错误处理器没有识别领域状态,于是返回 500。根因不是“加一个 try/catch”。 再排除相邻原因:
若 A 成立,应处理数据库环境,不能把连接故障伪装成 401;若 B 成立,修复位置应是时间解析,而不是错误处理器。

6. 环境与数据问题

无法复现不等于代码没有问题。先比较以下项目: 启动失败时先运行:
若输出为:
这是依赖或原生模块环境问题,不是 refresh 根因。确认允许后再运行仓库规定的:
不要为绕开缺包而把数据库替换成内存 mock。 数据缺失时输入:
重新创建数据后若仍复现,数据只是条件,不是根因;比较 expiresAt、revokedAt、jti 和时区即可,不要记录真实令牌。

7. 提出并实施最小修复

根因有证据后,先让 Codex 规划边界:
优先复用已有领域错误。若没有,按项目风格定义最小错误类型;在空值边界显式抛出;只给明确的 session 状态映射 401;数据库异常和未知编程错误继续 500;保留有效 token 的原成功路径。 明确拒绝宽泛修法:
它会把数据库宕机和编程错误也伪装成过期会话。也不要返回 200 加空 token,这会让客户端误以为刷新成功。 确认方案后输入:

8. 审查 diff

先看范围:
预期只出现实现、必要映射和回归测试。若出现 lockfile、配置、生成物或其他业务模块,要求:
再逐行问:
  1. 空结果判断是否早于 session.id?
  2. 只有明确 session 状态才返回 401 吗?
  3. 数据库异常仍为 500 吗?
  4. 有效 token 响应是否完全保持?
  5. 是否改变 JSON、路由或客户端依赖?
  6. 是否输出令牌、Cookie 或完整用户对象?
  7. 测试是否真的控制了过期时间?
  8. 是否夹带重命名、格式化或重构?
必要时执行:
Diff 大到无法解释时,先停止测试,拆掉无关改动。最小修复是只改变错误状态分支,不是单纯追求最少行数。

9. 分层验证

原失败测试

重新运行修复前的同一条命令:
预期:
仍失败时,不要连续改代码,输入:

相关测试和工程检查

再运行仓库已有的相关测试:
若脚本不存在,执行 npm run 并报告“未提供”,不要把命令不存在算成代码失败。相关场景至少应覆盖:

手工验收

本地服务可安全启动时:
预期:
有效测试 token 还必须返回原成功响应。手工结果不一致时比较环境变量、时钟、数据库和服务进程,不要直接修改实现迎合一次请求。

10. 失败分支

选错文件

若 Codex 改了请求链路不会经过的旧模块:

只加 try/catch

若数据库断开也变成 401:

测试绿但没复现

检查 mock 是否绕过生产分支、过期时间是否生效、请求是否命中路由、断言是否只检查“有响应”。让测试在修复前重新运行,必须能失败才能证明它锁住了 Bug。

环境失败

端口占用、缺少原生模块、数据库连接失败或 Node 版本不兼容先记录为环境失败:
只释放确认属于当前任务的本地进程,不要杀掉不明服务。环境未恢复时,报告代码验证未完成。

扩大成重构

确实需要重构时另开任务,先计划、分步并分别验证。

11. 最终验收与交付

执行:
逐项确认:
  • 目录、分支和既有用户改动正确保留。
  • 原始现象有可复制步骤。
  • 日志已脱敏,没有令牌和个人数据。
  • 回归测试在修复前确实失败。
  • 根因与调用链、错误类型和失败输出一致。
  • 只改变必要状态分支。
  • 有效 token 行为没有改变。
  • 过期 token 返回 401 SESSION_EXPIRED。
  • 数据库异常没有被吞成 401。
  • 原失败测试、相关测试和可用的 lint/build 已通过。
  • git diff --check 通过,文件范围可解释。
  • 没有提交、推送或访问生产服务。
交付摘要使用证据格式:

可直接使用的 Codex 输入

预期不是“一次生成补丁”,而是依次得到调用链、失败输出、根因证明、最小 diff 和验证结果。

小结

接口不再返回 500,只说明症状改变。只有失败测试变绿、有效路径不回归、异常没有被吞掉、diff 范围可解释,并且剩余环境假设被记录,这次 Bug 修复才算完成。 参考资料:参考/codex/14-workflows.md、参考/codex/06-first-task.md、参考/codex/13-prompting.md。