Decides what kind of failure a red CI test is before anyone starts fixing it. Extracts seven failure signals from the runner output, stack trace, run history, and environment metadata, then walks an ordered first-match-wins rule set to exactly one verdict: flaky-known, environment-drift, defect, timeout, flaky-pre-incident, or flake-of-unknown-cause. Emits the verdict together with the alternatives that were rejected and the specific condition each one failed, so the triage decision is auditable rather than asserted. Use when a test has just gone red and the next action depends on whether the cause is a product defect, a non-deterministic test, or drifted infrastructure.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
One test is red. Before anyone opens a ticket, reruns the job, or edits a line, someone has to answer a prior question: what kind of failure is this? This is the method for answering it, and for writing the answer down in a form a second person can check.
The cost of getting it wrong is measurable. Google reports that about 84% of the pass-to-fail transitions its CI observes involve a flaky test, and that almost 16% of its tests show some level of flakiness (Google 2016). A triage step that treats every red as a defect files mostly noise; one that treats every red as a flake ships regressions.
This owns classification: turning one failed run into one verdict, with the rejected alternatives recorded.
It deliberately does not own:
A red test is a failure: an error (a human action) introduces a defect (the fault in the work product), and the failure is the observed event during execution. Whether a defect actually sits behind this failure is exactly what this method decides.
The ISTQB glossary has no entry for "flaky test." Querying its API for
flaky-test, flaky, and non-deterministic-test returns 404 in all three
cases (glossary.istqb.org), so do not cite a flaky-test definition to ISTQB.
Use Martin Fowler's canonical definition instead: "A test is non-deterministic
when it passes sometimes and fails sometimes, without any noticeable change in
the code, tests, or environment" (Fowler). Google's operational definition is
narrower and worth keeping alongside it: "a test that exhibits both a passing
and a failing result with the same code" (Google 2016).
The environment-drift verdict below is about the test environment - the
hardware, tooling, and support elements a test runs against - changing
underneath a test that did not.
Record all seven before applying any rule. A rule that fires on a signal you never extracted is a guess.
| # | Signal | What to record |
|---|---|---|
| S1 | Failure mode | One token from the pattern table below. |
| S2 | Reproducibility window | Pass-to-fail ratio over the last 50 runs; length of the current red streak; the first red commit. |
| S3 | Co-failure pattern | Did other tests fail in the same run? Same suite? Same shard? |
| S4 | Time clustering | Are failures concentrated in one window (a deploy, off-hours, peak load)? |
| S5 | Environment delta | Did the runner image, container tag, or base build hash change inside the failure window? |
| S6 | Change-set proximity | Did files in the test's call graph change inside the window? |
| S7 | Quarantine status | Is the test on a formal quarantine list (an annotation, a decorator, a CI quarantine tag, a checked-in flake list)? |
The 50-run and 7-day windows are practitioner conventions, not values from any standard. Pick windows your history actually covers and state them in the verdict, because every rule below is relative to them.
Match in the order listed. The order is by decreasing specificity, and crash signatures come first because a dying process frequently emits a trailing timeout that would otherwise capture the match.
| Mode token | Pattern in the runner output or trace |
|---|---|
runner-crash | SIGSEGV, Segmentation fault, OutOfMemoryError, panic:, an unhandled exception that kills the process. |
assertion-mismatch | Paired expected and actual values. googletest prints Value of: ... / Actual: ... / Expected: ... (googletest); xUnit, Jest, and pytest print equivalent pairs. |
setup-error | A BeforeEach, setUp, @BeforeAll, or fixture frame in the trace. |
selector-breakage | no such element, the W3C WebDriver error for a locator that matches nothing (W3C WebDriver); NoSuchElementException; Playwright's strict mode violation for a locator resolving to the wrong count (Playwright). |
environmental | ENOENT, FileNotFoundException, a missing environment variable, connection refused. |
timed-out | Timeout or exceeded N ms with no other exception attached. |
unclassified | None of the above matched. |
timed-out is the mode. timeout is a verdict. They are not the same
claim: the mode says the clock ran out, the verdict says the CI infrastructure
is why. R3 and R4 below decide between them.
Change-set proximity means files on the test's call graph changed inside the window, and you read the change. Google's engineering practices put the obligation plainly for reviewers: read and understand the change before acting on it (Google eng-practices). A diff you only counted commits in does not satisfy S6.
The verdicts are mutually exclusive and single-valued. Evaluate R1 through R6 in order and stop at the first rule whose conditions all hold. A failure with several plausible causes gets the earliest matching verdict, not two verdicts.
flaky-knownFires when S7 shows the test on a formal quarantine list and the observed S1 mode matches the flake category already recorded for it.
Next step: rerun, and let the existing quarantine process handle it. No new triage. Fowler's advice is to quarantine non-deterministic tests out of the main pipeline and cap how long they may stay there (Fowler); R1 only recognises that state, it does not create it.
Informal evidence does not count. A code comment saying "this one sometimes fails" is not a quarantine list. If the project has no detectable quarantine convention, R1 can never fire and you fall through to R2.
environment-driftFires when all hold:
runner-crash, setup-error, or environmental (never
assertion-mismatch).Next step is not a defect ticket. It is a re-pin of the image or an investigation of how the environment is provisioned, routed to whoever owns the CI platform.
An environmental mode with no S5 delta is not drift. The environment did
not change; the test depends on state nobody controls. That case falls through
to R5 or R6 as a test-isolation problem.
defectFires when all hold:
assertion-mismatch, selector-breakage, or setup-error; or S1 is
timed-out and the diff touched a timing constant or the code path under
test, andThis is the highest-confidence verdict available, because four independent signals agree. Next step: capture whatever trace or artifact the runner can produce, draft the defect report against the schema the team uses, and lock the reproduction in a committed failing test before anyone edits production code.
timeoutFires when S1 is timed-out with no other exception and either:
Next step is a suite time-budget review or runner resource tuning, not a defect ticket. R3 has already claimed the timeouts that have code-change proximity, so anything reaching R4 has none.
flaky-pre-incidentFires when all hold:
Those three are the top root-cause categories in the largest published study of flaky-test fixes: of 161 classified fixes drawn from 201 flaky-test commits across 51 Apache projects, 45% were async wait, 20% concurrency, and 12% test order dependency (Luo et al., FSE 2014).
This is the "flaking, not yet quarantined" verdict. Next step: pattern attribution, then quarantine if the pattern is confirmed.
flake-of-unknown-causeThe fallback. Nothing above matched, or S1 came back unclassified. Emit a
low-confidence verdict and recommend bisection-style narrowing across run
conditions. R6 is an honest answer, and it is a better one than a defect
verdict propped up on two of four signals.
R2 sits ahead of R3 on purpose. Both can look plausible when a test starts failing after an image bump that also landed near a code change, and routing drift to the defect tracker sends the work to the wrong team while the tracker fills with false positives. Putting the environment check first makes the precedence explicit rather than leaving it to the mode preconditions.
R3 sits ahead of R5 on purpose too: when there is code-change proximity, an async-wait timeout is more likely a product that got slower than a test that got unlucky.
One fixed shape. The Not classified as block is mandatory, including when the verdict is high-confidence.
## Failure classification - `<test-id>`
**Verdict:** <flaky-known | environment-drift | defect | timeout | flaky-pre-incident | flake-of-unknown-cause>
**Confidence:** <high | medium | low>
**Windows used:** <e.g. last 50 runs, 7 days of history, N=5 green-run threshold>
**Evidence:**
- <one line per signal that carried the verdict, naming the signal (S1..S7) and the observed value>
**Recommended next step:**
1. <action>
2. <action>
**Not classified as:**
- `<verdict>` - <the specific condition that failed, with the observed value>
- `<verdict>` - <the specific condition that failed, with the observed value>A bare verdict is an assertion. A verdict plus the rejected alternatives and the exact condition each one failed is a decision another person can audit, reopen, and overturn when a signal later turns out to be wrong.
Rules for the block:
environment-drift - runner image
and container tag unchanged across the window" is auditable. "Does not look
like environment drift" is not.environment-drift
A full end-to-end walk-through - the seven signals extracted, the rules evaluated in order, and the emitted verdict with its Not-classified-as block - is in references/worked-example.md.
The catalogue of triage anti-patterns, each with why it fails and the rule that prevents it, is in references/anti-patterns.md.
unclassified is a real outcome. When S1 matches nothing in the pattern
table, record unclassified and let it flow to R6. Forcing a mode token to
make a rule fire is how a wrong verdict acquires false confidence.