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) androutes/assets.routes.test.ts+3 (now 85) pin one stored process per organisation and process id: the409 PROCESS_ID_TAKENrefusal, 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; andbesluitvorming-gedelegeerd-bundle.test.ts+3 (now 20) the bestuur besluit document.services/assets.service.tswent from 98.68% of 76 branches to 96.07% of 102, anddb/migrate.tsfrom 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) andphaseChecks.test.ts(11), andBpmnModeler/ProcessPhasesEditor.test.tsx(24) andPhaseMarkerSelector.test.tsx(12). The other two areutils/processIdentity.test.ts(13), for renaming the process id of an import, andutils/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; andRopaEditor/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:
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 viajest.isolateModuleswith 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.tsfiles with a single static mock.jest.doMockinsideisolateModulesAsyncdid not reliably override a file's top-leveljest.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
catchblocks 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.cjssetsALLOW_LOCAL_ENDPOINTS=falseandTRIPLYDB_ALLOWED_HOSTSbefore 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-kitpointer-drag gesture โhandleDragEnd's logic is exercised directly instead, by constructingDragEndEventobjects. - 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.jsonnor any config file in the repository mentions Playwright, under any casing. The lockfile names@vitest/browser-playwrightonly among Vitest's optional peers, and does not install it. - No lockfile entry.
package-lock.jsoncontains nonode_modules/@playwright,node_modules/playwrightornode_modules/cypresspackage โ so nothing is installed even transitively. - No runner script. Neither workspace defines an e2e entry point.
testisjest --coveragein the backend andvitest run --coveragein the frontend, and that is the whole surface. The root scriptnpm run e2e:deploy-fixtures(scripts/deploy-e2e-fixtures.mjs), new in v2026.10.0, is a deploy helper, not a runner: it puts thee2e-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 forjest.isolateModuleswhen a module captures state at import time. - Frontend: default to the
nodeenvironment and opt into jsdom per file with// @vitest-environment jsdom. Usemswfor 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/.