Summary
check_e2e_coverage.py's "does this test actually run" discriminator classifies any test.skip(true, …) in a test body as an unconditional skip. But test.skip(true, reason) is also the correct Playwright form for a conditional skip written inside an if guard — the single most common defensive idiom in the fleet's e2e suites. The true is the API's "skip from this point" shape; the call site is what carries the condition.
Result: gate-19 reports "referenced only by a test that never runs" about tests that run on every reachable environment — including, provably, tests that passed in the same CI run that produced the finding.
Minimal reproduction
Two tests in one file, identical but for a guarded skip:
// @e2e openspec/specs/demo/spec.md#s1-guarded-skip-inside-an-if
test('guarded: runs whenever the app is reachable', async ({ page }) => {
const response = await page.goto('/app').catch(() => null)
if (!response) {
test.skip(true, 'app not reachable')
return
}
await expect(page.locator('body')).not.toContainText('Internal Server Error')
})
// @e2e openspec/specs/demo/spec.md#s2-no-skip-at-all
test('clean: no skip anywhere', async ({ page }) => {
await page.goto('/app')
await expect(page.locator('body')).not.toContainText('Internal Server Error')
})
$ HYDRA_GATE_BASE_REF=base python3 check_e2e_coverage.py .
demo::s1-guarded-skip-inside-an-if — @e2e tag present but the test does not run:
referenced only by a test that never runs (tests/e2e/demo.spec.ts). …
[gate-19] e2e-coverage: FAIL — 1 scenario(s) without a running e2e test
S2 passes, S1 fails. The only difference is the guarded skip.
Live confirmation
On ConductionNL/procest PR #765, gate-19 flagged req-zak-004a and req-zak-004b. Both are tagged on:
test('cases index renders so a case dossier tab can be opened', async ({ page }) => {
const response = await page.goto('/index.php/apps/procest/cases').catch(() => null)
if (!response) {
test.skip(true, 'Procest dev container not reachable')
return
}
await expect(page.locator('body')).not.toContainText('Internal Server Error', { timeout: 10000 })
})
No .skip / .fixme modifier. Its only skip is inside if (!response). The E2E job on the same commit reported 87 passed / 0 failed / 38 skipped — this test is one of the 87. gate-19 said it never runs.
Why the current rule was chosen, and why the reasoning inverts here
The source comment is explicit and deliberate (around L440–L460):
test.skip(true) — an UNCONDITIONAL skip at the top of a body … so the discriminator is the argument, not the call.
That holds for test.skip(true) written at the top of a body, which is a permanent skip. It does not hold for the same call written inside a conditional block, where the guard is the condition and true is only how you tell Playwright "skip now". Both forms are legal, idiomatic Playwright and are textually identical at the call.
Blast radius
Counted with the gate's own patterns across the accessible fleet checkouts — occurrences of test.skip(true, …) that are inside an if / else / catch guard:
| repo |
guarded test.skip(true, …) |
| openregister |
81 |
| procest |
22 |
| hermiq |
4 |
| docudesk |
3 |
| nldesign |
1 |
111 occurrences, versus 4 genuinely unconditional ones in the same checkouts (all in one procest file, now fixed). So the rule currently misfires on ~96% of the call sites it inspects, and any PR touching one of those specs gets a false finding.
Why this is worse than a normal false positive
The remediation gate-19 offers is "replace the tag with a reason-bearing @e2e exclude" — i.e. declare the scenario not e2e-covered. Complying with a false finding therefore deletes a true coverage claim and permanently marks a genuinely-tested scenario as untestable. The gate actively pushes the codebase in the wrong direction.
Suggested fix
Determine conditionality from the call site, not the argument: a test.skip(true, …) is unconditional only when it is a direct statement of the test/describe body, not nested inside an if / else if / else / catch / try block. The existing _has_own_unconditional_skip() already walks block structure to exclude skips belonging to nested tests — the same walk can note enclosing conditional blocks.
Include the reproduction above as a fixture: it fails both ways today (S1 false-positive; and a genuine top-of-body test.skip(true) must still be caught).
Secondary observation — no representation for "quarantined pending an issue"
The same procest PR converted three permanently-skipped tests to test.fixme referencing a tracking issue. gate-19 correctly notes they do not run, but its only sanctioned end state is @e2e exclude <reason>, which means "this scenario is out of scope for e2e, forever". A scenario that should be e2e-tested and is blocked on a tracked fixture gap has no way to say so. Consider accepting a tracked form (e.g. @e2e blocked <issue> <reason>) that is visible in reporting without being either a silent pass or a permanent exclusion.
Found during a fleet-wide triage of disabled e2e declarations.
Summary
check_e2e_coverage.py's "does this test actually run" discriminator classifies anytest.skip(true, …)in a test body as an unconditional skip. Buttest.skip(true, reason)is also the correct Playwright form for a conditional skip written inside anifguard — the single most common defensive idiom in the fleet's e2e suites. Thetrueis the API's "skip from this point" shape; the call site is what carries the condition.Result: gate-19 reports "referenced only by a test that never runs" about tests that run on every reachable environment — including, provably, tests that passed in the same CI run that produced the finding.
Minimal reproduction
Two tests in one file, identical but for a guarded skip:
S2 passes, S1 fails. The only difference is the guarded skip.
Live confirmation
On ConductionNL/procest PR #765, gate-19 flagged
req-zak-004aandreq-zak-004b. Both are tagged on:No
.skip/.fixmemodifier. Its only skip is insideif (!response). The E2E job on the same commit reported 87 passed / 0 failed / 38 skipped — this test is one of the 87. gate-19 said it never runs.Why the current rule was chosen, and why the reasoning inverts here
The source comment is explicit and deliberate (around L440–L460):
That holds for
test.skip(true)written at the top of a body, which is a permanent skip. It does not hold for the same call written inside a conditional block, where the guard is the condition andtrueis only how you tell Playwright "skip now". Both forms are legal, idiomatic Playwright and are textually identical at the call.Blast radius
Counted with the gate's own patterns across the accessible fleet checkouts — occurrences of
test.skip(true, …)that are inside anif/else/catchguard:test.skip(true, …)111 occurrences, versus 4 genuinely unconditional ones in the same checkouts (all in one procest file, now fixed). So the rule currently misfires on ~96% of the call sites it inspects, and any PR touching one of those specs gets a false finding.
Why this is worse than a normal false positive
The remediation gate-19 offers is "replace the tag with a reason-bearing
@e2e exclude" — i.e. declare the scenario not e2e-covered. Complying with a false finding therefore deletes a true coverage claim and permanently marks a genuinely-tested scenario as untestable. The gate actively pushes the codebase in the wrong direction.Suggested fix
Determine conditionality from the call site, not the argument: a
test.skip(true, …)is unconditional only when it is a direct statement of the test/describe body, not nested inside anif/else if/else/catch/tryblock. The existing_has_own_unconditional_skip()already walks block structure to exclude skips belonging to nested tests — the same walk can note enclosing conditional blocks.Include the reproduction above as a fixture: it fails both ways today (S1 false-positive; and a genuine top-of-body
test.skip(true)must still be caught).Secondary observation — no representation for "quarantined pending an issue"
The same procest PR converted three permanently-skipped tests to
test.fixmereferencing a tracking issue. gate-19 correctly notes they do not run, but its only sanctioned end state is@e2e exclude <reason>, which means "this scenario is out of scope for e2e, forever". A scenario that should be e2e-tested and is blocked on a tracked fixture gap has no way to say so. Consider accepting a tracked form (e.g.@e2e blocked <issue> <reason>) that is visible in reporting without being either a silent pass or a permanent exclusion.Found during a fleet-wide triage of disabled e2e declarations.