> ## Documentation Index
> Fetch the complete documentation index at: https://aicoding.cscitech.top/llms.txt
> Use this file to discover all available pages before exploring further.

# 03-修复Bug

> 用一个可复现的登录会话 Bug，练习日志取证、失败测试、根因定位、最小修复、回归验证与 diff 审查。

## 用途

修 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 面板显示：

```text theme={null}
POST /api/session/refresh 500 Internal Server Error
```

脱敏后的服务端日志：

```text theme={null}
[session] refresh requested userId=42
TypeError: Cannot read properties of undefined (reading 'id')
    at refreshSession (src/auth/session-service.ts:58:31)
    at async refresh (src/routes/session.ts:27:20)
```

探索后可能找到的相关文件如下：

```text theme={null}
src/auth/session-service.ts
auth/routes/session.ts
src/middleware/error-handler.ts
src/client/session-client.ts
tests/auth/session-service.test.ts
tests/routes/session.test.ts
package.json
.env.example
```

这些路径只是案例上下文。让 Codex 报告你仓库中的实际路径，不要按示例强行创建文件。

## 1. 先建立安全检查点

### 检查目录、分支和工具

在项目根目录执行：

```bash theme={null}
pwd
git status --short --branch
node --version
npm --version
codex --version
```

PowerShell 可执行：

```powershell theme={null}
Get-Location
git status --short --branch
node --version
npm --version
codex --version
```

预期：路径、分支和工具版本都符合任务环境。若 `git status` 已有他人或其他任务的改动，记录文件名，不要使用 `git reset --hard` 或 `git restore .` 清理它们。

工作区干净且团队流程允许时，可以先打检查点：

```bash theme={null}
git log -1 --oneline
git add -A
git commit -m "chore: checkpoint before session refresh fix"
```

不要把别人的未提交改动一起提交。工作区不干净时，用补丁或独立分支保存状态。

### 先只读调查

启动 Codex：

```bash theme={null}
codex
```

输入：

```text theme={null}
这是一个登录会话刷新返回 500 的 Bug 调查。先只读，不要修改文件、安装依赖、提交或访问生产服务。

请读取 package.json、README、与 session 相关的路由、服务、错误处理和 tests。报告：
1. 实际的启动、测试、lint 命令；
2. refresh 的路由、服务调用链和错误处理链；
3. 过期刷新令牌按现有契约应返回什么；
4. 现有测试覆盖哪些分支；
5. 复现还缺哪些环境或数据前提。

只报告发现，不要猜测，不要编辑。
```

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

## 2. 把现象写成复现配方

“登录后有时失败”不是复现步骤。复现配方必须包含初始数据、操作顺序、实际输出和期望输出。时间相关问题使用固定时钟或固定过期时间，不要在测试中真的等待一小时。

先查看脚本，不要盲目安装依赖：

```bash theme={null}
node -e "const p=require('./package.json'); console.log(p.scripts)"
```

在确认仓库脚本后，让 Codex 只运行相关测试或服务。把脱敏事实补充给它：

```text theme={null}
调查结果与复现条件如下，请先不要改代码。

环境：Node 22.14.0，npm 10.9.2，测试数据库为本地 SQLite。
复现：
1. npm run dev 启动服务；
2. 使用测试用户 alice@example.test 登录；
3. 使用项目已有工具生成 exp=2026-09-01T10:00:00Z 的刷新令牌；
4. 在 2026-09-01T10:05:00Z 调用 POST /api/session/refresh；
5. 观察响应和脱敏后的服务端日志。

实际：HTTP 500，{"error":"internal_error"}；日志见上文。
期望：HTTP 401，{"error":"SESSION_EXPIRED"}，且不输出令牌原文。

请只复现并报告命令、退出码、完整相关输出和未确认的前提。
```

预期结果是：请求进入 refresh 路由，过期分支可被触发，在 `session-service.ts` 抛出异常，当前错误处理器把它包装成 500。无法复现时不能直接修，转到“环境与数据问题”。

## 3. 用日志取证

日志要帮助确认：是否进入正确路由；令牌被判断为有效、过期、撤销还是未知；数据库返回 `null`、`undefined` 还是连接异常；错误发生在哪个调用点；错误处理器如何映射；是否泄露秘密。

让 Codex 区分证据和假设：

```text theme={null}
请把刚才的复现日志整理成“证据 / 说明 / 尚未证明”三列表格。
区分日志已证明的事实、代码推断的可能性、必须用测试或命令确认的假设。
不要修改日志实现。
```

本案例中，日志证明 `refreshSession` 发生了 `TypeError`，但还不能单独证明查询为空的原因。候选原因包括过期令牌、用户被删除、数据库连接故障或 schema 不一致。堆栈行号是坐标，不是根因。

若日志出现下列内容，先遮盖后再交给 Codex：

```text theme={null}
refreshToken=eyJhbGciOiJIUzI1NiIs...
Cookie: session=真实值
email=真实客户地址
```

提示：

```text theme={null}
我已将令牌和个人信息替换为 <REDACTED>。不要读取或打印 .env、生产数据库、完整 Cookie 或令牌原文。
```

检查日志代码也要限定范围：

```text theme={null}
只读检查 session 相关 logger 调用，确认没有记录 refresh token、access token、Cookie 或完整用户对象。
列出日志改进建议，但本次只修错误映射，不重构日志系统。
```

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

没有失败测试，Codex 可能只让 500 消失。先让它阅读已有测试的 fixture、请求工具、时钟和断言风格：

```text theme={null}
只读查看与 session 相关的测试。说明测试框架、请求工具、时间控制、数据库工厂和断言约定。
不要创建测试，不要修改生产代码。
```

然后输入：

```text theme={null}
现在只新增一个最小回归测试，先不要修改生产代码。

测试目标：过期 refresh token 调用 POST /api/session/refresh 时，响应为 401，JSON error 为 SESSION_EXPIRED，日志不包含令牌原文。

遵循现有 fixture、时间控制和请求方式；使用固定过期时间，不等待真实时间；不改变公开 API，不新增依赖；只改直接相关测试文件。写完立即运行这一条测试，报告失败命令、退出码和关键输出。
```

修复前应看到类似失败：

```text theme={null}
FAIL tests/routes/session.test.ts
expected 401, received 500
expected "SESSION_EXPIRED", received "internal_error"
Tests: 1 failed, 0 passed
Process exited with code 1
```

如果测试直接通过，不要宣布 Bug 已修复。检查：过期时间是否真正生效；mock 是否绕过了生产分支；请求是否命中真实路由；工作区是否已经有未提交修复。用 `git diff` 和测试日志确认。

测试应表达契约，而不是实现行号：

```ts theme={null}
expect(response.status).toBe(401);
expect(response.body).toEqual({ error: "SESSION_EXPIRED" });
expect(logOutput).not.toContain(refreshToken);
```

语法以仓库风格为准。至少保留一条路由或服务边界测试，不能只 mock 一个函数然后断言它抛错。

## 5. 根据调用链定位根因

让 Codex 对每一步列文件、函数、输入、输出和异常：

```text theme={null}
根据失败测试和现有代码，追踪一次 refresh 请求：
路由入口 -> token 解析 -> 会话查询 -> 过期/撤销判断 -> 响应映射 -> error handler。

列出每一步的文件、符号、输入、输出和可能异常，并把“已确认根因”和“仍是猜测”分开。先不要修改。
```

案例中的调用链可能是：

```text theme={null}
routes/session.ts: refresh()
  -> auth/session-service.ts: refreshSession()
  -> token.verify(refreshToken)
  -> sessionRepository.findByTokenId(token.jti)
  -> 读取 session.id
  -> middleware/error-handler.ts 转成 500
```

用下面的输入要求根因证明：

```text theme={null}
请证明根因，而不是只提出修法：
1. 过期分支中哪个值为空，它的返回类型/契约是什么；
2. 为什么当前代码仍访问它的 id；
3. error-handler 为什么把它变成 500；
4. 有效 token 为什么不经过这个错误路径；
5. 项目是否已有 SessionExpiredError 或等价错误映射。
无法从代码或失败测试证明的内容标记为未确认。
```

合理结论是：不可用 session 查询结果被当成成功结果继续使用，访问 `session.id` 抛出通用 `TypeError`，统一错误处理器没有识别领域状态，于是返回 500。根因不是“加一个 try/catch”。

再排除相邻原因：

```text theme={null}
请逐项验证以下假设，不要改实现：
A：数据库连接失败导致查询无结果；
B：JWT exp 的秒/毫秒单位错误；
C：过期 token 查询为空是预期业务状态；
D：已有 error-handler 映射到 401；
E：测试 fixture 的 token 已过期但 session 数据仍被当作有效。
每项给出证据文件、命令或测试结果。
```

若 A 成立，应处理数据库环境，不能把连接故障伪装成 401；若 B 成立，修复位置应是时间解析，而不是错误处理器。

## 6. 环境与数据问题

无法复现不等于代码没有问题。先比较以下项目：

| 检查项      | 应确认                    | 不要做           |
| -------- | ---------------------- | ------------- |
| Node/npm | 版本与 lockfile 是否匹配      | 未确认就升级全局 Node |
| 配置       | 使用 `.env.example` 的测试值 | 粘贴生产 `.env`   |
| 数据库      | schema、迁移、测试库路径        | 修改生产数据        |
| 时间       | 时区、应用时钟、JWT 单位         | `sleep` 等一小时  |
| 依赖       | lockfile 和安装状态         | 随意换测试框架       |
| 网络       | 是否需要本地服务或 mock         | 默认允许联网        |

启动失败时先运行：

```bash theme={null}
node --version
npm --version
npm run
npm ls --depth=0
git diff -- package.json package-lock.json
```

若输出为：

```text theme={null}
Error: Cannot find module 'better-sqlite3'
```

这是依赖或原生模块环境问题，不是 refresh 根因。确认允许后再运行仓库规定的：

```bash theme={null}
npm ci
```

不要为绕开缺包而把数据库替换成内存 mock。

数据缺失时输入：

```text theme={null}
当前复现被数据前提阻塞。请先报告数据库是否为测试库、迁移版本、测试用户和 session fixture 是否存在。
不要读取生产数据，不要删除或批量更新。
若仓库有 seed/test factory，只使用它创建 alice@example.test 和固定过期 session，并先列出将执行的命令。
```

重新创建数据后若仍复现，数据只是条件，不是根因；比较 `expiresAt`、`revokedAt`、`jti` 和时区即可，不要记录真实令牌。

## 7. 提出并实施最小修复

根因有证据后，先让 Codex 规划边界：

```text theme={null}
根因已确认：不可用 session 仍被访问 session.id，TypeError 被统一映射为 500。
请先提出最小方案，不要编辑：需要改哪些文件；复用哪个错误类型；有效、过期、撤销、未知 token 和数据库异常各保持什么行为；如何验证和回滚。
不改路由、成功响应、schema、无关日志或依赖。
```

优先复用已有领域错误。若没有，按项目风格定义最小错误类型；在空值边界显式抛出；只给明确的 session 状态映射 401；数据库异常和未知编程错误继续 500；保留有效 token 的原成功路径。

明确拒绝宽泛修法：

```ts theme={null}
try {
  return await refreshSession(token);
} catch {
  return res.status(401).json({ error: "SESSION_EXPIRED" });
}
```

它会把数据库宕机和编程错误也伪装成过期会话。也不要返回 `200` 加空 token，这会让客户端误以为刷新成功。

确认方案后输入：

```text theme={null}
按最小方案实施。只允许修改实际调用链中的 session-service、必要的错误映射和直接相关测试。

要求：过期、撤销或不存在的 session 返回 401 SESSION_EXPIRED；有效 token 的 200 响应不变；数据库和未知错误仍走原有 500；不记录令牌，不引入依赖，不改路由、schema 或无关代码；不提交、不推送。
编辑后先展示 diff 和准备运行的命令，不要扩大范围。
```

## 8. 审查 diff

先看范围：

```bash theme={null}
git status --short
git diff --name-status
git diff --stat
git diff --check
```

预期只出现实现、必要映射和回归测试。若出现 lockfile、配置、生成物或其他业务模块，要求：

```text theme={null}
请撤回与本 Bug 无关的 package、配置、生成文件和其他模块改动，只保留最小修复及回归测试，再展示 diff。
```

再逐行问：

1. 空结果判断是否早于 `session.id`？
2. 只有明确 session 状态才返回 401 吗？
3. 数据库异常仍为 500 吗？
4. 有效 token 响应是否完全保持？
5. 是否改变 JSON、路由或客户端依赖？
6. 是否输出令牌、Cookie 或完整用户对象？
7. 测试是否真的控制了过期时间？
8. 是否夹带重命名、格式化或重构？

必要时执行：

```bash theme={null}
git diff --word-diff=plain -- src/auth/session-service.ts src/middleware/error-handler.ts tests/routes/session.test.ts
git diff --check
```

Diff 大到无法解释时，先停止测试，拆掉无关改动。最小修复是只改变错误状态分支，不是单纯追求最少行数。

## 9. 分层验证

### 原失败测试

重新运行修复前的同一条命令：

```bash theme={null}
npm run test:unit -- --run tests/routes/session.test.ts
```

预期：

```text theme={null}
PASS tests/routes/session.test.ts
refresh with expired token
  ✓ returns 401 SESSION_EXPIRED without logging the token
Tests: 1 passed
Process exited with code 0
```

仍失败时，不要连续改代码，输入：

```text theme={null}
回归测试仍失败。请只分析下面的完整输出，判断属于实现、断言、fixture、时间控制还是环境；先不要编辑。
<粘贴输出>
```

### 相关测试和工程检查

再运行仓库已有的相关测试：

```bash theme={null}
npm run test:unit -- --run tests/auth/session-service.test.ts tests/routes/session.test.ts
npm run lint
npm run build
```

若脚本不存在，执行 `npm run` 并报告“未提供”，不要把命令不存在算成代码失败。相关场景至少应覆盖：

| 场景          | 期望                    | 目的         |
| ----------- | --------------------- | ---------- |
| 有效 token    | 原 `200` 响应            | 保护主路径      |
| 过期 token    | `401 SESSION_EXPIRED` | 锁定本次 Bug   |
| 撤销/未知 token | 项目既有约定                | 检查相邻边界     |
| 数据库异常       | 原 `500`               | 防止宽泛 catch |

### 手工验收

本地服务可安全启动时：

```bash theme={null}
npm run dev
curl -i -X POST http://localhost:3000/api/session/refresh \
  -H 'content-type: application/json' \
  -d '{"refreshToken":"<EXPIRED_TEST_TOKEN>"}'
```

预期：

```text theme={null}
HTTP/1.1 401 Unauthorized
{"error":"SESSION_EXPIRED"}
```

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

## 10. 失败分支

### 选错文件

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

```text theme={null}
停止编辑。请从实际调用链证明 refresh 请求经过哪些符号和文件。
撤回未必要的文件改动，只保留经调用链和失败测试证明需要的文件。
```

### 只加 try/catch

若数据库断开也变成 401：

```text theme={null}
当前补丁把所有异常都返回 401，破坏数据库错误的 500 契约。
撤回宽泛 catch，基于已有错误类型或明确 session 状态精确映射。先展示方案，再编辑。
```

### 测试绿但没复现

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

### 环境失败

端口占用、缺少原生模块、数据库连接失败或 Node 版本不兼容先记录为环境失败：

```text theme={null}
命令：npm run test:unit -- --run tests/routes/session.test.ts
Node：22.14.0
退出码：1
错误：EADDRINUSE 127.0.0.1:3000
```

只释放确认属于当前任务的本地进程，不要杀掉不明服务。环境未恢复时，报告代码验证未完成。

### 扩大成重构

```text theme={null}
本任务只修过期 refresh token 的错误映射。撤回重构、命名调整、格式化和日志改造，只保留能让失败测试通过且不改变有效 token 行为的最小 diff。
```

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

## 11. 最终验收与交付

执行：

```bash theme={null}
git status --short
git diff --stat
git diff --check
git diff --name-only
```

逐项确认：

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

交付摘要使用证据格式：

```text theme={null}
现象：过期 refresh token 原返回 500。
复现：<命令或测试路径>。
根因：session 查询为空后访问 session.id，未转换为领域错误。
修复：在边界处识别不可用 session，复用现有 401 映射。
验证：<修复前失败输出>；<修复后命令、退出码和结果>。
变更文件：<git diff --name-only>。
未验证：<环境差异或无>。
回滚：保留当前 diff，按文件审查后恢复，不覆盖他人改动。
```

## 可直接使用的 Codex 输入

```text theme={null}
请修复登录会话刷新 Bug，但严格按“复现 -> 日志取证 -> 失败测试 -> 根因 -> 最小修复 -> 回归 -> diff 审查”执行。

Bug：已过期或已撤销 refresh token 调用 POST /api/session/refresh 返回 500。
期望：返回 401，JSON 为 {"error":"SESSION_EXPIRED"}；有效 token 的 200 响应不变；令牌原文不进入日志。

先只读调查实际调用链和 package.json，报告命令和数据前提，不要编辑。
再用仓库已有 fixture 和固定时钟新增回归测试，先运行确认修复前失败。
只有根因被调用链和失败输出证明后，才提出并实施最小修复。

约束：不改路由、成功响应、schema；不引入依赖；不读取 .env、生产数据、Cookie 或真实令牌；不提交、不推送、不访问生产服务；不把数据库异常或未知错误宽泛映射为 401。

验证：回归测试修复前失败、修复后通过；有效、过期、撤销/未知 token 和数据库异常的相关测试通过；按 package.json 执行 lint/build；执行 git diff --check，并报告每条命令、退出码和 git diff --name-status。

失败时停止扩大修改范围，先判断是代码、测试、数据还是环境问题。
```

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

## 小结

```text theme={null}
固定复现
  -> 采集脱敏日志
  -> 让测试先失败
  -> 对照调用链定位根因
  -> 区分代码、环境和数据问题
  -> 只修必要分支
  -> 运行分层回归测试
  -> 审查文件范围与行为 diff
```

接口不再返回 500，只说明症状改变。只有失败测试变绿、有效路径不回归、异常没有被吞掉、diff 范围可解释，并且剩余环境假设被记录，这次 Bug 修复才算完成。

参考资料：`参考/codex/14-workflows.md`、`参考/codex/06-first-task.md`、`参考/codex/13-prompting.md`。
