Skip to content

test(gates): real contract tests close gate-25 (41 -> 0); gate-26 3 -> 1 - #480

Merged
rubenvdlinde merged 2 commits into
developmentfrom
fix/coverage-gates-swc
Aug 11, 2026
Merged

test(gates): real contract tests close gate-25 (41 -> 0); gate-26 3 -> 1#480
rubenvdlinde merged 2 commits into
developmentfrom
fix/coverage-gates-swc

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Closes gate-25 (contract-coverage) and reduces gate-26, measured with the gate helpers at ConductionNL/.github@b8c7ead — the SHA this repo's CI floats on (no hydra-gates-ref is set, so the shared workflow's @main default applies).

gate before after
gate-25 contract-coverage 41 0 — PASS
gate-26 visual-coverage 3 1

gate-25: 9 new PHPUnit classes, 134 tests, 459 assertions

No @contract exclude was added and no annotation was placed over untested code. Every test calls the controller method under test and asserts its wire contract.

What these pin, beyond "a 200 comes back":

  • deny-before-grant on every registered public endpoint — the backing service is asserted never invoked for an anonymous caller, so an implementation that queried first and filtered afterwards fails.
  • GET /api/gebruik (@PublicPage): a gebruik-beheerder is narrowed to their own organisation before the _rbac:false bypass query is issued, and asking for another organisation's afnemer is denied outright rather than silently widened (vendor-visibility-rbac REQ-001/REQ-003).
  • /api/aangeboden-gebruik/ambtenaar{,/{id}} (@PublicPage + RBAC bypass): the admin/ambtenaar group check is the only thing between an anonymous caller and every organisation's records — both the empty envelope and the un-issued query are asserted.
  • GET /api/email/config: the non-admin 403. This endpoint once returned the SMTP password and provider API keys to any authenticated user; a test asserting only "200 for a logged-in user" would have passed on the broken version.
  • /api/progress/{id} and its SSE twin: another user's operation reads as 404 with no progress key — the ownership guard, on both variants.
  • /api/archimate/download/{fileName}: five traversal shapes refused 400 with the DI container asserted never consulted, proving the guard runs before any filesystem resolution.
  • /api/contactpersonen/change-password: the full ladder — non-admin on another account 403, self-service without the current password 400, wrong current password 403, <10 chars 400, and a policy-rejected setPassword() surfaced as a failure rather than swallowed.
  • /api/preferences/{key}: the key reaching IConfig is proven sanitised and pref_-namespaced, so ../apps/Password cannot read another app's user values.

Proven in both directions. Removing the anonymous guard from ViewController::getView() turns the corresponding test red (500 != 401); restored, green.

Skip count unchanged at 25. The suite goes 528 → 662 tests with no test passing by being skipped.

gate-26: 3 → 1

LifecycleRoadmapView.vue now has behavioural e2e asserting the component's own surface (root class, h2, intro, refresh control, org selector, and that .rm-groups is absent before an organisation is picked). The previous assertion was an OR over two strings that a breadcrumb or the nav entry alone satisfies — it could pass on a page that is not this component.

src/views/organisaties/OrganisatieIndex.vue is deleted as dead code, not waived: the manifest's Organisaties page is now type: index with config.cardComponent: OrganisatieCard (Phase 8), the file's own docblock names the CnIndexPage cardComponent gap as its reason to exist, and that gap is closed. Nothing in src/ imports it and no router or manifest entry names it.

Deliberately left open

src/views/gemmaviews/GemmaViewIndex.vue is likewise unreachable, but this repo's own openspec/changes/beta-surface-alignment/proposal.md defers its disposition to a maintainer ("may be dead code or a future menu item"). A @visual exclude reading "nothing routes to it" is a claim about the state of the world that rots the moment someone wires it up, so gate-26 stays at 1 pending a delete/keep decision rather than being closed with a waiver.

Related finding (not addressed here)

An audit of this repo's 124 @e2e exclude waivers found 73 false (58.9%). The dominant cause is structural: enable-newman: false in this workflow, so every waiver terminating at "verified by Newman" fails the "runs in this pipeline" test; and 26 waivers cite vitest component tests that do not exist. gate-19's 291 is therefore an undercount. Reported separately — no waiver text is changed in this PR.

🤖 Generated with Claude Code

…6 3 -> 1

gate-25 (contract-coverage) 41 -> 0 and gate-26 (visual-coverage) 3 -> 1,
measured with the gate helpers at ConductionNL/.github@b8c7ead — the SHA the
shared quality workflow floats on, which is what this repo's CI actually runs
(this repo sets no `hydra-gates-ref`, so it defaults to @main).

Nine new PHPUnit contract-test classes, 134 tests, 459 assertions. Every one
calls the controller method under test and asserts its wire contract; none of
them is an annotation over untested code, and no `@contract exclude` was added.

What the tests actually pin, beyond "a 200 comes back":

  * deny-before-grant on every registered public endpoint — the backing
    service is asserted NEVER invoked when the caller is anonymous, so an
    implementation that queried first and filtered afterwards fails.
  * `GET /api/gebruik` (@publicpage): a `gebruik-beheerder` is narrowed to
    their own organisation BEFORE the `_rbac:false` bypass query is issued,
    and asking for another organisation's `afnemer` is denied outright rather
    than silently widened (vendor-visibility-rbac REQ-001/REQ-003).
  * `/api/aangeboden-gebruik/ambtenaar{,/{id}}` (@publicpage + RBAC bypass):
    the admin/ambtenaar group check is the only thing between an anonymous
    caller and every organisation's records — both the empty envelope AND the
    un-issued query are asserted.
  * `GET /api/email/config`: the non-admin 403. This endpoint once returned
    the SMTP password and provider API keys to any authenticated user; a test
    asserting only "200 for a logged-in user" would have passed on the broken
    version.
  * `/api/progress/{id}` and its SSE twin: another user's operation reads as
    404 with no `progress` key — the ownership guard, on both variants.
  * `/api/archimate/download/{fileName}`: five traversal shapes refused 400
    with the DI container asserted never consulted, so the guard is proven to
    run before any filesystem resolution.
  * `/api/contactpersonen/change-password`: the full ladder — non-admin on
    another account 403, self-service without the current password 400, wrong
    current password 403, <10 chars 400, policy-rejected `setPassword()` false
    surfaced as a failure rather than swallowed.
  * `/api/preferences/{key}`: the key that reaches IConfig is proven
    sanitised and `pref_`-namespaced, so `../apps/Password` cannot read
    another app's user values.

Proven in both directions: removing the anonymous guard from
`ViewController::getView()` turns the corresponding test red (500 != 401);
restored, green. Skip count is unchanged at 25 — the suite goes 528 -> 662
tests with no test passing by being skipped.

gate-26: `LifecycleRoadmapView.vue` now has behavioural e2e that asserts the
component's OWN surface (root class, h2, intro, refresh control, org selector,
and that `.rm-groups` is ABSENT before an organisation is picked). The previous
assertion was an OR over two strings that a breadcrumb or the nav entry alone
satisfies — it could pass on a page that is not this component.

`src/views/organisaties/OrganisatieIndex.vue` is deleted as dead code, not
waived: the manifest's `Organisaties` page is now `type: index` with
`config.cardComponent: OrganisatieCard` (Phase 8), the file's own docblock
names the CnIndexPage `cardComponent` gap as its reason to exist, and that gap
is closed. Nothing in src/ imports it and no router or manifest entry names it.

Remaining, deliberately NOT waived: `src/views/gemmaviews/GemmaViewIndex.vue`
is likewise unreachable, but this repo's own
openspec/changes/beta-surface-alignment/proposal.md defers its disposition to a
maintainer ("may be dead code or a future menu item"). Writing a `@visual
exclude` whose reason is "nothing routes to it" would be a claim about the
state of the world that rots the moment someone wires it up, so gate-26 stays
at 1 pending that decision rather than being closed with a waiver.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/softwarecatalog @ cbd2cb3

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue-demi
test-l10n
composer ✅ 128/128
npm ✅ 718/718
PHPUnit
Newman ⏭️
Playwright
Hydra gates

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

Download the full PDF report from the workflow artifacts.

CI run 31475813082 failed on this assertion (75 passed, 1 failed) and it was my
bug, not the product's. The NcButton carries aria-label="Refresh data"; an
aria-label overrides text content when computing the accessible name, so
`getByRole('button', { name: 'Refresh', exact: true })` could never match the
visible label "Refresh".

Querying by the accessible name is also the better assertion — it is what a
screen-reader user actually hears, so a future change that drops the aria-label
now fails here.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/softwarecatalog @ b0cd7af

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue-demi
test-l10n
composer ✅ 128/128
npm ✅ 718/718
PHPUnit
Newman ⏭️
Playwright
Hydra gates

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

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 126eda6 into development Aug 11, 2026
29 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/coverage-gates-swc branch August 11, 2026 09:33
@rubenvdlinde
rubenvdlinde restored the fix/coverage-gates-swc branch August 14, 2026 08:35
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