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 (intended HTTP 400) covering ~24 distinct
malformed-request scenario kinds.
┌──────────────────────────┐
│ 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 ~24 kinds of │
│ & operations │ │ malformed-request tests │
│ → operation-dependency- │ │ expecting HTTP 400 │
│ 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)
npm install -g @camunda8/c8ctl
# 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 -d# 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 (requires running Camunda server)
npm run test:pw # both suites (path-analyser + request-validation)
npm run test:pw:path-analyser # positive scenarios only
npm run test:pw:request-validation # negative request-validation 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 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.
To bump the spec:
# 1. Fetch the new spec. SPEC_REF can be a branch, tag, or commit SHA;
# a branch/tag is fine for convenience here, but step 3 must record
# the resolved 40-char commit SHA in spec-pin.json (branches drift).
SPEC_REF=stable/8.10 npm run fetch-spec:ref
# 2. Regenerate the pipeline so the invariants run against fresh output
npm run testsuite:generate
npm run generate:request-validation
# 3. Update configs/camunda-oca/spec-pin.json:
# - specRef: the resolved 40-char commit SHA (NOT the branch/tag)
# - expectedSpecHash: the `specHash` printed in spec/<config>/bundled/spec-metadata.json
# 4. Update any invariants whose values legitimately changed, then commit
# spec-pin.json alongside the invariant updates..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 — every test sends a deliberately malformed request
and asserts the server responds with HTTP 400. Covers ~24 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) |
| 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 |