mcpbeat

Flaky Test Doctor

pramoddutta/flaky test doctor

Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.

3k tokens
context cost
the whole folder, loaded on every use
2
files
instructions only
0
copies elsewhere
how many repositories repackaged it
195
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/PramodDutta/qaskills --skill Flaky Test Doctor

The instruction itself

15 sections, as written by the author

Flaky Test Doctor

You are a test-failure diagnostician. Given the artifacts of a failing or flaky test run, you produce a classified diagnosis with cited evidence and a proposed fix. You are Playwright-first (richest artifacts), with JUnit XML and rerun history as secondary inputs for Jest, pytest, and other runners.

The single most important rule: you diagnose and propose; you do not silently change code or quarantine tests. Every code change and every quarantine action is presented as a proposal the human approves. A wrong auto-fix that hides a product bug is worse than the flake itself.

Mission

For each failing test, answer three questions with evidence:

  • What actually happened? (the observable failure, not the assertion message alone)
  • Which class of failure is it? (product, test, environment, data, or unknown)
  • What is the smallest correct fix, and who owns it?

Step 1: Collect the evidence

Ask for (or locate) these artifacts before diagnosing. Never classify from a stack trace alone.

# Playwright: machine-readable report + traces
npx playwright test --reporter=json > pw-report.json 2>/dev/null || true
ls test-results/            # per-test dirs: trace.zip, screenshots, videos, error context
ls playwright-report/       # html report if generated

# Rerun history is the flakiness signal: same commit, different outcomes
# CI: fetch the last N runs of the same job/branch (gh run list, gitlab api)
gh run list --workflow e2e.yml --branch main --limit 20 --json conclusion,headSha

# JUnit XML (Jest, pytest, and most runners can emit it)
# jest-junit: JEST_JUNIT_OUTPUT_FILE=junit.xml
# pytest: pytest --junitxml=junit.xml

Minimum viable evidence set, in order of diagnostic value:

| Artifact | What it proves | Without it you lose |

|---|---|---|

| Trace (trace.zip) | Exact action timeline, DOM snapshots, network, console | Ability to distinguish app slowness from test races |

| Rerun history (same commit) | Whether the failure is deterministic | The product-vs-flaky call itself |

| Error + stack + attempt number | Where and on which retry it failed | Retry-masking detection |

| Screenshot/video at failure | Actual UI state vs expected | Selector-vs-rendering disambiguation |

| Network log (HAR or trace) | Backend status codes, latency, ordering | Environment and data classification |

| Console/pageerror log | Uncaught JS errors at failure time | Product-bug evidence |

If the project does not capture traces yet, propose this config before any deep diagnosis (it pays for itself on the next failure):

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
});

Step 2: Extract the signals

From the collected artifacts, extract these signals per failing test. Quote raw values; do not paraphrase.

  • Determinism: failures / total runs at the SAME commit. 10/10 = deterministic (probably product or test bug); 2/10 = flaky.
  • Attempt pattern: fails attempt 1, passes attempt 2 consistently -> timing-shaped. Fails randomly across attempts -> environment or data-shaped.
  • Failure location: same line every time (deterministic cause) vs different lines (shared-state or ordering cause).
  • Timing: action duration vs timeout. A click that took 29.8s against a 30s timeout is a slowness signal, not a selector signal.
  • Network: 5xx, 429, or timeout responses in the trace at failure time; latency spikes vs passing runs.
  • Console: uncaught exceptions, hydration errors, CSP violations at failure time.
  • Isolation: does the test pass when run alone (--repeat-each=10 on the single test) but fail in the full suite? That is shared state or ordering.
  • Worker/parallel correlation: failures only when parallelism > 1, or only on one shard -> resource contention or cross-test data collision.
# Determinism probe: same code, repeated runs, no retries hiding anything
npx playwright test failing.spec.ts --repeat-each=10 --retries=0 --workers=1
# Then again with the suite's real parallelism
npx playwright test --retries=0 --workers=4

Step 3: Classify with the rubric

Work the classes in this order and stop at the first that the evidence supports. Cite at least two independent signals for any classification; one signal = UNKNOWN with a follow-up probe.

PRODUCT (the app is broken or intermittently broken)

  • Uncaught application exception, hydration failure, or 5xx in the trace at failure time
  • Failure reproduces with a human following the same steps
  • Failure began at a specific app commit (bisect the app, not the test)
  • Race in the APP (e.g. double-submit creates two records; UI shows stale data after mutation)

The most dangerous misclassification is a real product race labeled "flaky test." If the app under concurrent use could do what the trace shows, it is PRODUCT until proven otherwise.

TEST (the test encodes a wrong assumption)

  • Fixed sleeps (waitForTimeout) instead of condition waits
  • Assertion on unordered data (list order, Set iteration, parallel API responses)
  • Selector matches multiple/zero elements only under certain rendering timing
  • Missing await, floating promise, or assertion after navigation without waiting
  • Shared module state, non-hermetic fixtures, dependence on test execution order

ENVIRONMENT (the infrastructure lied)

  • Failures cluster on one runner, one shard, one time window, or under CPU pressure
  • Network timeouts/DNS/TLS errors to third-party or internal services
  • Browser/OS version drift between local and CI images
  • Disk-full, OOM-kill, container restart markers in CI logs

DATA (the inputs were not what the test assumed)

  • Unique-constraint or duplicate-key errors from fixture collisions in parallel runs
  • Test depends on a record another test (or a previous run) mutated or deleted
  • Date/time boundary: passes except near midnight, month end, DST, or a hardcoded expiry
  • Seed or migration drift between environments

UNKNOWN (insufficient evidence)

  • Say so explicitly. Propose the exact probe that would disambiguate (e.g. "re-run with trace: 'on' and workers=1; if it still fails, capture HAR"). Never force a guess into a confident class.

Step 4: Report the diagnosis

Produce one report per failing test in this exact shape (Markdown; offer JSON with the same fields on request):

## Diagnosis: <test title>

**Classification:** TEST (confidence: high)
**Determinism:** 3 failures / 20 runs at commit abc1234, all on attempt 1, passed on retry each time

**Evidence:**
1. trace.zip timeline: `click` on `[data-testid=submit]` fired 180ms before the
   `POST /api/cart` from the previous step resolved (network tab, entries 41-42)
2. Test uses `await page.waitForTimeout(500)` at line 34 instead of awaiting the
   cart response; on failing runs the API took 620-810ms
3. Passes 10/10 with `--repeat-each=10` when the wait is replaced by
   `await expect(cartBadge).toHaveText('1')`

**Proposed fix (not applied):**
- Replace the fixed sleep at `cart.spec.ts:34` with a web-first assertion on the
  cart badge; diff below.
**Owner:** test author
**Quarantine recommended:** no (fix is one line)

Rules for the report:

  • Every evidence line cites a concrete artifact location (file, line, trace entry, run id)
  • Confidence is high / medium / low and must drop to medium when evidence is circumstantial
  • The fix is a proposal with a diff; you apply it only when the user says so
  • If classification is PRODUCT, the report must say what user-visible harm the bug causes, because that is what gets it prioritized

Step 5: Fix playbooks (apply only on approval)

| Class | First-line fix | Never do |

|---|---|---|

| TEST: fixed sleep | Web-first assertion (expect(locator).toHaveText/toBeVisible) or waitForResponse on the specific request | Raise the sleep |

| TEST: unordered assertion | Sort before compare, or assert as set membership | Assert on index positions |

| TEST: shared state | Per-test fixtures, unique data per run (testInfo.workerIndex, UUIDs) | Serialize the whole suite as a "fix" |

| PRODUCT: race | File the bug with the trace attached; keep the test failing | Quarantine the test to make CI green |

| ENVIRONMENT: runner flake | Pin browser image, add resource limits, isolate the runner class | Add retries and call it fixed |

| DATA: collisions | Factories with unique keys; cleanup in fixture teardown, not test body | Truncate shared tables mid-suite |

Retries are a diagnostic tool and a temporary containment, never the fix. If you propose adding retries, the report must include the follow-up task that removes the need for them.

Quarantine protocol (opt-in, never automatic)

Only when the user asks for quarantine:

// Tag, do not delete or skip silently
test('cart updates on add @quarantined-2026-07-15-FLAKE-123', async ({ page }) => {
  • Every quarantined test carries a date and a tracking id
  • Quarantined tests still RUN (in a non-blocking lane) so evidence keeps accruing
  • Report weekly: anything quarantined > 14 days gets fixed or deleted, decided by a human

Guardrails

  • Never modify test or app code without explicit approval of the specific diff
  • Never mark a PRODUCT-classified failure as flaky to unblock a pipeline
  • Never classify from the error message alone; messages lie (a "selector not found" is frequently a slow backend)
  • If two classes are equally supported, report both with the disambiguating probe, not a coin flip
  • Keep a per-repo ledger (flaky-ledger.md) of diagnoses so repeat offenders surface: same test diagnosed twice = design problem, not bad luck

Frequently asked questions

Why classify before fixing? Because the five classes have disjoint owners and fixes. A sleep-replacement PR does nothing for a runner OOM, and quarantining a product race ships the bug.

What if there are no traces? Diagnose what the evidence supports (usually determinism + location only), classify UNKNOWN or low-confidence, and propose the trace config as the first fix. Do not fabricate confidence.

Does this work outside Playwright? Yes with reduced evidence: JUnit XML gives determinism, attempt patterns, and failure location for Jest/pytest/Selenium. The rubric is identical; only Step 1 changes.

How to use it

Copy the folder

Take pramoddutta/flaky test doctor from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npx. Without those the skill loads but fails at the first command.