test(gates): real contract tests close gate-25 (41 -> 0); gate-26 3 -> 1 - #480
Merged
Conversation
…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.
Contributor
Quality Report — ConductionNL/softwarecatalog @
|
| 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.
Contributor
Quality Report — ConductionNL/softwarecatalog @
|
| 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.
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.
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 (nohydra-gates-refis set, so the shared workflow's@maindefault applies).gate-25: 9 new PHPUnit classes, 134 tests, 459 assertions
No
@contract excludewas 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":
GET /api/gebruik(@PublicPage): agebruik-beheerderis narrowed to their own organisation before the_rbac:falsebypass query is issued, and asking for another organisation'safnemeris 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 noprogresskey — 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-rejectedsetPassword()surfaced as a failure rather than swallowed./api/preferences/{key}: the key reaching IConfig is proven sanitised andpref_-namespaced, so../apps/Passwordcannot 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.vuenow has behavioural e2e asserting the component's own surface (root class, h2, intro, refresh control, org selector, and that.rm-groupsis 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.vueis deleted as dead code, not waived: the manifest'sOrganisatiespage is nowtype: indexwithconfig.cardComponent: OrganisatieCard(Phase 8), the file's own docblock names the CnIndexPagecardComponentgap as its reason to exist, and that gap is closed. Nothing insrc/imports it and no router or manifest entry names it.Deliberately left open
src/views/gemmaviews/GemmaViewIndex.vueis likewise unreachable, but this repo's ownopenspec/changes/beta-surface-alignment/proposal.mddefers its disposition to a maintainer ("may be dead code or a future menu item"). A@visual excludereading "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 excludewaivers found 73 false (58.9%). The dominant cause is structural:enable-newman: falsein 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