Skip to content

Testing

RONL Business API is an npm-workspaces monorepo with five tested packages: the Express/TypeScript backend on Jest, and four on Vitest โ€” the caseworker portal (packages/frontend) with React Testing Library, the Public Affairs cockpit (packages/pa-cockpit), the public cockpit demo (packages/pa-demo) and the public search site (packages/public-site) with jsdom. All five run with coverage by default.

Figures on this page are measured, not estimated

Every unit-test count and percentage below was produced on 11 October 2026 against v2026.10.2, the release in production: main is at 7117914. The runs were made in the working checkout on acc at c51ec0a, whose tree is identical to 7117914, after npm run deps:check reported "Dependencies are ready." Each workspace was run on its own, one after another, serially: the backend as npx jest --coverage --runInBand followed by node scripts/check-conformance-coverage.cjs โ€” exactly what its test:serial script runs โ€” and each Vitest workspace with its own npm run test:serial. All five collect coverage, as their npm test counterparts do. Rerun the commands in Running the tests to reproduce them.

The method is the 3 and 9 October passes': test:serial, which is each workspace's npm test with --runInBand (Jest) or --no-file-parallelism (Vitest) added. The counts do not depend on that. The durations do, heavily โ€” see the footnote under the table โ€” and the frontend's coverage can move in the second decimal with it, which Coverage explains. Where this page compares against an earlier release it names the date that figure was taken, because not every figure here has been re-measured on every pass.

The runtime was Node 22.23.3, the version .nvmrc names at c51ec0a, with npm 11.21.0 โ€” the same Node as on 9 October.

The Playwright suites were not re-run for v2026.10.2. Their figures are the 9 October measurement of v2026.10.1, against the full local stack the developer had running then: frontend 42 passed, 5 skipped, pa-demo 11 passed, and public-site 6/6 serially, after two timeouts in its parallel run โ€” see E2E & live smoke. They were left as they are because nothing they exercise moved: no spec changed between ebec288 and c51ec0a, and no UI under test did either โ€” the one change in the four UI packages is the data of the in-app changelog, changelog-data.ts. The inventory is still seventeen specs โ€” fifteen frontend, plus pa-demo/e2e/plato-demo and public-site/e2e/publiek. The live-smoke shell scripts remain described from their configuration only.

npm run test:perf was re-run: 1 file ยท 1 test ยท passing. Because vite.config.ts excludes src/**/*.perf.test.ts, that one test is not part of the 130 frontend files or the repository-wide 331 / 5074 below. Wherever this page states a repository-wide file count, that count excludes the performance spec โ€” and the three self-contained script tests under scripts/, which no workspace runner collects: lockfile-diff.test.mjs, new in v2026.10.1, and dependency-review.test.mjs and write-sbom.test.mjs, new in v2026.10.2. All three passed on 11 October; see CI.

The root gates were re-run, and one of the checks beside them failed again โ€” for a smaller reason than on 9 October. type-check, lint, check-format, check-shared, check-supply-chain, check-swimlane-fixtures and the new audit:signatures all exited 0 on 11 October โ€” see Linting, formatting, git hooks, and CI. npm run sbom:check exited 1: the committed SBOM records the npm CI uses, 11.19.0, and the local npm is 11.21.0; every component matches. That is not a test failure and moves no figure on this page; it is set out under the gates table. lint:openapi was not re-run: v2026.10.2 changed only the description of two operations in openapi.yaml, and it last passed on 9 October. The backend's test:contract and test:openapi-coverage were not run on their own; their scope is contained in the full backend run. The branch rulesets were re-read on 11 October โ€” see What actually gates a merge.

At a glance:

Package Runner Files Tests Result Durationยน Statements Branches Functions Lines
packages/backend Jest + ts-jest 105 2638ยฒ all passing 110.81s 98.78% 94.95% 97.95% 99.10%
packages/frontend Vitest + RTL 130 1540 all passingยณ 413.76s 95.35% 93.18% 91.48% 95.85%
packages/pa-cockpit Vitest + RTL 43 515 all passing 121.54s 92.61% 93.89% 89.57% 93.13%
packages/pa-demo Vitest + jsdom 19 106 all passing 34.12s 93.47% 95.65% 85.00% 92.85%
packages/public-site Vitest + jsdom 34 275 all passing 68.13s 96.68% 97.41% 95.09% 97.23%

331 files ยท 5074 tests, all passing, nothing skipped, in about 12 minutes 28 seconds of runner-reported time across the five serial runs. The wall clock was recorded for the four Vitest runs only โ€” 419s, 131s, 48s and 70s, counting npm's own start-up โ€” and not for the backend's, so no repository-wide wall-clock figure is given this time. One performance spec runs separately and is excluded from both the 130 and the 331 โ€” 5075 in total. See Coverage for what those percentages mean and where the remaining gaps are.

Against the 9 October measurement of v2026.10.1 โ€” the previous release โ€” that is no new file and +5 tests, all of them in one backend file. The four Vitest workspaces reproduced their counts exactly; the only change in their packages is the frontend's changelog data. pa-cockpit, pa-demo and the public site also reproduced all four coverage figures to the decimal; the frontend's moved by three to eight hundredths on identical code, the run-to-run noise Coverage describes. The backend's moved by one or two hundredths on two measures โ€” statements 98.77 โ†’ 98.78, branches 94.97 โ†’ 94.95 โ€” on the larger operaton.service.ts. Written out:

30 Sep ยท v2026.09.15 3 Oct ยท v2026.10.0 9 Oct ยท v2026.10.1 11 Oct ยท v2026.10.2 ฮ” since 9 Oct
packages/backend 97 files ยท 2370 100 ยท 2457 105 ยท 2633 105 ยท 2638 +0 ยท +5
packages/frontend 120 files ยท 1318 124 ยท 1378 130 ยท 1540 130 ยท 1540 โ€”
packages/pa-cockpit 43 files ยท 515 43 ยท 515 43 ยท 515 43 ยท 515 โ€”
packages/pa-demo 19 files ยท 106 19 ยท 106 19 ยท 106 19 ยท 106 โ€”
packages/public-site 33 files ยท 265 34 ยท 275 34 ยท 275 34 ยท 275 โ€”
Total 312 ยท 4574 320 ยท 4731 331 ยท 5069 331 ยท 5074 +0 ยท +5

What v2026.10.2 added, by workspace:

  • Backend, no new file and +5 in one existing one: services/operaton.service.test.ts 192 โ†’ 197, all in getDecisionDocument. Since v2026.10.2 that returns the decision document of the path the case took (sgort/linked-data-explorer#246), where it used to take the first ronl:documentRef in the BPMN. The five cases: on the ordinary path, the document of the signing task; after escalation, the authority's document, even though the signing task ran first; canceled task instances skipped; the first entry of a documentRef list; and the fallback to the first documentRef when no completed task carries one. The other backend test changes moved no count: v2026.10.2 refreshed five Awb fixtures and the two declared ones under rip-swimlane/__fixtures__/, and bpmn-swimlane.test.ts lost one line of an expected mapping for the HR capacity claim and kept its 201 tests. See Backend suite.
  • Frontend, pa-cockpit, pa-demo and the public site: no test change. The one change in any of the four packages is the frontend's changelog-data.ts, the in-app release notes.

History, 9 October: what v2026.10.1 added, by workspace:

  • Backend, five new files and +108 in existing ones. The new files are auth/entra-token.service.test.ts (24), the stored Entra ID token a person reaches eDOCS with; routes/edocs.access.test.ts (17), who may use /v1/edocs and as whom; services/edocs-author.test.ts (11), the edocsAuthor and edocsAuthorName stamp; auth/citizen-services.test.ts (10), which services a citizen is offered; and utils/config.edocs.test.ts (6), the Entra settings a live eDOCS connection requires. The largest growth in existing files is services/edocs.service.test.ts 49 โ†’ 81, routes/process.routes.test.ts 104 โ†’ 117, routes/task.routes.test.ts 47 โ†’ 57, services/operaton.service.test.ts 182 โ†’ 192, mcp-servers/edocs/index.test.ts 16 โ†’ 25 and routes/edocs.routes.test.ts 36 โ†’ 44. See Backend suite.
  • Frontend, eight new files and two removed. New: pages/login-choice/tenants-landing.test.ts (26), vite-plugin-tenant-pages.test.ts (20, at the package root), components/LoginChoice/SingleBoardLanding.test.tsx (18), services/board-request.test.ts (12), components/LoginChoice/AccessDeniedDialog.test.tsx (11), pages/login-choice/landing-login.test.tsx (5), services/landing.test.ts (5) and pages/citizen/citizenServices.test.ts (3) โ€” the organisation landing pages, the no-access dialog and the citizen services. Removed with the DVTP sections: CaseworkerDashboard/DvtpStartSection.test.tsx (5) and DvtpTakenSection.test.tsx (7). The largest growth in existing files is services/tenant.test.ts 12 โ†’ 39, pages/LoginChoice.test.tsx 8 โ†’ 27, pages/Dashboard.test.tsx 32 โ†’ 45 and pages/AuthCallback.test.tsx 18 โ†’ 24.

Vitest's console reports only package totals, and Jest's default reporter prints no per-file counts either. On 11 October the backend's per-file counts were read from Jest's JSON report of the measured run (--json), which lists every test of every file: the 105 files sum to the runner's 2638, with operaton.service.test.ts at 197 and bpmn-swimlane.test.ts at 201. Every 9 October per-file figure above is counted from the source at ebec288 and at 0625d48, by parsing each test file with the TypeScript compiler and expanding every .each table and for โ€ฆ of loop whose table is a literal in the file. Six tables are not literals, and were resolved by hand: in the backend, SHELLS.slice(1) in bpmn-swimlane.test.ts (2 cases, not 1) and the router's route layers in public.routes.security.test.ts (16, not 1); in the frontend, boards.config.test.ts's boards with an Entra role (3) and the five tenants.json-driven tables in tenants-landing.test.ts (25 cases, not 5). With those, the parse gives 2633, 1540, 515, 106 and 275 โ€” each runner's total exactly โ€” and, at 0625d48, 2457 and 1378, the 3 October totals.

History, 3 October: the eight files v2026.10.0 added, by workspace:

  • Backend, three: middleware/error.middleware.test.ts (8) for the 404, catch-all and rate-limit handlers that now answer RFC 9457 problem details, utils/problem.test.ts (13) for sendProblem and the title derived from a code, and routes/besluitvorming.routes.test.ts (10) for GET /v1/besluitvorming/active and /completed. The other +56 went into existing files: rip-swimlane/bpmn-swimlane.test.ts +27 (process-declared phases, and the Gedelegeerd besluit and HR capacity claim BPMNs as fixtures), services/operaton.service.test.ts +11, routes/m2m.routes.test.ts +10 (the reserved-variable refusals, POST history and the deprecated GET), and +4 each in routes/validsign.routes.test.ts and services/validsignCompletion.service.test.ts (signing state per task, archive names). See Backend suite.
  • Frontend, four: CaseworkerDashboard/BesluitOverzichtSection.test.tsx (7) and BesluitStartSection.test.tsx (3) for Besluitvorming, components/signing/useTaskSignature.test.ts (5), and src/utils/problem.test.ts (23), the response interceptor's mapping of a problem back onto ApiResponse. SigningPanel.test.tsx (15 โ†’ 16) and resolveSigningUrl.test.ts (4) are not new: they moved from InfraBoardDashboard/ to the shared components/signing/. The largest growth in existing files is TakenInbox.test.tsx +4 โ€” the signing panel in the caseworker inbox โ€” and modes.config.test.ts, SectionRouter.test.tsx and ProcessWhere.test.tsx +3 each.
  • Public site, one: src/lib/problem.test.ts (9), which reads a problem's detail; lib/api.test.ts gained one.

On 3 October the backend's per-file figures were the 30 September run's JSON counts plus the parsed change in each touched file; the 9 October parse at 0625d48, described above, reproduces every one of them.

History, 30 September: the twelve files v2026.09.14 and v2026.09.15 added, by workspace:

  • Backend, one: openapi/testing/conformance.test.ts (20 tests), for the helper that checks a real response against the documented schema (#269) โ€” see Backend suite. The other +148 backend tests went into existing files, most of them in the route suites (+46), rip-swimlane (+56, the parser that now reads any laned process and its Awb phases) and services (+29); openapi/coverage.test.ts shrank from 7 tests to 3 when the pending list it policed was emptied and deleted.
  • Frontend, ten: seven in the new components/process/ directory โ€” the caseworker's process view โ€” plus PaletteActions.test.tsx, StartFailureNotice.test.tsx and src/indexHtml.test.ts, the link-preview tags per build mode. An eighth file in components/process/, PhaseSwimlane.test.tsx, is not new: it moved there from InfraBoardDashboard/ and grew from 22 tests to 38. The directory holds 119 tests across those eight. Vitest's console reports only the package total, so these per-file figures are counted from the source by parsing every test file with .each tables expanded โ€” a method that reproduces the runner's total exactly, 1318 at ae06c9e and 1122 at 963fe24.
  • Public site, one: src/indexHtml.test.ts, its own link-preview check, 23 tests across four build modes.

pa-cockpit gained 39 tests and no file in that window, all from the two branch-margin commits (see Coverage): 391b1a8 added four to DossierRow.test.tsx, and 73a6764 the other 35 โ€” pages/public-affairs-v2/Monitoring.test.tsx (+16) and services/pa.api.test.ts (+13) above all.

ยฒ The backend reported 2011 for several releases, of which 2008 ran and three were permanently skipped; those three went with the unreachable PHASE_NOT_MODELLED branch they guarded (issue #85). The 2638 measured here is growth on top of that 2008 โ€” 2011 โ†’ 2008 โ†’ 2028 โ†’ 2072 โ†’ 2198 โ†’ 2202 โ†’ 2370 โ†’ 2457 โ†’ 2633 โ†’ 2638 โ€” and no workspace skips anything. The same run ended with "โœ“ all 136 documented operations were checked against a real response", the conformance-coverage step npm test and test:serial carry after Jest โ€” the same number as on 3 and 9 October. v2026.10.2 documented no operation and removed none; it changed the description of two. On 9 October it was the same number for two opposite reasons: v2026.10.1 documented GET /v1/process/available and removed the deprecated GET spelling of /v1/m2m/process/history. See Backend suite.

ยณ The frontend is 1540/1540 green in its serial run, the one the table reports โ€” and 1540/1540 green in a default parallel run, the kind CI makes, of the same tree on the same morning, made to collect each Vitest workspace's per-file JSON: Duration 116.45s, with pa-cockpit, pa-demo and the public site green beside it at their table counts. That is the first parallel frontend run measured since 30 September, when it was 1318/1318 green; the 3 and 9 October passes made none. The file that failed under load in four of the five passes before that, ChangelogPanel.variants.test.tsx, was fixed at its own boundary in afc2001, which closed issue #199 on 28 September โ€” see A parallel failure is not a finding.

A coverage campaign, and every workspace moved at once

v2026.09.2 extended the backend-only coverage push to all five workspaces that have a test runner, against a per-file 80% branch floor: 53 files were below it, and none are now. Every package gained on every measure, which is unusual โ€” the previous release's figures moved in one package and held to the decimal in the others.

Branches moved furthest, which is the point of a branch floor, and the 11 October figures hold the gain: public-site 70.39% โ†’ 97.41%, pa-cockpit 75.55% โ†’ 93.89%, frontend 80.33% โ†’ 93.18%, backend 90.01% โ†’ 94.95%, pa-demo 86.95% โ†’ 95.65%.

The floor is a gate, and it is per file. v2026.09.6 configured it in all five runner configs โ€” a glob key './src/**/*.ts': { branches: 80 } in packages/backend/jest.config.js, and thresholds: { branches: 80, perFile: true } in the four Vitest configs โ€” so one file dropping below 80% branches exits the run non-zero and names that file. The five thresholds are unchanged at c51ec0a, and no run named a file; reading every file entry in the five workspaces' coverage reports independently finds zero of 337 files below 80% branches in any workspace.

The floor is still 80, and the 85 margin still fails in the same two places. On 30 September every file cleared 85, after 391b1a8 (#256) and 73a6764 had lifted the files at or near the line. v2026.10.0 brought two new frontend files in below it, and v2026.10.1 and v2026.10.2 left both exactly where they were. Restated from the 11 October coverage reports, every file below 85% branches in the repository:

Workspace File Branches
frontend components/CaseworkerDashboard/BesluitOverzichtSection.tsx 80.43 (37/46)
frontend components/process/phaseSet.ts 83.33 (10/12)

No other file in any workspace is below 85. The lowest file per workspace on 11 October โ€” the same five files as on 3 and 9 October, at the same figures:

Workspace Lowest file by branches Branches
backend pa-monitoring/pa-cache.ts, services/llm/OpenAILlmProvider.ts 86.36
frontend components/CaseworkerDashboard/BesluitOverzichtSection.tsx 80.43
pa-cockpit services/dossierbeheer.api.ts 85.10
pa-demo demo/changelog/DemoChangelogPanel.tsx 87.50
public-site lib/useQueryState.ts, pages/herkomst/HerkomstTrace.tsx 87.50

BesluitOverzichtSection.tsx โ€” the Lopende and Afgeronde besluiten list โ€” still passes the gate with 37 of 46 branches, half a point clear: one more uncovered branch there fails a required check. Of the files v2026.10.1 changed, the nearest to the margin are SigningPanel.tsx at 85.93%, the new routes/edocs.access.ts at 89.28% and routes/brp.routes.ts at 89.47%. The one source file under coverage that v2026.10.2 changed, services/operaton.service.ts, reads 92.78%. It is a margin, not a floor; the configs say 80.

The floor is branches only, deliberately: measured the same way, a functions floor at 80 would now fail 23 files (frontend 8, pa-cockpit 7, pa-demo 5, public-site 3, backend 0) โ€” as on 9 October, and one fewer than on 3 October, because DvtpTakenSection.tsx was deleted with the DVTP sections. The configs' own comments were corrected from 31 to 26 in 391b1a8, dated 28 September, and are three high today. See Coverage Floor and Coverage.

ยน These are elapsed times for one npm run test:serial per workspace โ€” Jest's Time: and Vitest's Duration โ€” run one workspace at a time, with file parallelism off. They are not comparable with the parallel figures earlier passes published, and they are not slow suites: on 28 September the frontend took 133.15s parallel and 372.31s under test:serial, about 2.8 times longer, and the serial runs since are the same shape โ€” 589.43s on 1378 tests on 3 October, 418.01s on 1540 on 9 October and 413.76s on the same 1540 on 11 October, against 116.45s for that morning's parallel run of the same tree, about 3.6 times faster. That the larger suite took less time on 9 October than the smaller one on 3 October says how much of the figure is the machine. Treat them as an order of magnitude rather than a figure to match, and compare like with like. The backend's 110.81s against 228.05s on 9 October, on five more tests, is the same lesson.

They are machine-dependent to a degree worth keeping in mind even within one mode: the same parallel backend suite was measured at 118.06s (24 Sep), 59.86s (26 Sep), 94.98s (28 Sep) and 54.67s (30 Sep), and the parallel frontend at 176.97s, 119.31s, 133.15s and 114.82s across the same dates. Spread like that says more about the host than about the suites, which is why nothing on this page is compared on time alone.

Vitest's tests line is not elapsed time

On 30 September the parallel frontend run took 114.82 seconds by Vitest's own Duration, and the same summary block printed tests 286.64s beside it โ€” and environment 588.41s, which is larger still. Those figures sum per-worker time across parallel workers; nobody ever waited for either. Read as elapsed, they turn a two-minute suite into a claimed ten-minute one โ€” which is the most likely origin of the ~444s this table carried for v2026.09.5. (On 11 October the same tree showed both shapes in one morning: the serial run read tests 129.01s and environment 150.33s against a Duration of 413.76s โ€” smaller than elapsed, the other way round โ€” and the parallel run tests 279.81s and environment 637.03s against 116.45s. Neither is quotable.) Quote Duration from Vitest and Time: from Jest, and quote nothing else.

The same trap has a second form in the JSON reporters. Vitest's numTotalTestSuites counts describe blocks, not files โ€” public-site reported 88 against its 32 files on 28 September. The file count is testResults.length, which is what the console's Test Files line shows.


Where to look

Page Covers
Coverage Headline and per-area coverage for all five packages, and why the last two decimals are noise
Backend suite The 105 files and 2638 tests in packages/backend, by area
Public site suite The 34 files and 275 tests in packages/public-site, plus its own Playwright suite
PA-demo suite The 19 files and 106 tests in packages/pa-demo, and the one Playwright suite that runs in CI
Caseworker ยท PA cockpit ยท Infra-board ยท Woo-dashboard The frontend and cockpit suites, split the way the product is โ€” one page per board
E2E & live smoke The Playwright suites, what they need running, and the cross-app shell scripts
Writing tests Conventions for adding tests here, and the traps that have already cost time

The four board pages do not add up to the frontend total, by design

Re-derived on 9 October 2026 at ebec288, and unchanged at c51ec0a, where no frontend test changed, they account for 923 of the 1540 frontend tests: Caseworker 464, the shared process view in components/process/ 123 (listed on the Caseworker page), Infra-board 234, the shared signing panel in components/signing/ 25 (listed on the Infra-board page) and Woo-dashboard 77. The PA cockpit's 515 live in their own package and are not part of the 1540. Per-file frontend counts are taken from the source, with .each tables expanded, because Vitest's console reports only the package total; summed, they reproduce that total exactly. Caseworker is down 11 on 3 October: the two DVTP section files (12) went, and CaseworkerDashboardV2.test.tsx gained one. The other 617 are not board-specific and so have no board page to live on โ€” and v2026.10.1's growth, the organisation landing pages and the citizen services, landed entirely here:

  • 304 in src/services (207), the three root test files (38 โ€” App.test.tsx, indexHtml.test.ts, pa-cockpit-class-coverage.test.ts), vite-plugin-tenant-pages.test.ts at the package root (20), src/hooks (5) and src/utils (34, of which problem.test.ts is 23)
  • 102 in shared widgets reused across boards โ€” ProcessStartFormViewer, TimeLine, DecisionViewer, StartFailureNotice, AltchaWidget, SessionExpiryWarning, PersonalDataPanel and the four LoginChoice components (38: SingleBoardLanding and AccessDeniedDialog new beside BoardCard and BoardPreview)
  • 40 in the frontend's host side of the PA cockpit โ€” components/PADashboardV2 (30), PaCockpitRoute and pa-cockpit-host (5 each)
  • 171 in pages that belong to no board โ€” the citizen's Dashboard (45) and pages/citizen (3), LoginChoice (27) and pages/login-choice (40), AuthCallback (24), ChangelogPanel and its variants (22) and no-eager-changelog (10)

The citizen dashboard and the landing pages have no board page of their own; their end-to-end coverage is tabulated under E2E & live smoke.

Note also that components/CaseworkerDashboard/ is counted under Caseworker but is the shared section-component library, reused across three of the four V2 dashboards. Its 232 tests protect more than that one board โ€” and components/process/ is shared the same way, since the Infra-board's ProjectDetail draws its phase stepper and swimlane with the same PhaseStepper and PhaseSwimlane the caseworker's process overview uses. Since v2026.10.0 so is components/signing/: the caseworker inbox and the Infra-board both render the same SigningPanel through useTaskSignature.


Running the tests

Run from the repository root after npm install (node_modules must already be installed in every workspace).

Command Scope Files Tests
npm test Every workspace with a test script (see below) 331 5074
npm run test:serial The same, without file parallelism โ€” what the 11 October figures were measured with, one workspace at a time 331 5074
npm test --workspace=@ronl/backend Backend only (Jest, coverage on by default), then the conformance-coverage check 105 2638
npm run test:contract --workspace=@ronl/backend src/openapi and src/routes โ€” the OpenAPI gates plus every route suite, coverage off 25 745
npm run test:openapi-coverage --workspace=@ronl/backend src/openapi only โ€” the coverage gate, the conformance helper and their helpers, coverage off 4 43
npm run lint:openapi --workspace=@ronl/backend Builds openapi/openapi.json and lints it with Spectral against the NL API Design Rules 2.2.1 ruleset, failing on error โ€” โ€”
npm test --workspace=@ronl/frontend Frontend only (Vitest, coverage on by default) 130 1540
npm test --workspace=@ronl/pa-cockpit The cockpit package 43 515
npm test --workspace=@ronl/pa-demo The public demo 19 106
npm test --workspace=@ronl/public-site Public site only (Vitest, coverage on by default) 34 275
npm run test:perf --workspace=@ronl/frontend The wall-clock budget, run without file parallelism 1 1
# Everything
npm test

# Everything, serially โ€” see the warning below
npm run test:serial

# One workspace
npm test --workspace=@ronl/backend
npm test --workspace=@ronl/pa-cockpit

# Single file / pattern
npx jest --config packages/backend/jest.config.js --no-coverage --testPathPattern=rules
npx vitest run --config packages/frontend/vite.config.ts session
npx vitest run --config packages/public-site/vite.config.ts SectionIndex

To reproduce the figures in the table above, rather than just run the suites, ask each runner for machine-readable output and read the counts from that instead of from the console:

# Backend โ€” Jest
npm test --workspace=@ronl/backend -- --json --outputFile=<OUT>/backend.json \
  --coverageReporters=json-summary --coverageReporters=text-summary

# Each Vitest workspace: frontend, pa-cockpit, pa-demo, public-site
npm test --workspace=@ronl/<pkg> -- --reporter=default --reporter=json \
  --outputFile.json=<OUT>/<pkg>.json \
  --coverage.reporter=json-summary --coverage.reporter=text

--outputFile.json resolves against the workspace, not the repo root

npm test --workspace=โ€ฆ runs with the cwd set to the package directory, so a relative --outputFile.json=../out/frontend.json lands in packages/ rather than where you meant. Give it an absolute path. Jest's --outputFile has the same behaviour. This cost a stray directory inside the clone during the 28 September pass, which is why it is written down.

Read the file count from testResults.length and the coverage figures from each workspace's coverage/coverage-summary.json โ€” never by grepping the console output, and never from numTotalTestSuites, which counts describe blocks (see the warning above).

lint:openapi was run on its own on 9 October and passed, with "No results with a severity of 'error' found!"; it was not re-run on 11 October, after a release that changed two operations' descriptions and nothing else in the document. The two contract test scripts were not run on their own on either date: their counts in the table are the full run's src/routes (21 files, 702 tests) and src/openapi (4 and 43), which is exactly what they select. When they were last run on their own, on 30 September, they reported 23 suites ยท 668 tests and 4 suites ยท 43 tests, reconciling with that day's full run the same way. The two test scripts are subsets of the backend's own npm test, which runs the same files with coverage on โ€” they exist to check the contract quickly, not as extra gates โ€” and, being filtered Jest runs, neither ends with the conformance-coverage check that npm test runs after Jest; see Backend suite. lint:openapi is the one that is not covered by npm test: both backend workflows run it as its own step, before the tests.

Reach for test:serial, not for a flag

This repository runs two test runners behind one command shape โ€” the backend on Jest, the other four on Vitest. Any serial flag you reach for is right in four places and wrong in the fifth: --no-file-parallelism is Vitest's, --runInBand is Jest's, and Jest rejects the Vitest flags outright. Every workspace defines a test:serial script for exactly this reason, so the runner's identity stops being something the caller has to know.

Note that a parallel-run failure is not a finding until it fails serially. The flakiness chased through August turned out to be timeouts at Vitest's 5000ms default under machine load, not a defect in the code under test; testTimeout is now 20s in all four Vitest workspaces.

A parallel failure is not a finding

The 24 September pass is the worked example of the rule above. The same file was the one to go red under load in four of five passes โ€” one test on 20 September, five on 24 September, none on 26 September, one again on 28 September โ€” which is what made the mechanism worth publishing rather than merely noting. On 30 September, at v2026.09.15, the frontend's default parallel run was 1318/1318 green, the first measured pass since the file was fixed at its own boundary in afc2001, which closed issue #199 on 28 September. One green run does not prove a fix; what the fix changed, and why it removes the cause rather than widening a timeout, is set out below. The 3 and 9 October passes ran every unit suite serially, so they add no parallel evidence either way. On 11 October, at v2026.10.2, a default parallel run was green again โ€” 1540/1540 in 116.45s โ€” beside the serial run the figures come from: the second measured parallel pass since the fix, and the second green one. The accounts that follow are history, kept as they were measured.

3 and 9 October: the rule applied to a Playwright suite

The same rule decided the one red result of the 3 October pass, and of the 9 October pass again. The public-site Playwright suite, run with its config's default parallel workers (six on that machine), came back 4 passed, 2 failed both times: both search-journey tests (publiek.spec.ts:6 and :27) timed out after 10s waiting for the search-result filter checkbox. Re-run serially with --workers=1, against the same running backend, it passed 6/6 โ€” in 7.8s on 3 October and 6.4s on 9 October. Recorded as 6/6 serially; 2 timeouts in the parallel run โ€” not as a defect. Two identical parallel results a week apart make the 10-second wait under six workers worth fixing at the spec's own boundary before the suite goes into CI, but they are still not a failure on its own. See E2E & live smoke.

History, 28 September: one failure, and it is contention โ€” established, not assumed

The v2026.09.13 pass ran the frontend's default parallel npm test and got 110 files ยท 1122 tests ยท 1121 passed ยท 1 failed ยท 133.15s. The failure was in ChangelogPanel.variants.test.tsx again, and again on findByRole('dialog'):

FAIL  src/pages/ChangelogPanel.variants.test.tsx >
      ChangelogPanel with an empty changelog >
      opens without expanding anything, rather than throwing on a missing first entry
TestingLibraryElementError: Unable to find role="dialog"

Two checks before drawing any conclusion, in this order, exactly as the rule requires:

  1. The file on its own โ€” npx vitest run --config packages/frontend/vite.config.ts --coverage.enabled=false src/pages/ChangelogPanel.variants.test.tsx โ†’ exit 0, 1 file ยท 7 tests ยท passing ยท 2.55s.
  2. The whole suite serially โ€” npm run test:serial --workspace=@ronl/frontend โ†’ exit 0, 110 files ยท 1122 tests ยท passing ยท 372.31s.

Then, for good measure, two further parallel runs of the whole suite, one with coverage off and one with it on: 1122/1122 green both times, in 69.40s and 45.02s, with the variants file taking 930ms in the second.

So it is contention, not a defect โ€” and the circumstance is worth recording because it is the same one every time. The failing run was the first of the five workspace runs after the backend's Jest suite, on a machine that had not yet quietened down; the three green runs came later, on a host with less on it. A contention failure needs contention.

That run did not close issue #199, and was not meant to. The fix landed the same evening, in afc2001 โ€” see Why this file.

The 24 September account, kept as it was measured. The frontend's default npm test came back 1112 passed, 5 failed, every one of them in the same file and with the same Unable to find role="dialog" error. The file on its own passed 7/7 twice in a row, in 5.32s and 3.70s; run from packages/frontend the command is npx vitest run --config vite.config.ts --coverage.enabled=false src/pages/ChangelogPanel.variants.test.tsx, and from the repository root npx vitest run --config packages/frontend/vite.config.ts ChangelogPanel.variants โ€” setupFiles resolves against the config rather than the cwd, so both work. The whole suite serially was 110 files ยท 1117 tests ยท passing ยท 408.71s, with the file taking 654ms inside that run against 7225ms inside the parallel one.

So the honest way to publish the frontend, through 28 September, was all green serially, with contention-only failures in the default parallel run in a file that passes in isolation. On 30 September it was simply all green, and on 3 and 9 October, run serially, it was again โ€” and on 11 October it was green both ways.

Why this file, and why the count moves

Closed โ€” issue #199, fixed in afc2001

Opened 24 September 2026 on the evidence below and closed on 28 September by afc2001, "stop the changelog variants test loading the real history", which reached production with v2026.09.14. The diagnosis below is kept as history, because the mechanism is not the one most people reach for and the same shape can recur in any test that awaits a lazy chunk.

What four passes showed was the shape of the symptom. One failing test, then five, then none, then one again โ€” and on 28 September two of the three parallel runs of the same suite on the same commit were green. The failure was intermittent and load-dependent, not the reliable, worsening one the 24 September reading suggested: the kind of check CI teaches people to re-run rather than read, which is why it was fixed rather than lived with.

The mechanism, as diagnosed before the fix:

  • ChangelogPanel is a lazy() + Suspense shim, split so the changelog data does not ship in the entry chunk. Its content therefore resolves asynchronously, and every test in ChangelogPanel.variants.test.tsx opens with await screen.findByRole('dialog') for exactly that reason โ€” the file's own comment says so, and warns that dropping those awaits would make the negative assertions pass against an empty DOM.
  • findBy* does not use testTimeout. It uses testing-library's own async-util timeout, which defaults to 1000 ms and is configured nowhere in this package. So the config's testTimeout: 20000 โ€” the setting that fixed the August flakiness โ€” has no bearing on this failure at all. A dynamic import slower than one second fails the assertion rather than extending it, and the error reads "Unable to find role=dialog" against an empty body rather than as a timeout.
  • The module it waits on keeps growing. changelog-data.ts was 572,299 bytes at v2026.09.9, 612,980 at v2026.09.11 (+7.1%), 648,881 at v2026.09.12, and 659,684 bytes at v2026.09.13 โ€” up 46,704 bytes, +7.6%, in the four days since v2026.09.11. Over the same window neither the test nor the component moved at all: git diff --name-status 86af73e 963fe24 -- 'packages/frontend/src/pages/ChangelogPanel*' returns nothing. It has kept growing since: 689,070 bytes at v2026.09.15, 701,168 at v2026.10.0, 735,513 at v2026.10.1 and 741,165 at v2026.10.2.

The test did not get worse. The module it waited on got bigger, and the one-second budget it was measured against did not move โ€” though four passes showed not a clean upward trend in failures, but an intermittent one whose count tracked how busy the machine was. The growth was real and monotonic; the failure count was not.

The fix was at the file's own boundary, and it stopped the clock rather than widening the budget. The issue had carried three candidates: an explicit findByRole('dialog', { timeout }) in that file, a package-wide configure({ asyncUtilTimeout }) in src/test/setup.ts, or decoupling the test from the real module. afc2001 took the third, and in doing so corrected the diagnosis above in one respect. This page had said the file "already mocks ./changelog-data"; it did, but through importActual, spreading the real module before overriding changelog, its only runtime export โ€” so every run loaded the whole release history inside the lazy() import the first findByRole waited on, only to throw the data away. The commit:

  • mocks ./changelog-data without importActual, so the real history is never loaded by this file at all; and
  • resolves the lazy ChangelogPanelContent chunk once in a beforeAll, under the hook timeout, so no findBy* pays for an import.

By the commit's own measurement the first test dropped from 409โ€“662 ms to 99โ€“125 ms on an idle machine, and it no longer grows with the changelog. Neither timeout was raised, and parallelism was not touched.

Its sibling was left as it is, deliberately

ChangelogPanel.test.tsx renders every real version entry and does not fail โ€” but on 28 September it took 44.1 seconds in the measured parallel run, and 30.4s in another run of the same suite on the same commit. It passes because testTimeout was raised to 20s package-wide and this file was given 60s on top of that. afc2001 left it alone on purpose: it imports the data and the content module statically, so its findBy* never waits on an import, and its real-history rendering is what its own 60s timeout is for. It still carries the changelog's growth; its time was not re-measured on 30 September or on 3, 9 or 11 October, because Vitest's console does not print per-file times.

This class of failure is not a surprise here; packages/frontend/vite.config.ts already documents it in the comment above testTimeout: 20000, which records the suite as green in five consecutive parallel runs on an idle machine and producing 8โ€“16 timeout failures under concurrent load, across a file set that changes with how busy the machine is. ChangelogPanel.test.tsx is named in that same comment as a file needing more headroom still. What that comment does not cover, and what every one of the variants file's failures was, is the other timeout โ€” the one testing-library owns rather than Vitest.

The fix is not to turn parallelism off

Disabling file parallelism globally would make the symptom go away and take the signal with it. It diverges local runs from CI, where the gating suites still run parallel; it costs real time on every later run โ€” 372.31s against 133.15s on this suite alone, as measured on 28 September, and 408.71s against 176.97s on 24 September; and flakiness under parallelism is usually a genuine defect worth finding, in shared module state, an unisolated temp directory or a port collision. test:serial exists to diagnose a suspect failure, not to become the default. Where a suite really is parallelism-sensitive, fix it at its own boundary โ€” the *.perf.test.ts convention and the per-file timeouts below are both that pattern.

What npm test at the root actually runs. The root script is npm run test --workspaces --if-present, fanning out over every package under packages/*. Five of the six have a test script โ€” @ronl/backend, @ronl/frontend, @ronl/pa-cockpit, @ronl/pa-demo, @ronl/public-site โ€” and --if-present silently skips the sixth, @ronl/shared, which has no test script at all (only build, prepare, clean, type-check). That is expected, not a gap: shared is a types-only package with nothing to unit test.

Clear the Jest cache before trusting a green backend run

ts-jest caches type diagnostics per file, so a warm cache can skip re-checking a file that no longer compiles and report the suite green. That is not hypothetical: nine backend test files were latently broken for weeks โ€” every top-level declaration landing in the global scope, colliding across files โ€” while npm test passed locally every time. The first CI run on a cold runner failed immediately.

npx jest --config packages/backend/jest.config.js --clearCache

Which files fail also varies between runs, because it depends on the order the type program reaches them. See Writing tests.

The performance budget

simEngine.ts carries a real budget: run(cfg) must process the default 3,150-application population in under 250ms. Its source comment is emphatic that if the assertion ever fails the threshold must not be loosened โ€” the intended remedy is a web worker, not a bigger number.

The problem was never the threshold. A single performance.now() call measures the machine as much as the engine: on a contended host the assertion was observed at 302ms, then 837ms, then 1297ms across three consecutive runs, and inside a full npm test โ€” where Vitest saturates every core with 133 parallel test files โ€” even the fastest of three CPU-time samples came out at 468ms, against ~100ms in isolation. Wiring the CI test gate would have made that a permanently red pipeline.

So the budget moved rather than moved up:

  • The assertion lives in simEngine.perf.test.ts, still asserting < 250ms, now with a warm-up run and the fastest of three samples so a JIT pause or a stray GC cannot decide it.
  • vite.config.ts excludes src/**/*.perf.test.ts from the default run.
  • vitest.perf.config.ts mirrors it โ€” same plugins, aliases and setup, but the perf specs are the only thing included, and fileParallelism is off. It spreads and overrides the base test block rather than using mergeConfig, which concatenates arrays and would have kept the base exclude, hiding the perf specs from their own run.
  • npm run test:perf runs it, and both frontend workflows run that as their own blocking CI step. Re-run on 11 October 2026: 1 file ยท 1 test ยท passing ยท Duration 1.98s, with coverage off โ€” of which the test itself took 743ms and the setup 229ms (9 October: 5.80s, 4.02s of it setup; 3 October: 1.18s in all; 12 September: 1.04s). Because vite.config.ts excludes src/**/*.perf.test.ts, the frontend's 130 files and 1540 tests above do not include it, and neither does the repository-wide 331 files / 5074 tests โ€” it is the one frontend test that has to be counted separately.

ChangelogPanel.test.tsx was the other casualty of the same contention: its 15-second timeout sufficed in isolation but not inside a full run, where it was observed taking 22s. changelog-data.ts renders every real version entry, so the test is genuinely slow rather than unreliable โ€” and a timeout exists to catch a hang, not to assert a speed. It was raised to 60s, which costs no coverage; the perf spec remains the one place a real budget is asserted. It was green in the 24 September serial run at 22.66s, which is the same figure that prompted the raise and a reminder of how little headroom 15s left.

On 28 September it took 44.1 seconds in the measured parallel run, and 30.4s in another parallel run of the same suite on the same commit. That is double the 24 September figure and three quarters of the 60s ceiling, on a file whose only input that changed is changelog-data.ts. It still passes, and it is the clearest quantity on this page for how fast that module is growing. It was not timed on 30 September or on 3, 9 or 11 October: the suite was run with Vitest's default console reporter, which prints no per-file times.

Its sibling ChangelogPanel.variants.test.tsx is the file that failed under parallel load on 20 September, five times over on 24 September, not at all on 26 September and once again on 28 September โ€” same component, same growth, but a different timeout. What it needed was not testTimeout, which is what was raised for this file, and it did not get a bigger budget of any kind: afc2001 stopped it loading the real history at all, closing issue #199. See Why this file, and why the count moves.


Linting, formatting, git hooks, and CI

All the root gates were run again on 11 October 2026, at the same tree as the suites above, and all exited 0, printing what the table shows โ€” all but lint:openapi, which was not re-run (see Running the tests). One output moved since 9 October: two more action pins, in the new dependency-review.yml. And one gate is new, audit:signatures, which v2026.10.2 added to the audit job. Times are from 11 October where the row gives one without a date.

Command What it does Result (measured 11 Oct)
npm run type-check npm run type-check --workspaces --if-present โ€” the fifth step of pre-push exit 0 (47s)
npm run lint npm run lint --workspaces --if-present โ€” eslint . in backend, frontend, public-site exit 0, no errors (68s, as on 20 Sep)
npm run check-format prettier --check "**/*.{ts,tsx,json,md}" --ignore-path .gitignore --ignore-path .prettierignore โ€” one repo-wide glob, not a per-workspace fan-out All matched files use Prettier code style! (21s; 15s on 20 Sep)
npm run lint:openapi --workspace=@ronl/backend Builds openapi/openapi.json, then spectral lint against the NL API Design Rules 2.2.1 ruleset with --fail-severity error โ€” new in v2026.09.12 Not re-run; on 9 Oct No results with a severity of 'error' found!
npm run check-shared node scripts/check-shared-declarations.mjs โ€” what stands in for tests in packages/shared check-shared-declarations: 13 file(s) in packages/shared/src/ โ€” declarations and constant data only. (13 on 9 Oct; 12 on 3 Oct, before citizen-services.ts; 11 on 20 Sep)
npm run check-supply-chain node scripts/check-supply-chain.mjs โ€” every action pin against its version comment and the register 43 pinned reference(s) across 6 action(s) in .github/workflows/ โ€ฆ OK โ€” digests, version comments and the register all agree. (41 on 9 Oct, before dependency-review.yml; 39 on 3 Oct, before the two lockfile-review jobs; 31 across 5 on 20 Sep, before the two workflows added in v2026.09.12)
npm run check-swimlane-fixtures node scripts/check-swimlane-fixtures.mjs โ€” the parser's RIP and declared-phase fixtures against their fingerprints (see Git hooks) โœ“ 14 swimlane fixtures match their fingerprints; 14 compared against ..\linked-data-explorer. (14 on 9 Oct, 12 on 3 Oct)
npm run audit:signatures bash scripts/audit-signatures.sh โ€” npm audit signatures over the installed tree: the registry signature of every package, and the provenance attestation of those that have one (#249) โ€” new in v2026.10.2 1575 packages have verified registry signatures, 213 packages have verified attestations (35s)

npm run sbom:check failed locally again on 11 October โ€” a check, not a test, and only the npm version

sbom:check (node scripts/write-sbom.mjs --check), a root script since v2026.10.1, regenerates the release SBOM from package-lock.json and compares it with the committed docs/sbom/ronl-business-api-<version>.cdx.json. Run beside the gates above, it exited 1: "docs\sbom\ronl-business-api-2026.10.2.cdx.json does not match the current lockfile. Run: npm run sbom". Neither the lockfile nor any component is the cause. The committed file records in its metadata.tools the npm that CI uses, 11.19.0 โ€” 3f9c3a3 regenerated it with that npm deliberately โ€” while the local npm is 11.21.0, and --check compares everything but serialNumber and metadata.timestamp, the npm version included. With the two versions set equal, all 487 components and the rest of the document match exactly. It moves no test count on this page, and it is not in pre-push or the audit job.

On 9 October the cause was larger: the v2026.10.1 file had been generated with npm 10.9.9 and listed 423 components, where npm 11 generated 409 from the same lockfile. v2026.10.2 also stopped relying on npm sbom --omit=dev, which dropped production packages such as react, and reads the production set from the lockfile instead (#368).

In CI the comparison can now fail. Until v2026.10.2 sbom.yml ran Generate the SBOM โ€” which rewrites the file in the runner's workspace โ€” before its --check and --verify-release steps, so each compared the freshly written file with itself and could not fail (issue 354). Since v2026.10.2 it tests the generator first, then runs --verify-release on a push to main and --check on a release pull request, and only then generates and uploads the SBOM โ€” so both checks compare the committed file. See CI/CD.

npm run lint skips @ronl/shared the same way test does โ€” no lint script there. npm run check-format, by contrast, is not scoped by workspace scripts at all: it is a single Prettier invocation over the whole tree (minus .gitignored paths), so it reaches packages/shared too even though shared defines no format-check script of its own. The second ignore file is new in v2026.09.14 (96fabb6): .prettierignore holds one pattern, *-handoff/, keeping design handoff folders โ€” scratch input that is never committed โ€” out of the check.

packages/shared has no test script, and that is the design

It defines build, prepare, clean and type-check and nothing else, so --if-present skips it. What polices it instead is check-shared, which asserts that every file under packages/shared/src/ holds declarations and constant data only โ€” no runtime behaviour, hence nothing to unit test. It runs in the audit job, a required check on both acc and main, so the package with no tests is in fact the one whose rule is enforced on every pull request.

Git hooks

Hook Runs Scope
pre-commit npx lint-staged Staged files only
pre-push npm run deps:check โ†’ build @ronl/shared โ†’ npm run check-swimlane-fixtures โ†’ npm run type-check โ†’ npm run lint โ†’ npm run check-format All workspaces (type-check, lint) / whole tree (check-format) / the fourteen BPMN fixtures (check-swimlane-fixtures)

The hooks do not run the tests

Re-read against .husky/pre-push at ae06c9e on 30 September 2026, and .husky/ is unchanged at c51ec0a, so still true: pre-commit runs npx lint-staged and nothing else, and pre-push runs the six commands above in that order. Neither invokes any test script, so nothing client-side stops a push that breaks a suite โ€” run npm test yourself before pushing anything nontrivial.

check-swimlane-fixtures is the sixth, and it is third in the order โ€” between the @ronl/shared build and the type check. These pages said five until 28 September 2026; the step was added without this table following. It does not weaken the claim above: it compares packages/backend/src/rip-swimlane/__fixtures__/'s twelve RIP phase BPMNs, and since v2026.10.1 the two under __fixtures__/declared/, against sha256 fingerprints committed identically in this repository and in linked-data-explorer, and byte-for-byte against the real files when that checkout is alongside. A fixture edited here fails it, because the edit belongs upstream. That is a file comparison, not a suite. Since v2026.09.14 (97e0534, #268) the check tells the two directions of drift apart from the fingerprint file: a fixture that disagrees with its fingerprint is stale and --sync fixes it, while one that agrees but differs from the linked-data-explorer checkout means that checkout is out of step โ€” and --sync now refuses to copy from it unless --force is given. The seven Awb fixtures added under __fixtures__/awb/ in the same release are not part of it: those seven are exercised by the parser's tests but not drift-checked against linked-data-explorer. The two fixtures v2026.10.0 added under __fixtures__/declared/ โ€” GedelegeerdBesluitProcess and ManagementCapacityClaimProcess, the HR capacity claim, whose copy here lost its .nl suffix in the same change โ€” were in the same position on 3 October, when the run reported twelve. v2026.10.1 brought them under contract (#312 item 3): a fingerprint entry's source now says where upstream a model lives, so the declared ones need not sit in a rip-phase-NN folder, and the 9 and 11 October runs report fourteen. The same check also runs in the audit job, between Verify registry signatures and Verify @ronl/shared holds no logic.

deps:check is new at the front of pre-push, and it is there for a reason the hook file records: on 14 September 2026 a clone still on Prettier 3.8.1, after the lockfile had moved to 3.9.6, failed check-format on seven correctly-formatted files and nothing said why. Running it first means a push from an install that no longer matches package-lock.json stops with the fix named โ€” npm ci โ€” rather than the later checks running against the wrong tool versions.

What has changed is on the server. Since 20 August 2026 each deploy workflow runs the suite of the package it ships; since v2026.09.6 the frontend workflows also run @ronl/pa-cockpit's; and since v2026.09.7 the backend suite runs on the pull request rather than only on the merge. As of 19 September 2026 those runs also gate on acc โ€” see What actually gates a merge.

CI

Fourteen workflows under .github/workflows/ since v2026.10.2, which added one and edited two โ€” git diff --name-status ebec288 c51ec0a -- .github/ โ€” and changed no unit-test step. What changed are the checks and script tests around the suites:

  • dependency-review.yml is new: a quarterly review that every direct dependency is still maintained (ICTU recommendation 11, #250). It runs on a clock โ€” cron 23 6 2 1,4,7,10 *, 06:23 UTC on the second day of each quarter โ€” on workflow_dispatch, and on a pull request that touches the workflow, scripts/dependency-review.mjs or scripts/lockfile-diff.mjs. Its first step runs the script's own test, scripts/dependency-review.test.mjs; then it gathers the evidence and opens the quarter's review issue, a step skipped on a pull request.
  • zizmor.yml's audit job gained an eleventh step, Verify registry signatures โ€” npm run audit:signatures, after Check formatting. Since audit is required on both branches, every pull request now fails on a package whose registry signature is invalid or missing.
  • sbom.yml now tests its generator first โ€” node scripts/write-sbom.test.mjs โ€” and runs its two checks before generating the file, so they compare the committed SBOM and can fail; see the warning under the gates table above.

That makes three self-contained script tests outside the five workspaces' runners โ€” no test runner, a pass count, exit 1 on any failure โ€” each run by the workflow that uses its script, and none of them part of the 331 files:

Script test Result, 11 Oct Runs in
scripts/lockfile-diff.test.mjs PASS: 77 checks zizmor.yml, job lockfile-review, before it compares package-lock.json with the base (since v2026.10.1)
scripts/dependency-review.test.mjs PASS: 24 checks dependency-review.yml, before the review (new in v2026.10.2)
scripts/write-sbom.test.mjs PASS: 7 checks sbom.yml, before the two checks (new in v2026.10.2)

See CI/CD.

History, 9 October: v2026.10.1 edited five workflows and added none โ€” git diff --name-status 0625d48 ebec288 -- .github/ listed only modifications โ€” and no unit-test step changed:

  • zizmor.yml gained two jobs, lockfile-review and lockfile-review-comment. The first runs scripts/lockfile-diff.test.mjs and then compares package-lock.json with the base branch; the second posts the result as a pull-request comment. That test file was then the one test in the repository outside the five workspaces' runners.
  • The three acc site workflows โ€” frontend, pa-demo, public-site โ€” added package-lock.json and package.json to their path filters, as the backend's already had, so a lockfile-only change is built and tested.
  • azure-pa-demo-acc.yml bounds Install Playwright browser with timeout-minutes: 10, after an unanswering Ubuntu mirror hung the step for over ten minutes on 7 October. The E2E step itself is unchanged.
  • sbom.yml gained the release-pull-request --check โ€” which, as the warning above explains, could not yet fail in CI.

Re-read at ae06c9e on 30 September against 2443adc, the last full reading before that: no workflow was added or removed, and no test step changed. What did change then is a node scripts/check-og.mjs acceptance|production check of the built link-preview tags inside the build step of the four frontend and public-site deploy workflows, and Node 24.21.0 in dependency-audit.yml and sbom.yml.

The fourteen are an acc/prod pair per deployable package, the supply-chain audit gate, the Semgrep scan job added in v2026.09.7, the promotion orchestrator added since v2026.09.9, two added in v2026.09.12 โ€” a daily dependency audit and a release SBOM โ€” and the quarterly dependency review added in v2026.10.2:

Workflow Lint Type-check Tests E2E Perf budget Build Deploys?
azure-backend-acc.yml / -prod.yml โœ… + OpenAPI (lint:openapi) โ€“ โœ…, with the conformance-coverage check โ€“ โ€“ โœ… Yes โ€” over OIDC since v2026.09.10, then a liveness check and a check that /v1/health reports the deployed commit; the acc workflow skips all three on a pull request
azure-frontend-acc.yml / -prod.yml โœ… โ€“ โœ… ร—2 โ€” pa-cockpit, then frontend โ€“ โœ… โœ… Yes
azure-pa-demo-acc.yml / -prod.yml โœ… โœ… โœ… โœ… (acc only) โ€“ โœ… Yes
azure-publicsite-acc.yml / -prod.yml โœ… โœ… โœ… โ€“ โ€“ โœ… Yes
promote-to-production.yml โ€“ โ€“ โ€“ โ€“ โ€“ โ€“ Orchestrates the four -prod workflows in order
zizmor.yml โ€“ โ€“ โ€“ โ€“ โ€“ โ€“ No โ€” the required audit gate, which since v2026.09.12 also checks that the lockfile matches package.json and since v2026.10.2 verifies registry signatures; and lockfile-review, required on acc, which runs lockfile-diff.test.mjs first
semgrep.yml โ€“ โ€“ โ€“ โ€“ โ€“ โ€“ No โ€” scan, required on acc and, since the end of September, on main
dependency-audit.yml โ€“ โ€“ โ€“ โ€“ โ€“ โ€“ No โ€” a daily scheduled audit of the dependency tree; with dependency-review, one of the two checks here that run on a clock rather than a commit
dependency-review.yml โ€“ โ€“ โ€“ โ€“ โ€“ โ€“ No โ€” the quarterly maintenance review of every direct dependency, which opens an issue; runs dependency-review.test.mjs first. New in v2026.10.2
sbom.yml โ€“ โ€“ โ€“ โ€“ โ€“ โ€“ No โ€” tests the generator (write-sbom.test.mjs), checks the committed release SBOM, then generates and uploads it on a push to main

None of the three workflows added since v2026.09.12 โ€” dependency-audit, sbom and dependency-review โ€” is a required check in the rules as read on 11 October (see What actually gates a merge).

The backend's contract is now linted before its tests run. Both backend workflows gained a Lint the OpenAPI document step in v2026.09.12, placed before Unit tests so a document that breaks the NL API Design Rules ruleset fails fast and names the rule. The checks that the document matches the routes actually served, and that every documented operation's response was compared against its schema, are not separate steps: they are src/openapi/coverage.test.ts and the conformance-coverage script that the backend's ordinary npm test runs after Jest โ€” see Backend suite.

The four -prod.yml workflows no longer trigger themselves

Read at 86af73e and still so at 2443adc: all four declare on: workflow_call and workflow_dispatch only. A push to main no longer starts them โ€” it starts promote-to-production.yml, which decides what changed and calls them in order, backend first so the three sites never deploy against a backend that has not been promoted yet. The test steps inside each of them are unchanged; what moved is who invokes them. See CI/CD.

Every deployable package has had a real CI test step since 20 August 2026 โ€” a failing test fails the pipeline in all eight Azure workflows. Public-site had one already. Backend CI runs Jest alongside its existing lint step; frontend CI before that ran neither lint nor test, going straight from npm ci to vite build.

@ronl/pa-cockpit was the package that phrasing missed. It is a library with no deploy workflow of its own, so until v2026.09.6 nothing triggered its suite and its tests ran nowhere in CI โ€” a suite third in size only to the backend's and the frontend's (515 tests on 11 October), covered locally and only locally. Both frontend workflows now run it, as a step placed before the frontend's own, because the frontend imports it: there is no point testing the consumer while the library is broken. The pa-demo workflows consume the cockpit too, and watch packages/pa-cockpit/** in their path filters, but run only pa-demo's own suite.

The backend suite now runs before a merge, not only after one. Both backend workflows triggered on push alone, so a backend pull request reached acc with audit as its only check and the backend suite ran retrospectively โ€” including, after v2026.09.6, the per-file branch floor it carries. A pull_request trigger on azure-backend-acc.yml closed that in v2026.09.7 (issue #87), and both path filters gained package-lock.json and package.json, so a lockfile-only change โ€” Renovate's above all โ€” is no longer built and tested by nothing. A concurrency group keyed on the pull-request number keeps a twice-pushed branch from running the suite twice. It paid for itself immediately: an axios 1.18 security bump broke the backend build on the pull request, 1859 tests passing and one suite failing to compile against a widened header type. azure-backend-prod.yml has no pull_request trigger, deliberately โ€” main is promoted from acc, where that same suite has already run.

What actually gates a merge

This changed on 19 September 2026, and the two branches are deliberately different. Read from the repository rulesets (gh api repos/sgort/ronl-business-api/rulesets) on 20 September 2026, not re-read on 24, 26 or 28 September, re-read on 30 September, not re-read on 3 October, and re-read on 9 and 11 October as the rules in force per branch (gh api repos/sgort/ronl-business-api/rules/branches/<branch>) โ€” a gating claim is only as current as the last time somebody ran that command:

Branch Ruleset Required status checks
acc acc supply-chain gate audit, scan, build, Build and Deploy ACC Frontend, Build and Deploy ACC PA Demo, Build and Deploy ACC Public Site, lockfile-review
main main promotion gate audit, scan

Unchanged on 11 October. acc has also required lockfile-review, the job v2026.10.1 added to zizmor.yml, since that release; its other six checks are unchanged since 20 September. main requires scan as well as audit since the end of September, where the 20 September reading had audit alone. v2026.10.2 added no required check, but it widened one: audit now also verifies registry signatures, so that verification gates every pull request to either branch. Re-checked on 11 October, both rulesets are active and neither branch carries classic branch protection โ€” gh api repos/sgort/ronl-business-api/branches/<branch>/protection answers Branch not protected for both, as on 30 September โ€” so the rulesets are the whole of the gate.

So on acc a red suite now does block the merge. build is azure-backend-acc.yml's build job, which runs the backend's npm test; the three Build and Deploy ACC โ€ฆ contexts are the frontend, pa-demo and public-site deploy jobs, each of which runs its package's suite โ€” and the frontend's runs @ronl/pa-cockpit's first. Between them, every one of the five suites is now a required check on acc. scan โ€” Semgrep โ€” became required in the same change; test files remain excluded from its Code scanning, a fixture credential not being a leaked one. Both rulesets also forbid deletion and non-fast-forward pushes, and require a pull request to change the branch at all โ€” merged with a merge commit, the only method either allows, and with no approving review required.

main requiring no suite is not an oversight

No production workflow has a pull_request trigger. A check that never runs on the pull request cannot be required of it: adding one would block every promotion forever. main is promoted from acc, where those same suites have already run and now also gate. audit and scan are the two workflows that do trigger on any pull request, which is what makes them requirable there โ€” and both now are.

This is now doubly true. Read at 86af73e, at 2443adc and again at ae06c9e, the four -prod.yml files have no push trigger either โ€” they are workflow_call and workflow_dispatch only, invoked by promote-to-production.yml. Through v2026.09.9 they fired on push to main, which is what this note used to say.

This page said the opposite until 20 September 2026

Through v2026.09.7 these pages stated that audit was the only required check on both branches and that "a suite that runs is not a suite that gates". That was true when written and is now false for acc. The claim outlived the ruleset by a day; a gating claim is only as current as the last time somebody read the rulesets, which is why the command to do so is quoted above rather than described.

azure-pa-demo-acc.yml is the only workflow that runs an end-to-end suite โ€” the first Playwright in CI anywhere in this repository. See PA-demo suite.

The backend workflows deploy too, since v2026.09.10. Until then they ended at Create deployment zip โ†’ Upload deployment artifact, and the artifact was deployed separately, from a developer machine โ€” which is what this paragraph still said through the 28 September pass, although the change (e3c7dd6, 20 September) was already in the 2443adc reading above. They now log in to Azure over OIDC, run az webapp deploy, poll /v1/health/live for liveness, and then fail the job unless /v1/health reports the commit that was just deployed as its build.sha โ€” a liveness check alone passes against the previous build while Azure starts the new one. On azure-backend-acc.yml all of that is skipped on a pull request, so build can stay a required check without a pull request shipping anything. See CI/CD and the supply-chain gate.

The coverage gap closed differently from the test gap. Gating on coverage while it was still climbing was judged theatre โ€” the same call the Linked Data Explorer made and has since reversed for the same reason โ€” so there is still no coverage job in any workflow, and there does not need to be.

It closed from the other direction rather than by adding a coverage job. Since v2026.09.6 the per-file branch floor lives in the five runner configs and npm test already runs with coverage, so coverage is checked wherever the suites run โ€” in CI and on a developer's machine, by the same command, with no separate step to keep in step.


Roadmap

E2E CI wiring is done for one of the three Playwright suites. The PA-demo suite runs as a blocking step of azure-pa-demo-acc.yml, which was possible because that demo needs no backend, database or Keycloak โ€” Playwright starts its own dev server and that is the whole environment.

The other two still run locally only. The frontend suite needs a webServer-style auto-boot for Keycloak/Postgres/Redis/backend/LDE-backend โ€” its playwright.config.ts declares no webServer at all, and there is no human to start the stack manually in CI โ€” plus an unbounded local-Operaton-history-growth gap to close first. The public-site suite has no such blocker and is the obvious next candidate: it already declares its own webServer and needs only the backend. One thing to settle first: on 3 October and again on 9 October its two search-journey tests timed out under six parallel workers and passed serially (see E2E & live smoke), so the 10-second wait for the filter checkbox is the first thing a CI run would test.

The Node v24-on-Windows exit crash that used to be listed here as a third blocker is fixed, and e2e/global-setup.ts records how: AbortSignal.timeout() left its internal timer uncleaned before the fetch settled, tripping a libuv UV_HANDLE_CLOSING assertion on process exit. A manually managed AbortController with an explicit clearTimeout replaced it.

One board has no end-to-end coverage. Woo-dashboard is well covered by unit tests and has no Playwright spec at all. That is a gap rather than a decision โ€” the PA cockpit work showed exactly which class of defect unit tests cannot see, and the Woo-dashboard has the same shape.

The Infra-board was in that sentence and should not have been: two specs driving it landed on 24 August 2026, and this roadmap item kept naming it through two subsequent syncs. E2E coverage per board is now tabulated on E2E & live smoke, which is the place to check it rather than this paragraph.

Doccle has no live-tested results yet. test-doccle-live.sh exists and is ready to run, but every run so far has been in DOCCLE_STUB_MODE=true โ€” see Doccle โ€” Live Testing.

Branch-coverage depth is the natural next target across the frontend and public site: defensive guards and catch-block edges inside already-tested files, not new files to reach. The two frontend files v2026.10.0 left below 85% branches โ€” BesluitOverzichtSection.tsx at 80.43% and phaseSet.ts at 83.33% โ€” are still the only two in the repository on 11 October, and the nearest such targets.

Deliberately out of scope for now: visual regression and screenshot diffing, a cross-browser matrix (Chromium only in both Playwright suites), and parallel or sharded E2E execution tuning โ€” none are blockers at current suite size.