Bug 复现与最小用例
从模糊现象到稳定复现:收集环境、版本、输入与时间因素,二分删除无关代码或数据,最终得到可提交的 failing test 或脚本。
SKILL 要求 Agent 先问:是否 100% 复现、影响哪些版本、是否与负载或数据量相关。再指导用户固定随机种子、关闭缓存或记录请求序列。
产出物应是可机读的最小片段:单测、curl 一行、或 docker-compose 片段,并附期望 vs 实际。修复 PR 必须带上该用例防止回归。
复现主流程(skill-flow-block)
[ 收集:现象、复现率、环境指纹(OS / 语言运行时 / 依赖锁文件)]
│
▼
┌─────────────┐ 固定:种子、时钟、缓存、并发度、数据快照
│ 稳定复现路径 │──── 记录:请求序列、配置 diff、feature 开关
└─────────────┘
│
▼
┌─────────────┐ 删模块 / 删行 / 删数据:每步标记「仍复现 / 消失」
│ 二分缩小范围 │──── 代码:注释分支、stub 下游;数据:半分数据集
└─────────────┘
│
▼
┌─────────────┐ 单测 assert、脚本、compose 片段;附期望 vs 实际
│ 最小可提交物 │──── PR:failing test 先行,修复后必须仍绿
└─────────────┘
Bug 报告标准格式(可直接提交到 issue tracker):
## Bug 报告
**标题**: 深色模式下结账页价格格式化错误显示为 NaN
### 复现环境
- OS: macOS 14.3 / Windows 11 22H2
- Browser: Chrome 122.0.6261.112
- App version: v3.2.1 (commit: abc1234)
- Node.js: 20.11.0
- 复现率: 100%(在上述环境)
### 复现步骤
1. 访问 http://localhost:3000,确认深色模式已开启(系统设置或 UI 切换)
2. 添加任意商品到购物车
3. 点击「结账」按钮,进入结账页
4. 观察订单总计金额显示
### 期望行为
订单总计正确显示金额,例如:¥299.00
### 实际现象
订单总计显示为「NaN」,控制台错误:
```
TypeError: Cannot read properties of undefined (reading 'toLocaleString')
at formatPrice (checkout.ts:142:23)
at CheckoutSummary.render (CheckoutSummary.tsx:89:12)
```
### 最小复现
```bash
git clone https://github.com/org/repo && cd repo
git checkout v3.2.1
npm ci && npm run dev
# 打开 http://localhost:3000,开启深色模式,访问 /checkout
```
### 已尝试
- [ ] 浅色模式下无法复现(正常显示)
- [ ] 清除浏览器缓存后仍复现
- [ ] 在 v3.2.0 上无法复现(新引入的回归)
### 相关 issue / PR
可能与 #1089 (dark mode currency formatting) 相关
无法复现时列出已尝试的假设与下一步观测点;涉及隐私数据时用合成数据替代。与「堆栈分析」技能衔接:复现后再对照栈顶帧。
最小复现(MRE)构建步骤
目标是把「整个应用」压成「仍触发同一根因」的最小表面:优先 failing test(同仓库内可 npm test / pytest 一条跑红),其次独立脚本或最小 HTTP 请求。
MRE 构建清单(逐步缩小范围):
# MRE 构建步骤清单
## 阶段 1: 稳定复现
# □ 记录复现率(每次/偶发/特定条件)
# □ 固定环境变量(随机种子、时区、locale)
export TZ=UTC
export LANG=zh_CN.UTF-8
node --experimental-vm-modules --seed=42 test.js
# □ 关闭缓存(HTTP 缓存、应用缓存)
curl -H "Cache-Control: no-cache" http://localhost:3000/api/checkout
# □ 记录最小触发条件(哪些参数触发,哪些不触发)
## 阶段 2: 缩小范围(二分法)
# □ 从集成测试下沉到单元测试
# 检查 formatPrice 函数在深色模式下的行为:
# 原始问题在 CheckoutSummary 组件
# → 提取 formatPrice 逻辑为独立测试
# test/formatPrice.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice } from '../src/utils/currency'
describe('formatPrice', () => {
it('formats valid price in CNY', () => {
expect(formatPrice(299, 'CNY')).toBe('¥299.00') // PASS
})
it('handles undefined price in dark mode context', () => {
// 复现 bug:deep mode context 中 price 为 undefined
const darkModeContext = { theme: 'dark', currency: undefined }
expect(formatPrice(299, darkModeContext.currency)).toBe('¥299.00') // FAIL: NaN
})
})
# 运行: npx vitest run test/formatPrice.test.ts
## 阶段 3: 生产数据脱敏(用于生成测试数据)
# 从生产 DB 导出脱敏样本(不含真实用户信息)
psql $PROD_DB -c "
SELECT
id,
ROUND(total_amount, 2) as amount, -- 保留金额结构
currency_code,
'test-user-' || floor(random()*1000) as user_id, -- 匿名化
created_at::date as date -- 仅保留日期
FROM orders
WHERE created_at > NOW() - INTERVAL '7 days'
LIMIT 100;
" > test/fixtures/orders-sample.csv
用 Docker Compose 复现多服务环境:
# docker-compose.repro.yml
# 用于在隔离环境中精确复现 bug
# 使用:docker compose -f docker-compose.repro.yml up
version: "3.9"
services:
# 固定使用出现 bug 的版本
app:
image: org/payment-app:v3.2.1 # 固定版本(非 latest)
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/app_test
THEME: dark # 触发 bug 的关键配置
NODE_ENV: production
ports:
- "3001:3000"
depends_on:
db:
condition: service_healthy
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: app_test
POSTGRES_PASSWORD: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- ./test/fixtures/seed.sql:/docker-entrypoint-initdb.d/seed.sql
# 使用方法:
# 1. docker compose -f docker-compose.repro.yml up -d
# 2. curl http://localhost:3001/api/checkout/summary
# 3. 观察返回值是否包含 NaN
# 4. docker compose -f docker-compose.repro.yml down
- 从集成/E2E 下沉:能单元化则单元化;必须保留多服务时,用 docker-compose 或录制的契约替身缩小外部。
- 期望与单列:一句话说明「应该怎样」,再附实际输出、日志片段或截图占位说明。
二分与 git bisect
输入与数据二分:对多文件、多参数场景,每次去掉一半仍验证是否复现,快速定位「最小必要集合」。对时间相关 bug,二分时间窗口或请求批次。
# git bisect 自动化示例
# 已知:v3.2.0 正常,v3.2.1 有 bug
git bisect start
git bisect bad HEAD # 标记当前版本为坏
git bisect good v3.2.0 # 标记已知好版本
# 自动 bisect 脚本(返回 0=好,1=坏,125=跳过)
cat > /tmp/bisect-test.sh << 'EOF'
#!/bin/bash
npm ci --silent 2>/dev/null || exit 125 # 编译失败则跳过此 commit
npm test -- --testPathPattern="formatPrice" --silent
# 测试通过返回 0,失败返回非零
EOF
chmod +x /tmp/bisect-test.sh
# 执行自动 bisect(约需 log2(commits) 次)
git bisect run /tmp/bisect-test.sh
# 完成后输出类似:
# abc1234 is the first bad commit
# Author: dev@example.com
# Date: Mon Mar 11 10:22:33 2024
# Commit: fix: update dark mode currency context initialization
git bisect reset # 恢复工作区
- 先确认坏提交在主线可构建、测试可跑,否则 bisect 会被编译失败干扰。
- 合并提交可用
git bisect --first-parent跳过合并点;Cherry-pick 热修后要重新标定好/坏边界。
复现步骤 Markdown 拼装
填写下方字段并勾选要纳入的章节,生成可粘贴到 issue / PR / 对话的 Markdown;内容仅保存在本机浏览器(localStorage),不上传。
空章节不会输出;至少勾选一项并填写对应正文。步骤中的空行会被忽略。生成后可再手工补上日志、截图链接或 failing test 路径。
---
name: bug-reproduction
description: 从报告到最小可复现
model: claude-sonnet-4-5
---
# 必问清单(信息不足时追问)
required_info:
- 复现率(100%/偶发/特定条件)
- 影响版本范围(first bad version)
- OS/运行时/依赖版本(锁文件)
- 是否与负载/数据量/并发相关
# MRE 构建步骤
mre_steps:
1. 稳定复现(固定随机种子、时钟、缓存)
2. 从集成测试下沉到单元测试
3. 二分缩小范围(代码/数据/配置)
4. 生产数据脱敏用于测试 fixture
5. Docker Compose 复现隔离环境
# 产出物标准
deliverables:
- failing test(npm test / pytest 一条跑红)
- 或:最小 curl 命令 / docker-compose 片段
- 附期望 vs 实际输出
- 修复 PR 必须包含此用例(防回归)
# 隐私数据处理
data_handling:
- 生产数据:脱敏(匿名化user_id,仅保留结构)
- PII 字段:替换为合成数据
- 禁止:在公开 issue 粘贴真实用户数据