test(gate-28): acceptance evidence for #178 — and the three assertions the suites were missing - #248
Merged
Conversation
…s missing #178 (gate-28 fails every PHP-free PR in a PHP repo) was already FIXED by #182 on 2026-08-06, released in v1.5.2 and v1.6.0. The issue was simply never closed. Measured, not assumed — all three states behave correctly on main today. This commit adds no runtime change; it adds the acceptance evidence and closes three real gaps in what the suites actually asserted. WHAT WAS NOT BEING ASSERTED 1. The suite never ran --require-full-coverage, so it proved "gate-28 prints NOT APPLICABLE" and INFERRED "the run is not failed". Those are different claims and have come apart before (#164 defaulted the flag TRUE in the workflow while every local suite ran without it). 2. Nothing exercised the _fail 28 branch end to end. A licence MISMATCH is the gate's actual purpose; only the weaker "no file carried a tag" control existed. The helper->log->wc -l->_fail wiring was untested. 3. check_license_triangle.py's declared_file_count() had NO test in either direction, despite being the single number the runner's PASS-vs-structural decision rests on, and nothing pinned the stdout/stderr split it travels over. A TRAP FOUND WHILE WRITING THIS The coverage-failure branch lives inside `if [ "${_FAILED}" -eq 0 ]`. Any other failing gate suppresses the sentence entirely, so "the coverage sentence is absent" passes automatically on a fixture that trips an unrelated gate — including on a fully reverted runner. The first draft wrote its Vue file to src/App.vue, which gate-38 fails; both runs exited 1, not 98, and both new assertions were measuring gate-38. Fixtures are now otherwise green (src/components/Widget.vue), the pass case reads the POSITIVE "ALL N APPLICABLE GATES GREEN" sentence, and the control uses a composer.json with no license field rather than untagged PHP (which also fails gate-1, making the coverage branch unreachable). MUTATION-CHECKED — every new assertion shown capable of failing: M1 empty diff scope -> structural (pre-#182) -> 2 new shell assertions RED, status 98 M2 structural -> na (gate stops failing) -> 2 new shell assertions RED M3 _fail 28 disabled -> 2 new shell assertions RED (drift became PASS) M4 declared_file_count -> len(files) -> 3 new python tests RED M5 count printed to stdout -> 4 new python tests RED; clean repo turns FAIL M6 empty scope invents a finding + a count -> 3 new python tests RED M7 declared_file_count -> 0 -> 5 new python tests RED; PASS became structural Counts: shell 5 -> 11 assertions, python 19 -> 29 tests. Both appended to, never overwritten. Full helper suite: 27 passed, 0 failed, 2 quarantined. No runtime file is touched: the diff is the two test files only. Closes #178
rubenvdlinde
pushed a commit
that referenced
this pull request
Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cause — and an honest correction up front
#178 was already fixed. #182 (
da23b92, merged 2026-08-06 07:20Z, 88 minutes after the issue was filed) decides gate-28's applicability from the diff, and it is released in v1.5.2 and v1.6.0. The issue was simply never closed.The original mechanism, for the record: scope came from the DIFF (
check_license_triangle.py::main()readsfiles = argv[2:]), while "subject matter exists" was judged from the REPO (lib/present + a composer licence). A PHP-free diff handed the gate zero files, it compared nothing, and reportedstructural— a job failure underhydra-gates-require-full-coverage. #182 made that statena, keepingstructuralto mean "PHP files WERE in scope and none declared anything".This PR contains no runtime change.
run-hydra-gates.shandcheck_license_triangle.pyare untouched — the diff is 340 added lines across the two test files, zero deletions. What it adds is the acceptance evidence, plus three gaps in what the suites were actually asserting.The three proofs, measured on this tree
All with the real runner,
--scope-to-diff --require-full-coverage, in a fixture repo that HASlib/and"license": "EUPL-1.2".(a) NOT APPLICABLE — diff =
README.md+src/components/Widget.vue, zero PHP:(b) TRUE POSITIVES STILL CAUGHT — the checker has not been widened into uselessness:
(c) PASS — tagged PHP in scope:
[gate-28] license-triangle: PASS,50 of 50 applicable gates ran.What the suites were NOT asserting
--require-full-coverage. It proved "gate-28 prints NOT APPLICABLE" and inferred "the run is not failed". Different claims — and they came apart once already, when fix(hydra-gates): a not-applicable gate is not a coverage gap — fix the accounting, then flip the flag #164 defaulted the flag TRUE in the workflow while every local suite ran without it._fail 28branch end to end. A licence mismatch is the gate's actual purpose; only the weaker "no file carried a tag" control existed. Thehelper stdout -> _lt_log -> wc -l -> _failwiring was untested — a helper printing findings to stderr would keep every unit test green and turn the gate into a no-op.declared_file_count()had no test in either direction, despite being the single number the PASS-vs-structuraldecision rests on, and nothing pinned the stdout/stderr split it travels over.A trap found while writing this — worth reading
The coverage-failure branch lives inside
if [ "${_FAILED}" -eq 0 ]. Any other failing gate suppresses that sentence entirely, so an assertion of the form "the coverage sentence is absent" passes automatically on a fixture that trips an unrelated gate — including on a fully reverted runner.That is not hypothetical: the first draft wrote its Vue file to
src/App.vue, which gate-38 (skip-link) fails as a root component. Both full-coverage runs exited 1, not 98, and both new assertions were measuring gate-38 rather than gate-28. Fixes now baked into the fixtures and documented in-file:src/components/Widget.vue);ALL N APPLICABLE GATES GREENsentence — a sentence that must be PRESENT cannot be satisfied by a run that died earlier;licensefield rather than untagged PHP, because untagged PHP also fails gate-1 and makes the coverage branch unreachable.Tests added
test_gate_license_triangle_scope.shtest_check_license_triangle.pyBoth appended to —
git diff --statis340 insertions(+), 0 deletions.Mutation-check — every new assertion shown capable of failing
structural(pre-#182)structural->na(gate stops failing)_fail 28branch disableddeclared_file_count->len(files)declared_file_count->0Each mutation was applied with
Edit, measured, then reverted withgit checkout --; the tree was confirmed byte-identical (git status --porcelainclean) before the next.Statuses are reported alongside the assertions but are never the assertion itself — per #209, where gate-19 returned its finding count as an exit status and 266 findings reported as 10.
Regression
hydra-gates/tests/run-helper-suites.sh: 27 passed, 0 failed, 2 quarantined (pre-existing, unrelated).Closes #178
🤖 Generated with Claude Code