@piwitests/reporter 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -399,7 +399,7 @@ function installPickerOverlay(arg) {
399
399
  banner.style.cssText = `position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:${Z + 2};background:#111827;color:#f9fafb;font:13px/1.5 system-ui,sans-serif;border:1px solid #312e81;padding:10px 16px;border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,.5);max-width:min(680px,86vw);`;
400
400
  const hlTokens = (expr) => {
401
401
  const escHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
402
- const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
402
+ const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
403
403
  let html = "";
404
404
  let last = 0;
405
405
  let m;
@@ -942,7 +942,7 @@ function showPickerChoices(arg) {
942
942
  panel.style.cssText = "background:#111827;color:#f9fafb;border-radius:10px;padding:20px;max-width:640px;width:90vw;max-height:70vh;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,.5);";
943
943
  const hlLocator = (expr) => {
944
944
  const escHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
945
- const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
945
+ const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
946
946
  let html = "";
947
947
  let last = 0;
948
948
  let m;
@@ -2045,6 +2045,7 @@ var DEFAULTS = {
2045
2045
  streamingBatchSize: 5,
2046
2046
  streamingBatchDelay: 2e3,
2047
2047
  maxStreamBufferBytes: 100 * 1024 * 1024,
2048
+ submitTimeout: 15 * 60 * 1e3,
2048
2049
  failOnFlakyTests: false,
2049
2050
  username: null,
2050
2051
  password: null,
@@ -2066,6 +2067,7 @@ var PIWI_ENV_KEYS = {
2066
2067
  streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
2067
2068
  streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
2068
2069
  maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
2070
+ submitTimeout: "PIWI_SUBMIT_TIMEOUT",
2069
2071
  liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
2070
2072
  failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
2071
2073
  uploadTraces: "PIWI_UPLOAD_TRACES",
@@ -2115,6 +2117,7 @@ var ENV_FALLBACK_SPECS = [
2115
2117
  { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
2116
2118
  { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
2117
2119
  { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
2120
+ { option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
2118
2121
  { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
2119
2122
  { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
2120
2123
  { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
@@ -2791,15 +2794,19 @@ function extractSelector(text) {
2791
2794
  return text.slice(start, start + 80);
2792
2795
  }
2793
2796
  var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
2794
- function extractTopFrame(text) {
2797
+ function extractStackFrames(text) {
2795
2798
  const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
2799
+ const frames = [];
2796
2800
  let m;
2797
2801
  while ((m = frameRe.exec(text)) !== null) {
2798
2802
  const file = m[1].replace(/\\/g, "/");
2799
2803
  if (file.includes("node_modules") || file.startsWith("node:")) continue;
2800
- return { file, line: Number(m[2]), column: Number(m[3]) };
2804
+ frames.push({ file, line: Number(m[2]), column: Number(m[3]) });
2801
2805
  }
2802
- return null;
2806
+ return frames;
2807
+ }
2808
+ function extractTopFrame(text) {
2809
+ return extractStackFrames(text)[0] ?? null;
2803
2810
  }
2804
2811
  function extractTopFrameFile(text) {
2805
2812
  return extractTopFrame(text)?.file ?? null;
@@ -2813,6 +2820,42 @@ var LONG_HEX_RE = /\b[0-9a-f]{8,}\b/gi;
2813
2820
  var SHORT_HEX_RE = /\b(?=[0-9a-f]*[a-f])(?=[0-9a-f]*[0-9])[0-9a-f]{6,7}\b/gi;
2814
2821
  var URL_RE = /\bhttps?:\/\/[^\s'"`)]+/gi;
2815
2822
  var EMAIL_RE = /\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b/gi;
2823
+ var SELECTOR_OPTION_KEY_RE = /\b(name|hasText|hasNotText|has|placeholder|label|title|alt|exact)\s*:\s*(['"`])/gi;
2824
+ var LINE_TERMINATORS = /* @__PURE__ */ new Set(["\n", "\r", "\u2028", "\u2029"]);
2825
+ function closingQuoteIndex(text, quote) {
2826
+ const close = new Int32Array(text.length + 1).fill(-1);
2827
+ for (let p = text.length - 1; p >= 0; p--) {
2828
+ const ch = text[p];
2829
+ if (ch === quote) {
2830
+ close[p] = p;
2831
+ continue;
2832
+ }
2833
+ const escaped = ch === "\\" && p + 1 < text.length && !LINE_TERMINATORS.has(text[p + 1]) ? close[p + 2] : -1;
2834
+ close[p] = escaped !== -1 ? escaped : close[p + 1];
2835
+ }
2836
+ return close;
2837
+ }
2838
+ function maskSelectorOptions(text) {
2839
+ const closers = /* @__PURE__ */ new Map();
2840
+ let out = "";
2841
+ let copied = 0;
2842
+ SELECTOR_OPTION_KEY_RE.lastIndex = 0;
2843
+ let match;
2844
+ while (match = SELECTOR_OPTION_KEY_RE.exec(text)) {
2845
+ const quote = match[2];
2846
+ let close = closers.get(quote);
2847
+ if (!close) closers.set(quote, close = closingQuoteIndex(text, quote));
2848
+ const end = close[SELECTOR_OPTION_KEY_RE.lastIndex];
2849
+ if (end === -1) {
2850
+ SELECTOR_OPTION_KEY_RE.lastIndex = match.index + 1;
2851
+ continue;
2852
+ }
2853
+ out += `${text.slice(copied, match.index)}${match[1]}: <STR>`;
2854
+ copied = end + 1;
2855
+ SELECTOR_OPTION_KEY_RE.lastIndex = copied;
2856
+ }
2857
+ return out + text.slice(copied);
2858
+ }
2816
2859
  function classifyError(text) {
2817
2860
  if (/strict mode violation/i.test(text)) return "strict-mode";
2818
2861
  if (/\bexpect\(|\bexpect\.|Expected (?:string|substring|pattern|value)|\.toHave|\.toBe|\.toContain|\.toEqual/.test(text))
@@ -2829,7 +2872,7 @@ function maskVolatile(text) {
2829
2872
  function extractErrorSignature(rawError) {
2830
2873
  const text = stripAnsi(rawError);
2831
2874
  const errorType = classifyError(text);
2832
- const normalizedMessage = maskVolatile(extractMessageHead(text));
2875
+ const normalizedMessage = maskVolatile(maskSelectorOptions(extractMessageHead(text)));
2833
2876
  const selector = extractSelector(text);
2834
2877
  const topFrameFile = extractTopFrameFile(text);
2835
2878
  const signature = (normalizedMessage.split("\n")[0] || "").slice(0, 200) || "Unknown error";
@@ -3212,7 +3255,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
3212
3255
 
3213
3256
  // src/internal/capture/pick-on-failure.ts
3214
3257
  var path5 = __toESM(require("path"));
3215
- var ANSI_RE2 = /\[[0-9;]*m/g;
3258
+ var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
3216
3259
  function parseLeafLocatorExpression(rawExpr) {
3217
3260
  const leaf = parseLeafLocatorCall(rawExpr);
3218
3261
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -85,12 +85,6 @@ declare const EXPECT_CAPTURE_EXPRESSIONS: ReadonlySet<string>;
85
85
  declare function extractAccessibleName(ariaSnapshot: string | null): string | null;
86
86
  /** Tags whose accessible name comes from an associated `<label>`. */
87
87
  declare const FORM_FIELD_TAGS: ReadonlySet<string>;
88
- /**
89
- * Roles whose accessible name is computed from the element's own content. For
90
- * these, an element carrying neither `aria-label` nor `aria-labelledby` is
91
- * named by its trimmed text — which the probe already returns.
92
- */
93
- declare const NAME_FROM_CONTENT_ROLES: ReadonlySet<string>;
94
88
  /** The subset of a probe result the accessible-name shortcut reads. */
95
89
  interface NameSource {
96
90
  tagName: string;
@@ -153,4 +147,4 @@ declare function suggestLocatorsFromAria(failed: FailedLocatorInfo, ariaSnapshot
153
147
  */
154
148
  declare function captureCallerLocation(stack?: string): string | null;
155
149
 
156
- export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, FORM_FIELD_TAGS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, NAME_FROM_CONTENT_ROLES, type NameSource, captureCallerLocation, dedupeSnapshotsByLocation, exactAccessibleName, extractAccessibleName, renderFailing, suggestLocatorsFromAria };
150
+ export { ACTION_METHODS, CHAIN_METHODS, EXPECT_CAPTURE_EXPRESSIONS, EXPECT_METHOD, FORM_FIELD_TAGS, type FailedLocatorInfo, LOCATOR_CREATING_CHAINS, LOCATOR_METHODS, type LocatorSuggestion, type NameSource, captureCallerLocation, dedupeSnapshotsByLocation, exactAccessibleName, extractAccessibleName, renderFailing, suggestLocatorsFromAria };
@@ -39,7 +39,6 @@ __export(locator_healing_exports, {
39
39
  INPUT_TYPE_TO_ROLE: () => INPUT_TYPE_TO_ROLE,
40
40
  LOCATOR_CREATING_CHAINS: () => LOCATOR_CREATING_CHAINS,
41
41
  LOCATOR_METHODS: () => LOCATOR_METHODS,
42
- NAME_FROM_CONTENT_ROLES: () => NAME_FROM_CONTENT_ROLES,
43
42
  TAG_TO_ROLE: () => TAG_TO_ROLE,
44
43
  approximateAccessibleName: () => approximateAccessibleName,
45
44
  captureCallerLocation: () => captureCallerLocation,
@@ -760,7 +759,6 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
760
759
  INPUT_TYPE_TO_ROLE,
761
760
  LOCATOR_CREATING_CHAINS,
762
761
  LOCATOR_METHODS,
763
- NAME_FROM_CONTENT_ROLES,
764
762
  TAG_TO_ROLE,
765
763
  approximateAccessibleName,
766
764
  captureCallerLocation,
@@ -343,7 +343,7 @@ function installPickerOverlay(arg) {
343
343
  banner.style.cssText = `position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:${Z + 2};background:#111827;color:#f9fafb;font:13px/1.5 system-ui,sans-serif;border:1px solid #312e81;padding:10px 16px;border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,.5);max-width:min(680px,86vw);`;
344
344
  const hlTokens = (expr) => {
345
345
  const escHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
346
- const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
346
+ const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
347
347
  let html = "";
348
348
  let last = 0;
349
349
  let m;
@@ -886,7 +886,7 @@ function showPickerChoices(arg) {
886
886
  panel.style.cssText = "background:#111827;color:#f9fafb;border-radius:10px;padding:20px;max-width:640px;width:90vw;max-height:70vh;overflow:auto;box-shadow:0 8px 40px rgba(0,0,0,.5);";
887
887
  const hlLocator = (expr) => {
888
888
  const escHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
889
- const re = /('(?:\\.|[^'])*'|"(?:\\.|[^"])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
889
+ const re = /('(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")|([A-Za-z_$][\w$]*)(?=\s*\()|([A-Za-z_$][\w$]*)(?=\s*:)|(true|false|null|\d+)|([{}(),.])/g;
890
890
  let html = "";
891
891
  let last = 0;
892
892
  let m;
@@ -1412,7 +1412,7 @@ function renderFailing(failed) {
1412
1412
  }
1413
1413
 
1414
1414
  // src/internal/capture/pick-on-failure.ts
1415
- var ANSI_RE = /\[[0-9;]*m/g;
1415
+ var ANSI_RE = /\x1b\[[0-9;]*m/g;
1416
1416
  function parseLeafLocatorExpression(rawExpr) {
1417
1417
  const leaf = parseLeafLocatorCall(rawExpr);
1418
1418
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piwitests/reporter",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "description": "Playwright reporter that streams results, traces and HTML reports to a Piwi Dashboard instance",
5
5
  "url": "https://github.com/PiwiTests/platform",
6
6
  "homepage": "https://piwitests.dev",
@@ -27,7 +27,7 @@ Prefer the **Piwi MCP server** if it is connected to this agent (tools named `li
27
27
 
28
28
  7. **Verify.** Re-run the affected spec(s): `npx playwright test <file>`. Confirm they pass and the new run is green in the dashboard (`get_run_insights` compares against the last green run: regressions cleared, nothing new broken).
29
29
 
30
- 8. **Close the loop (optional).** With packages/reporter/admin access, `set_cluster_status` marks the cluster resolved with a note so it drops off the triage queue.
30
+ 8. **Close the loop (optional).** With reporter or admin access, `set_cluster_status` marks the cluster resolved with a note so it drops off the triage queue.
31
31
 
32
32
  ## Guardrails
33
33
 
@@ -47,7 +47,7 @@ change it to import from the fixtures file instead, e.g. `import { test, expect
47
47
 
48
48
  ## Authentication
49
49
 
50
- If the dashboard has auth enabled, runs need an API key. Have the user create one in the dashboard (**Settings → Users → API keys**; keys start with `pd_`), then put it in `.env` as `PIWI_API_KEY=pd_...` and make sure `.env` is git-ignored. Never hardcode a key into `playwright.config.ts` or commit it. In CI, pass it as a secret (`PIWI_API_KEY`).
50
+ If the dashboard has auth enabled, runs need an API key. Have the user create one in the dashboard (**Settings → Account → API keys**; keys start with `pd_`), then put it in `.env` as `PIWI_API_KEY=pd_...` and make sure `.env` is git-ignored. Never hardcode a key into `playwright.config.ts` or commit it. In CI, pass it as a secret (`PIWI_API_KEY`).
51
51
 
52
52
  ## Verify it worked
53
53
 
@@ -1,36 +1,51 @@
1
1
  ---
2
2
  name: stabilize-flaky-tests
3
- description: Find the flakiest Playwright tests from Piwi's flaky analysis and fix the root cause of the intermittency, ranked by impact. Use when the user asks to "fix flaky tests", "reduce flakiness", "why is this test flaky", or wants to clean up an unreliable suite.
3
+ description: Find the flakiest Playwright tests from Piwi's flaky analysis, make each one fail on demand with Piwi's Flake Lab, fix what the reproducing condition points at, and prove the fix under that same condition. Use when the user asks to "fix flaky tests", "reduce flakiness", "why is this test flaky", or wants to clean up an unreliable suite.
4
4
  ---
5
5
 
6
6
  # Stabilize flaky tests with Piwi
7
7
 
8
- Piwi scores every test's flakiness over its whole run history and ranks it by impact, so you spend effort on the tests that actually cost the team — not whichever one failed most recently. This skill picks the worst offenders, fixes the *cause* of the intermittency, and confirms the score improves.
8
+ Piwi scores every test's flakiness over its whole run history and ranks it by impact, so you spend effort on the tests that actually cost the team — not whichever one failed most recently. For each test it also ranks **suspects** from that history (a route that is slower when it fails, a test running alongside, load), and its **Flake Lab** replays each suspect as a condition next to a control until the failure reproduces. A reproduced condition proves the cause, and the same condition later proves the fix. This skill follows that loop: rank, reproduce, fix, verify.
9
9
 
10
10
  ## How you reach Piwi
11
11
 
12
- Prefer the **Piwi MCP server** if it is connected (`list_flaky_tests`, `get_test_stability_trend`, `get_test_case`, `get_spec_health`). Otherwise use the dashboard's **Flaky** tab for the project and work from what it shows plus the repo.
12
+ Prefer the **Piwi MCP server** if it is connected (`list_flaky_tests`, `get_flake_profile`, `plan_flake_experiment`, `get_test_case`, `get_test_stability_trend`). Otherwise use the dashboard's **Flaky** tab and the test's **Flakiness** tab, and work from what they show plus the repo. The lab itself is a command you run from the project root: `npx @piwitests/reporter flake`. It needs the Piwi capture fixtures in the specs (`piwiFixtures`) and a dashboard URL and reporter API key (`PIWI_DASHBOARD_URL`, `PIWI_API_KEY`, or the project's `.env`).
13
13
 
14
14
  ## Steps
15
15
 
16
- 1. **Rank the flaky tests.** Call `list_flaky_tests` for the project. It returns each test's flaky score, impact ranking, and a **root-cause category**. Take the top few by impact — do not try to fix the whole list at once.
16
+ 1. **Rank the flaky tests.** Call `list_flaky_tests` for the project and take the top few by impact. Do not try to fix the whole list at once.
17
17
 
18
- 2. **Understand one test's pattern.** For a chosen test, call `get_test_stability_trend` (is it getting worse, or already stable again?) and `get_test_case` (recent pass/fail history). Read a representative failing execution (`get_test_run_case` / `explain_failure`) for the error, steps, console, and network at the moment it flaked.
18
+ 2. **Read the suspects.** For a chosen test, call `get_flake_profile`. Each suspect carries raw counts ("slow in 7 of 8 failures and 3 of 44 passes"), the condition that would test it, and, once the lab has run, its latest result (`lab`). `experiments` lists what the lab already found; if an experiment reproduced the test, skip to step 4. Read one failing execution (`get_test_run_case` / `explain_failure`) for the error at the moment it flaked.
19
19
 
20
- 3. **Identify the cause.** Common categories and their fixes:
21
- - **Timing / race** — replace fixed waits and manual sleeps with Playwright web-first assertions (`await expect(locator).toBeVisible()`), await the network/UI state the test depends on, not a timeout.
22
- - **Locator instability** — a selector that resolves ambiguously or intermittently; heal it (see the `apply-locator-healing` skill) or scope it.
23
- - **Test-order / shared state** — leaking storage, cookies, or a shared backend row; isolate setup, use a fresh context, or a unique fixture per test.
24
- - **Slow / unreliable endpoint** — check `get_network_requests`; if a real endpoint is intermittently slow or 500s, that is a product bug, not a test bug — report it rather than papering over it with retries.
20
+ 3. **Reproduce it.** Run the lab on the test (`plan_flake_experiment` gives the exact commands and the arms it will run, with an estimate):
25
21
 
26
- 4. **Fix the root cause.** Make the change in the spec (or the app, when the flake is a real defect). Do **not** "fix" flakiness by adding retries or increasing timeouts — that hides it; the goal is a test that passes deterministically.
22
+ ```bash
23
+ npx @piwitests/reporter flake <testCaseId>
24
+ ```
27
25
 
28
- 5. **Verify.** Re-run the test several times to shake out intermittency: `npx playwright test <file> --repeat-each=5` (raise the count for stubborn ones). It should pass every time. Over the next runs, confirm the flaky score falls in the dashboard (`get_test_stability_trend`).
26
+ It runs a control, then one arm per suspect, retries off, and counts only failures with the same error as in CI. Exit code `0` means an arm reproduced the failure ("reproduced by delaying GET /api/cart to 1.8 s: 3/4 against 0/10, p = 0.011"); `1` means nothing did; `2` is an error (read its message: often the fixtures are not in use). Use `--suspect <n>` to test one suspect, `--all` to also try every condition at once. If the output warns that this checkout differs from the commit of the failures, say so in your report.
29
27
 
30
- 6. **Report.** For each test you touched: the root-cause category, the change, and the repeat-run result. List any remaining high-impact flaky tests so the user can decide whether to continue.
28
+ If nothing reproduces, do not guess a fix from the suspects alone. Report the arms and their counts, and fall back to reading the failure and the attempts diff.
29
+
30
+ 4. **Fix what the reproducing condition points at.** The condition names the cause:
31
+ - **A delayed route** — the test (or the app) acts before that response arrives. Wait for the response or for the UI state it produces (`await page.waitForResponse(...)`, `await expect(locator).toHaveText(...)`), not for a timeout.
32
+ - **A failed route** — the app does not handle that error. If the endpoint really fails in CI, that is a product finding: report it rather than hiding it in the test.
33
+ - **Another test alongside or just before** — shared state: a backend row, storage, a fixture both use. Isolate it (unique data per test, a fresh context, its own fixture).
34
+ - **CPU load** — a race the load exposes; the fix is the same as a delayed route: await the state the test depends on.
35
+ Make the change in the spec, or in the app when the flake is a real defect.
36
+
37
+ 5. **Verify the fix under the same condition.** Run:
38
+
39
+ ```bash
40
+ npx @piwitests/reporter flake verify <testCaseId>
41
+ ```
42
+
43
+ It reruns the arm that reproduced the failure, and its control, for enough runs to say the fix holds at the rate it failed. Exit code `0` means the fix held; `1` means it still fails (or too few runs passed to say). "It passed ten times in a row" is not a verification; this is.
44
+
45
+ 6. **Report.** For each test you touched: the reproducing condition and its counts, the change, and the verify result. List any remaining high-impact flaky tests, and any test the lab could not reproduce, so the user can decide whether to continue.
31
46
 
32
47
  ## Guardrails
33
48
 
34
- - Fix the cause, never the symptom — no blanket `test.retry`, no bumped global timeout, no `waitForTimeout` sprinkles.
49
+ - Fix the cause, never the symptom — no blanket `test.retry`, no bumped global or per-test timeout, no `waitForTimeout` sprinkles. A longer timeout can make `flake verify` pass without fixing anything; do not do it.
35
50
  - A test that flakes because a *real* endpoint is unreliable is a product finding; surface it instead of hiding it in the test.
36
- - Stabilize a few high-impact tests well rather than lightly touching many. Prove each one with repeated runs before moving on.
51
+ - Stabilize a few high-impact tests well rather than lightly touching many. Prove each one with `piwi flake verify` before moving on.