gates: close gate-54 and gate-25, take gate-26 to PASS — and the three defects the contract test found - #465
Conversation
…target ConsultationRequest.achterbanraadpleging carried format:uuid and a relation description with no $ref, justified as keeping a 'cross-capability dependency soft' because constituency-consultation 'lands in any order'. That reason is refuted by the file it is written in: fragment 47 already hard-$refs schemas owned by fragments 42, 46 and 55, and the register.d set carries 61 such cross-fragment $refs in total. MemberConsultation (slug member-consultation, fragment 48) is the named target and resolves in the same register set, so the relation is expressible. Optionality is now carried by nullable, where it belongs, instead of by withholding the target. Voordracht.voordragendePartij.referentie is genuinely polymorphic — persoon resolves against Person, fractie/orgaan against GovernanceBody — so it declares x-relation-schema-field: type. That is a claim the gate verifies (the named sibling must exist on the same object), not a waiver.
This reverts commit 2d9af0d.
…d nothing
gate-25 reported four public endpoints no automated test had ever called.
Writing the contract test for them found three defects, every one of which
answered a healthy 200/201 with an empty or wrong body:
1. A citizen could cast unlimited advisory votes on one proposal. The dedup
query filtered on '_relations.budget-proposal'. OpenRegister keys the
_relations JSONB by the property path it walked ('relations.0.id'), never
by the related schema's slug, so it returned zero rows, the guard was
skipped, and a second CitizenVote was written carrying the SAME
deterministic 'idempotency' slug as the first.
2. tallyAdvisoryProposal() wrote votesFor=0/votesAgainst=0 back over real
votes, from the same filter.
3. publishBudgetResults() published proposals:[], participationCount:0 and
allocatedAmount:0 for every round, because fetchValidatedProposals()
filtered on '_relations.participatory-budget' rather than the flat
participatoryBudget property PortalContributionProvider actually writes.
lib/Service/ObjectRelationFilter.php already documents this exact failure and
ships filterFor() as the remedy; seven call sites use it and twenty-three still
hand-write the slug. The two structured-relations queries here now use
filterFor(); the proposal query uses the plain property, with relatesToBudget()
still re-checking both write shapes.
Measured on a live instance, before -> after: duplicate vote 201 -> 409;
votesFor after two votes 0 -> 1; published summary 0/0/[] -> 1/2000/[1].
This reverts commit b0347c4.
…f them A green local run was a green over an unopened scope — the exact shape the fleet keeps paying for. Glob the directory so the local runner and CI's `for collection in *.postman_collection.json` measure the same set, and fail loudly rather than silently when the glob is empty.
…renders gate-26 flagged four page components with no visual proof. Rendering all four in a browser showed two of them cannot render at all. /decisions/:id/integrations and /agenda-items/:id/integrations are manifest pages of type:"detail" carrying a full declarative body (config.widgets + config.layout). The renderer builds that body and never resolves `component`; only type:"custom" consults src/registry.js. Both routes rendered their manifest widget groups and the manifest description, and neither .vue's root data-testid ever appeared. The positive control is in the same run: /motions/:id/integrations is type:"custom" and DID render MotionIntegrations.vue's root and copy. So DecisionIntegrations.vue and AgendaItemIntegrations.vue were imported, registered, bundled and unreachable — the same state as the MeetingIntegrations .vue deleted in #458, except these two also carried the `component` key, which is precisely the evidence a reader uses to call a file live. Deleted, with their registrations and the two inert `component` keys. The declarative pages lose nothing: config.sidebar.useRegistry:true was the only behaviour the .vue files added, and the manifest already declares it. src/views/settings/UserSettings.vue goes too, on separate evidence: nothing imports it, no manifest page names it, and the string only it renders ('Decidesk settings') appears in none of the built bundles while a string only its live twin UserSettingsPage.vue renders does appear in decidesk-main.js. It duplicated that twin's four preference sections exactly, so no field is orphaned by removing it. Coverage is three executed browser tests in tests/e2e/workflows/ — the directory CI's playwright.config.ts actually runs, unlike tests/e2e/visual/, which its only chromium project ignores. Each asserts a handle its own surface alone emits AND the absence of both siblings', because all three routes share one renderer and a resolution fall-through is a failure this app has shipped.
gate-19 984 -> 983, the exact figure computed before the run. settings-page.spec.ts anchored the REQUIREMENT slug requirement-req-adm-mode-001-organisatie-modus-tenant-setting. gate-19 indexes SCENARIO slugs, so it resolved to nothing — and a dangling anchor is reported by no one, which is why it survived while reading like proof. It now names admin-selects-a-tenant-mode, and the body was extended to cover that scenario's third THEN clause rather than only its first two: the mode had to be shown reaching a consumer, not just round-tripping through the form that wrote it. Reading the relabelled nav in the SPA proves persistence AND the relabel in one navigation, so the test got stronger and one page load cheaper. Cleanup moved to the API because a third SPA boot spent the whole 20 s budget in teardown — the fix for that is removing a navigation, never widening the timeout. Twelve dangling anchors remain and are filed (#461) rather than repointed: two sit on a test whose body asserts app-root and two nav links, so aiming them at the KPI scenarios would be .github#343 by hand. integration-surfaces.spec.ts drops the two anchors I had written for slugs that sounded right and resolved to nothing, and states why no anchor replaces them: these three routes have no Scenario, and writing one here would be authoring the spec this suite is checked against. Also tightens that spec's role locators with exact: true. That is not tidiness. getByRole's name option matches a SUBSTRING by default, so renaming a widget id to MUTANT-di-deck still satisfied { name: 'di-deck' } and the mutation control came back green — a null result that reads exactly like a test which cannot fail. With exact: true the matrix discriminates: mutating the decision widget id and the agenda-item copy fails those two tests and not the motion one; removing the MotionIntegrations registration fails the motion one and not the other two.
…prose I hit the defect I was working around. gate-26 matches component names as plain substrings over the RAW text of everything under tests/e2e, comments included (.github#358). `MotionIntegrations` appeared in this file only in two explanatory comments — the tests address that page by its data-testid, `motion-integrations`, a different string — so the gate was reporting the page as covered on the strength of my prose while a real, executed, mutation-verified test sat right below it. Caught only by re-running is_covered() against a comment-stripped corpus, which is the check worth keeping: the gate going green says nothing about which text made it green. The fix is a MANIFEST_PAGES table in executable code plus an assertion that uses it, and the assertion is worth having on its own account — it is the regression guard for the bug that removed two .vue files here. A page of type "detail" never resolves `component`; flipping the motion page to "detail", or re-adding a component key to either of the other two, silently recreates a named-but-unrenderable component, and no other test would notice because all three routes render something either way. Verified: both discovered page components stay covered with comments stripped.
…s to prevent shipped
The coverage ratchet failed this branch — correctly, and for a reason worth
stating: collapsing two multi-line filter arrays into single expressions removed
exactly 3 statements, all of them covered, and removing covered statements from
a codebase below 100% necessarily lowers the ratio (merge base 9222/15214 =
60.6152%, head 9219/15211 = 60.6075%). Nothing became untested; the arithmetic
moved. The honest response is to cover more, not to bypass the gate.
So this covers the class whose contract the original bug violated, which had no
test at all. The fixtures are not invented — they are the @self.relations
payloads dumped from a live OpenRegister instance, which is the only place the
storage shape is actually decided:
citizen-vote {proposalId, relations.0.id, relations.0.schema}
budget-proposal {participatoryBudget}
Neither map contains the related schema's SLUG. testAFilterKeyIsNeverASchemaSlug
pins exactly that, and asserts no hyphen survives the prefix, because a slug is
kebab-cased in this register and that makes the wrong shape cheap to detect.
Control: setting RELATION_FILTER_FIELD back to '_relations.budget-proposal' —
the original defect — fails 3 of the 8 tests, the guard among them, naming the
key. Restored: 8 tests, 10 assertions, OK. matching() is covered in both
directions, including a row that must be REJECTED, since a filter that cannot
reject is not a filter.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 548/548 | |||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-11 17:39 UTC
Download the full PDF report from the workflow artifacts.
|
Not merging: E2E Tests (Playwright) has not reported. 25 checks pass, 3 skip, that one is still Everything else that exercises this change is green on a fresh CI instance, not just my rig:
The Playwright evidence that does exist is local and scope-independent — the mutation matrix in the PR body, where each of the three integrations tests fails when and only when its own target breaks, plus the |
|
Correction to my previous comment — please disregard the "may be stuck" note. The E2E job is healthy. I wrote that Playwright "also failed to report on the previous push, so two data points suggest it may be stuck rather than slow." Both data points were wrong, and I had not measured either before saying so:
I also said the job id had changed, implying re-dispatch. It has not — I had compared it against a job id from a different run and against the Newman job's id. What produced the false picture: I was printing elapsed time by looping over every So there is nothing to diagnose here and no reason to cancel or re-run this job. Everything else on the PR stands unchanged; I am simply waiting for it, and the merge decision is still gated on it reporting normally. |
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 548/548 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-11 18:11 UTC
Download the full PDF report from the workflow artifacts.
|
Resolved: E2E Tests (Playwright) has now reported, and it was never stuck. Retracting my earlier "stuck at step 20/36" reading. The job ran 27m49s (Playwright step 25m32s), which is squarely inside this job's own observed band on decidesk — 25m31s, 27m40s, 28m02s, 29m26s, 29m36s across its five previous completed cells. The earlier comment was posted 4m56s into a step whose floor is 25m31s, i.e. about 17% of the way through. And the "previous push also failed to report" data point was my own doing: that run's E2E job (93868932640) was Merging on the same basis as #458: the red is pre-existing and strictly not worsened. Merge base is
Nothing was weakened to get there: no
|
|
E2E reported: red, and the red is provably not mine. Formal set-diff against
The 7 pre-existing failures, unchanged from And my own tests are positively confirmed to have run, not merely absent from the failure list — 58 tests were skipped in this run, so absence would have proved nothing: Merging on the same standard #458 used — pre-existing red, not increased, nothing skipped or weakened to achieve it. |
Closes gate-54 and gate-25; takes gate-26 to PASS; moves gate-19 by one. Every number below is full-tree scope (empty-tree base
4b825dc…), which is the scope the failingworkflow_dispatchrun used, and the base is named because these gates report the same word over an empty scope as over a clean one.🔴 The Hydra Gates cell on this PR is not evidence for any of them. gate-19 and gate-26 are diff-scoped and this branch's diff will scope them differently. The evidence is the controls below, which are scope-independent.
The controls
Committed plants (a staged-only plant can manufacture a false "the gate is blind"):
FAIL — 1namingDecision.zzPlantProbeRelationDialect→ PASS. Restore verified byte-identical.FAIL — 1namingsettings#zzPlantProbeContract→ PASS. Restore verified byte-identical.Both plants are in this branch's history and revert to nothing;
git diff origin/development..HEADcontains no plant line.Newman, on a live instance — 35 assertions. Reverting only the two service files: 6 fail, and exactly the 6 that describe the three defects; the other 29 (auth postures, enum normalisation, response shapes, round-trip) stay green.
Playwright mutation matrix — each test fails when and only when its own target breaks:
di-deckid + agenda-item description copyMotionIntegrationsregistry entry removedMutating
modeLabels.js's assoc label fails the mode-switch test and none of the other three in its file.getByRole'snameoption matches a substring by default, soMUTANT-di-deckstill satisfied{ name: 'di-deck' }. Fixed withexact: true. A null control result reads exactly like a test that cannot fail.What writing the tests found
gate-25 said four endpoints had never been called by an automated test. Writing that test found three defects, all answering a healthy 200/201 with an empty or wrong body:
votesForafter two votesPOST /api/participation/budgets/{id}/publishproposals: [],participationCount: 0,allocatedAmount: 0One cause: a
findAll()filter keyed on a schema slug. OpenRegister keys_relationsby the property path it walked, so_relations.budget-proposalmatches nothing.lib/Service/ObjectRelationFilter.phpdocuments this exact failure and shipsfilterFor()as the remedy — 7 call sites used it, 23 did not. The 20 outside this change are filed as #463 with the discriminator (a kebab-case key is a slug and is dead; camelCase may be a real property) and are not asserted to be broken, because I measured two key shapes, not nine.gate-26 flagged four page components. Rendering all four in a browser showed two cannot render at all:
/decisions/:id/integrationsand/agenda-items/:id/integrationsare manifest pages oftype: "detail", which are built fromconfig.widgets+config.layoutand never resolvecomponent. Both.vueroots never appeared; the positive control in the same run is/motions/:id/integrations,type: "custom", which rendered its component and passed first time. This refutes the earlier conclusion that all three*Integrations.vueare live — being named as a manifestcomponentis not evidence when the page type never consults it. Deleted, with their registrations and the two inert keys; a new test asserts each page'stype/componentpairing so the state cannot silently return.src/views/settings/UserSettings.vuegoes on separate evidence: nothing imports it, no manifest page names it, and the string only it renders (Decidesk settings) is in none of the built bundles while a string only its live twin renders is indecidesk-main.js. It duplicated that twin's four sections exactly, so no field is orphaned.Honesty notes
.github#361).check_spec_coverage.pyhardcodes the base toorigin/development, so a full-scope run on development diffs a branch against itself:origin/development→ count=0 (PASS),origin/beta→ 5,origin/mainand--mode report→ 91. decidesk's honest gate-16 is 91. Not addressed here.app-rootand two nav links, so aiming them at the KPI scenarios would be.github#343by hand. The one repoint carried its own control: gate-19984 → 983, the figure computed before the run.exclude_reason, positive control asserted first): 220 of 1345 scenarios excluded across 116 distinct reasons, 0 bare; split 31.4 % whole-spec / 10.5 % requirement-inherited / 58.2 % scenario-level. The largest single marker blankets 22 voting scenarios on a reason this repo's own CI refutes — filed as One whole-spec @e2e exclude blankets 22 voting scenarios on a reason this repo's own CI refutes #462, not removed here, because removing it raises the gate-19 count and is a sequencing decision..github#358while writing this:MotionIntegrationsappeared undertests/e2e/only in my own comments, so the gate was green on prose. Fixed with an executable table, and verified by re-runningis_covered()against a comment-stripped corpus — both pages stay covered.data?.results || data || []anywhere, and all 33 frontend/apps/decidesk/...URLs resolve to declared routes. Controls run first — a known-absent route returns HTTP 200text/html, 38 KB, a known-present one 200application/json, 91 bytes.Filed rather than forced: decidesk#461, #462, #463, #464, and
.github#363(gate-25's auth lookback is a fixed 20 lines, so it reads the previous method's attribute — and prose).