fix(e2e): the Standards pages read element from a register that does not carry it - #540
Conversation
…t does not carry it `src/manifest.json` binds Standaarden + StandaardDetail to `register: "@resolve:voorzieningen_register"` with `schema: "element"`, but `lib/Settings/softwarecatalogus_register.json` attaches `element` to the SECOND register it declares — `vng-gemma` (title "AMEF") — and not to `voorzieningen`. Declaring a schema is not attaching it. Only an attached schema is fetchable through `/api/objects/{register}/{schema}`, and OpenRegister's 2026-08-16 change to `ObjectService::setSchema()` turned a register-scoped slug miss from a silent fallback into a throw. Measured from the CI Playwright trace of run 31981873526: GET /api/objects/14/element?_limit=20&_page=1&gemmaType=standaard&_facets=extend -> 404 {"message":"Schema not found: 'element'"} Attaching `element` to `voorzieningen` was considered and rejected: the request would then succeed and return an EMPTY list, because objects live per register and AMEF elements are written to the AMEF one. That trades a visible error for an invisible pass. The fix points the pages at the register that carries the schema, through a second `@resolve:` sentinel. `amef_register` is provisioned in `Application::boot()` from the `amef_config` blob written by `SettingsService::configureAmef()`, exactly as `voorzieningen_register` is from `voorzieningen_config`. configureAmef() detects its register by the PRESENCE of the AMEF core schemas rather than by slug, which is the property this sentinel needs: whatever it selects carries `element` by construction. The nested `st-compliance` object-list on StandaardDetail deliberately keeps `@resolve:voorzieningen_register` — `compliancy` really does live there.
… rows
Repointing the page removes the console error. It does not prove the page works
— a repointed page with no rows is quiet, renders "No items found", and passes
every surface assertion in the suite. That is the invisible pass the register
fix was chosen to avoid, so the test has to go past "the error is gone".
ci-seed.sh:
* requires `vng-gemma` alongside `voorzieningen`. It was unchecked, so an
import producing only one register reported a clean seed.
* resolves the AMEF register from the app's own `/api/amef/config`, i.e. the
same value the `@resolve:amef_register` sentinel resolves to, and fails
loudly when it or `element_schema` is unset.
* probes `/api/objects/<amef>/element` — the request the PAGE makes. Verifying
a slug is present in /api/schemas is a different question from whether it is
attached to the register you are addressing, and only the second one decides
whether the endpoint answers. This is the check whose absence let the defect
ship.
* seeds two `element` objects, idempotent by `identifier`: `Digikoppeling`
(gemmaType `standaard`) and `Zaakregistratiecomponent` (gemmaType
`referentiecomponent`). GEMMA elements normally arrive via the ArchiMate
import of GEMMA_release.xml, which no CI job runs.
* verifies them with a FRESH read (never the save response, which echoes back
properties OpenRegister discarded) under three guards: at least one
standaard; a nonsense gemmaType matching zero; and strictly fewer standaards
than elements, so the page's filter has something to exclude.
All three guards were demonstrated able to fail against a live instance:
deleting the referentiecomponent trips the third, deleting both trips the first,
and the nonsense-value control was measured at 0 with the positive control at 1
of 2.
index-pages.spec.ts now asserts a POPULATED list ("Showing N of M", which
CnIndexPage renders only for a non-empty collection), the seeded standard by
name, and the ABSENCE of the seeded referentiecomponent. The absence assertion
has a real subject: that row exists in the same register and schema, is listed
by an unfiltered page, and the seed fails the job if it is missing — so a zero
means the filter worked, not that the string never existed.
…es the schema
The defect this PR fixes was invisible to every static check in the repo. The
manifest validator checks the value's SHAPE; the gate package's manifest
cross-reference deliberately skips sentinels (its `isLiteralSlug()` excludes any
value containing `@`); and nothing at all compares a page's `(register, schema)`
pair against `lib/Settings/softwarecatalogus_register.json`. It took an E2E run
and a Playwright network trace to see a 404.
This closes both holes from the repository's own files:
1. every `@resolve:<key>` register sentinel in the manifest is provisioned by
`Application::boot()` — an unprovisioned one substitutes null and the page
fetches `/api/objects/null/<schema>`;
2. every `(sentinel, schema)` pair names a register that ATTACHES that schema.
The sentinel -> register-slug map is declared in the test on purpose: nothing in
the app declares it (the ids are discovered at runtime by configureVoorzieningen
/ configureAmef), so an unmapped sentinel FAILS rather than being skipped. A new
sentinel has to be a decision, not a silent gap.
Both checks were shown able to fail before being committed: reverting the
manifest to `@resolve:voorzieningen_register` fails check 2 with the register's
actual schema list quoted in the message, and renaming the provisioned
initial-state key fails check 1. A third test is a standing positive control on
the fixture itself — it asserts `element` is declared, is NOT attached to
voorzieningen, and IS attached to vng-gemma, so check 2 passing cannot be
explained by every register listing every schema.
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 07:45 UTC
Download the full PDF report from the workflow artifacts.
CI verdict — the last red on
|
| job | base development |
this PR |
|---|---|---|
| E2E Tests (Playwright) | failure — 94 passed / 1 failed | success — 95 passed / 0 failed / 3 skipped (8.9m) |
| Quality Report | failure (pure aggregator) | success |
| Hydra Gates | success | success |
The repaired test is the exact one that was red, not a bystander:
✓ 58 [chromium] › tests/e2e/spec-coverage/index-pages.spec.ts:162:5 ›
index standards: nav entry reaches the CnIndexPage surface (toggle + add + list body) (6.5s)
E2E passed in two independent runs of the same commit (the pull_request run and the push run), so this is not a lucky pass.
The seed proved itself in CI, not just locally
[ci-seed] amef config: {"register": "16", ..., "element_schema": "27", ...}
[ci-seed] amef register id: 16
[ci-seed] GET /api/objects/16/element -> 200
[ci-seed] created element fixture e2e-gemma-standaard-digikoppeling (HTTP 201)
[ci-seed] created element fixture e2e-gemma-referentiecomponent-zaakregistratie (HTTP 201)
[ci-seed] element objects: 2 total, 1 with gemmaType=standaard, 0 with a nonsense gemmaType
[ci-seed] standards the index page will list: ['Digikoppeling']
[ci-seed] AMEF element fixtures verified (positive + negative control).
Three things worth reading off that:
GET /api/objects/16/element -> 200is the same request that answered404 Schema not found: 'element'on the failing run. That line is the fix, measured through the page's own endpoint.register: "16"— not14, and not thevng-gemmaid you would guess.configureAmef()picks its register by which one carries the AMEF core schemas, so the sentinel is not tied to a slug. This is exactly why the seed asks/api/amef/configinstead of hardcoding one: seed and page now read the same source.2 total, 1 with gemmaType=standaard, 0 with a nonsense gemmaType— the discriminator row was really created and really excluded, so the spec'stoHaveCount(0)onZaakregistratiecomponenthad a live subject to deny. The page listed['Digikoppeling']and nothing else.
Scope note for the fleet
ObjectService::setSchema() stopped falling back to a global slug lookup after a register-scoped miss. Measured both behaviours in the same hour on the same request shape: a dev instance on an older openregister returned 200 with the row, CI returned 404. Any app in the fleet that addresses a (register, schema) pair where the schema is not attached to that register is red in CI today and may still look healthy locally — worth a sweep, and tests/Unit/AppInfo/ManifestRegisterSentinelTest.php here is portable to any app whose manifest resolves registers through sentinels.
What
Closes softwarecatalog's last failing job on
development:E2E Tests (Playwright)(94 passed / 1 failed), and with itQuality Report, which is a pure aggregator.The one failure is
tests/e2e/spec-coverage/index-pages.spec.ts:141—index standards: nav entry reaches the CnIndexPage surface, failingexpectNoAppErrorson:The measured cause
The console message names neither the status nor the cause. The Playwright trace of run
31981873526does:src/manifest.jsonbinds the Standards pages toregister: "@resolve:voorzieningen_register"+schema: "element", butlib/Settings/softwarecatalogus_register.jsonattacheselementto the second register it declares:voorzieningenelementvng-gemma(title "AMEF")element,model,property-definition,relation,viewSame family as openconnector#1275's
synchronization_run: declaring a schema is not attaching it, and only an attached schema is fetchable through/api/objects/{register}/{schema}.ObjectService::setSchema()used to fall back to a global slug lookup after a register-scoped miss; it now throws. A dev instance on an older openregister still serves this page, so "it works here" is not evidence — measured both ways (see below).Why the obvious fix was rejected
Adding
elementtoregisters.voorzieningen.schemasmakes the request succeed and return nothing, because objects live per register and AMEF elements are written to the AMEF one. That trades a visible error for an empty list — an invisible pass, worse than the red.The fix
Point the pages at the register that carries the schema, through a second
@resolve:sentinel, mirroring the existing one exactly:@resolve:voorzieningen_register@resolve:amef_registervoorzieningen_configblob,.registeramef_configblob,.registerSettingsService::configureVoorzieningen()SettingsService::configureAmef()(already existed)Application::boot()Application::boot()No new mechanism, and no new app-config key was invented:
amef_configis already written byconfigureAmef()and already served byGET /api/amef/config.configureAmef()detects its register by the presence of the AMEF core schemas rather than by slug, which is exactly the property this sentinel needs — whatever it selects carrieselementby construction.The nested
st-complianceobject-list onStandaardDetaildeliberately keeps@resolve:voorzieningen_register;compliancyreally does live in voorzieningen.Proof the page LISTS rows, not merely that the error went
A repointed page with no rows is quiet, renders "No items found", and passes every surface assertion in the suite. That is the failure mode the fix above was chosen to avoid, so the test goes past it.
GEMMA elements normally arrive via the ArchiMate import of the 13 MB
GEMMA_release.xml, which no CI job runs, sotests/e2e/ci-seed.shnow seeds twoelementobjects into the AMEF register (idempotent byidentifier):Digikoppeling—gemmaType: standaard, which the page must list;Zaakregistratiecomponent—gemmaType: referentiecomponent, which it must not. This is the discriminator: without it,config.filter.gemmaTypecould be deleted outright and every assertion would still hold.The seed verifies them with a fresh read (never the save response, which echoes back properties OpenRegister discarded) under three guards, all of which were demonstrated able to fail against a live instance before being committed:
allstandaardThe spec now asserts a populated list (
Showing N of M, whichCnIndexPagerenders only for a non-empty collection — it fails on both an empty register and a failed fetch), the seeded standard by name, and the absence of the seeded referentiecomponent. That absence assertion has a real subject: the row exists in the same register and schema, is listed by an unfiltered page, and the seed fails the job if it is missing — so a zero means the filter worked, not that the string never existed.The seed also probes
/api/objects/<amef>/element— the request the page itself makes. Verifying that a slug is present in/api/schemasis a different question from whether it is attached to the register you are addressing, and only the second one decides whether the endpoint answers. That is the check whose absence let this ship: the old seed printed a clean bill of health on the run that 404'd.A static test so this cannot come back
The defect was invisible to every check in the repo. The manifest validator checks the value's shape; the gate package's manifest cross-reference deliberately skips sentinels (
isLiteralSlug()excludes any value containing@); and nothing compared a page's(register, schema)pair against the register JSON. It took an E2E run and a network trace to see a 404.tests/Unit/AppInfo/ManifestRegisterSentinelTest.phpcloses both holes from the repository's own files:@resolve:<key>register sentinel is provisioned byApplication::boot();(sentinel, schema)pair names a register that attaches that schema.Both were shown able to fail before being committed — reverting the manifest fails check 2 with the register's actual schema list quoted in the message; renaming the provisioned key fails check 1. A third test is a standing positive control on the fixture, so check 2 passing cannot be explained by every register listing every schema.
Local measurements
742f370e,node_modulespresent: full-treeALL 60 APPLICABLE GATES GREEN — and all 60 of them ran; diff-scoped againstorigin/developmentALL 62 APPLICABLE GATES GREEN — and all 62 of them ran(gate-16 spec-coverage PASS, gate-22 PASS, gate-25 PASS, gate-26 PASS, gate-47 PASS, gate-53 PASS). Runner exit 0 both ways. gate-19's 266 is the pre-existing advisory WARNING.phpunit-unit.xml, base vs head on an identical rig: base712 tests / 2459 assertions / 130 errors / 1 failure / 25 skipped, head715 / 2575 / 130 / 1 / 25. +3 tests, +116 assertions, identical red set — strictly additive. (Those 130/1 are a local-rig artefact of the stub bootstrap; CI runsphpunit.xmlagainst a real openregister and is green there.)[OK] No errors— and shown to be reading the file, not a zero-file run: the same invocation at--level=9reports 20+ errors in it.--checkclean,node tests/validate-manifest.jsAjv validation: PASS (0 errors),tsc --noEmitclean on the changed spec.gemmaTypebare-property filter measured with positive and negative controls against a real OpenRegister — 1 of 2 filtered, 0 for a nonsense value, 2 unfiltered.Not changed on purpose
defaults-outside-their-enum and the Dutchx-openregister-lifecycleblock fixed earlier are untouched — re-verifiedelementis still declared, still unattached tovoorzieningen, still attached tovng-gemma.components.objects. Shipping catalogue content to every instance is a product decision; the authorised decision here was config ownership. The seed's fixtures are test-scoped.🤖 Generated with Claude Code