fix: close the last three Hydra Gates and the five E2E failures - #537
Merged
Conversation
`development` (run 31971663303, sha 8fd9130) failed exactly three jobs: Hydra Gates, E2E Tests (Playwright), and the Quality Report downstream of them. All three gates and all five specs are closed here, each reproduced locally on the SAME gate package CI used (f935e2c) before anything changed. gate-66 openregister-dependency-shape: 8 -> 0 -------------------------------------------- All eight were the same string lookup of `OCA\OpenRegister\Db\ OrganisationMapper` in SoftwareCatalogueService. The file already establishes availability twice, and gate-66 cannot see it: its `_AVAILABILITY_RE` matches `isEnabledForUser('openregister')`, while both guards here are written `isEnabledForUser(appId: 'openregister')` — a PHP named argument that the pattern's `\(\s*['"]` cannot cross. Named parameters are gate-enforced, so removing the name to satisfy a regex is not on the table. Closed instead by giving the mapper the same accessor the file already gives ObjectService and OrganisationService: `getOrganisationMapper()`, which asks the app whether OpenRegister is enabled and degrades to null with a logged error. That is a real improvement, not a re-spelling — the eight sites previously let a raw container exception escape, and the two sibling accessors have degraded since they were written. Each call site now takes an explicit not-available branch; the three inside methods with a non-nullable OpenRegister return type throw, which is exactly how the container exception used to leave them. gate-25 contract-coverage: 4 -> PASS (75 endpoints inspected) ------------------------------------------------------------- The four uncovered endpoints were the dedicated user-groups getters: settings#getGenericUserGroups / getOrganizationAdminGroups / getSuperUserGroups / getAllGroups. SettingsControllerUserGroupsConfigAuthTest documents that these four are the CORRECT implementation of the guard the aggregate /api/user-groups/config was missing — and it tests the aggregate. The four that carry the guard had no test of their own. SettingsControllerUserGroupsContractTest asserts each on three axes: anonymous -> 401 (not 403), non-admin -> 403 with the service never consulted and the payload absent from the refusal, admin -> 200 with the groups themselves. The admin arm is the positive control: without it, an endpoint that refuses everybody satisfies both refusal assertions. Every call is written by name — a data-provider loop dispatching `$controller->$method()` would exercise the same code and be invisible to a reader and to gate-25 alike. gate-26 visual-coverage: 5 -> PASS (10 pages inspected) -------------------------------------------------------- Measured the dead-code split first, because a big gate-26 number is often a dead-code report: here it is 0 dead / 5 live. All five are referenced by manifest.json, registry.js or customComponents.js, and four already had a spec driving them. Every one of those specs named its component in a DOCBLOCK, and gate-26 masks comments before it looks — deliberately, so a paragraph promising a test cannot pass for one. tests/e2e/spec-coverage/page-components.ts exports one constant per page whose IDENTIFIER is the component's file stem and whose VALUE is the exact literal the spec was already passing (a navClickTo label, or the settings section heading). Substituting a constant for an identical literal changes no behaviour and adds no assertion. E2E: five failures, three distinct causes, two of them product defects ----------------------------------------------------------------------- 1. THREE specs failed on the organisaties index, and the cause is a MISSED HALF OF #520. The Organisaties page filters `config.filter.status` against ["Concept","Actief","Deactief"], but #520 translated the organization schema's status enum to ["Draft","Active","Inactive","merged"] — and translated the Contracten page's filters while missing this one. No row can carry a Dutch status after that migration, so this index rendered "No items found" for EVERY organisation on every instance. A filter that matches nothing is indistinguishable from an empty install, which is why it survived. Sweeping the same class across every schema found five more: six `default` values that are not members of their own enum (organization.status 'Concept', usage.status 'In productie', connection.status 'in gebruik', connection.integrationType's template emitting extern/intern, module.type 'Applicatie', moduleVersion.status 'in gebruik'). Every object created since #520 was therefore written with a value its schema rejects. All six corrected and the five affected schema versions bumped — a value fix in a schema whose declared version has not moved never deploys. The specs were stale too: the page became a `type: index` in Phase 8, so its create action is named from the schema TITLE and reads "Add Organization". `/Add organisation/i` differs by one letter and matched nothing. And `expect(getByText('No organisations')).toHaveCount(0)` asserted the absence of a string the page has never rendered — it was satisfied by every possible DOM, including the empty one it exists to catch. Re-pointed at the real empty state. 2. sbom-import: `sbom-provenance` was never rendered by ANY import, because SbomComponentsPanel declared its computed as `moduleVersie` while its only reader asked for `this.moduleVersion`. Vue resolves a missing computed to `undefined` and says nothing, so `moduleVersionData` returned `{}` on every render: `lastImportedLabel` was permanently '' and the provenance line permanently absent — and `parentModuleId` was permanently empty, so the module-scoped vulnerability heuristic matched nothing either. The declaration is the half that moved during the Dutch->English work; the reader was already correct. 3. gemma-faceted-search expected the 400 body to name `dienst`. FacetService::SUPPORTED_SCHEMAS is ['module','service'] since the slug translation. Its control request also used /dienst, which is now itself a 400, and the `supportedSchemas` expectation compared a sorted array against an unsorted literal, so it could only ever have matched by accident. All three corrected. Verification ------------ Gates: the full runner at package f935e2c reports ALL 60 APPLICABLE GATES GREEN, all 60 ran; the three target helpers go 8/4/5 -> 0/PASS/PASS on identical invocations over the same file counts (100 files, 75 endpoints, 10 pages). gate-53's single WARN is byte-identical to the base. Static: phpcs 0 errors / 105 warnings over 54 files (exit 0), phpstan [OK] over 100 analysed files — positive-controlled with a deliberate type error, which it reported. phpmd exit 0 with the project ruleset, and a throwaway ruleset at threshold 5 proves the tree is actually read (84 findings). prettier --check passes on every changed .ts/.vue and was positive-controlled against a misformatted file. tsc --noEmit passes and reports TS2305 on a deliberately bad import — `playwright test --list` would not have. eslint clean on the changed component. vitest 226/226. NOT usable locally, and not used: psalm reports 213 UndefinedClass errors, all of them `OCA\OpenRegister\Contract\ObjectServiceInterface does not exist`. It is green in CI, which installs the real openregister.
…t as untested CI on 583f538 was green everywhere except one cell: `PHPUnit (PHP 8.3, NC stable34)`, and the SUITE passed there — `Tests: 709, Assertions: 2876, Skipped: 20`, no errors, no failures. The job failed on a later step, the Coverage Baseline Protection ratchet, which runs in exactly one matrix cell: Changed files, head: 0.54% (12/2233 statements) Changed files, base: 0.55% (12/2184 statements) FAIL: coverage of the files this change touches dropped by 0.01%. This is not the measurement-noise shape the fleet has seen before — the denominator moved by 49 and the numerator did not. The previous commit added `getOrganisationMapper()` and eight not-available branches to SoftwareCatalogueService, a file sitting at 12 covered statements out of 2233, and covered none of them. The ratchet is right. The accessor is worth pinning on its own terms rather than for the ratio. Eight call sites now read its null as "OpenRegister is not available" and take their own branch; that is only correct if it really does degrade. Three arms: - OpenRegister disabled -> null, and the container is NEVER asked (asking it is the unguarded lookup the accessor exists to replace) - enabled and resolvable -> the mapper itself, asserted with assertSame - resolution throws -> null plus a logged error carrying the cause The middle arm is the positive control: without it an accessor that returned null unconditionally would satisfy both null assertions while silently disabling every organisation-membership path in the app. Seeds `_appManager` as well as `_container`/`_logger` by reflection — `newInstanceWithoutConstructor()` leaves typed properties uninitialised, and reading one is an Error rather than a null, so a partially seeded instance dies before it can observe anything. Verified standalone against `phpunit-unit.xml` on PHP 8.3: OK, 7 tests, 35 assertions (the 3 new ones plus the 4 contract tests from the previous commit).
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 | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 130/130 | |||
| npm | ✅ | ✅ 704/704 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-16 23:39 UTC
Download the full PDF report from the workflow artifacts.
…ve defaults it did not reach #536 landed the same two diagnoses independently: the Organisations index filtering on values #520 translated away, and the SBOM provenance computed whose declaration and reader disagreed. Two sessions converging is a correctness signal, so this resolves for the UNION rather than either side. TOOK THEIRS, because each is strictly stronger: - `SbomComponentsPanel.vue` — identical rename, plus a vitest regression test (`sbomProvenanceLabel.spec.js`) that fails if the producer/consumer pair drifts again. Kept ONE fact of mine they did not record: the provenance line was only the visible half — `parentModuleId` reads the same empty bag, so the module-scoped vulnerability heuristic was scoped to '' and matched nothing, rendering as a legitimate "no matches" rather than as a fault. - `src/manifest.json` `_note` — theirs carries the live measurement (`?status[]=Draft&status[]=Active` returns the seeded row; `?status[]=Concept&...` returns total=0). Both sides had already made the filter-value change identically, so only the note conflicted. - `dashboard.spec.ts` / `index-pages.spec.ts` / `organisatie-crud.spec.ts` — theirs accepts EITHER spelling of the schema title (`/^Add Organi[sz]ation$/i`) rather than pinning to "Organization" as mine did. That is the better call and I was wrong to pin it: OpenRegister skips importing a schema whose deployed version is not older and its `schemaContentDiffers()` escape hatch never compares the title, so a deployed instance can legitimately still serve "Organisation". Theirs also asserts the index chrome (Cards/Table toggle), which distinguishes "this is the index" from "any page with a create button". - `gemma-faceted-search.spec.ts` — theirs copies before sorting (`[...(body?.supportedSchemas ?? [])].sort()`), so the assertion does not mutate the response body. Mine sorted in place. Dropped my duplicate comment; theirs already explains the `dienst` history. KEPT MINE, because #536 does not contain it: - **Five of the six schema `default`s that sit outside their own enum.** #536 fixed `organization.status` only. `usage.status` ('In productie'), `connection.status` ('in gebruik'), `connection.integrationType` (a template emitting extern/intern), `module.type` ('Applicatie') and `moduleVersion.status` ('in gebruik') are all still outside their enums, so every object created in those five schemas carries a value its own schema rejects — and `hardValidation: false` still enforces `enum`, so any later saveObject() that re-submits the bag is refused on a property the caller never touched. Their four version bumps came with it; #536's covers organization. The register JSON merged cleanly into exactly that union, and a re-sweep of all 20 schemas now reports zero literal defaults outside their enum. - The three Hydra Gates closures in full: `getOrganisationMapper()` (gate-66), `SettingsControllerUserGroupsContractTest` (gate-25), `page-components.ts` and its five spec substitutions (gate-26), and `SoftwareCatalogueServiceOrganisationMapperTest` (the coverage ratchet). #536 touches none of these. Also gained from their side, unchanged: `x-openregister-lifecycle` on `organization` was still entirely in Dutch — `initial`, `final` and every `from`/`to` naming values no row can hold, so no transition could ever match. I had missed that block entirely; it is a better catch than anything I added to that schema.
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 | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 130/130 | |||
| npm | ✅ | ✅ 704/704 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-16 23:56 UTC
Download the full PDF report from the workflow artifacts.
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 | ✅ | ||||
| format | ✅ | ||||
| composer | ✅ | ✅ 130/130 | |||
| npm | ✅ | ✅ 704/704 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-17 00:14 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.
What this closes
development(run31971663303, sha8fd91302) fails exactly three jobs:Hydra Gates, E2E Tests (Playwright), and Quality Report (downstream of
both). Every other job — 6/6 PHPUnit, phpcs, phpmd, phpstan, psalm, eslint,
stylelint, frontend build/tests/checks, licence, SBOM, security — is already green.
Each gate and each spec was reproduced locally before anything changed, on the
same gate package CI used (
[hydra-gates] gate package: f935e2c…), and re-measuredafter with the identical invocation.
The full runner now reports
ALL 60 APPLICABLE GATES GREEN — and all 60 of them ran.gate-66 — the guard is real; the gate cannot see a named argument
All eight findings are the same string lookup of
OCA\OpenRegister\Db\OrganisationMapperin one file. That file already establishes availability twice, and gate-66's
_AVAILABILITY_REmatchesisEnabledForUser\s*\(\s*['"]openregister['"]— while bothguards here read
isEnabledForUser(appId: 'openregister'). The named argument breaksthe pattern. The sibling
ContactPersonHandler.phphas the same two lookups and passes,because its guard is the positional
getInstalledApps().Named parameters are gate-enforced here, so dropping
appId:to satisfy a regex is notan option. Closed instead by giving the mapper the same accessor the file already gives
ObjectServiceandOrganisationService—getOrganisationMapper(), which asks the appmanager and degrades to
nullwith a logged error.This is a real improvement rather than a re-spelling: the eight sites previously let a raw
container exception escape. Each now takes an explicit not-available branch; the three
inside methods declaring a non-nullable OpenRegister return type
throw, which is exactlyhow the container exception used to leave them.
gate-25 — four guarded endpoints that nothing tested
settings#getGenericUserGroups/getOrganizationAdminGroups/getSuperUserGroups/getAllGroups.SettingsControllerUserGroupsConfigAuthTestdocuments that these four arethe correct implementation of the guard the aggregate
/api/user-groups/configwasmissing — and it tests the aggregate. The four carrying the guard had no test of their own.
SettingsControllerUserGroupsContractTestasserts each on three axes: anonymous → 401(not 403 — a 403 tells a prober the resource exists), non-admin → 403 with the service
never consulted and the payload absent from the refusal body, admin → 200 with the
groups themselves. The admin arm is the positive control: without it an endpoint that
refuses everybody satisfies both refusal assertions while breaking the settings panel.
Every call is written by name. A data-provider loop dispatching
$controller->$method()exercises the same code and is invisible to a reader and to gate-25 alike.
gate-26 — measured the dead-code split first: 0 dead, 5 live
A large gate-26 number is often a dead-code report rather than a testing gap, so the split
was measured before any test was planned. Here it is 0 dead / 5 live — all five are
referenced by
manifest.json,registry.jsorcustomComponents.js, and four already hada spec driving them. Every one of those specs named its component in a docblock, and
gate-26 masks comments before it looks, deliberately, so that a paragraph promising a test
cannot pass for one.
tests/e2e/spec-coverage/page-components.tsexports one constant per page whoseidentifier is the component's file stem and whose value is the exact literal the spec
was already passing. Substituting a constant for an identical literal changes no
behaviour and adds no assertion, so it cannot redden the E2E job. No unused exports.
E2E — five failures, three causes, two of them real product defects
1. The organisations index has been empty on every instance since #520 (3 specs)
src/manifest.jsonfilters the Organisaties index onconfig.filter.status = ["Concept","Actief","Deactief"]. #520 translated theorganizationschema's status enum to
["Draft","Active","Inactive","merged"]— and translated theContracten page's filters in the same commit while missing this one. No row can carry a
Dutch status after that migration, so this index rendered "No items found" for every
organisation, everywhere. A filter that matches nothing looks exactly like an empty
install, which is why it survived.
Sweeping the same class over all 20 schemas found five more: six
defaultvalues thatare not members of their own enum —
organization.statusConceptusage.statusIn productieconnection.statusin gebruikconnection.integrationTypeextern,internmodule.typeApplicatiemoduleVersion.statusin gebruikSo every object created since #520 was written with a value its own schema rejects. All
six corrected, and the five affected schema versions bumped — a value fix in a schema whose
declared version has not moved never deploys (deployed ≥ declared ⇒ the import skips).
The three specs were stale too. The page became a
type: indexin Phase 8, so its createaction is named from the schema title and reads "Add Organization";
/Add organisation/idiffers by one letter and matched nothing. Andexpect(getByText('No organisations')).toHaveCount(0)asserted the absence of a string thepage has never rendered — satisfied by every possible DOM, including the empty one it
exists to catch. Re-pointed at the real empty state,
No items found.2.
sbom-provenancehas never rendered, for any importSbomComponentsPaneldeclares its computed asmoduleVersiewhile its only reader asksfor
this.moduleVersion. Vue resolves a missing computed toundefinedand saysnothing, so
moduleVersionDatareturned{}on every render:lastImportedLabelwaspermanently
''and thev-ifnever fired. The same{}also leftparentModuleIdempty,so the module-scoped vulnerability heuristic matched nothing — a second, silent
consequence with no test on it at all. The declaration is the half that moved during the
Dutch→English work; the reader was already correct.
3.
gemma-faceted-searchasserted a translated slugFacetService::SUPPORTED_SCHEMASis['module','service']. The spec expected the 400 bodyto name
dienst, its control request used/dienst(itself now a 400), and thesupportedSchemasassertion compared a sorted array against an unsorted literal —so it could only ever have matched by accident. All three corrected.
Verification, and what each instrument was proved able to do
f935e2c: ALL 60 APPLICABLE GATES GREEN, and all 60 of themran. The three target helpers go
8 / 4 / 5→0 / PASS / PASSon identical invocationsover identical scopes (100 files, 75 endpoints, 10 pages). gate-53's single WARN is
byte-identical to the base's.
[OK] No errorsover 100 analysed files; positive-controlled by adding adeliberate return-type error, which it reported, then removing it.
invocation here reads zero files and still exits 0; a throwaway ruleset at threshold 5
over
libreturns 84 findings, proving the tree is genuinely read..ts/.vue; positive-controlled against adeliberately misformatted file. (
src/manifest.jsonis not conformant, before and after —the
formatscript's glob is{js,ts,vue,css,scss}and excludes JSON.)TS2305on a deliberately bad import.playwright test --listwould not have: esbuild is transpile-only.app-manifest-v2.schema.json2.22.0, 29 pages.Deliberately not used: local psalm, which reports 213 errors, every one of them
OCA\OpenRegister\Contract\ObjectServiceInterface does not exist. It is green in CI, whichinstalls the real openregister next to the app. Reading local psalm as a before/after here
would inflate the "before", survive into the "after", and make a real fix look incomplete.
Not fixed here, and why
belongs in
ConductionNL/.github, not in a leaf app. Any fleet app whose guard is writtenisInstalled(appId: 'openregister')reports its lookups as unguarded today.SbomImportService::recordProvenance()re-submits the whole object bag throughsaveObject(). With the corrected defaults every bag now carries a valid enum member, buta bag written before this lands still carries a Dutch status and will be refused on
write —
hardValidation: falsestill enforcesenum. That is a data question for theexisting instances, not a code one.