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.ts192 โ 197, all ingetDecisionDocument. 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 firstronl:documentRefin 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 adocumentReflist; and the fallback to the firstdocumentRefwhen 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 underrip-swimlane/__fixtures__/, andbpmn-swimlane.test.tslost 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/edocsand as whom;services/edocs-author.test.ts(11), theedocsAuthorandedocsAuthorNamestamp;auth/citizen-services.test.ts(10), which services a citizen is offered; andutils/config.edocs.test.ts(6), the Entra settings a live eDOCS connection requires. The largest growth in existing files isservices/edocs.service.test.ts49 โ 81,routes/process.routes.test.ts104 โ 117,routes/task.routes.test.ts47 โ 57,services/operaton.service.test.ts182 โ 192,mcp-servers/edocs/index.test.ts16 โ 25 androutes/edocs.routes.test.ts36 โ 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) andpages/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) andDvtpTakenSection.test.tsx(7). The largest growth in existing files isservices/tenant.test.ts12 โ 39,pages/LoginChoice.test.tsx8 โ 27,pages/Dashboard.test.tsx32 โ 45 andpages/AuthCallback.test.tsx18 โ 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) forsendProblemand the title derived from a code, androutes/besluitvorming.routes.test.ts(10) forGET /v1/besluitvorming/activeand/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,POSThistory and the deprecatedGET), and +4 each inroutes/validsign.routes.test.tsandservices/validsignCompletion.service.test.ts(signing state per task, archive names). See Backend suite. - Frontend, four:
CaseworkerDashboard/BesluitOverzichtSection.test.tsx(7) andBesluitStartSection.test.tsx(3) for Besluitvorming,components/signing/useTaskSignature.test.ts(5), andsrc/utils/problem.test.ts(23), the response interceptor's mapping of a problem back ontoApiResponse.SigningPanel.test.tsx(15 โ 16) andresolveSigningUrl.test.ts(4) are not new: they moved fromInfraBoardDashboard/to the sharedcomponents/signing/. The largest growth in existing files isTakenInbox.test.tsx+4 โ the signing panel in the caseworker inbox โ andmodes.config.test.ts,SectionRouter.test.tsxandProcessWhere.test.tsx+3 each. - Public site, one:
src/lib/problem.test.ts(9), which reads a problem'sdetail;lib/api.test.tsgained 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) andservices(+29);openapi/coverage.test.tsshrank 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 โ plusPaletteActions.test.tsx,StartFailureNotice.test.tsxandsrc/indexHtml.test.ts, the link-preview tags per build mode. An eighth file incomponents/process/,PhaseSwimlane.test.tsx, is not new: it moved there fromInfraBoardDashboard/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.eachtables expanded โ a method that reproduces the runner's total exactly, 1318 atae06c9eand 1122 at963fe24. - 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.tsat the package root (20),src/hooks(5) andsrc/utils(34, of whichproblem.test.tsis 23) - 102 in shared widgets reused across boards โ
ProcessStartFormViewer,TimeLine,DecisionViewer,StartFailureNotice,AltchaWidget,SessionExpiryWarning,PersonalDataPaneland the fourLoginChoicecomponents (38:SingleBoardLandingandAccessDeniedDialognew besideBoardCardandBoardPreview) - 40 in the frontend's host side of the PA cockpit โ
components/PADashboardV2(30),PaCockpitRouteandpa-cockpit-host(5 each) - 171 in pages that belong to no board โ the citizen's
Dashboard(45) andpages/citizen(3),LoginChoice(27) andpages/login-choice(40),AuthCallback(24),ChangelogPaneland its variants (22) andno-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:
- 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. - 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:
ChangelogPanelis alazy()+Suspenseshim, split so the changelog data does not ship in the entry chunk. Its content therefore resolves asynchronously, and every test inChangelogPanel.variants.test.tsxopens withawait 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 usetestTimeout. It uses testing-library's own async-util timeout, which defaults to 1000 ms and is configured nowhere in this package. So the config'stestTimeout: 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.tswas 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-datawithoutimportActual, so the real history is never loaded by this file at all; and - resolves the lazy
ChangelogPanelContentchunk once in abeforeAll, under the hook timeout, so nofindBy*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.
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.tsexcludessrc/**/*.perf.test.tsfrom the default run.vitest.perf.config.tsmirrors it โ same plugins, aliases and setup, but the perf specs are the only thing included, andfileParallelismis off. It spreads and overrides the base test block rather than usingmergeConfig, which concatenates arrays and would have kept the baseexclude, hiding the perf specs from their own run.npm run test:perfruns 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). Becausevite.config.tsexcludessrc/**/*.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.ymlis new: a quarterly review that every direct dependency is still maintained (ICTU recommendation 11, #250). It runs on a clock โ cron23 6 2 1,4,7,10 *, 06:23 UTC on the second day of each quarter โ onworkflow_dispatch, and on a pull request that touches the workflow,scripts/dependency-review.mjsorscripts/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'sauditjob gained an eleventh step, Verify registry signatures โnpm run audit:signatures, after Check formatting. Sinceauditis required on both branches, every pull request now fails on a package whose registry signature is invalid or missing.sbom.ymlnow 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.ymlgained two jobs,lockfile-reviewandlockfile-review-comment. The first runsscripts/lockfile-diff.test.mjsand then comparespackage-lock.jsonwith 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.jsonandpackage.jsonto their path filters, as the backend's already had, so a lockfile-only change is built and tested. azure-pa-demo-acc.ymlbounds Install Playwright browser withtimeout-minutes: 10, after an unanswering Ubuntu mirror hung the step for over ten minutes on 7 October. The E2E step itself is unchanged.sbom.ymlgained 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.