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 it | Step 1: Extract the implicit repro |
| A messy 14-step repro to clean up | Step 2: The reproduce-minimize-isolate-capture loop |
| "Worked last month, broken now" | Step 3: Bisect to the introducing commit |
| A repro that passes/fails inconsistently | Step 4: Make the repro deterministic |
| A clean repro, no fix yet | Step 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 done | Step 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 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 skill.
.agents/qa-project-context.mdqa-project-context- 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 to the introducing commit; no known-good point means you debug forward instead.
git bisect - What is the test runner and stack? Vitest/Jest vs Playwright changes the determinism
API (vs
vi.setSystemTime) and where the regression test lands.page.clock - 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.mdqa-project-context- **原始报告内容是什么?**报告越简略,你越需要在接触代码前提取所有信息——一句话报告决定了整个处理流程(步骤1)。
- 目前是否能复现,以及复现的可靠性如何?「每次都能复现」vs「有时能复现」决定了你是直接进入最小化步骤,还是先处理确定性问题。
- **该功能曾经正常工作过吗?**已知正常的过往版本可以通过定位引入问题的提交;如果没有已知正常版本,则需要正向调试。
git bisect - **使用的测试运行器和技术栈是什么?**Vitest/Jest与Playwright的确定性API不同(vs
vi.setSystemTime),回归测试的存放位置也不同。page.clock - **非确定性因素有哪些?**时间规则、随机性、第三方API、区域设置——这些都必须固定,才能让复现结果可信。
Core Principles
核心原则
-
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.
-
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.
-
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.
-
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.
-
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.
-
先复现,再推理。最容易犯的错误是看到症状就直接猜测根因(「听起来像是浮点数舍入Bug,我来修改总额计算逻辑」)。不要这么做。先提取复现信息,让Bug可以按需触发,之后再形成假设。没有复现案例的修复只是无法验证的猜测。
-
复现案例是确定性产物,而非描述。「午夜时分使用随机代码时会出现问题」是描述。需要冻结时钟、设置随机种子、存根网络,让相同的输入在每次运行、每台机器上都产生相同的失败。如果复现结果不确定,你就无法进行二分查找、测试或证明问题已修复。
-
每次只最小化一个变量,每次修改后重新确认。简化复现案例是一个搜索过程,而非重写。移除一个步骤、数据字段或依赖,然后重新运行,确认Bug仍然可以复现。如果一次移除多个元素,你无法知道哪个元素是关键的。
-
回归测试要先写成红态(失败),再修复。断言真实的预期值,比如,先让测试失败(证明它能捕获该Bug),再确认修复后测试转为绿色。在修复后添加的测试,或被禁用/标记为待处理的测试,无法证明任何事情。
expect(total).toBe(2754) -
测试变绿不代表完成——回滚验证才是关键。测试通过可能是因为无关原因。临时移除修复,确认测试再次变红。只有在没有修复时会失败的测试,才能真正防止该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 may be correct in
de-DE.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 for the full extraction checklist, why each load-bearing
dimension matters for "wrong total," and the repro-spec template.
references/intake.md简略报告(比如「结账功能坏了,订单总额有时不正确」)只描述了症状,没有给出触发路径。在接触代码前,需要提取或询问所有与复现相关的维度。绝不要凭空想象复现步骤,也不要现在就推理根因——这两者都要等到Bug可以复现之后再做。
对于「总额错误」/数据正确性类Bug,简略报告最常遗漏的关键维度包括:
- 准确的复现步骤——点击操作的详细路径,而非「结账功能坏了」。
- 构建/版本/提交(git SHA)——报告者可能使用的版本中Bug已经被修复。
- 环境——浏览器及版本、操作系统、设备。
- 输入数据——购物车内容、数量、账户/用户、优惠券、确切的测试数据。总额是输入的纯函数;没有输入数据,你只是在猜测。
- 预期值 vs 实际值——用户预期的数值和实际看到的数值。
- 出现频率——每次都出现,还是间歇性出现?「有时出现」指向非确定性因素。
- 区域设置/时区/货币——舍入、税费和格式都与区域设置相关;在下「错误」的总额在
de-DE下可能是正确的。en-US - 出现时间戳,以及任何日志/截图/网络追踪信息。
在接触代码前,将这些信息整理成一份复现规格。如果有空白项,你的下一个任务是向报告者询问——而不是开始编写修复或推理根因。
查看获取完整的提取清单、每个关键维度对「总额错误」Bug的重要性,以及复现规格模板。
references/intake.mdStep 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:
- REPRODUCE / confirm. First establish a baseline: run the full repro and confirm it actually fails. You can only minimize something that currently reproduces.
- 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.
- 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.
- 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步的版本交给开发,也不要全盘重写。执行以下循环:
- 复现/确认。首先建立基准:运行完整的复现流程,确认Bug确实会失败。你只能简化当前可以复现的内容。
- 最小化。移除一个步骤、字段或依赖。重新运行。如果Bug仍然可以复现,保留这次修改;如果不再失败,说明该元素是关键的——恢复它。重复此过程,每次只修改一个变量,直到剩余的每个元素都是必要的。核心规则:在确认可以复现前不要开始最小化,每次修改后都要验证Bug是否仍然失败。
- 隔离。将失败范围缩小到最小的仍能出现Bug的层级——从跨3个页面的UI流程缩小到单个页面,再如果Bug存在于UI之下,缩小到针对问题函数的单元/API调用。
- 捕获。将现在的最小化复现案例记录为证据:最简化的步骤或单个命令,加上日志/追踪/截图。这是开发和回归测试要使用的内容。
输出结果是最小的仍能复现Bug的流程——而非原始的操作指南。
Step 3: Bisect to the introducing commit
步骤3:二分查找定位引入问题的提交
The bug is on but a past release was clean, and you have a command that exits
non-zero when the bug is present. Use to binary-search history
automatically — do not manually check out each commit, and do not use to hunt
for it.
HEADgit bisect rungit revertsh
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 suiteBug出现在版本,但过往某个版本是正常的,且你有一个在Bug存在时会返回非零退出码的命令。使用自动进行历史二分查找——不要手动检出每个提交,也不要使用来查找问题。
HEADgit bisect rungit revertsh
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 (skip) on build failure, distinguishing it from a real bad commit.
exit 125 - Flaky network/timing failures. A transient un-stubbed third-party call exits 1 and
gets blamed. Force determinism during bisect — stub the network, pin , seed the RNG — so only the real bug can fail the step. If a commit's result flips between runs, treat it as untestable (
TZ), not bad.exit 125
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 . Full wrapper in
.
git bisect run ./bisect-step.shreferences/bisect.md两种情况会破坏简单的二分查找,而默认的错误处理——「任何失败都返回1」——会同时触发这两种情况:
- 旧提交无法构建。编译错误会返回1,二分查找会将其解读为「存在Bug」,标记正常提交为异常。错误做法:无法构建的提交是无法测试的——你的包装脚本必须在构建失败时(跳过),将其与真正的异常提交区分开。
exit 125 - 不稳定的网络/时序失败。临时未存根的第三方调用返回1并被归咎为Bug。在二分查找期间强制确定性——存根网络、固定、设置随机种子——只有真正的Bug会导致步骤失败。如果某个提交的结果在多次运行中变化,将其视为无法测试(
TZ),而非异常。exit 125
将步骤包装在一个脚本中,返回0 = 正常,1 = 异常,125 = 跳过(有效的异常范围是1–127,排除125),检查构建状态、存根网络,并重试一次以处理不稳定情况。然后运行。完整的包装脚本见。
git bisect run ./bisect-step.shreferences/bisect.mdStep 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
// to paper over timing.
sleepsetTimeoutwaitForTimeout| Source | Vitest | Playwright |
|---|---|---|
| Time | | |
| Randomness | | seed the app's RNG via an init hook |
| Network | MSW | |
Key points:
- Vitest: only works after
vi.setSystemTime. Seed faker invi.useFakeTimers(). Set MSWbeforeEachso a missed stub fails loudly.onUnhandledRequest: 'error' - Playwright: /
page.clock.installmust run beforesetFixedTime.page.gotois the supported API — it exists; do not fall back to a hand-rolledpage.clockoverride, and do not bump the timeout to "make it pass."Date - Pin locale/timezone/currency (,
TZ=UTC) when the bug is locale-sensitive.LANG
Avoid: (and ) — legacy fake timers are
deprecated and don't mock /, so the clock stays live and your "frozen"
repro still drifts. Modern timers are the default since Jest 27 — just call
+ , or in Vitest. (Jest 30,
2025)
jest.useFakeTimers('legacy')timers: 'legacy'DateDate.nowjest.useFakeTimers()jest.setSystemTime()vi.useFakeTimers()See for the full Vitest ( +
references/determinism.mdvi.useFakeTimersvi.setSystemTime- + MSW
faker.seed) and Playwright (setupServer+page.clock) recipes, plus a 10-run determinism check.page.route
「仅在午夜出现,使用随机折扣码,调用第三方定价API」的Bug有三个非确定性因素。需要固定所有三个因素,让它每次运行都以相同方式失败——不要等待午夜,不要让它调用真实的定价API,绝不使用//来掩盖时序问题。
sleepsetTimeoutwaitForTimeout| 非确定性来源 | Vitest | Playwright |
|---|---|---|
| 时间 | | |
| 随机性 | | 通过初始化钩子设置应用的随机种子 |
| 网络 | MSW | |
关键点:
- Vitest:必须在
vi.setSystemTime之后调用。在vi.useFakeTimers()中设置faker种子。设置MSW的beforeEach,以便遗漏的存根会直接报错。onUnhandledRequest: 'error' - Playwright:/
page.clock.install必须在setFixedTime之前运行。page.goto是官方支持的API——不要使用手动编写的page.clock覆盖,也不要通过延长超时时间来「让测试通过」。Date - 当Bug与区域设置相关时,固定区域设置/时区/货币(,
TZ=UTC)。LANG
避免:(以及)——旧版假计时器已被弃用,无法模拟/,所以时钟仍然是实时的,你的「冻结」复现案例仍会产生偏差。自Jest 27起,现代计时器是默认选项——只需调用 + ,或在Vitest中调用。(Jest 30,2025)
jest.useFakeTimers('legacy')timers: 'legacy'DateDate.nowjest.useFakeTimers()jest.setSystemTime()vi.useFakeTimers()查看获取完整的Vitest( + + + MSW )和Playwright( + )配置示例,以及10次运行的确定性检查方法。
references/determinism.mdvi.useFakeTimersvi.setSystemTimefaker.seedsetupServerpage.clockpage.routeStep 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:
- Encode the minimal repro as a test that asserts the real expected value —
. A tautological assertion that always passes proves nothing.
expect(total).toBe(2754) - 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.
- Commit the test (or stage it on the fix branch) so the test guards the fix in CI.
- 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 for the deterministic test bodies the assertion sits in.
references/determinism.md你已有清晰的确定性复现案例,且开发尚未修复Bug。现在编写回归测试,遵循Bug驱动的TDD流程:
- 将最小化复现案例编码为测试,断言真实的预期值——比如。总是通过的同义反复断言无法证明任何事情。
expect(total).toBe(2754) - 运行测试,确认它在修复前失败——必须先处于红态。不会变红的测试无法覆盖该Bug。
- 提交测试(或在修复分支中暂存),让测试在CI中守护修复。
- 开发的修复落地后,重新运行测试:它应该转为绿色。使用同一个测试,不要修改。
预期状态:修复前红态,修复后绿态。
违背目标的错误做法——测试必须真正失败直到该Bug被修复:修复落地后才编写测试;禁用失败测试(标记为待处理、使用同义反复断言、移除断言)以保持CI绿色;先修复再添加测试。保持断言生效,让测试变红。
查看获取包含断言的确定性测试示例。
references/determinism.mdStep 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:
- Revert the fix temporarily (stash it, or /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.
git stash - Restore the fix and confirm green returns.
- Re-run deterministically several times (/ a loop) to confirm the green is stable, not a lucky pass.
--repeat-each
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.
开发推送了修复,回归测试现在显示通过。绿态是必要条件,但不是充分条件——测试通过可能是因为无关原因。不要仅因绿态就关闭工单,也不要轻信开发的说法。执行有效性检查:
- 临时回滚修复(暂存修复,或使用/注释修复代码),重新运行测试。确认在没有修复时测试仍然失败。这证明测试确实覆盖了该Bug(测试能捕获Bug)——测试通过是因为修复,而非与缺陷无关的其他原因。
git stash - 恢复修复,确认测试再次转为绿色。
- 多次确定性运行(使用/循环),确认绿态是稳定的,而非偶然通过。
--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:
| Diagnosis | Discriminating evidence | What you do |
|---|---|---|
| Flaky | Same code, same env, passes and fails on the same commit — run | Find the non-determinism (time/RNG/network/race), make it deterministic (Step 4) |
| Environment-specific | Reproduces only under a different config — timezone, locale, viewport, OS, browser version, CI vs local — and is stable within that config | Match the user's environment: reproduce their timezone/locale/OS/browser, then minimize |
| Data-dependent | Reproduces only with the user's specific account/input | Get and replicate their data/fixture; the bug rides on the input, not the platform |
| Genuinely not reproducible | None of the above reproduces after matching env + data + repeat runs | Document 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. in the same env isolates true flakiness; cross-env
divergence points to environment-specific; input-dependence points to data-specific.
--repeat-each你花了两小时仍无法复现,但用户那边确实出现了问题。不要立即标记为「无法复现」,不要假设是不稳定Bug并隔离,也不要得出「我这里无法复现所以不是真实Bug」的结论。这是三个不同的诊断,需要不同的证据:
| 诊断结果 | 区分证据 | 处理方式 |
|---|---|---|
| 不稳定Bug | 相同代码、相同环境,同一提交时而通过时而失败——在同一环境中运行 | 找到非确定性因素(时间/随机数/网络/竞态),让复现具有确定性(步骤4) |
| 环境特定Bug | 仅在特定配置下复现——时区、区域设置、视口、操作系统、浏览器版本、CI vs 本地环境——且在该配置内结果稳定 | 匹配用户的环境:复现用户的时区/区域设置/操作系统/浏览器,然后最小化复现案例 |
| 数据依赖Bug | 仅使用用户的特定账户/输入时复现 | 获取并复制用户的数据/测试用例;Bug与输入相关,而非平台 |
| 真正无法复现 | 在匹配环境+数据+多次运行后仍无法复现 | 记录你尝试的内容(环境、运行次数、数据)和负面结果——不要悄悄关闭工单 |
常见的错误尝试:在判断前匹配报告的用户环境和数据——复制用户的配置(时区、区域设置、操作系统、浏览器版本)和环境,然后重新运行。在同一环境中使用可以隔离真正的不稳定Bug;跨环境差异指向环境特定Bug;输入依赖指向数据特定Bug。
--repeat-eachStep 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:
- Minimal steps / repro command — the smallest path, not the walkthrough.
- Environment + build/commit — exact SHA and platform.
- Expected vs actual — the concrete numbers.
- Introducing commit — the offending commit from .
git bisect - Regression test — link to the committed test file and path.
- Evidence — logs, screenshot, trace, or artifact.
- Determinism notes — seed, frozen time, and stubs so anyone can re-run identically.
See for the copy-paste Markdown structure.
references/ticket-writeback.md复现案例和已提交的回归测试已完成。用结构化内容替换模糊的原始报告——不要粘贴原始的14步UI操作指南,也不要只写「已复现,关闭工单」。包含以下7个元素:
- 最小化步骤/复现命令——最简化的路径,而非操作指南。
- 环境+构建/提交——确切的SHA和平台。
- 预期值 vs 实际值——具体的数值。
- 引入问题的提交——来自的问题提交。
git bisect - 回归测试——指向已提交测试文件的链接和路径。
- 证据——日志、截图、追踪信息或产物。
- 确定性说明——随机种子、冻结时间和存根信息,以便任何人都能以相同方式重新运行。
查看获取可直接复制的Markdown结构。
references/ticket-writeback.mdAnti-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:allgit bisect rungit bisect run npm test:allgit bisect run5. 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 and 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)。
new Date()Math.random7. 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 before judging (Step 7).
--repeat-each将不稳定/环境特定/无法复现混为一谈,直接关闭工单。先匹配用户的环境和数据,使用再判断(步骤7)。
--repeat-eachFailure Modes
故障模式
| Symptom | Likely cause | Fix / check |
|---|---|---|
| Bisect lands on an obviously unrelated commit | Full suite or flaky failures marking commits bad | Switch 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 |
| Test is green but you're not sure it catches the bug | Never ran it red | Revert the fix and confirm it fails (Step 6) |
| Called before | Call |
| | Move clock setup before navigation |
| Repro flips pass/fail run to run | Live time/RNG/network not pinned | Apply Step 4; confirm with |
| 症状 | 可能原因 | 修复/检查方法 |
|---|---|---|
| 二分查找定位到明显无关的提交 | 全量测试套件或不稳定失败标记提交为异常 | 切换为单个针对性测试;使用返回125的跳过逻辑+网络存根进行包装 |
| 本地复现通过,CI中失败(反之亦然) | 环境特定(时区、区域设置、操作系统、浏览器) | 固定 |
| 测试变绿,但不确定是否能捕获Bug | 从未运行过红态测试 | 回滚修复,确认测试失败(步骤6) |
| 在 | 先调用 |
| | 将时钟设置移至导航之前 |
| 复现结果时而通过时而失败 | 未固定实时时间/随机数/网络 | 执行步骤4;使用 |
Verification
验证标准
- The repro command/test fails on demand: run it 10× (or a loop) and confirm it fails every time before the fix.
--repeat-each 10 - terminates with "
git bisect run …is the first bad commit" and<sha>leaves you on the originalgit bisect reset.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 …is the first bad commit」,且<sha>将你恢复到原始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, has named the introducing commit SHA and it is recorded in the ticket.
git bisect - 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,已定位到引入问题的提交SHA,并记录在工单中。
git bisect - 复现案例具有确定性:时间冻结、随机种子设置、网络存根——通过10次连续相同运行验证。
- 已提交回归测试,该测试修复前红态,修复后绿态,且回滚修复后会再次失败。
- 工单包含结构化证据块,包含所有7个元素(最小化步骤、环境/构建、预期值vs实际值、引入问题的提交、回归测试链接、证据产物、确定性说明);原始模糊报告被替换,而非保留。
- 如果无法复现,已分类为(不稳定/环境特定/数据依赖/无法复现),并记录了得出该结论的证据——绝不悄悄关闭工单。
Related Skills
相关技能
- — 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-bug-triage - — 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.
ai-test-generation - — 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.
test-reliability - — Stack, test runner, environment matrix, and known-flaky areas that shape every step above. Check it first.
qa-project-context - 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.
superpowers:systematic-debugging
- ——对现有失败进行分类、去重和严重程度路由。分类决定了失败是否重要以及归属;本技能用于实际复现失败并编写测试。分类是前置步骤;Bug复现是后续工作。
ai-bug-triage - ——从需求规格/PRD/用户故事生成测试。当来源是需求时使用该技能;当来源是缺陷且测试必须先针对Bug变红时,使用本技能。
ai-test-generation - ——针对不稳定测试的运行时自愈和隔离。当步骤7诊断为真正的不稳定Bug时,使用该技能进行稳定化或隔离;本技能仅负责诊断。
test-reliability - ——技术栈、测试运行器、环境矩阵和已知不稳定区域,会影响上述所有步骤。首先查看该技能的内容。
qa-project-context - systematic-debugging()——获得确定性复现案例后的通用根因调试循环;本技能生成该复现案例和守护最终修复的测试。",
superpowers:systematic-debugging