Skip to content

test(e2e): remove 7 latent false @e2e anchors — proven by a +7/-7 control - #182

Merged
rubenvdlinde merged 1 commit into
developmentfrom
test/gate19-round2
Aug 11, 2026
Merged

test(e2e): remove 7 latent false @e2e anchors — proven by a +7/-7 control#182
rubenvdlinde merged 1 commit into
developmentfrom
test/gate19-round2

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

test(e2e): remove 7 latent false @e2e anchors — they would have manufactured coverage the moment anyone un-skipped them

Seven @e2e anchors sat on test.skiped tests whose bodies do not assert the
scenario they name. gate-19 does not credit a skipped test, so none of them
moves a number today. That is precisely why they are dangerous: un-skipping is
the obvious way to burn gate-19 down, and doing it would have scored seven
scenarios as covered without one new assertion being written.

MEASURED, expectation computed BEFORE the run (hydra-gates @ 81c8c97,
1735-line checker), openbuild@development ff25c1b:

as shipped {scenarios: 767, covered: 98, excluded: 509, uncovered: 160}
the 7 tests un-skipped, nothing
else changed {scenarios: 767, covered: 105, excluded: 509, uncovered: 153}
restored {scenarios: 767, covered: 98, excluded: 509, uncovered: 160}

+7 exactly, which is the count of anchors, naming those seven scenarios.

WHAT IS WRONG WITH EACH, read from the body, not the title:

openbuild-rbac two anchors about the VIEWER and the EDITOR sit
on tests titled for the OWNER and the ADMIN, both
driving the shared admin session. The second one's
only assertion is
expect(count).toBeGreaterThanOrEqual(0) on a
locator count — a count is never negative, so the
assertion CANNOT FAIL.
application-detail-overview a requirement naming six rows IN DOM ORDER plus
four sidebar tabs, anchored to a test that asserts
main is visible and the title matches
/openbuild/i. Its sibling computes iconCount and
never asserts on it.
app-icon-management "uploads a light icon" uploads nothing; "removes
the dark icon" removes nothing and both of its
branches pass without the feature; "non-SVG is
rejected" puts its rejection assertion under
if (fileInputCount > 0) and then
if (errorCount > 0), so the product failing to
reject is exactly the case where nothing is
asserted. That body already says so: "the test
passes vacuously because the UI is not built".

THE NUMBER DOES NOT MOVE, and that is the point:

after removal {scenarios: 767, covered: 98, excluded: 509, uncovered: 160}

No test was skipped, un-skipped, deleted or loosened; no exclusion was written;
no timeout was widened. What changed is the count of uncovered scenarios
carrying a dead anchor: 35 -> 28 (the gate's own collect_ref_status).

The anchors are DELETED rather than commented out. Per ConductionNL/.github#358
both gate-19 and gate-26 parse prose, so an anchor quoted in an explanatory
comment is re-read as a live anchor — the replacement notes name no slug.

SCOPE, stated because it is weak: this change touches three TEST files and no
spec file, so changed_spec_files() returns the empty set and gate-19's own
cell on this PR is EMPTY SCOPE (exit 3, NOT APPLICABLE) — it corroborates
nothing. The full-scope figures above and the +7/-7 control are the only
evidence, and the control is the load-bearing half.

Refs #178, ConductionNL/.github#343, ConductionNL/.github#358


Also measured in this session (no code change here)

gate-16 is dead on openbuild's --full runs — .github#361 reproduces exactly

openbuild's fleet table has never carried a gate-16 line. One tree
(ff25c1be5 = origin/development), only the base changed. Every number is
named with its base
, because the same checker prints PASS or 445 depending on
nothing but this input:

base gate-16
origin/development (ff25c1be5 — the unguarded CI --full default) # count=0 → PASS
origin/beta (5ab286fba) 259
origin/main (e9c22311e) 445
--mode report, whole tree 439 (413 frontend / 26 backend)

main and report mode agree to within 6, which is the corroboration; the PASS
is the outlier.

Positive control, committed and then reverted (an uncommitted plant reports
PASS for an unrelated reason — check_spec_coverage.py:157 derives the changed
set from committed history while reading @spec from disk). A public function plantedGateSixteenProbe(): JSONResponse with no @spec was added to
lib/Controller/SettingsController.php and committed:

tree base result
plant committed origin/development count=1lib/Controller/SettingsController.php::plantedGateSixteenProbe — missing @spec
same tree, plant still on disk HEAD count=0

The second row is the whole finding: the gate reports PASS over a defect it had
just named, because base == HEAD makes the diff empty. Plant reverted.

Mechanism, confirmed in the canonical package (ConductionNL/.github
@ 81c8c97): bin/hydra-gates builds --scope-to-diff --base "${BASE_REF}"
inside if [ "${SCOPE_DIFF}" = "1" ], so on --full no base is forwarded and
run-hydra-gates.sh:204's BASE_REF="origin/development" survives to
HYDRA_GATE_BASE_REF="${BASE_REF}" at line 3034. The base-IS-HEAD rescue
(resolve-push-base.sh) also lives under SCOPE_DIFF=1, so it never fires on
--full. Not a checker bug — the checker honours an explicit base.

.github#356: the split, with the exclude_reason key — refining round 1

Round 1 reported 80 distinct reasons, 42 covering more than one scenario, 471
of 509 (92.5%) on a shared reason, largest bucket 32
. Grouped by
(spec, reason) — its actual key — every one of those reproduces exactly,
and its headline (one reason covering 32) is a whole-spec marker and stands.

⚠️ I nearly filed this as a retraction. My first attempt grouped by reason alone,
got 58/21/472, and read that as "round 1's method was an artifact". It was my
key that differed. Reproduce a predecessor's number under their method before
correcting it
— an unreproduced number looks exactly like a wrong one.

What does need refining: _make_scenario_entry writes the literal string
"<inherited from requirement>" as the exclude_reason of every
requirement-level exclusion — 238 of the 509 here. So for nearly half the
corpus the grouping key is a sentinel, and requirement-level exclusions
collapse together within a spec regardless of what their requirements say.
Recovering the real requirement text, same key, same tree:

distinct covering >1 on a shared reason biggest
raw value (round 1) 80 42 471 (92.5%) 32
requirement text recovered 172 100 437 (85.9%) 32

Full layer split, from the same instrumented run (positive control asserted
first: 509 exclusions parsed, 509 with a non-empty reason, 509/509 layers
assigned — and the sample value read, not just counted):

scenarios share of exclusions share of all 767
whole-spec 217 42.6% 28.3%
requirement-level 238 46.8% 31.0%
scenario-level 54 10.6% 7.0%

Bare exclusions: 0. 14 spec files are blanket-excluded by a whole-spec
marker, the largest covering 32, 29, 26, 25 and 20 scenarios.

openbuild's profile is not nldesign's (61.5% whole-spec / 31.6% scenario /
6.8% requirement). Here the #356 requirement-inheritance shape is the single
largest bucket. Measure each app.

How many of the 160 are genuinely uncovered: all 160

Inheritance can only move scenarios OUT of uncovered and INTO excluded,
never the reverse — so it cannot be inflating this number. Proven rather than
asserted, by re-running report mode with both inherited layers disabled and
nothing else changed:

covered excluded uncovered
as shipped 98 509 160
scenario-level markers only 100 54 613

455 scenarios are excluded by an inherited marker only; 453 of them carry no
anchor at all and would land straight in uncovered, and 2 would become
covered. So 160 is the floor and 613 is the honest ceiling — the reported
figure understates openbuild's e2e debt by 3.8×.

What the 160 actually consist of

  • 35 already carry an @e2e anchor inside a test.skip — gate-19 correctly
    refuses them. 7 of those are the false anchors this PR removes; the other
    28 are honest, quarantined behind a named blocker (openbuild#41,
    vitest-covered) or documenting real product defects.
  • 125 have no anchor anywhere.

Two of the 28 have a rotted reason and are the cheapest real coverage left:
openbuild-runtime's empty-list-for-a-roleless-caller and
editor-sees-Save-but-not-Publish both skip saying they need fixture users that
do not exist — but tests/e2e/global-setup.ts:325 provisions rbac-owner,
rbac-editor, rbac-viewer and rbac-outsider and mints a storage state for
each. Those reasons name a state of the world, and it has changed. They are
not written here because they need a rig to prove they can fail, and no
single-owner rig was available (host at 97% disk).


Filed / extended alongside this PR:

Not touched, deliberately: #177 and #173 are review-only and left for Ruben.
I added one factual comment to #177 recording that its remaining red E2E cell is
the shared builder-host failure, not its own.

…actured coverage the moment anyone un-skipped them

Seven `@e2e` anchors sat on `test.skip`ed tests whose bodies do not assert the
scenario they name. gate-19 does not credit a skipped test, so none of them
moves a number today. That is precisely why they are dangerous: un-skipping is
the obvious way to burn gate-19 down, and doing it would have scored seven
scenarios as covered without one new assertion being written.

MEASURED, expectation computed BEFORE the run (hydra-gates @ 81c8c97,
1735-line checker), openbuild@development ff25c1b:

  as shipped                       {scenarios: 767, covered:  98, excluded: 509, uncovered: 160}
  the 7 tests un-skipped, nothing
  else changed                     {scenarios: 767, covered: 105, excluded: 509, uncovered: 153}
  restored                         {scenarios: 767, covered:  98, excluded: 509, uncovered: 160}

+7 exactly, which is the count of anchors, naming those seven scenarios.

WHAT IS WRONG WITH EACH, read from the body, not the title:

  openbuild-rbac              two anchors about the VIEWER and the EDITOR sit
                              on tests titled for the OWNER and the ADMIN, both
                              driving the shared admin session. The second one's
                              only assertion is
                              `expect(count).toBeGreaterThanOrEqual(0)` on a
                              locator count — a count is never negative, so the
                              assertion CANNOT FAIL.
  application-detail-overview a requirement naming six rows IN DOM ORDER plus
                              four sidebar tabs, anchored to a test that asserts
                              `main` is visible and the title matches
                              /openbuild/i. Its sibling computes `iconCount` and
                              never asserts on it.
  app-icon-management         "uploads a light icon" uploads nothing; "removes
                              the dark icon" removes nothing and both of its
                              branches pass without the feature; "non-SVG is
                              rejected" puts its rejection assertion under
                              `if (fileInputCount > 0)` and then
                              `if (errorCount > 0)`, so the product failing to
                              reject is exactly the case where nothing is
                              asserted. That body already says so: "the test
                              passes vacuously because the UI is not built".

THE NUMBER DOES NOT MOVE, and that is the point:

  after removal                    {scenarios: 767, covered:  98, excluded: 509, uncovered: 160}

No test was skipped, un-skipped, deleted or loosened; no exclusion was written;
no timeout was widened. What changed is the count of uncovered scenarios
carrying a dead anchor: 35 -> 28 (the gate's own `collect_ref_status`).

The anchors are DELETED rather than commented out. Per ConductionNL/.github#358
both gate-19 and gate-26 parse prose, so an anchor quoted in an explanatory
comment is re-read as a live anchor — the replacement notes name no slug.

SCOPE, stated because it is weak: this change touches three TEST files and no
spec file, so `changed_spec_files()` returns the empty set and gate-19's own
cell on this PR is EMPTY SCOPE (exit 3, NOT APPLICABLE) — it corroborates
nothing. The full-scope figures above and the +7/-7 control are the only
evidence, and the control is the load-bearing half.

Refs #178, ConductionNL/.github#343, ConductionNL/.github#358
@rubenvdlinde
rubenvdlinde merged commit 7e38e79 into development Aug 11, 2026
31 of 33 checks passed
@rubenvdlinde
rubenvdlinde deleted the test/gate19-round2 branch August 11, 2026 18:47
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openbuild @ 552f56d

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
test-l10n
check-gitignore
check-nc-floor
composer ✅ 101/101
npm ✅ 654/654
PHPUnit
Newman ⏭️
Playwright
Hydra gates

Quality workflow — 2026-08-11 19:09 UTC

Download the full PDF report from the workflow artifacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant