写作与公文
当收到模糊缺陷报告时,如何端到端复现、最小化…
把一句模糊的 bug 报告变成可验证的最小复现、确定性的失败回归测试,并用 git bisect 找出引入问题的提交。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
当收到模糊缺陷报告时,如何端到端复现、最通用
解决思路
- 先检查项目根目录的 .agents/qa-project-context.md,它包含技术栈、测试运行器、已知不稳定区域和环境矩阵;已覆盖的问题不再重复询问;若缺失,建议用 qa-project-context 技能创建。
- 在动代码前先提取隐式复现信息:逐字记录报告原文、是否已能复现及可靠程度、以前是否正常、测试运行器与技术栈、非确定性来源(时间规则、随机性、第三方 API、locale)。
- 对“金额算错”这类数据正确性 bug,补齐报告常缺的关键维度:精确复现步骤、构建/版本/commit(git SHA)、环境(浏览器+版本、OS、设备)、输入数据(购物车内容、数量、账号、优惠券、精确 fixture)、期望值 vs 实际值、频率、locale/时区/货币、发生时间戳及日志/截图/网络抓包。
- 把上述维度写成一份复现规格(repro spec);空白项就是下一个要问报告者的问题,不要凭空编造复现步骤,也不要提前推测根因。
- 执行 reproduce→minimize→isolate→capture 循环:先跑完整复现确认基线确实失败;然后一次只删一个步骤/字段/依赖并重跑,仍失败则保留该删减,不再失败则说明该元素是关键,恢复它;重复直到每个剩余元素都必要。
- 隔离:把失败缩小到仍能复现的最小层——从 3 页 UI 流程降到单页,再降到针对问题函数的单元/API 调用。
- 捕获:把最小复现记录为证据——最小步骤或单条命令,加上日志/追踪/截图,供开发者和回归测试使用。
- 若 bug 是回归(HEAD 有问题、过去版本正常)且有一条“有 bug 时退出非零”的命令,用 git bisect run 自动二分查找:git bisect start;git bisect bad HEAD(别名 git bisect new);git bisect good v2.4.0(别名 git bisect old);git bisect run npm test -- checkout-total.spec.ts(只跑一个针对性测试,绝不跑全量套件);bisect 打印 “<sha> is the first bad commit”;最后必须 git bisect reset 恢复原 HEAD。
- 遵守 git bisect run 的退出码约定:退出码 0 = good(无 bug),非零(1–124)= bad(有 bug),退出码 125 = skip(无法测试);命令必须在功能正常时返回 0、bug 复现时返回非零。
- 处理 bisect 的不可测试/不稳定提交:老提交编译失败时包装脚本必须 exit 125(skip),不能当作 bad;bisect 期间强制确定性(stub 网络、固定 TZ、seed RNG),避免偶发网络/时序失败被误判为 bad;若某提交结果在多次运行间翻转,按不可测试(exit 125)处理。
- 编写包装脚本,返回 0 = good、1 = bad、125 = skip(合法 bad 范围是 1–127 但排除 125),守护构建、stub 网络、重试一次以捕获偶发失败,然后执行 git bisect run ./bisect-step.sh。
- 让复现确定化:固定时间、随机性和网络,使每次运行都以相同方式失败;不要等午夜、不要打真实定价 API、不要用 sleep/setTimeout/waitForTimeout 掩盖时序问题。
- Vitest 中:vi.useFakeTimers() 后再 vi.setSystemTime(new Date('…'));在 beforeEach 中 faker.seed(1337)(或 stub Math.random);用 MSW setupServer + http.get → HttpResponse.json,并设 onUnhandledRequest: 'error' 让漏掉的 stub 显式失败。
- Playwright 中:page.clock.install({ time }) + page.clock.setFixedTime(…) 必须在 page.goto 之前执行;用 page.route(...) → route.fulfill(...) stub 网络;不要手写 Date 覆盖,也不要靠调大超时让它通过。
- 当 bug 与 locale 相关时,固定 locale/时区/货币(TZ=UTC、LANG)。
- 避免 jest.useFakeTimers('legacy')(及 timers: 'legacy')——legacy 假定时器已废弃且不 mock Date/Date.now,时钟仍在走;现代定时器自 Jest 27 起为默认,直接 jest.useFakeTimers() + jest.setSystemTime(),Vitest 用 vi.useFakeTimers()。
- 在修复前先写失败的回归测试(TDD-for-bugs):把最小复现编码为断言真实期望值的测试(如 expect(total).toBe(2754)),不要写恒真的同义反复断言。
- 运行并确认它在修复前失败——必须先红;不红的测试没有覆盖该 bug。
- 提交该测试(或暂存到修复分支),让它在 CI 中守护修复。
- 开发者的修复落地后重跑同一测试(不做任何修改),应翻转为绿;期望状态是修复前红、修复后绿。
- 避免这些错误做法:修复已落地后才写测试;为保持 CI 绿而禁用失败测试(pending 标记、恒真断言、删除断言);先修复再补测试。
- 验证修复确实修好了:临时 revert 修复(stash 或注释掉修复行)并重跑测试,确认没有修复时仍然失败,证明测试确实覆盖此 bug、且是因为修复才通过。
- 恢复修复并确认重新变绿。
- 多次确定性重跑(--repeat-each 或循环)确认绿色稳定,不是侥幸通过。
- 区分 flaky / 环境特定 / 数据依赖 / 真正无法复现:flaky 的判别证据是同一 commit、同一环境下时过时不过(用 --repeat-each 50 或多次重跑同一命令观察翻转),处理办法是找出非确定性来源并确定化。
- 环境特定的判别证据是只在不同配置下复现(时区、locale、视口、OS、浏览器版本、CI vs 本地)且在该配置内稳定;处理办法是匹配用户环境(复现其 timezone/locale/OS/浏览器)后再最小化。
- 数据依赖的判别证据是只在用户特定账号/输入下复现;处理办法是拿到并复刻其数据/fixture。
- 真正无法复现的判别证据是匹配环境+数据并多次重跑后仍无法复现;处理办法是记录尝试过的环境、运行次数、数据和负面结果,不要静默关闭。
- 不要一复现不出来就关闭为 “cannot reproduce”,不要直接假设 flaky 并隔离,也不要因为“我机器上不复现”就断定不是真 bug。
- 把证据写回工单:用结构化块替换模糊的原始报告,不要粘贴原始 14 步 UI 走查,也不要只写“已复现,关闭”。
- 工单证据块包含全部七项:最小步骤/复现命令;环境+构建/commit(精确 SHA 和平台);期望 vs 实际(具体数字);引入提交(git bisect 得到的 commit);回归测试(已提交测试文件链接和路径);证据(日志、截图、trace 或 artifact);确定性说明(seed、冻结时间、stub),以便任何人可一致重跑。
- 验证清单:修复前复现命令/测试按需失败——跑 10 次(--repeat-each 10 或循环)确认每次都失败;git bisect run 以 “<sha> is the first bad commit” 结束且 git bisect reset 回到原 HEAD;revert 修复时回归测试退出非零、应用修复时退出 0;工单块包含全部七项写回元素(可 grep commit SHA、测试路径和确定性说明)。
- 完成标准:存在文档化的最小复现(最小步骤或单条命令)且按需失败、多次重跑验证失败;若是回归,git bisect 已命名引入提交 SHA 并记录在工单;复现确定(时间冻结、RNG 播种、网络 stub),由连续 10 次相同运行证明;已提交的回归测试修复前红、修复后绿,且 revert 修复时确认失败;工单带有全部七项结构化证据块,原始模糊报告被替换而非保留;若未复现,则分类(flaky/环境特定/数据依赖/无法复现)并附上得出该结论的证据,绝不静默关闭。
适用边界 · 注意事项
- 不要用于对已有失败进行分类、去重、严重级路由而不复现它们——那是 ai-bug-triage。
- 不要用于从规格/PRD/故事生成测试——那是 ai-test-generation;本技能用于来源是缺陷、测试必须先对 bug 变红的场景。
- 不要用于 flaky 测试的运行时自愈和隔离——那是 test-reliability;本技能只做诊断。