Generates Playwright integration test suites from the Camunda REST API OpenAPI specification.
Analyses the spec's semantic type annotations (x-semantic-type) to build an operation
dependency graph, then emits scenario-driven test files with full request/response synthesis.
Also emits negative request-validation tests covering 35 distinct scenario kinds. Most send a
malformed request and expect a refusal (HTTP 400, or 401, 403 or 404 for the auth and not-found kinds);
a few send an edge case the server accepts (a pagination offset past the total, a search filter that is
never validated) and expect HTTP 200.
Hub team? Start with How it works. It is one page: what runs, what you will see in Slack and on your PRs, what is yours to act on, and who owns what. The guides it links to are for when you need detail: PR check red or adding an endpoint · a message in the nightly channel · the weekly coverage report and the fix agent.
Questions or help: ask in
#ask-qaand tag@test-automation-medic.Everything below is for people changing the generator itself. You do not need it to run or read the Hub checks.
┌──────────────────────────┐
│ camunda-schema-bundler │ Fetches & bundles the upstream multi-file OpenAPI spec
│ (devDependency) │ → spec/<config>/bundled/rest-api.bundle.json
└────────────┬─────────────┘
│
├──────────────────────────────────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ semantic-graph-extractor │ │ request-validation │
│ │ │ (negative-test generator)│
│ Parses bundled spec, │ │ │
│ extracts semantic types │ │ Synthesizes 35 kinds of │
│ & operations │ │ negative scenario tests │
│ → operation-dependency- │ │ expecting 4xx (some 200) │
│ graph.json │ │ → generated/<config>/ │
└────────────┬─────────────┘ │ request-validation/ │
│ │ *.spec.ts │
▼ └──────────────────────────┘
┌──────────────────────────┐
│ path-analyser │ Reads graph + spec, plans positive scenarios
│ │ per endpoint
│ │ → generated/<config>/feature-output/*.json
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ materializer │ Reads planned scenarios, emits self-contained
│ │ Playwright suites (other targets pluggable)
│ │ → generated/<config>/playwright/*.spec.ts
└──────────────────────────┘
- Node.js ≥ 22
- npm ≥ 10 (ships with Node 22+)
- Camunda 8 server — either via
c8ctl(recommended) or Docker Compose
Option A — c8ctl (recommended)
c8ctl is the Camunda 8 CLI. It manages a local cluster lifecycle for you and
is the fastest path from clone to a running server:
# One-off install (Node ≥ 22) — the published package is @camunda8/cli, which installs the `c8ctl` binary
npm install -g @camunda8/cli
# Start a cluster pinned to a specific Camunda version
c8ctl cluster start 8.9
# Stop it when you're done
c8ctl cluster stopThe Camunda REST API will be available at http://localhost:8080.
If you prefer to run Compose directly:
cd docker
docker compose up -d
docker compose logs camunda # health check
docker compose down # stopOverride the REST port with CAMUNDA_REST_PORT:
CAMUNDA_REST_PORT=9080 docker compose up -dFirst-time setup required — see
LOCAL_SETUP_NOTES.mdin the camunda-hub repo. Expectscamunda-hubto be cloned as a sibling directory alongside this repo.
# Start (Docker infrastructure + restapi + frontend)
./docker/start-hub.sh
# Stop
./docker/start-hub.sh stopThe Hub UI will be available at http://localhost:${HUB_UI_PORT:-8088}. Log in with demo / demo.
# Generate hub request-validation tests
CONFIG=camunda-hub npm run generate:request-validation
# Get an OAuth token from Keycloak (hub uses Bearer auth, not Basic)
TOKEN=$(curl -s -X POST "http://localhost:${KEYCLOAK_PORT:-18080}/auth/realms/camunda-platform/protocol/openid-connect/token" \
-d "client_id=c8-client&client_secret=c8-secret&grant_type=client_credentials" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])")
# Run the generated tests
# Unset Basic auth vars — if set, they take precedence over BEARER_TOKEN and hub tests will fail
unset CAMUNDA_BASIC_AUTH_USER CAMUNDA_BASIC_AUTH_PASSWORD
BEARER_TOKEN=$TOKEN \
CORE_APPLICATION_URL=http://localhost:${HUB_UI_PORT:-8088}/api \
CONFIG=camunda-hub npm run test:pw:request-validationThe default profile runs the 400s. To run the 401 (secured) or 403 (rbac) negatives, set RV_PROFILE:
# 401 — auth-absent / auth-invalid. The secured profile also includes 400
# body-validation scenarios, which require admin auth to get past authentication.
# BEARER_TOKEN is therefore still needed; the auth-absent/invalid tests send
# no credentials / a bad one on their own.
BEARER_TOKEN=$TOKEN \
RV_PROFILE=secured \
CORE_APPLICATION_URL=http://localhost:${HUB_UI_PORT:-8088}/api \
CONFIG=camunda-hub npm run test:pw:request-validation
# 403 — auth-deny. Mint a token from the reduced-permission deny client and pass it
# as RBAC_DENY_PROBE_BEARER_TOKEN. It authenticates (audience mapper added by
# start-hub.sh) but holds no public-api authority, so keyless, no-required-body,
# no-required-non-path-param secured ops return 403 (by-key, required-body, and
# required-query/header/cookie ops are excluded — Hub checks 400/404 before the
# authority gate).
DENY_TOKEN=$(curl -s -X POST "http://localhost:${KEYCLOAK_PORT:-18080}/auth/realms/camunda-platform/protocol/openid-connect/token" \
-d "client_id=c8-client-deny&client_secret=c8-deny-secret&grant_type=client_credentials" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])")
RV_PROFILE=rbac \
RBAC_DENY_PROBE_BEARER_TOKEN=$DENY_TOKEN \
CORE_APPLICATION_URL=http://localhost:${HUB_UI_PORT:-8088}/api \
CONFIG=camunda-hub npm run test:pw:request-validationThe Hub server must be running with
CAMUNDA_MODELER_FEATURE_PUBLIC_API_V2_ENABLED=true— set this incamunda-hub/restapi/config/config-common/src/main/resources/application-common-local.ymlbefore starting.
scripts/e2e/ wraps the whole flow into one command per config — generate both
suites, run them with Playwright, then re-issue each request-validation test with
an independent curl oracle that compares expected vs Playwright vs curl
status (printing the curl response body on a mismatch):
./scripts/e2e/run-hub.sh # Hub: mints Keycloak Bearer tokens; base http://localhost:8088/api
./scripts/e2e/run-oca.sh # OCA: HTTP Basic (demo/demo); base http://localhost:8080Both are env-configurable (no edits needed):
STEPS="generate run curl"— pick which steps to run (e.g.STEPS=curlto re-curl only).RV_PROFILES="secured rbac"(Hub) /"unsecured"(OCA) — which request-validation profiles.SKIP_POSITIVE=1— skip the positive lifecycle suite (runs by default).E2E_SOFT=1— don't exit non-zero when the oracle finds mismatches.
Output lands in test-results/e2e-<config>/: the Playwright report (pw-<profile>.json)
and the curl-compare report as both text (curl-compare-<profile>.txt) and a
self-contained, color-coded HTML view (curl-compare-<profile>.html, with a
"show only mismatches" toggle and expandable request/response detail). The positive
suite's base URL is baked in per config (configs/<config>/codegen/playwright/config.json),
so API_BASE_URL only needs setting to override it.
# Install all workspace dependencies (runs in all sub-packages)
npm install
# Fetch the upstream OpenAPI spec and bundle it
npm run fetch-spec
# Run the positive pipeline: extract graph → generate scenarios → emit Playwright tests
npm run pipeline
# Generate the negative request-validation suite (HTTP 400 tests, all supported scenario kinds)
npm run generate:request-validation
# Run the generated tests against a local OCA instance
# (omit CAMUNDA_BASIC_AUTH_* if your instance has no auth enabled)
CORE_APPLICATION_URL=http://localhost:8080 \
CAMUNDA_BASIC_AUTH_USER=demo \
CAMUNDA_BASIC_AUTH_PASSWORD=demo \
npm run test:pw:request-validation # negative request-validation only
npm run test:pw # both suites (path-analyser + request-validation)
npm run test:pw:path-analyser # positive scenarios onlyThis is an npm workspaces monorepo with five packages:
api-test-generator/
├── package.json ← root workspace orchestrator
├── spec/ ← (gitignored) bundled OpenAPI spec output
│ └── <config>/bundled/
│ ├── rest-api.bundle.json
│ └── spec-metadata.json
├── semantic-graph-extractor/ ← workspace: graph extraction from OpenAPI
├── path-analyser/ ← workspace: positive scenario planner (writes JSON only)
├── materializer/ ← workspace: emits runnable Playwright suites from planned scenarios
├── request-validation/ ← workspace: negative request-validation test generator (HTTP 400)
└── optional-responses/ ← workspace: optional response field analyser
All five sub-packages are registered as npm workspaces.
A single npm install at the root installs dependencies for every package.
# Add a dependency to a specific workspace
npm install <pkg> -w semantic-graph-extractor
# Add a dev dependency to a specific workspace
npm install -D <pkg> -w path-analyser
# Add a dependency to the root
npm install -D <pkg> -w .
# Remove a dependency
npm uninstall <pkg> -w semantic-graph-extractor# Run a script in a specific workspace
npm run build -w semantic-graph-extractor
npm run build -w path-analyser
# Run a script in all workspaces that define it
npm run build --workspaces --if-present| Script | Description |
|---|---|
npm run fetch-spec |
Fetch and bundle the upstream OpenAPI spec (from main branch) |
npm run fetch-spec:ref |
Fetch a specific branch/tag: SPEC_REF=stable/8.8 npm run fetch-spec:ref |
npm run bump-spec-pin |
Re-pin a config's spec: npm run bump-spec-pin -- --config <name> [--ref <sha|branch|tag>] [--dry-run] (see Spec pin → Bumping the spec pin) |
npm run extract-graph |
Build the semantic graph extractor and extract the dependency graph |
npm run generate:scenarios |
Build the path analyser and generate scenario JSON files |
npm run codegen:playwright |
Build and emit a Playwright test for a single endpoint |
npm run codegen:playwright:all |
Build and emit Playwright tests for all endpoints |
npm run build:request-validation |
Build the request-validation generator |
npm run generate:request-validation |
Emit negative request-validation tests with all supported scenario kinds (deep coverage by default) |
npm run generate:request-validation:shallow |
Emit only the core kinds (missing-required, type-mismatch, union) — fast iteration |
npm run test:pw |
Run both generated Playwright suites (path-analyser + request-validation) |
npm run test:pw:path-analyser |
Run only the positive path-analyser suite |
npm run test:pw:request-validation |
Run only the negative request-validation suite |
npm run testsuite:generate |
Full positive-generation pipeline: extract graph → scenarios → Playwright tests |
npm run testsuite:observe:run |
Generate tests, run them, and aggregate runtime observations |
npm run observe:aggregate |
Aggregate runtime observation data |
npm run optional-responses |
Run the optional response field analyser |
npm run pipeline |
End-to-end: fetch spec + generate entire test suite |
npm run lint |
Lint all workspaces with Biome |
npm run lint:fix |
Lint and apply safe Biome fixes |
npm run format |
Format all workspaces with Biome |
npm test |
Run the regression test suite (extractor + planner fixtures, bundled-spec invariants) |
npm run build:ontology |
Regenerate ontology/vocabulary/*.schema.json from their TypeScript source of truth. Run whenever you edit a schema const under path-analyser/src/ontology/ (e.g. edgeSchema.ts). A drift-detector invariant fails if the committed JSON is stale. |
This repo uses Biome for both linting and formatting,
configured at the root in biome.json. The recommended ruleset is enabled
with three additional escalations to error:
suspicious/noExplicitAnysuspicious/noImplicitAnyLetsuspicious/noEvolvingTypes
A custom GritQL plugin (plugins/no-unsafe-type-assertion.grit) bans as T
type assertions outside of imports and as const. Use type guards,
narrowing, or satisfies instead — and only suppress with
// biome-ignore lint/plugin: <reason> when genuinely unavoidable.
The pipeline emits hundreds of generated files (semantic graph, scenario JSON, Playwright tests, validation tests). The regression strategy is layered (see #36):
- Layer 1 — extractor construct fixtures (tests/fixtures/extractor/).
Hand-curated minimal OpenAPI snippets paired with property assertions.
Each
itblock is one regression statement; failures point at one construct, not at hundreds of hashed files. - Layer 2 — planner contract fixtures (tests/fixtures/planner/).
Tiny dependency-graph fixtures paired with chain-shape assertions on
generateScenariosForEndpoint. - Layer 3 — bundled-spec invariants (configs/camunda-oca/regression-invariants.test.ts).
Named, human-readable invariants over the real bundled spec output.
Per-config (#128 PR 3): each named config under
configs/<name>/owns its ownregression-invariants.test.ts; vitest'sdescribe.skipIfscopes each file to its config so a CI matrix leg only runs the invariants for the active CONFIG.
There is no Layer 4 end-to-end snapshot. The previous SHA-256 manifest guard was retired in favour of the layered strategy: a 412-file diff is not a useful signal, whereas a fixture or invariant failure names the broken property directly.
Standing rule: every bug fix to the extractor or planner should land with (a) a fixture demonstrating the bug BEFORE the fix, and (b) an invariant if the property is observable at the chain or graph level. See CONTRIBUTING.md.
Generator output is byte-reproducible by default. The seeding module
(materializer/src/playwright/support/seeding.ts) uses TEST_SEED to seed all
deterministicSuffix(...) calls; if unset, it falls back to the constant
'snapshot-baseline', so npm run pipeline produces identical output across
runs and machines without needing TEST_SEED to be set explicitly.
To opt out (for example, when generating a one-off suite for live-broker exploration where unique-per-run identifiers are useful), set:
TEST_SEED=random npm run pipelineAny other non-empty value is treated as a custom deterministic seed.
The bundled-spec invariants test the real upstream spec output, so it is
only meaningful against a fixed upstream spec content.
configs/camunda-oca/spec-pin.json records
the expectedSpecHash plus the specRef CI fetches. A vitest
globalSetup (tests/regression/spec-pin.setup.ts)
aborts the entire run with a single actionable error if the bundled spec
drifts from that hash, so reviewers don't have to debug a confusing
invariant failure when the real cause is upstream drift.
Each config has its own pin at configs/<config>/spec-pin.json.
Use the bump-spec-pin script — it resolves the target ref, fetches + bundles
the spec, and rewrites configs/<config>/spec-pin.json (specRef as a resolved
40-char commit SHA + the new expectedSpecHash, preserving the $comment). It
does not commit — you review the diff, verify, then commit.
# camunda-oca — public, fetched from camunda/camunda. Defaults to the repo's
# default-branch tip (resolved via `git ls-remote <repo> HEAD`); pass
# --ref <sha|branch|tag> for a specific one (resolved to a SHA).
npm run bump-spec-pin -- --config camunda-oca
npm run bump-spec-pin -- --config camunda-oca --ref stable/8.10
# camunda-hub — private, bundled in "local mode" from the sibling clone
# ../camunda-hub (SPEC_REF is ignored). Bumps to that clone's checked-out HEAD;
# pass --ref <sha|branch|tag> to check that out first (the clone is restored
# afterwards). Requires camunda-hub cloned as a sibling:
# <parent>/{api-test-generator, camunda-hub}.
npm run bump-spec-pin -- --config camunda-hub
# Preview any bump without writing:
npm run bump-spec-pin -- --config <name> --dry-runDrop --dry-run to write the pin.
Then verify the new spec flows through cleanly, update any invariants whose
values legitimately changed, and commit spec-pin.json + the invariant updates
together:
CONFIG=<name> npm run testsuite:generate \
&& CONFIG=<name> npm run generate:request-validation \
&& CONFIG=<name> npm testspecRef is always a resolved commit SHA (branches drift). For camunda-oca that's a
camunda/camundaSHA; for camunda-hub, acamunda/camunda-hubSHA. See AGENTS.md → Spec pin for the OCA (network-fetch) vs hub (local-bundle) modes in detail.
.github/workflows/ci.yml runs on every PR to main (and on pushes to
main). It executes:
npm run lint— Biometsc --noEmitagainst each workspace tsconfig- Builds for downstream consumers (
build:analyser,build -w @camunda8/emitter-sdk) — needed so the materializer typecheck can resolve.d.tsfor the subpathexports npm run fetch-spec:refat the pinnedspecRef- Full pipeline regeneration with
TEST_SEED=snapshot-baseline npm test— spec-pin guard, layered regression, and unit tests
On failure the generated outputs are uploaded as the
pipeline-outputs artifact for inspection.
The bundled spec is produced by camunda-schema-bundler,
installed as a dev dependency. It fetches the multi-file upstream spec from
camunda/camunda and produces a single
normalised JSON file.
# Fetch from main (default)
npm run fetch-spec
# Fetch a specific branch or tag
SPEC_REF=stable/8.9 npm run fetch-spec:ref
# Or use the CLI directly for more options
npx camunda-schema-bundler --helpThe output lands in spec/<config>/bundled/ (gitignored).
All spec-reading code supports the OPENAPI_SPEC_PATH environment variable:
OPENAPI_SPEC_PATH=/path/to/my-spec.yaml npm run testsuite:generateAnalyses the OpenAPI spec and builds an operation dependency graph based on
x-semantic-type annotations. The graph captures which operations produce and
consume which semantic types, enabling dependency-aware test ordering.
npm run build -w semantic-graph-extractor
npm run extract-graph # build + extract
npm run analyze-graph -w semantic-graph-extractor # human-readable analysis reportReads the dependency graph and the OpenAPI spec to plan positive
scenarios for every endpoint — happy paths, oneOf variant selection,
dependency chaining, response-shape assertions, and artifact deployment
coverage. Writes scenario JSON to generated/<config>/feature-output/.
Emission of runnable suites is handled downstream by
materializer. Negative-request scenarios (missing
required fields, wrong types, etc.) are owned exclusively by
request-validation.
npm run build -w path-analyser
npm run generate:scenarios # build + plan scenarios into JSONSee path-analyser/README.md for the public subpath-exports surface consumed by the materializer.
Reads planned scenarios from generated/<config>/feature-output/ and
emits a self-contained, runnable Playwright project under
generated/<config>/playwright/ — *.feature.spec.ts files plus
package.json, playwright.config.ts, tsconfig.json, vendored
runtime helpers (support/), fixtures, and a README.
Template-derived suites (#268 Phase 2 / #270) land under
generated/<config>/playwright/templates/EdgeLifecycle/<EdgeName>.lifecycle.spec.ts —
one per ABox-declared edge. Each suite runs the full lifecycle
(establish → present-observe → revoke → absent-observe) and is sourced
from generated/<config>/scenarios/templates/EdgeLifecycle/<EdgeName>.json,
which the planner instantiates from the EdgeLifecycle TBox template
(see ontology/README.md).
npm run codegen:playwright -- <operationId>
npm run codegen:playwright:all
npm run test:pw # run the generated tests
npm run observe:aggregate # aggregate runtime observationsSuite generation is layered behind a small Emitter strategy
interface (materializer/src/emitter.ts). The CLI selects an emitter
via --target=<id> and falls back to playwright when omitted:
tsx materializer/src/index.ts --target=playwright createWidget
tsx materializer/src/index.ts --target=playwright --allThe codegen scripts (npm run codegen:playwright, npm run codegen:playwright:all) invoke the materializer via tsx so config-side role hooks (configs/<config>/codegen/playwright/roles/<role>/hook.ts, Lift 19 / #261) load transparently alongside the materializer source.
The current built-in is playwright. Additional targets (e.g. SDK-based
suites — see #8)
register themselves through registerEmitter() and are listed in
--help. The emitter contract is experimental and is being formalised
in #233.
See materializer/README.md for the workspace boundary, role-bundle layout, and emitter contract details.
A spec-driven generator that synthesizes negative Playwright tests targeting
request-validation surfaces. Most tests send a deliberately malformed request and assert
the server refuses it (HTTP 400, or 401, 403 or 404 for the auth and not-found kinds); a few send
an edge case the server accepts and assert HTTP 200. Covers 35 scenario kinds
including missing required fields (single + combinations), wrong primitive
types, root-body type mismatches, oneOf ambiguity / no-match / cross-bleed,
discriminator mismatches, enum / format / multipleOf / uniqueItems /
length / pattern violations, allOf conflicts, additional-property rejection
and multipart-only adaptation.
npm run build:request-validation # compile the generator
npm run generate:request-validation # all supported scenario kinds (deep coverage by default)
npm run generate:request-validation:shallow # only missing-required, type-mismatch, unionOutput lands in generated/<config>/request-validation/:
| File | Description |
|---|---|
<resource>-validation-api-tests.spec.ts |
Playwright specs grouped by resource |
MANIFEST.json |
Global counts per scenario kind + generation options |
COVERAGE.json / COVERAGE.md |
Per-operation coverage matrix and missing-kind list |
The generator consumes the bundled spec produced by npm run fetch-spec. To
point it at a different OpenAPI document, set REQUEST_VALIDATION_SPEC to the
absolute or repo-relative path.
A lightweight utility that scans the OpenAPI spec for response schemas with optional fields — useful for understanding which response fields may be absent.
npm run optional-responsesThese live under configs/<active-config>/:
| File | Purpose |
|---|---|
domain-semantics.json |
Domain-level semantic requirements, runtime states, and value bindings |
filter-providers.json |
Maps fields to value providers (ctx, const, enumFirst, etc.) |
request-defaults.json |
Default values for request body fields per operation |
fixtures/ |
BPMN, DMN, and form files used by deployment tests, plus deployment-artifacts.json registry |
codegen/playwright/roles/<role>/ |
Per-role bundles consumed by the materializer (call-site.tmpl, optional imports.tmpl, support.<ext> or support.<ext>.tmpl, match.json) |
The table above lists some of the inputs a config can have (the files differ per config, and it is not a complete list for any of them). For camunda-hub the main ones under configs/camunda-hub/ are positive-suppress.json, request-validation.json, conflict-replay.json, search-paging.json, optional-fields.json, coverage-floors.json, spec-pin.json, regression-invariants.test.ts, ontology/, fixtures/ and codegen/. What each does is in the weekly coverage guide and the PR-check cookbook.
| Variable | Description |
|---|---|
OPENAPI_SPEC_PATH |
Override the path to the OpenAPI spec file (consumed by semantic-graph-extractor / path-analyser) |
OPERATION_GRAPH_PATH |
Override the path to the dependency graph JSON |
SPEC_REF |
Git ref for fetch-spec:ref (branch, tag, or SHA) |
CAMUNDA_BASE_URL |
Base URL of the Camunda instance for Playwright tests |
REQUEST_VALIDATION_SPEC |
Override the spec path consumed by the request-validation generator |