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 count and percentage below was produced on 26 September 2026
against v2026.09.12, whose production main is 2443adc. The runs were
made in the working checkout on acc at 3c44b9e, which differs from
main only in scripts/check-previews.sh β no test file and no source file
β after npm run deps:check reported the installed dependencies in step
with the lockfile. That is one difference from the previous pass, which
used a clean npm ci in a separate clone; the method was otherwise the
same. Each workspace was run on its own, one after another, with its own
npm test; all five of those scripts already include coverage. Rerun the
commands in Running the tests to reproduce them.
The runtime was Node 24.14.1 / npm 11.11.0, and .nvmrc names
22.23.2. Every figure on this page was taken on a newer Node than the one
the repository pins, as the previous passes were. Nothing here is known to
depend on that, but it is a difference, and it is stated rather than
assumed away.
The Playwright suites were not re-run in this pass β their figures on
E2E & live smoke remain those measured on 30 August 2026, and they
are further out of date than a stale figure usually is: the spec inventory
on main is thirteen specs β eleven frontend (including
thuisbatterij-journey and zorgtoeslag-journey), pa-demo/e2e/plato-demo
and public-site/e2e/publiek β and the 27-test frontend figure predates
thuisbatterij-journey.spec.ts entirely. That spec was edited again in this
release (0e71fd3) to accept the review tasks' new Dutch names alongside
the old English ones. Read those counts as a floor, not a total. The four
live-smoke shell scripts remain described from their configuration only.
Two of the root gates were re-run in this pass, and the branch rulesets
were not re-read. npm run lint and npm run check-format passed on
26 September, alongside the backend's new contract and OpenAPI-lint gates;
check-shared, check-supply-chain and the rulesets still carry their
20 September result β see
Linting, formatting, git hooks, and CI.
What was re-read at 2443adc is the workflow tree itself.
At a glance:
| Package | Runner | Files | Tests | Result | DurationΒΉ | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|---|---|---|---|
packages/backend |
Jest + ts-jest | 96 | 2198Β² | all passing | 59.86s | 98.47% | 92.51% | 97.50% | 98.89% |
packages/frontend |
Vitest + RTL | 110 | 1117 | all passingΒ³ | 119.31s | 93.23% | 89.92% | 87.96% | 94.06% |
packages/pa-cockpit |
Vitest + RTL | 43 | 476 | all passing | 33.02s | 90.11% | 88.52% | 86.52% | 91.33% |
packages/pa-demo |
Vitest + jsdom | 19 | 106 | all passing | 13.77s | 93.47% | 95.65% | 85.00% | 92.85% |
packages/public-site |
Vitest + jsdom | 32 | 235 | all passing | 21.03s | 95.92% | 96.31% | 95.07% | 96.41% |
300 files Β· 4132 tests, all passing, nothing failed and nothing skipped, in about 4 minutes 7 seconds of runner-reported time across the five runs (about 5 minutes 1 second of wall clock, counting npm's own start-up). One performance spec runs separately β 4133 in total. See Coverage for what those percentages mean and where the remaining gaps are.
Against v2026.09.11 that is +7 files and +126 tests, all of it in the
backend. The frontend, pa-cockpit, pa-demo and public site reproduced their
file and test counts exactly. The frontend's source did change β
brp.api.ts, bsn.mapping.ts, SectionErrorBoundary.tsx and
IouZakenSection.tsx, besides the changelog data β but none of its test files
did.
Β² 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 2198 measured here is
growth on top of that 2008 β 2072 at v2026.09.11, then +126 in this release,
most of it the OpenAPI contract gate and the tenant-access module β and no
workspace skips anything.
Β³ The frontend was 1117/1117 green in its default parallel run on
26 September β the run in the table, and the one CI makes. On 24 September
the same parallel run had shown five contention-only failures in
ChangelogPanel.variants.test.tsx, and the row carried the serial figures
instead. They did not recur this time; one green run does not close
issue #199, which is
still open β 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 26 September figures hold the gain: public-site 70.39% β 96.31%, pa-cockpit 75.55% β 88.52%, frontend 80.33% β 89.92%, backend 90.01% β 92.51%, pa-demo 86.95% β 95.65%.
The floor is now 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. All five configurations are still in place on 26 September
2026, and no run named a file; scanning every file entry in the five
coverage-final.json reports independently finds zero of 305 files
below 80% branches in any workspace. The floor is branches only,
deliberately: measured the same way, a functions floor at 80 would fail
26 files (frontend 10, pa-cockpit 8, pa-demo 5, public-site 3,
backend 0) β while the configs' own comments still say 31. See
Coverage Floor and
Coverage.
ΒΉ These are elapsed times for one npm test per workspace, each runner's
own file parallelism left on, run one workspace at a time. Treat them as an
order of magnitude rather than a figure to match, and compare like with like β
the same frontend suite took 176.97s parallel and 408.71s under
test:serial on 24 September, about 2.3 times longer (test:serial was not
re-run on 26 September). They are also machine-dependent to a degree worth
keeping in mind: every one of these five is faster than its 24 September
counterpart β the backend's 118.06s became 59.86s while it gained seven files β
and four of the five packages did not change a test, which says more about the
host than about the suites.
Vitest's tests line is not elapsed time
The frontend run takes 119.31 seconds by Vitest's own Duration, and
the same summary block prints tests 238.75s beside it β and
environment 655.05s, 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. 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
reports 88 against its 32 files. 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 96 files and 2198 tests in packages/backend, by area |
| Public site suite | The 32 files and 235 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 four 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
They account for 841 of the 1155 frontend tests as the split was last derived, on 30 August 2026. It was not re-derived on 24 or 26 September, when the frontend measured 1117 tests with the cockpit's 476 living in their own package β so read the shares below as proportions rather than as current counts. The other 314 were not board-specific and so had no board page to live on:
- 223 in
src/services(211),App.test.tsx(5),src/hooks(4) andsrc/test(3) - 49 in shared widgets reused across boards β
ProcessStartFormViewer,TimeLine,DecisionViewer,LoginChoice,AltchaWidget,SessionExpiryWarning,PersonalDataPanel - 42 in pages that belong to no board β
AuthCallback,Dashboard,ChangelogPanel,LoginChoice
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 185 tests
protect more than that one board.
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) |
300 | 4132 |
npm run test:serial |
The same, without file parallelism | 300 | 4132 |
npm test --workspace=@ronl/backend |
Backend only (Jest, coverage on by default) | 96 | 2198 |
npm run test:contract --workspace=@ronl/backend |
src/openapi and src/routes β the OpenAPI gate plus every route suite, coverage off |
22 | 606 |
npm run test:openapi-coverage --workspace=@ronl/backend |
src/openapi only β the coverage gate and its helpers, coverage off |
3β΄ | 27β΄ |
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) | 110 | 1117 |
npm test --workspace=@ronl/pa-cockpit |
The cockpit package | 43 | 476 |
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) | 32 | 235 |
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
β΄ test:contract was run on its own on 26 September β 22 suites Β· 606
tests Β· passing β and lint:openapi passed with "No results with a severity
of 'error' found!". test:openapi-coverage was not run separately; its row is
the src/openapi share of the full backend run, which the test:contract
count reconciles with (src/routes 19 files and 579 tests, plus these 3 and
27). All three scripts are new in v2026.09.12. 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. 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 a worked example of the rule above, and the reason the frontend row in the table carries a footnote. It was the second pass running in which the same file had been the one to go red under load β one test on 20 September, five on 24 September β which is what finally made the mechanism worth publishing rather than merely noting.
26 September: green in parallel, and that settles nothing
The v2026.09.12 pass ran the frontend's default parallel npm test and got
110 files Β· 1117 tests Β· passing Β· 119.31s, with no failure in
ChangelogPanel.variants.test.tsx or anywhere else. Neither that test file
nor ChangelogPanel.tsx changed; changelog-data.ts grew again, to
648,881 bytes at v2026.09.12 (+5.9% on 612,980). What did change is the
machine: every one of the five suites ran markedly faster than on
24 September, the frontend by a third. A contention failure needs
contention, and this run evidently had less of it.
So the prediction below β more of the file's tests failing each release β
did not show on this run, and the diagnosis does not depend on it: the
one-second findBy* budget is still unconfigured, and the module it waits
on is still growing. Issue #199
remains open. The account below is the 24 September one, kept as it was
measured.
The frontend's default npm test β file parallelism on, as CI runs it β came
back 1112 passed, 5 failed, every one of them in the same file:
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, in this order, before drawing any conclusion from that:
- The file on its own, run from
packages/frontend:npx vitest run --config vite.config.ts --coverage.enabled=false src/pages/ChangelogPanel.variants.test.tsxβ exit 0, 1 file Β· 7 tests Β· passing, twice in a row, in 5.32s and 3.70s. From the repository root the same run isnpx vitest run --config packages/frontend/vite.config.ts ChangelogPanel.variants;setupFilesresolves against the config rather than the cwd, so both work. - The whole suite serially.
npm run test:serial --workspace=@ronl/frontendβ exit 0, 110 files Β· 1117 tests Β· passing Β· 408.71s. Inside that run the file took 654ms; inside the parallel run it took 7225ms and failed five of its seven tests.
So the failure is contention under parallel load, not a defect in the code under test, and the honest way to publish the frontend is all green β 1117/1117 serially, with five contention-only failures in the default parallel run in a file that passes in isolation.
Why this file, and why five instead of one¶
Open work β issue #199
Opened 24 September 2026 on the evidence below, and still open at v2026.09.12. What follows is a diagnosis with a fix still to choose, not a closed explanation: nothing in the repository yet stops this recurring, and the trend line says it will.
The mechanism is worth stating, because it is not the timeout most people reach for and because it explains the growth from one failure to five without any change to the test:
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.tsis 612,980 bytes at v2026.09.11, up 7.1% from 572,299 at v2026.09.9, whileChangelogPanel.variants.test.tsxandChangelogPanel.tsxare both untouched between the two releases. At v2026.09.12 it is 648,881 bytes, and both files are still untouched.
The test did not get worse. The module it waits on got bigger, and the one-second budget it is measured against did not move. That is also the prediction, and the reason #199 is open rather than this being a footnote: left alone, this will fail in more of the file's seven tests each release, because the changelog only grows.
Whatever the fix turns out to be, it belongs at the file's own boundary β
and not at a parallelism flag. CI runs npm test parallel and should keep
doing so. The issue carries three candidates, none yet chosen:
- an explicit
findByRole('dialog', { timeout })in that file; - a
configure({ asyncUtilTimeout })in the frontend'ssrc/test/setup.ts, which would cover everyfindBy*in the package rather than this one file; - decoupling the variants test from the real 600 KB module altogether, with a
fixture β the option that removes the trend line instead of raising the
budget ahead of it. The file already mocks
./changelog-data; what it waits on is the lazy chunk behind the panel itself.
The first two buy time proportional to the number chosen. The third is the only one that stops the clock.
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 the five failures above are, 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 β 408.71s against
176.97s on this suite alone, as measured 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. Not re-run on 24 or 26 September 2026 either β its last measured result is still from 12 September 2026: 1 file Β· 1 test Β· passing Β· 1.04s, with coverage off. Becausevite.config.tsexcludessrc/**/*.perf.test.ts, the frontend's 110 files and 1117 tests above do not include it β it is the one test in the repository that has to be counted separately, and the one unit-test figure on this page that was carried over rather than re-measured.
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. (The
26 September run passed it too; Vitest's summary prints no per-file time, so
there is no newer figure to quote.)
Its sibling ChangelogPanel.variants.test.tsx is the file that failed under
parallel load on 20 September and again, five times over, on 24 September β
and passed in the parallel run of 26 September β
same component, same root cause, still the variants file rather than this one
that has yet to be given headroom. The headroom it needs is not
testTimeout, which is what was raised for this file. That is
issue #199; see
Why this file, and why five instead of one.
Linting, formatting, git hooks, and CI¶
All four root gates were run on 20 September 2026 and all four passed. On
26 September two of them were run again and passed again β lint and
check-format β together with the backend's new lint:openapi. The other two
were not re-run since 20 September, and their rows below are carried over
rather than re-measured; so are the timings, which the 26 September runs did
not record.
| Command | What it does | Result (measured) |
|---|---|---|
npm run lint |
npm run lint --workspaces --if-present β eslint . in backend, frontend, public-site |
exit 0, no errors β 26 Sep (68s on 20 Sep) |
npm run check-format |
prettier --check "**/*.{ts,tsx,json,md}" --ignore-path .gitignore β one repo-wide glob, not a per-workspace fan-out |
All matched files use Prettier code style! β 26 Sep (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 |
No results with a severity of 'error' found! β 26 Sep |
npm run check-shared |
node scripts/check-shared-declarations.mjs β what stands in for tests in packages/shared |
check-shared-declarations: 11 file(s) in packages/shared/src/ β declarations and constant data only., 1s β 20 Sep |
npm run check-supply-chain |
node scripts/check-supply-chain.mjs β every action pin against its version comment and the register |
31 pinned reference(s) across 5 action(s) in .github/workflows/ β¦ OK β digests, version comments and the register all agree., 4s β 20 Sep, before the two workflows added in v2026.09.12 |
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.
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, which is the one required check,
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 type-check β npm run lint β npm run check-format |
All workspaces (type-check, lint) / whole tree (check-format) |
The hooks do not run the tests
Re-read against .husky/ on 20 September 2026 and still true: pre-commit
runs npx lint-staged and nothing else, and pre-push runs the five
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.
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¶
Thirteen workflows under .github/workflows/, read at 2443adc β 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, and two added in v2026.09.12: a daily dependency audit and a
release SBOM:
| Workflow | Lint | Type-check | Tests | E2E | Perf budget | Build | Deploys? |
|---|---|---|---|---|---|---|---|
azure-backend-acc.yml / -prod.yml |
β
+ OpenAPI (lint:openapi) |
β | β | β | β | β | No β packages and uploads an artifact |
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 |
semgrep.yml |
β | β | β | β | β | β | No β scan, reporting and not required |
dependency-audit.yml |
β | β | β | β | β | β | No β a daily scheduled audit of the dependency tree, the one check here that runs on a clock rather than a commit |
sbom.yml |
β | β | β | β | β | β | No β generates and uploads the release SBOM on a push to main |
Whether either of the two new workflows is a required check was not established: the rulesets were not re-read in this pass (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 check that the document matches the routes
actually served is not a separate step: it is src/openapi/coverage.test.ts,
inside the backend's ordinary npm test β 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 476 tests ran nowhere in CI β a count third only to the
backend's and the frontend's, 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 and
not re-read on 24 or 26 September β 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 |
main |
main promotion gate | audit |
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.
main requiring only audit 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 audit
requirable there.
This is now doubly true. Read at 86af73e and again at 2443adc, 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 are the exception to "deploy": they end at Create deployment zip β Upload deployment artifact. Nothing in them calls a deploy action, and there is no post-deployment health check β the artifact is deployed separately, from a developer machine. 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.
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.
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.