Skip to content

Testing

The Linked Data Explorer is an npm-workspaces monorepo, and its two packages are tested with different runners: the Express/TypeScript backend on Jest, the Vite/React frontend on Vitest. Both were built from zero in v2026.07.0 โ€” before that the repository had no test files at all and npm test exited 1 with "No tests found".

Figures on this page are measured, not estimated

The headline counts, package coverage, per-area tables, per-file coverage and command results below were produced by running the suites against v2026.10.2 on 10 October 2026, at a87f64b on acc โ€” the same tree as 82f461f on main, so ACC and production both run what was measured. The runs used the working checkout, with npm run deps:check reporting the installed dependencies in step with the lockfile, on Node 24.21.0, the version .nvmrc pins.

The two packages were not run the same way. The backend ran serially, npx jest --coverage --runInBand; the frontend ran with Vitest's default file parallelism, npx vitest run --coverage. Counts and coverage do not depend on that choice; the backend's runner time does, and is not comparable with the parallel figures earlier releases quoted. Rerun the commands in Running the tests to reproduce them.

At a glance: 158 test files ยท 3418 tests ยท all passing ยท backend 98.39% statements, frontend 95.57%.

Package Runner Files Tests Runner time Statements Branches Functions Lines
packages/backend Jest + ts-jest 72 1966 60.5 s (serial) 98.39% 92.23% 97.60% 99.10%
packages/frontend Vitest + RTL 86 1452 105.7 s 95.57% 92.71% 93.62% 96.64%

packages/ropa-site has no package.json and is not an npm workspace member, so npm test never reaches it. Five further test files live outside both packages and outside the total above โ€” see Script harnesses outside npm test.

Against v2026.10.1 that is +8 files and +184 tests, all eight files in the frontend. It was the release of the Modeler's phase editor, and most of the growth follows it.

  • Backend, no new file and +24 tests, all in five existing files. services/assets.service.test.ts +8 (now 31) and routes/assets.routes.test.ts +3 (now 85) pin one stored process per organisation and process id: the 409 PROCESS_ID_TAKEN refusal, a save that loses a race against the unique index, the exempt E2E and placeholder rows, the deploy stamp keyed by organisation, and the ?organization= scope. db/migrate.test.ts +5 (now 12) covers the start-up deduplication, the archive table it writes to and the unique index; example-fixture-parity.test.ts +5 (now 9) the Heusden bundles and the authored-copy rule; and besluitvorming-gedelegeerd-bundle.test.ts +3 (now 20) the bestuur besluit document. services/assets.service.ts went from 98.68% of 76 branches to 96.07% of 102, and db/migrate.ts from one branch to four, all of them covered. Statements moved 98.42% โ†’ 98.39%, branches 92.25% โ†’ 92.23%, functions 97.57% โ†’ 97.60%, lines 99.09% โ†’ 99.10%.
  • Frontend, +8 files and +160 tests. Six of the new files are the phase editor: utils/phases/assignPhases.test.ts (20), which includes parity with the RONL Business API on its declared-phase examples, phaseCommands.test.ts (16), phaseSet.test.ts (12) and phaseChecks.test.ts (11), and BpmnModeler/ProcessPhasesEditor.test.tsx (24) and PhaseMarkerSelector.test.tsx (12). The other two are utils/processIdentity.test.ts (13), for renaming the process id of an import, and utils/ropaGrouping.test.ts (9), for nesting RoPA records under the shell that calls them. Growth in seven existing files: BpmnModeler/BpmnCanvas.test.tsx +12 (now 72), for the phase panel, the badges and the deploy dialog's phase findings; BpmnModeler/ronlModdleDescriptor.test.ts +9 (now 11), for the phase attributes and their round trip; DocumentComposer/defaultTemplates.test.ts +8 (now 55), for the bestuur besluit template; BpmnModeler/BpmnModeler.test.tsx +5 (now 55), for a process id the organisation already uses; services/bpmnService.test.ts +4 (now 16); BpmnModeler/FormTemplateSelector.test.tsx +3 (now 7), for the binding on the confirmation card; and RopaEditor/RopaList.test.tsx +2 (now 8). Statements moved 95.36% โ†’ 95.57%, branches 92.92% โ†’ 92.71%, functions 93.22% โ†’ 93.62%, lines 96.40% โ†’ 96.64%.

Every per-file delta above is the difference between two runner reports โ€” the Jest and Vitest JSON reports of the 6 October run at v2026.10.1 and of the 10 October run โ€” compared test by test, not inferred from the diff.

v2026.10.1 added 4 files and 111 tests, two files in each package. It was the /v2/norms release. The backend gained routes/norms.v2.routes.test.ts (35) โ€” defaults, parameter validation, caching and conditional requests, failures, and every status checked against the /v2 OpenAPI document โ€” and utils/amsterdamDate.test.ts (23), and 43 tests in eleven existing files; its three new source files read 100% on all four metrics. The frontend gained utils/deployBundle.test.ts (5), for the lookups both deploy handlers share, BpmnModeler/ronlModdleDescriptor.test.ts (2), and three tests in two existing files. Backend branches moved 91.98% โ†’ 92.25%, frontend 92.89% โ†’ 92.92%. Those deltas, too, compared two runner reports: the 3 October run at v2026.10.0 and the 6 October run.

v2026.10.0 added 2 files and 40 tests, one file in each package, and no backend source file changed. The backend gained src/besluitvorming-gedelegeerd-bundle.test.ts (17): the Besluitvorming bundle is hand-written BPMN, DMN, form and document files that refer to each other by id and variable name, and its tests check that GedelegeerdBesluitRoute is a FIRST-hit decision reading exactly the five inputs the forms produce, that the twelve forms set every DMN input and every value the besluit document binds, that the process draws six lanes, declares six phases, calls the DMN untenanted, signs the besluit document and escalates a declined signature, and that it keeps the schema's element order. The frontend gained utils/documentRefs.test.ts (7), for the comma-separated ronl:documentRef list, and 16 tests in four existing files.

v2026.09.8 added no file and one test, in the backend: the buildInfo.test.ts case "reports the build it loaded with, even after the file changes underneath", which pins that deploy/build-info.json is read once, at start-up, rather than on first request. The frontend count and all four of its coverage figures were unchanged. Backend branches moved 92.49% โ†’ 91.98%; the only backend source files changed in that release were utils/buildInfo.ts, routes/openapi.routes.ts and services/quality.service.ts, and which of them moved the average was not measured per file.

v2026.09.6 added 8 files and 261 tests โ€” backend +174, frontend +87. It was a DSO release, and its tests follow the new services. Six modules arrived with their own suites, and two of them landed clean (coverage as measured at v2026.09.6):

New module Package Tests Statements Branches Functions Lines
src/services/dossier.service.ts backend 57 97.77% 95.52% (134 br) 100% 98.71%
src/services/quality.service.ts backend 38 96.47% 91.11% (45 br) 95.45% 100%
src/services/ozon.service.ts backend 13 100% 100% (28 br) 100% 100%
src/utils/ttl-cache.ts backend 13 100% 100% (11 br) 100% 100%
components/DsoExplorer/QualityProfileTab.tsx frontend 43 98.77% 90.65% (246 br) 98.76% 99.55%
src/services/deployTargetService.ts frontend 5 100% 91.66% (12 br) 100% 100%

ozon.service.ts โ€” the client for Omgevingsdocumenten Presenteren โ€” is the cleanest new module in the release at 100% on all four metrics, and ttl-cache.ts matches it. Both are small and pure enough to test exhaustively, which is the point: the branching that resists coverage lives in the components, not here. The Ozon and dossier suites are backed by a captured production annotation payload, src/__fixtures__/annotaties-gm0995-prod.json, rather than a hand-written stub โ€” so the parsing they assert is parsing that a real response actually requires.

The two largest existing suites grew to meet them: routes/dso.routes.test.ts to 110 tests and services/dso.service.test.ts to 92, and on the frontend DsoExplorer.test.tsx to 88 and services/dsoService.test.ts to 53. A new DsoExplorer/shared.tsx extracted from the explorer reads 100% on all four.

Package branch averages barely moved โ€” backend 92.41% โ†’ 92.49%, frontend 93.26% โ†’ 92.93% โ€” which is the expected shape when a release adds heavily branching code and covers it to roughly the existing standard. The average is not what holds a release to account here; see The per-file branch floor.

v2026.09.5 added 579 tests, 498 of them in the backend, almost all of them new code under test rather than margin on old. The outbound guard alone brought 82 โ€” outboundUrl.test.ts 62 and outboundHttp.test.ts 20. src/openapi is a new area of 38 tests across four files, and the route suites grew by validating every response against the published OpenAPI document. Problem details, build provenance and the extracted CORS middleware each gained their own file. Backend branches moved 92.96% โ†’ 92.41%: the release added a great deal of branching code, and the per-file floor, not the package average, is what holds each file to account. The frontend added 81 tests and four files โ€” among them vite/cspPlugin.test.ts for the Content-Security-Policy generator and src/data/dsoAuthorities.test.ts for the generated authority list โ€” and moved branches 92.88% โ†’ 93.26%.

The frontend nearly doubled, and then was given margin. v2026.09.1 raised per-file branch coverage above 80% across both packages: the frontend gained 8 files and 447 tests, taking branches from 65.67% to 90.62% and statements from 74.72% to 94.30%. The backend moved far less because it had further to go โ€” six tests, and branches from 91.49% to 92.22%.

v2026.09.2 took the frontend from 1020 to 1073 tests. 31 of those came from one commit that added them to twelve existing files and no new file, taking branches to 92.88% and changing no production code โ€” margin above the floor rather than new behaviour. See The per-file branch floor. The rest of the delta is ordinary work in the same release, minus the nine tests deleted with the Tutorial component.

v2026.09.3 added a test file to each package, both for defects the first Semgrep triage found rather than for coverage. publicPaths.test.ts asserts that wildcard CORS reaches the two public mounts and paths below them, and never a sibling that shares the prefix โ€” /v1/ropa/publications, /v1/ropa/public-admin and /v1/bundles/publicity must all fall through to the allowlist. ronlAttributes.test.ts pins the escaping of BPMN process metadata, including the $' value that String.replace used to expand into the rest of the document. Across v2026.09.3 and v2026.09.4 together, backend branches moved 92.22% โ†’ 92.96%; the frontend's held at 92.88%.

Two jsdom limitations had to be worked around to reach the d3 drag and zoom paths, and they are the kind that make a branch look untestable when it is not: jsdom rejects view in the MouseEvent constructor, and SVGSVGElement carries no width/height baseVal for the default extent d3-zoom computes.

An invocation caveat that no longer reproduces

Through v2026.08.9 this page warned that backend branch coverage read 91.49% via npm test -w packages/backend but 92.22% when Jest was invoked directly from the repository root, with the same config and the same tests. The measurement above used the workspace invocation and read 92.22%, so the discrepancy is not reproducible as described. Whether v2026.09.1 closed it or the original diagnosis was wrong is not established here โ€” only that the two invocations no longer disagree in the way the warning claimed. The root invocation was not separately re-run.


Running the tests

Run from the repository root after npm ci. The root scripts fan out over both workspaces with --workspaces --if-present.

Command Scope Files Tests Time
npm test Both packages 158 3418 โ€”
npm test -w packages/backend Backend only (Jest, with coverage) 72 1966 60.5 s, measured serially with --runInBand
npm test -w packages/frontend Frontend only (Vitest, with coverage) 86 1452 105.7 s
npm run test:contract -w packages/backend src/openapi and src/routes, without coverage 28 819 not run on its own
npm run test:scripts Two of the five script harnesses in scripts/ โ€” not part of npm test 2 24 + 24 checks โ€”
node scripts/promotion-targets.test.mjs The promotion decision script alone 1 24 checks โ€”
node scripts/dso-dossier.test.mjs The dossier renderer alone 1 24 checks โ€”
node scripts/lockfile-diff.test.mjs The lockfile review script โ€” in no npm script; CI runs it 1 77 checks (counted, not run locally) โ€”
node scripts/write-sbom.test.mjs The SBOM generator's production filter โ€” in no npm script; CI runs it 1 7 checks (counted, not run locally) โ€”
node scripts/dependency-review.test.mjs The quarterly dependency review script โ€” in no npm script; CI runs it 1 24 checks (counted, not run locally) โ€”

Times are the runner's own figures from the 10 October 2026 measurement. The backend row was measured with npx jest --coverage --runInBand, one file at a time; npm test runs Jest with its default workers, so its time will differ. The frontend row ran with Vitest's default file parallelism. The contract row's count is taken from the full backend run's report; the subset was not run on its own in this measurement. npm run test:scripts passes on Windows as well as Linux: both harnesses printed PASS: 24 checks on Windows in the same measurement. The three CI-only harnesses were not run locally. Their figures are counts of the check and throws calls in each file, each adding one to the PASS figure it prints โ€” all at top level, except one check in dependency-review.test.mjs that runs once for each of four repository URLs, and counts four.

npm run sbom:check (node scripts/write-sbom.mjs --check), new in v2026.10.1, is not a test: it exits 1 when the committed SBOM for the current version is missing or no longer matches the lockfile. sbom.yml runs the same --check on the release pull request. In the 10 October measurement it reported docs\sbom\linked-data-explorer-2026.10.2.cdx.json matches the lockfile.

Both packages' test scripts include coverage by default, so a plain run always produces a report. Watch modes are test:watch in either package; the backend additionally has test:coverage as an explicit alias.

# Everything
npm test

# One package
npm test -w packages/backend
npm test -w packages/frontend

# The contract subset โ€” the fast loop while editing openapi.yaml or a route
npm run test:contract -w packages/backend

# Watch mode while working on the backend
npm run test:watch -w packages/backend

# Two of the five script harnesses, which npm test does not run
npm run test:scripts

# The three harnesses CI runs, which no npm script runs
node scripts/lockfile-diff.test.mjs
node scripts/write-sbom.test.mjs
node scripts/dependency-review.test.mjs

# The dossier renderer's harness alone
node scripts/dso-dossier.test.mjs

# Diagnostic only โ€” see "When the frontend suite fails only in parallel"
cd packages/frontend && npx vitest run --no-file-parallelism --coverage

The contract subset is a backend run, selected by path

test:contract is a subset of the backend run, not an addition to it, and not a frontend concern:

jest --config jest.config.js src/openapi src/routes --coverage=false

Selection is by path prefix. Not a filename convention, not a tag, not a describe name: every *.test.ts under src/openapi or src/routes is in the subset, and everything else is out. That is the whole rule, and it is what you need to know to add a test to it โ€” put the file under one of those two directories and it is included; put a contract assertion anywhere else and it will never run in the fast loop.

At v2026.10.2 that is 28 files and 819 tests, all passing: 772 tests from src/routes and 47 from src/openapi โ€” three more than at v2026.10.1, all in assets.routes.test.ts. The count comes from the full backend run of 10 October 2026; the subset was not run on its own in that measurement, so no time is quoted for it (at v2026.10.1 Jest reported 39.5 seconds). It is the loop to use while editing openapi.yaml, openapi.v2.yaml or a route.

There is no frontend contract subset. packages/frontend defines no test:contract script and carries no test file with contract in its name; its only test entry points are test and test:watch. If this page's contract figure is ever quoted beside the frontend total, that is a misreading โ€” the number belongs to the backend row.

Script harnesses outside npm test

Five test files live in scripts/, outside both workspaces, and npm test runs none of them. The root script npm run test:scripts runs two, in order: node scripts/promotion-targets.test.mjs && node scripts/dso-dossier.test.mjs. The other three have no npm script; each runs in CI, in the workflow that uses the script it tests: lockfile-diff.test.mjs, new in v2026.10.1, and write-sbom.test.mjs and dependency-review.test.mjs, new in v2026.10.2.

Harness Covers Runs in CI Checks
promotion-targets.test.mjs scripts/promotion-targets.mjs, which decides which production deploys a promotion needs โ€” including a drift guard that the two production site workflows' pull_request paths agree with its patterns Yes โ€” promote-to-production.yml runs it in its changes job, before the script it tests makes the decision 24
dso-dossier.test.mjs The DSO activity dossier renderer No โ€” no workflow and neither git hook invokes it 24
lockfile-diff.test.mjs scripts/lockfile-diff.mjs, the lockfile review of a pull request: parsing, the diff by package name, downgrades, the blocking rules (a non-npmjs origin, a missing integrity hash), licence changes against the allow-list in lockfile-review.json, install scripts, new direct dependencies and the checklist the report gives each, the report and its truncation, and the command line's exit codes Yes โ€” zizmor.yml runs it in its lockfile-review job, before the script it tests compares the lockfile 77
write-sbom.test.mjs scripts/write-sbom.mjs's production filter: the production set read from the lockfile โ€” every package it does not mark dev, devOptional included, plus the workspace packages โ€” the dependency graph pruned to match, a production package npm did not list reported rather than left out, and the version in the output path Yes โ€” sbom.yml runs it in its release-sbom job, before the SBOM checks and the generation 7
dependency-review.test.mjs scripts/dependency-review.mjs, the evidence for the quarterly review of every direct dependency: the quarter, the direct dependencies read from the lockfile, the registry and GitHub evidence, the assessment and the report โ€” with the registry's and GitHub's answers as fixtures Yes โ€” dependency-review.yml runs it before gathering the evidence 24

So the promotion and lockfile harnesses are gates: a promotion whose decision script fails its own test deploys nothing, and a pull request whose lockfile review script fails its test fails lockfile-review, a required check on acc. The SBOM and dependency-review harnesses stop their own run โ€” a failing SBOM test means no SBOM is checked, generated or uploaded, and a failing review test means no evidence is gathered and no issue opened โ€” but neither workflow is a required check. The dossier harness still protects its renderer only when someone runs it โ€” by hand, or through test:scripts.

None is a runner. All five are hand-rolled ESM harnesses โ€” not node:test โ€” that count passing checks, print PASS: <n> checks, and call process.exit(1) naming any check that failed. The three CI-only harnesses' figures were counted from their source, not from a run: they execute only in CI, and were not run for this measurement.

The two in test:scripts both run on Windows since v2026.10.0. Until then the promotion harness located its script with new URL(...).pathname, which on Windows yields /C:/โ€ฆ, a path Node cannot run, so its command-line checks failed there โ€” and because test:scripts joins the two with &&, the dossier harness never ran. It now uses fileURLToPath, and stops with a named failure if the resolved path does not exist, so a regression reports its cause instead of four unexplained command-line failures.

Their checks are deliberately excluded from the 3418. They are not comparable to a Jest or Vitest test: they have no per-case isolation, no setup or teardown, no reporter and no coverage instrumentation. Folding them into the headline would inflate it with a different unit of measurement. Counted as files rather than tests, the repository has 163 executable test files โ€” 72 in the backend, 86 in the frontend and 5 in scripts/ โ€” of which npm test runs 158.

Linting and formatting

Command Backend Frontend
npm run lint โœ… runs โœ… runs
npm run typecheck โœ… runs โœ… runs
npm run check-format โœ… runs โœ… runs
npm run lint:openapi โœ… runs โ€“

lint:openapi builds both OpenAPI documents โ€” openapi/openapi.json from openapi/openapi.yaml and openapi/openapi.v2.json from openapi/openapi.v2.yaml โ€” and lints the two with Spectral against the NL API Design Rules 2.2.1. In the v2026.10.2 measurement at a87f64b, lint, typecheck and check-format exited 0; lint:openapi was not run in that pass. The repository-level checks check-supply-chain and check-rip-bpmn exited 0 in the same pass, sbom:check found the committed SBOM in step with the lockfile, and deps:check reported the install in step with the lockfile.

Both root commands fan out over both packages. The backend checks src/**/*.ts; the frontend checks the whole package.

check-format skipped the backend until 19 August 2026

The two packages used to name their Prettier check differently โ€” the backend format:check, the frontend check-format. The root script runs npm run check-format --workspaces --if-present, so --if-present quietly skipped the backend rather than failing, and since pre-push runs exactly that script, backend formatting was never gated on push.

The backend now defines check-format as its canonical name, with format:check retained as an alias so existing muscle memory and tooling references keep working. On a checkout from before this change, check the backend explicitly with npm run format:check -w packages/backend.

lint-staged had the same blind spot at pre-commit โ€” it was configured for packages/frontend/** only โ€” and was extended to the backend at the same time. Neither change had formatting fallout: the backend already passed both its lint and its format check.

Git hooks

Hook Runs Scope
pre-commit npx lint-staged Staged files only โ€” Prettier --write then ESLint --fix, per package with that package's own config
pre-push npm run deps:check, then npm run check-rip-bpmn, then npm run lint, then npm run check-format Both packages in full, plus the RIP BPMN copies

deps:check runs first, since v2026.09.5, so a clone whose install has fallen behind the lockfile stops there with npm ci named โ€” instead of failing lint or the format check on the wrong tool versions, with nothing in the output saying why.

check-rip-bpmn runs second. The twelve RIP phase models exist three times: the source under examples/organizations/flevoland/, the copies in e2e-fixtures/flevoland/, and the RONL Business API's parser fixtures. The check compares the e2e-fixtures copies byte for byte with their source, and the source against the sha256 fingerprints in rip-bpmn-fingerprints.json, which is committed identically in both repositories. Since v2026.10.1 it also fingerprints two declared-phase models that are RONL Business API parser fixtures without being RIP phase models โ€” GedelegeerdBesluitProcess.bpmn under packages/frontend/public/examples/flevoland/besluitvorming-gedelegeerd/ and ManagementCapacityClaimProcess.bpmn under examples/organizations/flevoland/HR-capacity/ โ€” so an edit to either needs --write too. --write regenerates the fingerprints and refreshes the e2e-fixtures copies. In the 10 October run it reported 12 RIP phase models and 2 declared-phase models fingerprinted, 2 e2e-fixtures copies identical to source.

The hook is no longer the only place it gates. Since v2026.10.1 the required audit job in zizmor.yml runs it on every pull request and on every push to acc and main, so a push made around the hook, or an edit made in the GitHub interface, is caught before it merges.

lint-staged matches packages/frontend/**/*.{js,jsx,ts,tsx,json,css,md} and packages/backend/src/**/*.ts. The backend glob mirrors exactly what its check-format gates, so the pre-commit and pre-push scopes agree rather than one rewriting files the other never checks.

The hooks do not run the tests

Neither hook invokes a test script, so nothing client-side stops a push that breaks the suite โ€” run npm test yourself before pushing. What has changed is what happens next: since 20 August 2026 the deploy pipelines run the suites and a failure blocks the deploy, so a broken push no longer reaches acceptance. See CI.


CI โ€” the test gate

Four of the six Azure workflows run the suites, and a failure blocks the deploy:

Workflow Lint Typecheck OpenAPI lint Tests Build Deploy
azure-backend-acc / -production โœ… โœ… โœ… โœ… โœ… โœ…
azure-frontend-acc / -production โœ… โœ… โ€“ โœ… โœ… โœ…
azure-ropa-site-acc / -prod โ€“ โ€“ โ€“ โ€“ โ€“ โœ…

The production workflows in the table are no longer started by a push to main: promote-to-production.yml calls them, and runs one test of its own first โ€” scripts/promotion-targets.test.mjs, in its changes job, before the decision that test covers. See How a promotion reaches production.

Outside the Azure workflows, three more workflows run test code or a test-like gate. zizmor.yml has no path filter and so runs on every pull request; sbom.yml and dependency-review.yml run their script's harness first, on their own triggers:

Workflow ยท job Runs Required on acc
zizmor.yml ยท audit Among its steps, npm run audit:signatures (since v2026.10.2) โ€” the registry signature of every installed package, and the provenance attestation of those that have one โ€” and npm run check-rip-bpmn (since v2026.10.1) โœ…
zizmor.yml ยท lockfile-review node scripts/lockfile-diff.test.mjs, then the lockfile review itself โ€” it blocks only on a package from a non-npmjs origin or without an integrity hash, or on a lockfile it cannot read, and succeeds at once when the lockfile is unchanged โœ…
sbom.yml ยท release-sbom node scripts/write-sbom.test.mjs (since v2026.10.2), before the SBOM checks and the generation โ€” on a push to main, by hand, and on a pull request touching the SBOM tooling or docs/sbom/ โ€“
dependency-review.yml ยท dependency-review node scripts/dependency-review.test.mjs (since v2026.10.2), before the review gathers its evidence โ€” once a quarter, by hand, and on a pull request touching the workflow, dependency-review.mjs or lockfile-diff.mjs โ€“

The acc supply-chain gate ruleset requires audit, scan, deploy, Build and Deploy Frontend, Build and Deploy ROPA Site and lockfile-review; main requires audit and scan.

Fixture-only and example-only pull requests run the backend suite

azure-backend-acc.yml triggers on every pull request into acc, and its changes job then matches the changed files against one pattern:

^(\.nvmrc$|packages/backend/|\.github/workflows/azure-backend-acc\.yml$|package-lock\.json$|package\.json$|examples/|e2e-fixtures/|packages/frontend/public/examples/)

When nothing matches, the deploy job โ€” which holds the lint, typecheck and test steps โ€” is skipped and reports success. Until v2026.10.1 the pattern stopped at package\.json$, so a pull request that changed only e2e-fixtures/, examples/ or packages/frontend/public/examples/ merged without running the backend tests that read exactly those directories: the four fixture-integrity and parity files and besluitvorming-gedelegeerd-bundle.test.ts. The three directories are now in the pattern and in the push paths, so such a change runs the full backend suite on its pull request, and its merge redeploys the unchanged backend to acceptance, which is harmless. The production workflow has no path filter of its own; scripts/promotion-targets.mjs decides which production deploys a promotion needs.

The two ropa-site workflows run nothing, correctly: packages/ropa-site is a static index.html plus a staticwebapp.config.json, with no build and no test script to run.

The backend workflows already installed and linted, so the test step slots in beside the existing npm run lint. The frontend workflows had no npm steps at all โ€” the Static Web Apps action builds inside its own container and runs none of the repository's scripts โ€” so they gained an explicit install, lint and test sequence ahead of the deploy action, installing from the workspace root since that is where the only lockfile lives.

Why this was deferred, and what changed

There was no test step anywhere until 20 August 2026. That was a deliberate P7 decision taken when coverage was first measured: 109 backend tests at 13.82% statements against 557 frontend tests at 74.03%. The reasoning was that the backend gap was breadth โ€” whole route and service files never touched โ€” rather than depth, and gating on a number that low would be theatre. The same call the RONL Business API made at 83.39%.

The condition recorded for revisiting it was "once backend breadth improves". v2026.08.2 took the backend from 16.79% to 98.06% statements, which met it.


Backend suite (Jest)

packages/backend, jest.config.js with the ts-jest preset. collectCoverageFrom spans all of src/**/*.ts, so untested files report as 0% rather than being omitted from the report โ€” a deliberate choice that keeps the headline number honest. A coverage tool that only reports files a test happened to import will show a clean table for a feature nobody tested at all; this one makes that feature appear as a 0% row. Four things are excluded, each for a stated reason: *.test.ts, src/types/** (declarations, no runtime), index.ts (the boot script) and db/seed-ropa.ts (a self-executing CLI, not an importable module). db/migrate.ts is pointedly not excluded โ€” it exports a plain migrate() that a mocked pool can drive, so it stays visible rather than being written off. The table instruments 61 backend files: the 58 of v2026.09.6 to v2026.10.0, plus the three v2026.10.1 added โ€” routes/norms.v2.routes.ts, routes/norms.shared.ts and utils/amsterdamDate.ts. v2026.10.2 added none.

Area Files Tests Covers
src/services 21 793 Every service: operaton, sparql, dso, dossier, quality, ozon, norms, edocs, ropa, vendor, assets, template, triplydb, orchestration, shacl-validation, dmn-validation, externalTaskWorker. norms.service is 62, assets.service 31
src/routes 24 772 Every route module plus routes/index and routes/registry, each mounted in isolation (a fresh express() app per file, not the full index.ts) with the service layer mocked and supertest driving requests โ€” and each response validated against the OpenAPI document of its major version. assets.routes 85, norms.routes 37, norms.v2.routes 35
src/utils 12 247 outboundUrl 62, config 27, amsterdamDate 23, outboundHttp 23, errors 23, publicPaths 15, etag 14, ttl-cache 13, rootViews 13, buildInfo 13, logger 11, problem 10
src/openapi 4 47 The conformance helpers routes are checked with (16), route-to-operation matching per major version (13), the two served documents (12), and the coverage rule that fails when a /v1 or /v2 route is undocumented, or a documented operation is not served (6)
src/middleware 3 34 error.middleware 22, cors.middleware 8, version.middleware 4
src/db 3 30 mappers 15, migrate 12, pool 3
src/e2e-fixtures.test.ts 1 5 The e2e-fixtures/ bundle consumed by the RONL Business API's E2E suite โ€” manifest parses, every declared file exists, each BPMN's process id matches its processDefinitionKey, each shell's calledElement references resolve, and every process keeps its artifacts after its flow elements
src/e2e-fixture-decisions.test.ts 1 5 The bundle's DMN dependencies, which the manifest used to omit entirely: every declared decision file ships, each provides exactly the decisions it claims, those match the decisionRefs in its BPMN, every decision any fixture calls is shipped or declared external, and every businessRuleTask resolves the untenanted decision
src/example-fixture-parity.test.ts 1 9 Every file in a mirrored bundle is byte-identical to its twin in e2e-fixtures/ โ€” the two RIP bundles examples/organizations/flevoland/rip-phase-21/ and rip-phase-22/ against e2e-fixtures/flevoland/, and since v2026.10.2 the Heusden swimlane bundle and three Heusden DMNs against e2e-fixtures/heusden/. Bundles opt in through src/__fixtures__/mirrored-bundles.json, which the looser parity test reads too; an entry with files mirrors only those files. A second describe, new in v2026.10.2, requires every file under packages/frontend/public/examples/flevoland/ to have exactly one authored copy under examples/organizations/flevoland/, byte-identical โ€” matched by file name, since the authored tree groups bundles in folders the public tree keeps flat
src/public-example-fixture-parity.test.ts 1 4 The looser parity rule for the pair the Modeler serves against the one LDE deploys โ€” packages/frontend/public/examples/ against e2e-fixtures/ โ€” which must match once four sanctioned labels are stripped, and where the fixtures copy must actually carry the labels it is allowed to carry. Since v2026.10.1 also that every subprocess a fixture calls has an E2E key no example uses โ€” a rule from which v2026.10.2 exempts the bundles in mirrored-bundles.json, whose copies cannot differ
src/besluitvorming-gedelegeerd-bundle.test.ts 1 20 The hand-written Besluitvorming bundle under packages/frontend/public/examples/flevoland/besluitvorming-gedelegeerd/ agrees with itself: the FIRST-hit GedelegeerdBesluitRoute decision and its five inputs, the twelve forms, the six lanes and six declared phases, the untenanted DMN call, the signed besluit document and its bindings, the escalation of a declined signature, and schema element order โ€” and, since v2026.10.2, the bestuur besluit besluit-gb-besluit-bestuur that Neem besluit attaches: the same process, the besluitnemer rather than the gemachtigde ondertekenaar, and only variables a form writes

Techniques worth knowing before adding tests here:

  • Module-level singletons (db/pool, utils/logger, utils/config) are re-imported per test case via jest.isolateModules with a patched environment, since their behaviour is fixed at import time.
  • "Pool is null" branches โ€” the database-not-configured path for the ropa and assets services โ€” live in their own *.no-pool.test.ts files with a single static mock. jest.doMock inside isolateModulesAsync did not reliably override a file's top-level jest.mock('../db/pool').
  • ESM-only dependencies (@rdfjs/dataset, rdf-validate-shacl) are stubbed, so the SHACL suite exercises layer loading and issue mapping rather than the RDF libraries themselves.
  • A few defensive outermost catch blocks are left deliberately uncovered. health.routes.ts, the example this page used to give at 89.74%, now reads 100% statements.
  • The outbound-guard tests pin their own environment. scripts/jest-env.cjs sets ALLOW_LOCAL_ENDPOINTS=false and TRIPLYDB_ALLOWED_HOSTS before any module reads .env; dotenv never overrides a variable that is already set. Without that, a developer configured for local Jena saw 46 of them fail.

Frontend suite (Vitest)

packages/frontend. vite.config.ts's defineConfig comes from vitest/config; test.environment defaults to node with a per-file // @vitest-environment jsdom opt-in, so the many pure-logic tests never pay for a DOM. Uses @testing-library/react, jest-dom, user-event, jsdom and msw.

Area Files Tests Notes
components/BpmnModeler 12 227 bpmn-js and the properties panel mocked outright via a small fake modeler class built in a vi.hoisted block. BpmnCanvas 72, BpmnModeler 55, ProcessPhasesEditor 24, PhaseMarkerSelector 12; ronlModdleDescriptor 11 pins documentRef and signatureRef on a user task, registers the four phase attributes, and round-trips them through three examples
src/utils 17 204 Pure logic: exportService 38, the phase engine under utils/phases/ โ€” assignPhases 20, phaseCommands 16, phaseSet 12, phaseChecks 11 โ€” ronlAttributes 17, logoResolver 14, processIdentity 13, testData 11, exampleVersions 10, ropaGrouping 9, buildInfo 8, documentRefs 7, exportFormats 6, deployBundle 5, problem 5, constants 2
components/DocumentComposer 10 190 The heaviest area, coupling @tiptap/react and @dnd-kit. Real ProseMirror runs under jsdom once Range.getClientRects/getBoundingClientRect and document.elementFromPoint are polyfilled
components/ChainBuilder 13 185 First @dnd-kit-coupled area. The hooks render fine with no DndContext wrapper โ€” dnd-kit's context hooks fall back to sane defaults
src/services 12 168 All 12 service modules โ€” msw for the network-calling ones, jsdom localStorage for the two storage modules. dsoService is the largest at 53. sparqlService is tested against the backend route only; its browser-direct path and CORS-proxy fallback were removed in v2026.09.5
src/components (top level) 7 153 ShaclValidator 40, DmnValidator 30, ResultsTable 26, GraphView 23, OrganizationCard 12, Changelog 11, OrganizationsView 11
components/DsoExplorer 2 131 DsoExplorer 88 and QualityProfileTab 43. dsoService is mocked via vi.mock + vi.importActual so the pure URL/URN helpers stay real
components/FormEditor 3 58 @bpmn-io/form-js mocked โ€” exercising the real library would mean mounting a full canvas editor
components/RopaEditor 3 47 Record fields, legal-basis SPARQL lookup, hydrate-from-linked-forms, the BPMN ronl:ropaRef tab, confirm-gated status transitions, and the list nesting each subprocess record under the shell that calls it
src/App.test.tsx 1 33 App.tsx renders 11 feature components as inspectable stubs, isolating the orchestrator's own state machine. Includes the regression test for the error overlay, which is asserted on the view that raises it
components/common 4 26 Toolbar, language and organisation selectors
src/data 1 21 dsoAuthorities โ€” the authority list generated from the government organisations register: levels, OINs, and type-ahead-friendly names
vite/cspPlugin.test.ts 1 9 The build-time Content-Security-Policy generator, including the build failing on a missing or invalid API URL

These are the runner's own per-file counts, summed

Every row above was derived from Vitest's own JSON reporter, grouped by directory โ€” never from counting it( with grep, which miscounts every test.each, every commented-out case and every string containing the word. The rows therefore sum to exactly 1452, and their file column to 86. The backend table is the same, from Jest's --json report, and sums to 1966 across 72 files โ€” 1923 in the six directory areas, plus five files at the root of src/: the four fixture-integrity files (23) and the Besluitvorming bundle test (20).

Both tables were regenerated at v2026.10.2, from the JSON reports of the 10 October 2026 runs.

The previous revision of this table did not. Its rows summed to 573 against a stated total of 1020, because they had been gathered per area at different times rather than from one run, and the top-level src/components files were never a row at all. A per-area table that does not reconcile with the headline is worth less than no table, so this one is generated rather than maintained.

components/Tutorial was a row here until v2026.09.2, which removed the Tutorial view outright โ€” the component, its nine tests and the 596 lines of tutorial.json behind it.

The component strategy throughout is critical interactions, not exhaustive branch coverage: orchestrators mock their already-tested children as clickable stubs and assert on their own state machine.

Deliberately not covered

Documented rather than silently skipped:

  • bpmnTemplates.ts (0%) โ€” confirmed to be pure static XML template data.
  • The actual @dnd-kit pointer-drag gesture โ€” handleDragEnd's logic is exercised directly instead, by constructing DragEndEvent objects.
  • Three submit-guard branches, each sitting behind a button that is already disabled on the same condition. Reaching them means driving the component into a state the interface does not offer.

Two entries left this list. exportService.ts was 0% โ€” only 2 of its 6 functions are exported, the rest reachable only through real DOM manipulation and JSZip archive building โ€” and now reads 100% statements, 94.80% branches. DsoExplorer.tsx was the largest genuine gap at 72% statements and now sits at 94.40%, over 358 branches โ€” the most branching file in the frontend, and still 89.38% of them covered after v2026.09.6 grew it again.


The per-file branch floor

Since v2026.09.2 both runners enforce 80% branch coverage per file, natively: jest.config.js takes a glob key it applies to each matching file individually, vite.config.ts takes thresholds: { branches: 80, perFile: true }.

perFile is the mechanism, not a detail. Against a package average the threshold is inert โ€” the frontend sits at 92.71% and the backend at 92.23%, so a single file dropping to 40% would barely move either. Branches specifically, because statement and line coverage largely restate "was this file imported", while an uncovered branch is a decision no test has ever checked.

Branches only. A functions floor would fail today, and is not a companion setting to add without measuring first.

Every file meets it. At v2026.10.2, measured with the thresholds live, zero files are below 80% branches in either package โ€” 61 instrumented files in the backend, 96 in the frontend. Both runs exit 0 with the floor enforcing, which is the only claim worth making here: a threshold that is never exercised is a comment, not a gate.

The margin, and why it needed widening

The floor landed measured-clean but with none to spare: ChainBuilder/TestCasePanel.tsx sat at exactly 80.00%, with thirteen more files between 80 and 85. The first uncovered branch added to any of them would have turned CI red on an unrelated change โ€” which is how a floor stops being read as a floor and starts being read as an obstacle.

v2026.09.2 raised twelve files. Of the frontend files carrying at least one branch โ€” 85 of 96 at v2026.10.2 โ€” two are below 85%: GraphView.tsx at 82.25%, and BpmnModeler/BpmnCanvas.tsx at 84.03%, which v2026.10.2 took below 85% (see below). GraphView.tsx's eleven remaining uncovered branches are all inside d3's force-simulation tick and drag handlers โ€” d.x || 0 position fallbacks, if (!event.active) drag guards โ€” which need a running simulation and synthesised drag events to reach. That is a d3 harness, not a test of this component. The config comment says so, so that whoever next turns this red knows the answer is to test their new branch rather than lower the floor.

The backend is no longer the thinner margin. Of its 61 instrumented files, 55 carry a branch, and two sit between the floor and 85%: services/sparql.service.ts at 82.85% and services/dmn-validation.service.ts at 84.97%. utils/outboundHttp.ts, flagged here at v2026.09.5 as the likeliest place for the next uncovered branch to land, was given margin in v2026.09.6 and now reads 85.71%; utils/outboundUrl.ts sits just above it at 85.48%.

The per-file figures in this section are from the v2026.10.2 measurement of 10 October 2026, taken from the coverage/coverage-final.json each package's run wrote. The backend run printed no coverage table; the frontend's package totals, computed from its file, match the table that run printed. One row moved this release: BpmnCanvas.tsx, which gained the phase panel, the phase badges and the deploy dialog's phase findings, went from 85.87% of 177 branches to 84.03% of 238 โ€” 38 uncovered, and now the second-tightest file in the frontend. The new phase modules read between 88.88% (utils/phases/phaseGraph.ts, 36 branches) and 100% (phaseSet.ts, phaseChecks.ts), and none enters the table. No backend row moved: services/assets.service.ts, the backend file the release changed most, reads 96.07% of 102.

The tightest files in each package, for anyone about to add a branch to one:

Package File Branches Of
backend services/sparql.service.ts 82.85% 70
backend services/dmn-validation.service.ts 84.97% 233
backend routes/dso.routes.ts 85.04% 107
backend utils/outboundUrl.ts 85.48% 62
backend utils/outboundHttp.ts 85.71% 21
frontend components/GraphView.tsx 82.25% 62
frontend BpmnModeler/BpmnCanvas.tsx 84.03% 238
frontend DocumentComposer/AssetLibrary.tsx 85.71% 21
frontend DocumentComposer/DocumentComposer.tsx 86.25% 80
frontend BpmnModeler/DmnTemplateSelector.tsx 86.66% 45

Note the branch counts beside the percentages. AssetLibrary.tsx at 85.71% of 21 branches is three uncovered decisions and one new if away from red; dmn-validation.service.ts at 84.97% of 233 has thirty-five uncovered and far more room to absorb a change. A percentage alone does not tell you which files are actually fragile.

Every new test was mutation-checked

A test written against code that already exists passes on its first run, which proves nothing about whether it can fail. Each of the 31 tests written for the floor had the branch it targets deliberately broken and had to fail before it was kept.

That caught five tests asserting nothing. Four share a shape worth knowing on any React codebase: an assertion that "nothing happened" stays true when the handler throws partway through, because React surfaces an error thrown inside a click handler on window's error event rather than rejecting the click. The test sees no state change and passes โ€” for the wrong reason. See Raising coverage without writing hollow tests.


When the frontend suite fails only in parallel

Vitest runs test files in parallel by default, and on a loaded machine that can produce failures that are not defects. This happened during the v2026.09.6 measurement and is documented here because the diagnosis is reusable, not because anything was wrong with the code.

What was seen. The first npm test -w packages/frontend run reported Test Files 1 failed | 74 passed (75) and Tests 2 failed | 1257 passed (1259). Both failures were in components/DsoExplorer/DsoExplorer.test.tsx, both with the same message, Error: Test timed out in 5000ms:

  • Authority option labels carry no level prefix, so native type-ahead works on the short name
  • switching Activities โ†’ Quality Profile โ†’ Activities preserves the selected authority and its filtered list

What establishes it as contention. A failure inside a full parallel run is not a finding until it has been reproduced on its own, so three runs were done before any conclusion was drawn โ€” and the failing run itself took 201.5 s, more than three times its normal 64.6:

Run Result
The file alone โ€” npx vitest run src/components/DsoExplorer/DsoExplorer.test.tsx 88 passed, 0 failed, 64.55 s. The two named tests took 1632 ms and 676 ms
The whole suite serially โ€” npx vitest run --no-file-parallelism --coverage 75 files, 1259 tests, all passed, 375.0 s
The same parallel command again, unchanged 75 files, 1259 tests, all passed, 64.58 s

(The first and third rows landing within 0.03 s of each other is coincidence, not a transcription slip โ€” one file under no contention happens to cost about what all seventy-five cost when the pool is healthy.)

Two tests that need 1.6 s and 0.7 s do not fail a 5000 ms budget because of anything in the component. And the retry passing outright settles it: this was not even a reproducible ordering problem, it was one starved run.

The diagnostic that makes it legible is in Vitest's own summary line. Compare the environment figure across the three runs:

Run environment
The failing parallel run 862 s
Serial 121 s
The single file alone 2.95 s

862 seconds spent constructing test environments, against 121 for the same work done one file at a time, is a worker pool contending for a machine that had nothing left to give. When environment setup alone eats that much wall clock, a 5000 ms findBy* budget that normally resolves in 1.6 s expires โ€” and the test that reports the timeout is simply whichever one was waiting when the machine stalled, not the one at fault.

Do not answer this with a standing --no-file-parallelism

Serial execution makes the symptom go away and costs more than it saves. It is 5.8ร— slower here โ€” 375 s against 64.6 s โ€” on every run, forever. It diverges local runs from CI, where the Azure workflows still run the suite in parallel, so the configuration that is green locally is not the one being gated. And flakiness under parallelism is usually a real defect โ€” shared module state, an unisolated temp directory, a port collision โ€” so switching it off converts a signal into silence.

--no-file-parallelism belongs in the diagnosis, as above, and not in a config file.

If it recurs, the fix belongs at that file's own boundary: a per-file testTimeout on DsoExplorer.test.tsx with a comment naming the contention it is sized to survive, so the next reader knows the number is deliberate rather than arbitrary. Not a global timeout, and not a global parallelism setting.

The v2026.09.6 figures were the clean runs. Its 75 files and 1259 tests were confirmed by both the serial run and the second parallel run, which agree exactly; the 64.6 s quoted above was the parallel run's time, since parallel is what npm test and CI actually do. The v2026.10.2 figures at the top of this page come from a parallel run in which all 1452 passed.


Defects the tests found

Six real problems surfaced from writing tests rather than from use. The two at the end are the reverse case โ€” one a test locked in, and one the suite was well placed to catch and did not.

tsconfig.eslint.json excluded every test file from linting. It extended tsconfig.json without overriding its exclude of **/*.test.ts, so ESLint's type-aware parser could not see a single test file. Latent only because the repository had no test files until P0.

.gitignore's blanket *.js rule blocked jest.config.js. The rule existed for compiled TypeScript output, but silently prevented the hand-authored Jest config from ever being tracked.

Ten CPRMV validation rules were dead code. The backend coverage campaign in v2026.08.2 documented โ€” as a testing-scope decision, then fixed two commits later โ€” that cprmvAttr() called libxmljs2's attribute setter rather than a namespaced getter, threw, and had its throw swallowed by its own catch. EXEC-002โ€“EXEC-010 and CON-001โ€“CON-003 had therefore never fired for any DMN while the validator reported clean results. See DMN Validation Reference.

assetService.ts had a cross-test-polluting module-level cache, found in P5 and fixed by giving each test a distinct dataset name.

The last two were first written up as documented quirks โ€” pre-existing, unrelated to the test that found them, and left alone on purpose. Both were fixed on 20 August 2026 once the CI gate made an unexplained red run expensive.

The error overlay was unreachable from the Orchestration view. It lived inside App's right panel, which is itself hidden whenever viewMode is Orchestration โ€” so a failed cache refresh, triggerable only from that view, set the error state correctly and had nowhere to render until the user happened to navigate elsewhere. Documented as a quirk when first found, fixed on 20 August 2026 by lifting the overlay to be a direct child of the workspace container, so it renders in every view. App.test.tsx now asserts the error appears on the view that caused it, and is dismissible there.

The deploy modal's process key was always the literal string "process". doc.querySelector('process') is a CSS type selector, which matches only the null namespace and so never found the <bpmn:process> element that real bpmn-js output always emits; BpmnCanvas fell through to its own fallback every time. Sub-process lookups by calledElement failed the same way. Also documented as a jsdom quirk when first found โ€” the fixture behind that reading declared none of the prefixes it used, so DOMParser rejected it outright and returned a <parsererror> document in which nothing was findable under any lookup, masking the real defect underneath. Fixed on 20 August 2026 with a findProcessElement helper matching on local name across namespaces, and the fixture made well-formed.

The one a test locked in

The SHACL validator passed every file it had not checked, and a unit test required it to. Production reported every file Valid ยท All checks passed with all three shape layers Not loaded, while acceptance found the same file invalid with 25 errors. The production deploy workflow had never copied the shapes into the package, and the service computed valid from errors alone โ€” so a layer that never loaded, and therefore reported no errors, counted as a pass. A test asserted exactly that: missing shapes, zero errors, valid: true. It was green for the wrong reason, and it would have stayed green through any fix that kept the behaviour. v2026.09.5 made valid require every layer to have loaded, and the test now asserts the opposite.

A test that pins today's behaviour is worth only as much as the behaviour. Before locking one in, ask what the output should be when the input is missing โ€” not what it happens to be.

The one the tests missed

Four of the five E2E fixture BPMNs were undeployable for weeks. BPMN 2.0's tProcess is an ordered sequence โ€” laneSet*, flowElement*, artifact*, resourceRole*, correlationSubscription*, supports* โ€” so once an artifact appears, no further flow element may follow it. The commit that added the on-canvas "E2E FIXTURE" warning inserted its textAnnotation and association directly after the first flow element in each file, and Operaton's XSD validation rejects the whole deployment on sight:

cvc-complex-type.2.4.a: Invalid content was found starting with element
'{http://www.omg.org/spec/BPMN/20100524/MODEL}scriptTask'. One of
'{โ€ฆ}artifact, {โ€ฆ}resourceRole, {โ€ฆ}correlationSubscription, {โ€ฆ}supports'
is expected.

TreeFellingPermitSubProcessE2E was the one file with its banner correctly at the end of the process, and the only one that deployed.

This is worth recording precisely because the suite had every opportunity. The manifest integrity test read each BPMN, checked that the file existed, that its process id matched the declared processDefinitionKey, and that a shell's calledElement references resolved to its nested sub-processes โ€” but never that the document would survive the validator it was written for. These fixtures are hand-edited and never round-trip through bpmn-js, which would have re-serialised them into schema order and silently repaired the mistake, so nothing else stood between the edit and a failed deploy.

The fix was a pure move of each banner to just before </bpmn:process>; all five now validate against bpmn-moddle's BPMN20.xsd. The integrity test gained a fifth case asserting the ordering rule directly, confirmed failing against the broken fixtures before it was allowed to pass.


There is no browser suite in this repository

Stated as established rather than assumed, because the directory name invites the opposite guess. Three checks at 82f461f:

  • No dependency. Neither package.json nor any config file in the repository mentions Playwright, under any casing. The lockfile names @vitest/browser-playwright only among Vitest's optional peers, and does not install it.
  • No lockfile entry. package-lock.json contains no node_modules/@playwright, node_modules/playwright or node_modules/cypress package โ€” so nothing is installed even transitively.
  • No runner script. Neither workspace defines an e2e entry point. test is jest --coverage in the backend and vitest run --coverage in the frontend, and that is the whole surface. The root script npm run e2e:deploy-fixtures (scripts/deploy-e2e-fixtures.mjs), new in v2026.10.0, is a deploy helper, not a runner: it puts the e2e-fixtures/ bundle on the local Operaton through the local LDE backend โ€” the shared decisions without a tenant, then each manifest process with its sub-processes, forms and documents under its tenant โ€” and refuses an LDE backend whose Operaton is not on this machine. It moved here from the RONL Business API, which keeps a script of the same name that calls this one.

e2e-fixtures/ is not a test suite. It holds 70 files โ€” 38 .form, 12 .document, 10 .bpmn, 9 .dmn and one manifest.json โ€” and no spec of any kind; a search for a filename containing spec or test under it returns nothing. The directory name describes what the fixtures represent, a deployable end-to-end bundle, not a runner that lives here.

What consumes them is four backend Jest tests that parse and cross-check the bundle as data โ€” e2e-fixtures.test.ts, e2e-fixture-decisions.test.ts, example-fixture-parity.test.ts and public-example-fixture-parity.test.ts, 23 tests between them. The bundle is executed by the RONL Business API's E2E suite, in that repository. This one is their source of truth, with a manifest and integrity tests; it is not their runner. See The one the tests missed for what that division of labour cost once.


What this page does not measure

Three gaps, named so that nobody reads a figure here as covering them:

The backend under parallel workers. The backend was measured serially, with --runInBand, so its 60.5 s says nothing about how long npm test or the CI step takes with Jest's default workers, and cannot be set against the parallel backend times earlier releases quoted. The counts and coverage are unaffected. The measurement ran on Node 24.21.0, the version .nvmrc pins.

Per-test-file coverage attribution on the frontend. Vitest's v8 coverage is whole-run: it reports what the suite as a whole covered, not what each test file contributed. Where the v2026.09.6 table near the top of this page pairs a new test file with a coverage figure, it is pairing a file with its obvious subject, which is an editorial judgement, not a measurement. Two test files touching the same module cannot be told apart by these numbers.

Deltas before v2026.10.1. The v2026.10.2 delta at the top compares two runner reports, both kept: the 6 October run at v2026.10.1 and the 10 October run. v2026.10.1's compared the 3 October run at v2026.10.0 with the 6 October run. Earlier releases' deltas, v2026.10.0's +2 files and +40 tests among them, were arithmetic against the previously published figures of the release before, not a fresh measurement of the older commit. If those figures were wrong, the deltas inherit the error.

The three CI-only harnesses' checks โ€” 77, 7 and 24 โ€” are the figures on this page that no run produced: they execute only in CI and were counted from their source.


Adding tests

  • Colocate โ€” foo.ts โ†’ foo.test.ts, beside the source.
  • Backend: mock the service layer and mount routes in isolation rather than booting index.ts. Reach for jest.isolateModules when a module captures state at import time.
  • Frontend: default to the node environment and opt into jsdom per file with // @vitest-environment jsdom. Use msw for network-calling services; mock heavyweight editor libraries outright, but prefer the real library when it works under jsdom with a small polyfill, as ProseMirror does.
  • Scope components to critical interactions. Mock already-tested children as stubs and test the orchestrator's own state machine.
  • Document what you skip. Every deliberate omission above is written down with its reason; silent gaps are what this suite was built to remove.
  • Update the figures on this page when a phase lands, from a real run rather than an estimate.

Roadmap

Close the remaining frontend gaps. Both items that stood here are done โ€” DsoExplorer.tsx went from 72% to 94.40% statements, and exportService.ts got its DOM/JSZip harness and reads 100%. What is left is GraphView.tsx at 82.25% branches, which is a deliberate hold rather than a gap, and BpmnCanvas.tsx, which v2026.10.2 took to 84.03% of a larger branch count: see The per-file branch floor.

Put the dossier harness in CI. The root has a script for two of the five harnesses, npm run test:scripts; the promotion, lockfile, SBOM and dependency-review harnesses run in CI, and since v2026.10.0 test:scripts runs on Windows too. One thing remains: scripts/dso-dossier.test.mjs still runs in no workflow and no hook, so its 24 checks protect the dossier renderer only as long as someone remembers. None of the five harnesses has been ported to node:test. See Script harnesses outside npm test.

Deliberately out of scope for now: visual regression, a cross-browser matrix, and E2E in this repository โ€” see There is no browser suite in this repository for what does and does not live under e2e-fixtures/.