Skip to content

fix(e2e): the Standards pages read element from a register that does not carry it - #540

Merged
rubenvdlinde merged 3 commits into
developmentfrom
fix/swc-standards-register
Aug 17, 2026
Merged

fix(e2e): the Standards pages read element from a register that does not carry it#540
rubenvdlinde merged 3 commits into
developmentfrom
fix/swc-standards-register

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

What

Closes softwarecatalog's last failing job on development: E2E Tests (Playwright) (94 passed / 1 failed), and with it Quality Report, which is a pure aggregator.

The one failure is tests/e2e/spec-coverage/index-pages.spec.ts:141index standards: nav entry reaches the CnIndexPage surface, failing expectNoAppErrors on:

Error fetching 14-element collection: Proxy(Object)

The measured cause

The console message names neither the status nor the cause. The Playwright trace of run 31981873526 does:

GET /api/objects/14/element?_limit=20&_page=1&gemmaType=standaard&_facets=extend
  -> 404 {"message":"Schema not found: 'element'"}

src/manifest.json binds the Standards pages to register: "@resolve:voorzieningen_register" + schema: "element", but lib/Settings/softwarecatalogus_register.json attaches element to the second register it declares:

register schemas
voorzieningen 15 — no element
vng-gemma (title "AMEF") element, model, property-definition, relation, view

Same 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}.

⚠️ This became a hard failure only on 2026-08-16. OpenRegister's 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 element to registers.voorzieningen.schemas makes 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:

voorzieningen AMEF (new)
sentinel @resolve:voorzieningen_register @resolve:amef_register
config home voorzieningen_config blob, .register amef_config blob, .register
written by SettingsService::configureVoorzieningen() SettingsService::configureAmef() (already existed)
provisioned in Application::boot() Application::boot()

No new mechanism, and no new app-config key was invented: amef_config is already written by configureAmef() and already served by GET /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 carries element by construction.

The nested st-compliance object-list on StandaardDetail deliberately keeps @resolve:voorzieningen_register; compliancy really 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, so tests/e2e/ci-seed.sh now seeds two element objects into the AMEF register (idempotent by identifier):

  • DigikoppelinggemmaType: standaard, which the page must list;
  • ZaakregistratiecomponentgemmaType: referentiecomponent, which it must not. This is the discriminator: without it, config.filter.gemmaType could 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:

state all standaard nonsense verdict
both fixtures 2 1 0 PASS
referentiecomponent deleted 1 1 0 guard 3 trips
both deleted 0 0 0 guard 1 trips

The spec now asserts a populated list (Showing N of M, which CnIndexPage renders 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>/elementthe request the page itself makes. Verifying that 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. 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.php closes both holes from the repository's own files:

  1. every @resolve:<key> register sentinel is provisioned by Application::boot();
  2. every (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

  • Hydra gates, package 742f370e, node_modules present: full-tree ALL 60 APPLICABLE GATES GREEN — and all 60 of them ran; diff-scoped against origin/development ALL 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, phpunit-unit.xml, base vs head on an identical rig: base 712 tests / 2459 assertions / 130 errors / 1 failure / 25 skipped, head 715 / 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 runs phpunit.xml against a real openregister and is green there.)
  • phpcs exit 0, phpstan [OK] No errors — and shown to be reading the file, not a zero-file run: the same invocation at --level=9 reports 20+ errors in it.
  • prettier --check clean, node tests/validate-manifest.js Ajv validation: PASS (0 errors), tsc --noEmit clean on the changed spec.
  • Live: the gemmaType bare-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

  • No schema was touched, so no version bump was needed. The six defaults-outside-their-enum and the Dutch x-openregister-lifecycle block fixed earlier are untouched — re-verified element is still declared, still unattached to voorzieningen, still attached to vng-gemma.
  • No GEMMA reference data was added to 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

…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.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/softwarecatalog @ 0299629

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.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

CI verdict — the last red on development is closed

Code Quality run 32006146798: completed / success — 32 success, 5 skipped, 0 failures. Base development (run 31981873526) was 30 success / 5 skipped / 2 failures.

job base development this PR
E2E Tests (Playwright) failure — 94 passed / 1 failed success95 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:

  1. GET /api/objects/16/element -> 200 is the same request that answered 404 Schema not found: 'element' on the failing run. That line is the fix, measured through the page's own endpoint.
  2. register: "16" — not 14, and not the vng-gemma id 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/config instead of hardcoding one: seed and page now read the same source.
  3. 2 total, 1 with gemmaType=standaard, 0 with a nonsense gemmaType — the discriminator row was really created and really excluded, so the spec's toHaveCount(0) on Zaakregistratiecomponent had a live subject to deny. The page listed ['Digikoppeling'] and nothing else.

Scope note for the fleet

⚠️ This became a hard failure only on 2026-08-16, when OpenRegister's 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.

@rubenvdlinde
rubenvdlinde merged commit 6d91a5b into development Aug 17, 2026
81 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/swc-standards-register branch August 17, 2026 07:47
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