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 粘贴真实用户数据

返回技能库 更多技能入口