bug-reproduction

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
<objective> A bug you cannot reproduce is a bug you cannot fix or prove fixed. This skill takes a thin, hand-wavy report ("order total is wrong sometimes") and drives it to a VERIFIED minimal reproduction, a deterministic failing test written BEFORE the fix, a `git bisect` that names the introducing commit, and a structured evidence block in the ticket. The discipline it enforces: reproduce before theorizing, minimize one cut at a time, freeze time/seed/network so the repro fails identically every run, watch the test go red first, and confirm the fix flips it green — and that reverting the fix turns it red again. </objective>
<objective> 无法复现的Bug是你无法修复或证明已修复的Bug。本技能会将一份简略、模糊的报告(比如「订单总额有时不正确」)转化为已验证的最小复现案例、修复前编写的确定性失败测试、能定位引入问题提交的`git bisect`结果,以及工单中的结构化证据块。它遵循的准则:先复现再推理,每次只最小化一个变量,冻结时间/种子/网络以确保复现每次运行都产生相同的失败,先让测试变红(失败),再确认修复后转为绿色——并且回滚修复后测试会再次变红。 </objective>

Quick Route

快速指引

You have…Go to
A thin report and no idea how to trigger itStep 1: Extract the implicit repro
A messy 14-step repro to clean upStep 2: The reproduce-minimize-isolate-capture loop
"Worked last month, broken now"Step 3: Bisect to the introducing commit
A repro that passes/fails inconsistentlyStep 4: Make the repro deterministic
A clean repro, no fix yetStep 5: Write the failing regression test (red) first
"The dev says it's fixed, test is green"Step 6: Verify the fix actually fixes it
"It won't reproduce for me but does for the user"Step 7: Flaky vs environment vs not-reproducible
Repro + test doneStep 8: Write the evidence back into the ticket
你遇到的情况…前往步骤
报告简略,完全不知道如何触发Bug步骤1:提取隐含的复现信息
已有复杂的14步复现流程需要简化步骤2:复现→最小化→隔离→捕获循环
「上个月还能用,现在坏了」步骤3:二分查找定位引入问题的提交
复现结果不稳定,时而通过时而失败步骤4:让复现具有确定性
已有清晰的复现案例,但尚未修复步骤5:先编写失败的回归测试(红态)
「开发说已经修复了,测试显示绿色」步骤6:验证修复是否真正解决问题
「我这里无法复现,但用户那边可以」步骤7:不稳定Bug vs 环境特定Bug vs 无法复现
复现案例和测试已完成步骤8:将证据写回工单

Discovery Questions

调研问题

First, check
.agents/qa-project-context.md
in the project root — it carries the tech stack, test runner, known-flaky areas, and environment matrix. Pass over any question it already answers. If it is missing, suggest creating one with the
qa-project-context
skill.
  • What is the report, verbatim? The thinner it is, the more you must extract before touching code — a one-line report sets the whole intake agenda (Step 1).
  • Does it reproduce at all yet, and how reliably? "Every time" vs "sometimes" decides whether you go straight to minimizing or into determinism work first.
  • Did it ever work? A known-good past release unlocks
    git bisect
    to the introducing commit; no known-good point means you debug forward instead.
  • What is the test runner and stack? Vitest/Jest vs Playwright changes the determinism API (
    vi.setSystemTime
    vs
    page.clock
    ) and where the regression test lands.
  • What are the non-determinism sources? Time-of-day rules, randomness, third-party APIs, locale — each must be pinned for the repro to be trustworthy.

首先查看项目根目录下的
.agents/qa-project-context.md
——其中包含技术栈、测试运行器、已知不稳定区域和环境矩阵。跳过其中已回答的问题。如果该文件缺失,建议使用
qa-project-context
技能创建它。
  • **原始报告内容是什么?**报告越简略,你越需要在接触代码前提取所有信息——一句话报告决定了整个处理流程(步骤1)。
  • 目前是否能复现,以及复现的可靠性如何?「每次都能复现」vs「有时能复现」决定了你是直接进入最小化步骤,还是先处理确定性问题。
  • **该功能曾经正常工作过吗?**已知正常的过往版本可以通过
    git bisect
    定位引入问题的提交;如果没有已知正常版本,则需要正向调试。
  • **使用的测试运行器和技术栈是什么?**Vitest/Jest与Playwright的确定性API不同(
    vi.setSystemTime
    vs
    page.clock
    ),回归测试的存放位置也不同。
  • **非确定性因素有哪些?**时间规则、随机性、第三方API、区域设置——这些都必须固定,才能让复现结果可信。

Core Principles

核心原则

  1. Reproduce before you theorize. The strongest wrong instinct is to read a symptom and jump to a root cause ("sounds like a float rounding bug, let me patch the total calc"). Don't. Extract the repro, make it fail on demand, and only then form a hypothesis. A fix without a reproduction is a guess you can't falsify.
  2. A repro is a deterministic artifact, not a story. "It happens around midnight with a random code" is a story. Freeze the clock, seed the RNG, and stub the network so the same inputs produce the same failure on every run and every machine. If it isn't deterministic, you can't bisect it, test it, or prove it fixed.
  3. Minimize one variable at a time, and re-confirm after every cut. Shrinking the repro is a search, not a rewrite. Remove one step, data field, or dependency, then re-run and confirm it still reproduces. Removing several at once tells you nothing about which one mattered.
  4. The regression test is written RED, before the fix. Assert the real expected value, watch it fail first (proving it catches this bug), then watch the fix flip it green. A test added after the fix, or one disabled / marked pending, proves nothing.
  5. Green isn't done — revert-to-verify is. A passing test can pass for the wrong reason. Temporarily remove the fix and confirm the test goes red again. Only a test that fails without the fix actually guards against the bug.

  1. 先复现,再推理。最容易犯的错误是看到症状就直接猜测根因(「听起来像是浮点数舍入Bug,我来修改总额计算逻辑」)。不要这么做。先提取复现信息,让Bug可以按需触发,之后再形成假设。没有复现案例的修复只是无法验证的猜测。
  2. 复现案例是确定性产物,而非描述。「午夜时分使用随机代码时会出现问题」是描述。需要冻结时钟、设置随机种子、存根网络,让相同的输入在每次运行、每台机器上都产生相同的失败。如果复现结果不确定,你就无法进行二分查找、测试或证明问题已修复。
  3. 每次只最小化一个变量,每次修改后重新确认。简化复现案例是一个搜索过程,而非重写。移除一个步骤、数据字段或依赖,然后重新运行,确认Bug仍然可以复现。如果一次移除多个元素,你无法知道哪个元素是关键的。
  4. 回归测试要先写成红态(失败),再修复。断言真实的预期值,比如
    expect(total).toBe(2754)
    ,先让测试失败(证明它能捕获该Bug),再确认修复后测试转为绿色。在修复后添加的测试,或被禁用/标记为待处理的测试,无法证明任何事情。
  5. 测试变绿不代表完成——回滚验证才是关键。测试通过可能是因为无关原因。临时移除修复,确认测试再次变红。只有在没有修复时会失败的测试,才能真正防止该Bug再次出现。

Step 1: Extract the implicit repro

步骤1:提取隐含的复现信息

A thin report ("Checkout is broken, order total is wrong sometimes") names a symptom, not a path. Before any code, extract or ask for every reproducibility dimension. Never invent the repro steps from imagination, and never theorize a root cause yet — both come after the bug reproduces.
For a "wrong total" / data-correctness bug, the load-bearing dimensions a thin report most often omits are:
  • Exact steps to reproduce — the click-by-click path, not "checkout is broken."
  • Build / version / commit (git SHA) — they may be on a build where it's already fixed.
  • Environment — browser + version, OS, device.
  • Input data — cart contents, quantities, the account/user, coupon, the exact fixture. A total is a pure function of its inputs; without them you are guessing.
  • Expected vs actual — the number they expected and the number they saw.
  • Frequency — every time, or intermittent? "Sometimes" points at non-determinism.
  • Locale / timezone / currency — rounding, tax, and formatting are locale-specific; a total "wrong" in
    de-DE
    may be correct in
    en-US
    .
  • Timestamp of occurrence, plus any logs/screenshots/network trace.
Write these into a single repro spec before touching code. If a row is blank, that is your next question to the reporter — not a license to start writing the fix or theorizing.
See
references/intake.md
for the full extraction checklist, why each load-bearing dimension matters for "wrong total," and the repro-spec template.

简略报告(比如「结账功能坏了,订单总额有时不正确」)只描述了症状,没有给出触发路径。在接触代码前,需要提取或询问所有与复现相关的维度。绝不要凭空想象复现步骤,也不要现在就推理根因——这两者都要等到Bug可以复现之后再做。
对于「总额错误」/数据正确性类Bug,简略报告最常遗漏的关键维度包括:
  • 准确的复现步骤——点击操作的详细路径,而非「结账功能坏了」。
  • 构建/版本/提交(git SHA)——报告者可能使用的版本中Bug已经被修复。
  • 环境——浏览器及版本、操作系统、设备。
  • 输入数据——购物车内容、数量、账户/用户、优惠券、确切的测试数据。总额是输入的纯函数;没有输入数据,你只是在猜测。
  • 预期值 vs 实际值——用户预期的数值和实际看到的数值。
  • 出现频率——每次都出现,还是间歇性出现?「有时出现」指向非确定性因素。
  • 区域设置/时区/货币——舍入、税费和格式都与区域设置相关;在
    de-DE
    下「错误」的总额在
    en-US
    下可能是正确的。
  • 出现时间戳,以及任何日志/截图/网络追踪信息。
在接触代码前,将这些信息整理成一份复现规格。如果有空白项,你的下一个任务是向报告者询问——而不是开始编写修复或推理根因。
查看
references/intake.md
获取完整的提取清单、每个关键维度对「总额错误」Bug的重要性,以及复现规格模板。

Step 2: The reproduce→minimize→isolate→capture loop

步骤2:复现→最小化→隔离→捕获循环

Given a confirmed-but-messy repro (e.g. 14 manual UI steps across 3 pages), do not hand the 14-step version to the developer and do not rewrite it wholesale. Run this loop:
  1. REPRODUCE / confirm. First establish a baseline: run the full repro and confirm it actually fails. You can only minimize something that currently reproduces.
  2. MINIMIZE. Remove one step, field, or dependency. Re-run. If it still reproduces, keep the cut; if it no longer fails, that element was load-bearing — restore it. Repeat, one variable at a time, until every remaining piece is necessary. This is the core ordering rule: never minimize before confirming it reproduces, and never omit verifying it still fails after each cut.
  3. ISOLATE. Narrow the failure to the smallest layer that still shows it — drop from a 3-page UI flow to a single page, then to a unit/API call against the offending function if the bug lives below the UI.
  4. CAPTURE. Record the now-minimal repro as evidence: the smallest steps or the single command, plus logs/trace/screenshot. This is what the developer and the regression test consume.
The output is the smallest sequence that still reproduces — not the original walkthrough.

如果已有可确认但复杂的复现案例(比如跨3个页面的14步手动UI操作),不要把14步的版本交给开发,也不要全盘重写。执行以下循环:
  1. 复现/确认。首先建立基准:运行完整的复现流程,确认Bug确实会失败。你只能简化当前可以复现的内容。
  2. 最小化。移除一个步骤、字段或依赖。重新运行。如果Bug仍然可以复现,保留这次修改;如果不再失败,说明该元素是关键的——恢复它。重复此过程,每次只修改一个变量,直到剩余的每个元素都是必要的。核心规则:在确认可以复现前不要开始最小化,每次修改后都要验证Bug是否仍然失败。
  3. 隔离。将失败范围缩小到最小的仍能出现Bug的层级——从跨3个页面的UI流程缩小到单个页面,再如果Bug存在于UI之下,缩小到针对问题函数的单元/API调用。
  4. 捕获。将现在的最小化复现案例记录为证据:最简化的步骤或单个命令,加上日志/追踪/截图。这是开发和回归测试要使用的内容。
输出结果是最小的仍能复现Bug的流程——而非原始的操作指南。

Step 3: Bisect to the introducing commit

步骤3:二分查找定位引入问题的提交

The bug is on
HEAD
but a past release was clean, and you have a command that exits non-zero when the bug is present. Use
git bisect run
to binary-search history automatically — do not manually check out each commit, and do not use
git revert
to hunt for it.
sh
git bisect start
git bisect bad HEAD          # current commit has the bug   (alias: git bisect new)
git bisect good v2.4.0       # last clean release            (alias: git bisect old)
git bisect run npm test -- checkout-total.spec.ts   # ONE targeted test, never the full suite
Bug出现在
HEAD
版本,但过往某个版本是正常的,且你有一个在Bug存在时会返回非零退出码的命令。使用
git bisect run
自动进行历史二分查找——不要手动检出每个提交,也不要使用
git revert
来查找问题。
sh
git bisect start
git bisect bad HEAD          # 当前提交存在Bug   (别名: git bisect new)
git bisect good v2.4.0       # 最后一个正常版本            (别名: git bisect old)
git bisect run npm test -- checkout-total.spec.ts   # 仅运行一个针对性测试,绝不要运行全量测试套件

bisect prints "<sha> is the first bad commit"

二分查找会打印 "<sha> is the first bad commit"

git bisect reset # ALWAYS clean up — restores the original HEAD

The exit-code contract `git bisect run` uses: **exit code 0 = good** (bug absent),
**non-zero (1–124) = bad** (bug present), **exit 125 = skip** (untestable). So your command
must return 0 when the feature is fine and non-zero when the bug reproduces — most runners
already do this. Run **one targeted test**, not `npm test:all` / the whole suite: an
unrelated failure at an old commit would mark it bad and send the search down the wrong half.

The `good`/`bad` pair assumes a regression (good in the past, bad now). The `old`/`new`
aliases mean the same search and read better when hunting any state transition.

See `references/bisect.md` for the full happy path and the skip/untestable wrapper.
git bisect reset # 务必清理——恢复到原始HEAD

`git bisect run`使用的退出码规则:**退出码0 = 正常**(无Bug),**非零(1–124)= 异常**(存在Bug),**退出码125 = 跳过**(无法测试)。所以你的命令必须在功能正常时返回0,Bug复现时返回非零——大多数测试运行器已经满足此要求。运行**一个针对性测试**,而非`npm test:all`/全量测试套件:旧提交中的无关失败会被标记为异常,导致搜索方向错误。

`good`/`bad`配对适用于回归问题(过往正常,现在异常)。`old`/`new`别名含义相同,在查找任何状态变化时可读性更好。

查看`references/bisect.md`获取完整的正常流程和跳过/无法测试的包装脚本。

Bisect skip and determinism (untestable or flaky commits)

二分查找的跳过逻辑与确定性(无法测试或不稳定的提交)

Two things corrupt a naive bisect, and the default bad answer — "exit 1 on any failure" — walks into both:
  • Old commits won't build. A compile error exits 1, which bisect reads as "bug present" and marks a clean commit bad. Wrong: an unbuildable commit is untestable — your wrapper script must
    exit 125
    (skip) on build failure, distinguishing it from a real bad commit.
  • Flaky network/timing failures. A transient un-stubbed third-party call exits 1 and gets blamed. Force determinism during bisect — stub the network, pin
    TZ
    , seed the RNG — so only the real bug can fail the step. If a commit's result flips between runs, treat it as untestable (
    exit 125
    ), not bad.
Wrap the step in a script that returns 0 = good, 1 = bad, 125 = skip (the valid bad range is 1–127 excluding 125), guards the build, stubs the network, and retries once to catch flakiness. Then
git bisect run ./bisect-step.sh
. Full wrapper in
references/bisect.md
.

两种情况会破坏简单的二分查找,而默认的错误处理——「任何失败都返回1」——会同时触发这两种情况:
  • 旧提交无法构建。编译错误会返回1,二分查找会将其解读为「存在Bug」,标记正常提交为异常。错误做法:无法构建的提交是无法测试的——你的包装脚本必须在构建失败时
    exit 125
    (跳过),将其与真正的异常提交区分开。
  • 不稳定的网络/时序失败。临时未存根的第三方调用返回1并被归咎为Bug。在二分查找期间强制确定性——存根网络、固定
    TZ
    、设置随机种子——只有真正的Bug会导致步骤失败。如果某个提交的结果在多次运行中变化,将其视为无法测试(
    exit 125
    ),而非异常。
将步骤包装在一个脚本中,返回0 = 正常,1 = 异常,125 = 跳过(有效的异常范围是1–127,排除125),检查构建状态、存根网络,并重试一次以处理不稳定情况。然后运行
git bisect run ./bisect-step.sh
。完整的包装脚本见
references/bisect.md

Step 4: Make the repro deterministic

步骤4:让复现具有确定性

The bug "only around midnight, with a random discount code, via a third-party pricing API" has three non-determinism sources. Pin all three so it fails the same way every single run — do not wait for midnight, do not let it hit the real pricing API, and never use a
sleep
/
setTimeout
/
waitForTimeout
to paper over timing.
SourceVitestPlaywright
Time
vi.useFakeTimers()
+
vi.setSystemTime(new Date('…'))
page.clock.install({ time })
+
page.clock.setFixedTime(…)
Randomness
faker.seed(1337)
(or stub
Math.random
)
seed the app's RNG via an init hook
NetworkMSW
setupServer
+
http.get
HttpResponse.json
page.route(...)
route.fulfill(...)
Key points:
  • Vitest:
    vi.setSystemTime
    only works after
    vi.useFakeTimers()
    . Seed faker in
    beforeEach
    . Set MSW
    onUnhandledRequest: 'error'
    so a missed stub fails loudly.
  • Playwright:
    page.clock.install
    /
    setFixedTime
    must run before
    page.goto
    .
    page.clock
    is the supported API — it exists; do not fall back to a hand-rolled
    Date
    override, and do not bump the timeout to "make it pass."
  • Pin locale/timezone/currency (
    TZ=UTC
    ,
    LANG
    ) when the bug is locale-sensitive.
Avoid:
jest.useFakeTimers('legacy')
(and
timers: 'legacy'
) — legacy fake timers are deprecated and don't mock
Date
/
Date.now
, so the clock stays live and your "frozen" repro still drifts. Modern timers are the default since Jest 27 — just call
jest.useFakeTimers()
+
jest.setSystemTime()
, or
vi.useFakeTimers()
in Vitest. (Jest 30, 2025)
See
references/determinism.md
for the full Vitest (
vi.useFakeTimers
+
vi.setSystemTime
  • faker.seed
    + MSW
    setupServer
    ) and Playwright (
    page.clock
    +
    page.route
    ) recipes, plus a 10-run determinism check.

「仅在午夜出现,使用随机折扣码,调用第三方定价API」的Bug有三个非确定性因素。需要固定所有三个因素,让它每次运行都以相同方式失败——不要等待午夜,不要让它调用真实的定价API,绝不使用
sleep
/
setTimeout
/
waitForTimeout
来掩盖时序问题。
非确定性来源VitestPlaywright
时间
vi.useFakeTimers()
+
vi.setSystemTime(new Date('…'))
page.clock.install({ time })
+
page.clock.setFixedTime(…)
随机性
faker.seed(1337)
(或存根
Math.random
通过初始化钩子设置应用的随机种子
网络MSW
setupServer
+
http.get
HttpResponse.json
page.route(...)
route.fulfill(...)
关键点:
  • Vitest
    vi.setSystemTime
    必须在
    vi.useFakeTimers()
    之后调用。在
    beforeEach
    中设置faker种子。设置MSW的
    onUnhandledRequest: 'error'
    ,以便遗漏的存根会直接报错。
  • Playwright
    page.clock.install
    /
    setFixedTime
    必须在
    page.goto
    之前运行。
    page.clock
    是官方支持的API——不要使用手动编写的
    Date
    覆盖,也不要通过延长超时时间来「让测试通过」。
  • 当Bug与区域设置相关时,固定区域设置/时区/货币(
    TZ=UTC
    LANG
    )。
避免
jest.useFakeTimers('legacy')
(以及
timers: 'legacy'
)——旧版假计时器已被弃用,无法模拟
Date
/
Date.now
,所以时钟仍然是实时的,你的「冻结」复现案例仍会产生偏差。自Jest 27起,现代计时器是默认选项——只需调用
jest.useFakeTimers()
+
jest.setSystemTime()
,或在Vitest中调用
vi.useFakeTimers()
。(Jest 30,2025)
查看
references/determinism.md
获取完整的Vitest(
vi.useFakeTimers
+
vi.setSystemTime
+
faker.seed
+ MSW
setupServer
)和Playwright(
page.clock
+
page.route
)配置示例,以及10次运行的确定性检查方法。

Step 5: Write the failing regression test (red) first

步骤5:先编写失败的回归测试(红态)

You have a clean deterministic repro and the dev hasn't fixed it yet. Write the regression test now, before the fix, as TDD-for-bugs:
  1. Encode the minimal repro as a test that asserts the real expected value
    expect(total).toBe(2754)
    . A tautological assertion that always passes proves nothing.
  2. Run it and confirm it fails before the fix — it must be red first. A test that doesn't go red isn't exercising the bug.
  3. Commit the test (or stage it on the fix branch) so the test guards the fix in CI.
  4. After the dev's fix lands, re-run: it should flip to green. Same test, no edits.
Expected state: red before the fix, green after the fix.
Wrong moves that defeat the point — a test that genuinely fails until this bug is fixed: writing the test only after the fix already landed; disabling the failing test (pending markers, an always-true tautology, or removing the assertion) just to keep CI green; or fixing first and bolting a test on afterward. Keep the assertion live and let it go red.
See
references/determinism.md
for the deterministic test bodies the assertion sits in.

你已有清晰的确定性复现案例,且开发尚未修复Bug。现在编写回归测试,遵循Bug驱动的TDD流程:
  1. 将最小化复现案例编码为测试,断言真实的预期值——比如
    expect(total).toBe(2754)
    。总是通过的同义反复断言无法证明任何事情。
  2. 运行测试,确认它在修复前失败——必须先处于红态。不会变红的测试无法覆盖该Bug。
  3. 提交测试(或在修复分支中暂存),让测试在CI中守护修复
  4. 开发的修复落地后,重新运行测试:它应该转为绿色。使用同一个测试,不要修改。
预期状态:修复前红态,修复后绿态
违背目标的错误做法——测试必须真正失败直到该Bug被修复:修复落地后才编写测试;禁用失败测试(标记为待处理、使用同义反复断言、移除断言)以保持CI绿色;先修复再添加测试。保持断言生效,让测试变红。
查看
references/determinism.md
获取包含断言的确定性测试示例。

Step 6: Verify the fix actually fixes it

步骤6:验证修复是否真正解决问题

The dev pushed a fix and the regression test now passes. Green is necessary but not sufficient — a test can pass for an unrelated reason. Do not close on green alone or trust the dev's word. Run the validity check:
  1. Revert the fix temporarily (stash it, or
    git stash
    /comment the fix line) and re-run the test. Confirm it still fails without the fix. That proves the test actually exercises this bug (the test catches the bug) — that it passes because of the fix, and is not passing for another reason unrelated to the defect.
  2. Restore the fix and confirm green returns.
  3. Re-run deterministically several times (
    --repeat-each
    / a loop) to confirm the green is stable, not a lucky pass.
Only when the test is red-without-fix and green-with-fix, repeatably, is the fix verified. This revert-to-verify step is the whole point of the regression test and is the one most often left out.

开发推送了修复,回归测试现在显示通过。绿态是必要条件,但不是充分条件——测试通过可能是因为无关原因。不要仅因绿态就关闭工单,也不要轻信开发的说法。执行有效性检查:
  1. 临时回滚修复(暂存修复,或使用
    git stash
    /注释修复代码),重新运行测试。确认在没有修复时测试仍然失败。这证明测试确实覆盖了该Bug(测试能捕获Bug)——测试通过是因为修复,而非与缺陷无关的其他原因。
  2. 恢复修复,确认测试再次转为绿色。
  3. 多次确定性运行(使用
    --repeat-each
    /循环),确认绿态是稳定的,而非偶然通过。
只有当测试在无修复时变红、有修复时变绿,且结果可重复时,修复才被验证通过。这个回滚验证步骤是回归测试的核心意义,也是最常被忽略的步骤。

Step 7: Flaky vs environment vs not-reproducible

步骤7:不稳定Bug vs 环境特定Bug vs 无法复现

You spent two hours and it won't reproduce for you, but it clearly happens for the user. Do not close as "cannot reproduce" immediately, do not assume flaky and quarantine, and do not conclude "doesn't repro on my machine so it isn't real." These are three distinct diagnoses with distinct evidence:
DiagnosisDiscriminating evidenceWhat you do
FlakySame code, same env, passes and fails on the same commit — run
--repeat-each 50
(or rerun the same command many times) in one environment and watch it flip
Find the non-determinism (time/RNG/network/race), make it deterministic (Step 4)
Environment-specificReproduces only under a different config — timezone, locale, viewport, OS, browser version, CI vs local — and is stable within that configMatch the user's environment: reproduce their timezone/locale/OS/browser, then minimize
Data-dependentReproduces only with the user's specific account/inputGet and replicate their data/fixture; the bug rides on the input, not the platform
Genuinely not reproducibleNone of the above reproduces after matching env + data + repeat runsDocument what you tried (envs, run counts, data) and the negative result — don't silently close
The step bare attempts miss: match the reported user environment and data before judging — replicate their config (timezone, locale, OS, browser version) and reproduce their env, then re-run.
--repeat-each
in the same env isolates true flakiness; cross-env divergence points to environment-specific; input-dependence points to data-specific.

你花了两小时仍无法复现,但用户那边确实出现了问题。不要立即标记为「无法复现」,不要假设是不稳定Bug并隔离,也不要得出「我这里无法复现所以不是真实Bug」的结论。这是三个不同的诊断,需要不同的证据:
诊断结果区分证据处理方式
不稳定Bug相同代码、相同环境,同一提交时而通过时而失败——在同一环境中运行
--repeat-each 50
(或多次运行同一命令),观察结果变化
找到非确定性因素(时间/随机数/网络/竞态),让复现具有确定性(步骤4)
环境特定Bug仅在特定配置下复现——时区、区域设置、视口、操作系统、浏览器版本、CI vs 本地环境——且在该配置内结果稳定匹配用户的环境:复现用户的时区/区域设置/操作系统/浏览器,然后最小化复现案例
数据依赖Bug仅使用用户的特定账户/输入时复现获取并复制用户的数据/测试用例;Bug与输入相关,而非平台
真正无法复现在匹配环境+数据+多次运行后仍无法复现记录你尝试的内容(环境、运行次数、数据)和负面结果——不要悄悄关闭工单
常见的错误尝试:在判断前匹配报告的用户环境和数据——复制用户的配置(时区、区域设置、操作系统、浏览器版本)和环境,然后重新运行。在同一环境中使用
--repeat-each
可以隔离真正的不稳定Bug;跨环境差异指向环境特定Bug;输入依赖指向数据特定Bug。

Step 8: Write the evidence back into the ticket

步骤8:将证据写回工单

Reproduction and the committed regression test are done. Replace the vague original report with a structured block — do not paste the raw 14-step UI walkthrough, and do not just write "reproduced, closing." Include all seven elements:
  1. Minimal steps / repro command — the smallest path, not the walkthrough.
  2. Environment + build/commit — exact SHA and platform.
  3. Expected vs actual — the concrete numbers.
  4. Introducing commit — the offending commit from
    git bisect
    .
  5. Regression test — link to the committed test file and path.
  6. Evidence — logs, screenshot, trace, or artifact.
  7. Determinism notes — seed, frozen time, and stubs so anyone can re-run identically.
See
references/ticket-writeback.md
for the copy-paste Markdown structure.

复现案例和已提交的回归测试已完成。用结构化内容替换模糊的原始报告——不要粘贴原始的14步UI操作指南,也不要只写「已复现,关闭工单」。包含以下7个元素:
  1. 最小化步骤/复现命令——最简化的路径,而非操作指南。
  2. 环境+构建/提交——确切的SHA和平台。
  3. 预期值 vs 实际值——具体的数值。
  4. 引入问题的提交——来自
    git bisect
    的问题提交。
  5. 回归测试——指向已提交测试文件的链接和路径。
  6. 证据——日志、截图、追踪信息或产物。
  7. 确定性说明——随机种子、冻结时间和存根信息,以便任何人都能以相同方式重新运行。
查看
references/ticket-writeback.md
获取可直接复制的Markdown结构。

Anti-Patterns

反模式

1. Jumping to the fix before reproducing

1. 未复现就直接修复

Reading "total is wrong" and patching the total calc, or guessing "looks like a rounding bug," before you can make it fail on demand. You can't prove an unreproduced fix works. Extract the repro first (Step 1).
看到「总额错误」就修改总额计算逻辑,或猜测「看起来像是舍入Bug」,但还无法按需触发Bug。你无法证明未复现的修复有效。先提取复现信息(步骤1)。

2. Minimizing before confirming it reproduces

2. 未确认复现就开始最小化

Stripping steps from a repro you never confirmed actually fails. You end up "minimizing" something that was never broken. Confirm the baseline fails, then cut.
从未确认是否真正失败的复现案例中移除步骤。你最终会「简化」一个从未出现问题的流程。先确认基准流程会失败,开始删减。

3. Removing several variables at once

3. 一次移除多个变量

Cutting three steps in one pass, so when it stops reproducing you don't know which one mattered. Remove one variable at a time and re-run after each.
一次移除三个步骤,当Bug不再复现时,你无法知道哪个变量是关键的。每次只移除一个变量,之后重新运行。

4. Bisecting the whole suite, or by hand

4. 对全量测试套件进行二分查找,或手动查找

git bisect run npm test:all
lets unrelated failures mark commits bad; manual checkout-and-test is slow and error-prone. Run one targeted test under
git bisect run
.
git bisect run npm test:all
会让无关失败标记提交为异常;手动检出并测试既慢又容易出错。在
git bisect run
下运行一个针对性测试。

5. Exit 1 on build failure during bisect

5. 二分查找时构建失败返回1

Treating an unbuildable commit as "bug present." It marks clean commits bad and corrupts the search. Return exit 125 (skip) for untestable commits; reserve non-zero for the genuine bug.
将无法构建的提交视为「存在Bug」。这会标记正常提交为异常,破坏搜索结果。对无法测试的提交返回exit 125(跳过);将非零退出码留给真正的Bug。

6. Live time, RNG, or network in the repro

6. 复现案例中使用实时时间、随机数或真实网络

Leaving
new Date()
and
Math.random
unmocked, hitting the real pricing API, or "waiting for midnight" with a sleep. The repro becomes a coin flip. Freeze time, seed the RNG, stub the network (Step 4).
未模拟
new Date()
Math.random
,调用真实的定价API,或用sleep「等待午夜」。复现结果会变得随机。冻结时间、设置随机种子、存根网络(步骤4)。

7. Writing the test after the fix, or disabling it

7. 修复后才编写测试,或禁用测试

Adding the regression test once the bug is already gone, or neutering a failing test (pending markers, a tautological assertion, a removed assertion) to keep CI green. The test never proves it catches the bug. Write it red, before the fix.
Bug修复后才添加回归测试,或弱化失败测试(标记为待处理、使用同义反复断言、移除断言)以保持CI绿色。测试无法证明它能捕获Bug。先编写红态测试,再修复。

8. Closing on green without revert-to-verify

8. 仅因绿态就关闭工单,未进行回滚验证

"The test passes, close it." A test can pass for the wrong reason. Revert the fix, confirm it goes red again, restore, confirm green.
「测试通过,关闭工单」。测试通过可能是因为无关原因。回滚修复,确认测试变红,恢复修复,确认变绿。

9. Closing as "cannot reproduce" on first failure to repro

9. 首次无法复现就标记为「无法复现」

Collapsing flaky / environment-specific / not-reproducible into one dismissal. Match the user's environment and data and use
--repeat-each
before judging (Step 7).

将不稳定/环境特定/无法复现混为一谈,直接关闭工单。先匹配用户的环境和数据,使用
--repeat-each
再判断(步骤7)。

Failure Modes

故障模式

SymptomLikely causeFix / check
Bisect lands on an obviously unrelated commitFull suite or flaky failures marking commits badSwitch to one targeted test; wrap with exit-125 skip + network stub
Repro passes locally, fails in CI (or vice versa)Environment-specific (TZ, locale, OS, browser)Pin
TZ
/
LANG
; match the failing environment (Step 7)
Test is green but you're not sure it catches the bugNever ran it redRevert the fix and confirm it fails (Step 6)
vi.setSystemTime
has no effect
Called before
vi.useFakeTimers()
Call
useFakeTimers()
first
page.clock
time not applied to app startup
install
/
setFixedTime
ran after
page.goto
Move clock setup before navigation
Repro flips pass/fail run to runLive time/RNG/network not pinnedApply Step 4; confirm with
--repeat-each 10

症状可能原因修复/检查方法
二分查找定位到明显无关的提交全量测试套件或不稳定失败标记提交为异常切换为单个针对性测试;使用返回125的跳过逻辑+网络存根进行包装
本地复现通过,CI中失败(反之亦然)环境特定(时区、区域设置、操作系统、浏览器)固定
TZ
/
LANG
;匹配失败的环境(步骤7)
测试变绿,但不确定是否能捕获Bug从未运行过红态测试回滚修复,确认测试失败(步骤6)
vi.setSystemTime
无效
vi.useFakeTimers()
之前调用
先调用
useFakeTimers()
page.clock
时间未应用到应用启动
install
/
setFixedTime
page.goto
之后运行
将时钟设置移至导航之前
复现结果时而通过时而失败未固定实时时间/随机数/网络执行步骤4;使用
--repeat-each 10
确认

Verification

验证标准

  • The repro command/test fails on demand: run it 10× (
    --repeat-each 10
    or a loop) and confirm it fails every time before the fix.
  • git bisect run …
    terminates with "
    <sha>
    is the first bad commit" and
    git bisect reset
    leaves you on the original
    HEAD
    .
  • With the fix reverted the regression test exits non-zero; with the fix applied it exits 0.
  • The ticket block contains all seven write-back elements (Step 8) — grep it for the commit SHA, the test path, and the determinism notes.

  • 复现命令/测试可按需失败:运行10次(
    --repeat-each 10
    或循环),确认修复前每次都失败。
  • git bisect run …
    执行完成后显示「
    <sha>
    is the first bad commit」,且
    git bisect reset
    将你恢复到原始
    HEAD
  • 回滚修复后回归测试返回非零退出码;应用修复后返回0。
  • 工单内容包含所有7个写入元素(步骤8)——检查是否包含提交SHA、测试路径和确定性说明。

Done When

完成标准

  • A documented minimal reproduction exists — smallest steps or a single command — that fails on demand, verified failing across repeated runs.
  • If the bug is a regression,
    git bisect
    has named the introducing commit SHA and it is recorded in the ticket.
  • The repro is deterministic: time frozen, RNG seeded, network stubbed — proven by 10 identical consecutive runs.
  • A regression test is committed that was red before the fix and green after, and was confirmed to fail when the fix is reverted.
  • The ticket carries the structured evidence block with all seven elements (minimal steps, environment/build, expected vs actual, introducing commit, regression-test link, evidence artifact, determinism notes); the original vague report is replaced, not left.
  • If it did not reproduce, it is classified (flaky / environment-specific / data-dependent / not-reproducible) with the evidence that led there — never silently closed.

  • 存在文档化的最小化复现案例——最简化的步骤或单个命令,可按需失败,且多次运行均验证失败。
  • 如果是回归Bug,
    git bisect
    已定位到引入问题的提交SHA,并记录在工单中。
  • 复现案例具有确定性:时间冻结、随机种子设置、网络存根——通过10次连续相同运行验证。
  • 已提交回归测试,该测试修复前红态,修复后绿态,且回滚修复后会再次失败。
  • 工单包含结构化证据块,包含所有7个元素(最小化步骤、环境/构建、预期值vs实际值、引入问题的提交、回归测试链接、证据产物、确定性说明);原始模糊报告被替换,而非保留。
  • 如果无法复现,已分类为(不稳定/环境特定/数据依赖/无法复现),并记录了得出该结论的证据——绝不悄悄关闭工单。

Related Skills

相关技能

  • ai-bug-triage
    — Classify, deduplicate, and severity-route existing failures. Triage decides whether and where a failure matters; come here to actually reproduce one and write the failing test. Triage hands off; bug-reproduction picks up.
  • ai-test-generation
    — Generate tests from specs/PRDs/stories. Use it when the source is a requirement; use this skill when the source is a defect and the test must first go red against the bug.
  • test-reliability
    — Runtime self-healing and quarantine for a flaky test. When Step 7 diagnoses true flakiness, go there to stabilize or quarantine; here you only diagnose.
  • qa-project-context
    — Stack, test runner, environment matrix, and known-flaky areas that shape every step above. Check it first.
  • systematic-debugging (
    superpowers:systematic-debugging
    ) — The general root-cause debugging loop once you have a deterministic repro; this skill produces that repro and the failing test that guards the eventual fix.
  • ai-bug-triage
    ——对现有失败进行分类、去重和严重程度路由。分类决定了失败是否重要以及归属;本技能用于实际复现失败并编写测试。分类是前置步骤;Bug复现是后续工作。
  • ai-test-generation
    ——从需求规格/PRD/用户故事生成测试。当来源是需求时使用该技能;当来源是缺陷且测试必须先针对Bug变红时,使用本技能。
  • test-reliability
    ——针对不稳定测试的运行时自愈和隔离。当步骤7诊断为真正的不稳定Bug时,使用该技能进行稳定化或隔离;本技能仅负责诊断。
  • qa-project-context
    ——技术栈、测试运行器、环境矩阵和已知不稳定区域,会影响上述所有步骤。首先查看该技能的内容。
  • systematic-debugging
    superpowers:systematic-debugging
    )——获得确定性复现案例后的通用根因调试循环;本技能生成该复现案例和守护最终修复的测试。",