citrusframework / citrusframework/citrus-simulator
Citrus Simulator — Next Generation: fork overview and upstream contribution proposal
- Dominant language
- Java
- Stars
- 47
- Forks
- 45
- Avg merge
- 4d 10h
- Merged PRs (30d)
- 7
Description
# Citrus Simulator — Next Generation
**A fork overview and upstream proposal for the Citrus Simulator maintainers**
> Status: discussion document · Fork version `4.0.0-SNAPSHOT` · Prepared 2026-09-06
> Fork base: `citrusframework/citrus-simulator` @ `3.2.0-SNAPSHOT` (imported 2026-03-13)
---
## 0. TL;DR for maintainers
I forked Citrus Simulator in March 2026 to build a shared "Test Hub" — one simulator that
several teams could point their integration tests at. Six months and ~310 commits later, the
fork is still recognisably Citrus Simulator at its core — the scenario/starter model, the endpoint
adapters, the mapper chain, and the entity model are all intact — but it has grown a second
half that upstream does not have: **an operations and governance plane**.
Concretely, on top of upstream I added:
| Layer | What I added |
|---|---|
| **Runtime** | Hot-loadable scenario bundles (JARs, no restart), config-driven starters, declarative message validations, a workflow/chaining engine, runtime-provisioned JMS endpoints, per-execution log capture and live streaming |
| **Data & API** | Flyway-managed schema (56 migrations, where upstream has none), ~30 new entities, 28 REST controllers (upstream has 8), execution search/saved filters/export (HTML/MD/PDF/XLS), dashboard API |
| **Operations** | A Configuration Store (versioned, inherited, encrypted, backed up, git-exportable), workspaces, users/roles, API credentials, a unified audit trail, global settings |
| **Clients** | A fluent Java client SDK, and an MCP server exposing ~97 governed tools so AI agents can drive and author against the hub |
**Why I am writing this.** Several of these pieces feel like they belong to Citrus Simulator
rather than to me, and I would rather contribute them than maintain a permanent fork.
But some of them are opinionated product decisions that you may deliberately not want in the
simulator's scope. This document exists so you can make that call from the design level
before anyone reads a diff.
**What I am asking for.** A decision about where this should live. There are two futures and I
am genuinely open to either: parts of it come under the Citrus organisation — as contributions
to `citrus-simulator`, or as a separate distribution alongside it — or none of it does, and I
carry on maintaining it under my own account as an independent downstream fork. I would prefer
the first, but the second is a perfectly reasonable answer and I would rather hear it than guess
at it. Section 6 sets out what I think is worth taking, so that the choice can be made per item
rather than all-or-nothing.
**One honest caveat up front:** this fork was imported as a squashed file copy, so it shares
**no git history** with upstream. Nothing here can be cherry-picked or merged — every
contribution would be a hand-written patch against current upstream `main`. Section 7 covers
the full cost.
---
## 1. Why this fork exists
### 1.1 The original problem
Upstream Citrus Simulator answers a well-scoped question:
> *How do I stand up a simulated counterpart of the system my tests talk to?*
That is exactly what I needed — at first. I had REST, SOAP, and JMS integrations to
exercise, and Citrus Simulator gave me scenarios, starters, the mapper chain, and a UI to see
what happened. I forked it, added per-execution log capture and a live log terminal, and
shipped it to one team.
> **A note on dates.** This repository's git history starts at the March 2026 import, but that
> import already carried earlier fork work from a predecessor repository — workspaces, log
> capture, and the user/token model were present on day one here. The ~310 commits below
> therefore *understate* the total divergence.
### 1.2 The problem that actually appeared
Then more teams wanted it, and the question changed:
> *How do several teams operate a shared simulator as a service — without redeploying it every
> time a test parameter changes, without leaking each other's test runs, and without any of them
> being able to break the others?*
That is a different question, and upstream reasonably does not answer it. Every feature in
Section 5 traces back to one of five pressures that came out of that shift:
| Pressure | Where it led |
|---|---|
| "I changed one dropdown value and now I need a redeploy" | Configuration Store, config-driven starters |
| "My scenario is in a JAR; I need it live in the hub in 10 seconds, not in a release" | Runtime scenario bundles |
| "Whose test run is this, and who changed that config last Tuesday?" | Workspaces, users, audit trail, configuration history |
| "The test is 6 steps and step 4 needs step 2's output" | Workflow engine, context variables, output extraction |
| "I want to run this from my own test project / from an AI agent, not from the UI" | Client SDK, MCP server |
### 1.3 Where it landed
The honest one-line description of this fork is:
> **Citrus Simulator + a control plane.**
The simulator half is upstream's, extended. The control plane half — configuration
management, identity, authorisation, audit, workspaces, backup/restore, governed remote
access — is mine, and it is roughly half the code. I am not claiming this is what Citrus
Simulator *should* be. I am showing you what happened when I pushed it in that direction,
so you can decide which parts are simulator-shaped and which are product-shaped.
---
## 2. Honest relationship to upstream
### 2.1 Fork mechanics
| | |
|---|---|
| Fork base | `citrusframework/citrus-simulator`, `3.2.0-SNAPSHOT` |
| Imported | 2026-03-13, as a **squashed single commit** (`[feat] imported citrus-simulator fork`) |
| Shared git history | **None.** `git merge-base` against upstream `main` is empty |
| Commits since | ~310, 2026-03-10 → 2026-09-06 |
| Package root | Unchanged: `org.citrusframework.simulator.*` |
| Maven coordinates | Unchanged group/artifact `org.citrusframework:citrus-simulator`, version bumped to `4.0.0-SNAPSHOT` |
The squashed import is the single most consequential decision in this fork's history, and it
was a mistake I would not repeat. It makes every exchange in either direction a manual port.
### 2.2 Platform divergence
Both sides independently migrated to the Citrus 5 / Spring Boot 4 line — there was no
coordination, and this fork landed later. The figures below are read directly from upstream
`main` as of 2026-09-06.
| | This fork | Upstream `main` (2026-09-06) |
|---|---|---|
| Version | `4.0.0-SNAPSHOT` | `3.2.0-SNAPSHOT` |
| Citrus | 5.0.0 | **5.0.1** |
| Spring Boot | 4.1.1 | 4.1.0 |
| Java | 25 | 17 |
| Angular | **19** | **22.1.1** |
| Jackson | 3 (`tools.jackson`) | — |
| Maven modules | 6 | 5 |
| REST controllers | 28 | 8 |
| JPA entities | ~40 | 8 |
| Schema management | Flyway, 56 migrations | Hibernate-managed (no migration tool) |
Two things stand out, and both cut against this fork:
- **The Angular gap is the sharpest practical problem** for any UI-side contribution: my UI work
is a large, opinionated rebuild on a version upstream has already moved past. The *ideas*
transfer; the code does not.
- **This fork is behind on Citrus and Java.** Upstream is on Citrus 5.0.1 and Java 17; the fork
is on Citrus 5.0.0 and Java 25. Anything I contribute has to be re-based onto upstream's
platform, not the other way round.
I also **dropped two upstream modules** — `simulator-archetypes` and `simulator-samples` — because
neither served a single shared deployment at the time. That was a scoping decision I plan to
revisit: it left the fork with no runnable examples and no generated starting point, which is
exactly what a newcomer needs. **Both are on the list to reinstate in a future release** —
`simulator-archetypes` adapted to generate a scenario-bundle project rather than a whole
simulator, and `simulator-samples` restored so the fork has the same self-contained, runnable
examples upstream does.
### 2.3 What I did to upstream's own code
I tried to keep the core recognisable, and mostly succeeded.
**Preserved essentially as-is** — the packages that make it Citrus Simulator:
`scenario` (scenario/starter model, `ScenarioRunner`, `ScenarioEndpoint`) · `endpoint` ·
`http` · `jms` · `ws` · `correlation` · `dictionary` · `template` · `exception` ·
`listener` · `repository` · all eight upstream entities (`Message`, `MessageHeader`,
`ScenarioAction`, `ScenarioExecution`, `ScenarioParameter`, `ScenarioParameterOption`,
`TestParameter`, `TestResult`) and all eight upstream REST resources, contract-compatible.
Upstream's mapper chain (`AbstractScenarioMapper`, `ContentBasedJsonPathScenarioMapper`,
`ContentBasedXPathScenarioMapper`, `HeaderValueScenarioMapper`, `ScenarioMappers`) is intact;
my `@MessageMapping` work (§5.4) sits alongside it rather than replacing it.
**Patched in place** — small, targeted changes to upstream classes:
| Upstream class | Change | Why |
|---|---|---|
| `ScenarioExecution` | `executionLog` CLOB, `scenarioType`, `targetEnvironment`, context-variables JSON | Per-execution transcript & provenance |
| `SimulatorAutoConfiguration` | Additional packages in component scan | Register new subsystems |
| `SimulatorStatusListener` | Start/flush log capture around execution | Log capture lifecycle |
| `ScenarioExecutionServiceImpl` | Persist execution log; completion contract | Log capture + typed result payloads |
| `ScenarioLookupServiceImpl` | Consult the dynamic scenario registry first | Runtime bundles |
| `DefaultScenarioExecutorService` | Registry lookup before `ApplicationContext.getBean()` | Runtime bundles |
| `SpaWebFilter` | Exclude `/ws` | WebSocket endpoint |
| `SecurityConfiguration` | Substantially rewritten | Auth is a fork concern (see §5.9) |
| `ScenarioParameterMapper` | Bugfix: `createdDate` unset on REST launch | Genuine upstream bug |
**Structurally changed** — the three decisions that would matter most to you:
- **`simulator-api` extracted as a new module.** Entities, repositories, service interfaces,
DTOs, MapStruct mappers, criteria, and Flyway migrations moved out of `simulator-spring-boot`
into a contracts-only module that the client SDK can depend on without pulling in the runtime.
Dependency direction is strictly `simulator-api → simulator-spring-boot`.
- **Schema is Flyway-managed.** Upstream lets Hibernate manage the schema and ships no migration
tool. I introduced Flyway with 56 migrations under `classpath:db/migration/`, because a
long-lived shared deployment with real execution history cannot regenerate its schema. This is
a philosophy difference, not just an implementation detail, and it is the single biggest
obstacle to any entity-touching contribution (§7.3).
- **Two upstream modules dropped** — `simulator-archetypes`, `simulator-samples`. Planned for
reinstatement in a future release (§2.2); nothing in the fork's design prevents it.
**Added** — new packages under the same root: `action`, `annotation`, `context`, `events`,
`logging`, `plugin`, `security`, `workflow`, `support`, `util`, plus a much larger `config`,
`service`, and `web/rest`.
---
## 3. Scale
Rough numbers, so you know what you would be reviewing.
| | |
|---|---|
| Maven modules | 6 |
| Java sources (main) | ~840 |
| Java sources (test) | ~410 |
| Java test methods | ~3,480 |
| Integration test classes (`*IT.java`) | 100 |
| TypeScript sources | 479 (204 spec files) |
| Jest test cases | ~3,530 |
| Playwright/Serenity Screenplay E2E specs | 20 |
| Flyway migrations | 56 (V1–V56) |
| REST controllers | 28 |
| MCP tools | 97 |
| SDD spec documents | 675 markdown files across 120 feature folders |
JaCoCo coverage reports are produced on every `test` run in the four Java modules.
---
## 4. Architecture at a glance
### 4.1 Modules
```
citrus-simulator (4.0.0-SNAPSHOT)
├── simulator-api # contracts: entities, repos, service interfaces,
│ # DTOs, MapStruct mappers, criteria, Flyway migrations
├── simulator-spring-boot # forked core + service impls, REST, auto-config,
│ # scenario framework, bundle plugin, workflow engine,
│ # dynamic endpoints ← the runnable app
├── simulator-ui # Angular 19 UI + the Spring Security configuration
├── simulator-docs # AsciiDoc reference manual (served at /docs)
├── simulator-client-sdk # fluent Java client (depends on simulator-api only)
└── simulator-mcp-server # standalone Spring Boot app; MCP over the client SDK
(upstream's simulator-archetypes and simulator-samples are not carried yet —
both are planned for a future release, see §2.2)
```
One of those is deliberately decoupled and could travel on its own: `simulator-client-sdk`
depends only on `simulator-api` contracts, never on the runtime.
### 4.2 Data model growth
Upstream's eight entities are still there and still shaped the same way. Around them I added
roughly thirty more, in five clusters:
- **Configuration** — `Configuration`, `ConfigurationHistory`, `ConfigurationBackup`,
`ConfigurationBackupLock`
- **Bundles** — `ScenarioBundle`, `ScenarioBundleEntry`, `ScenarioBundleAudit`
- **Parameters & presets** — `ScenarioParameterGroup`, `ScenarioParameterOption`,
`ScenarioParameterColumn`, `ScenarioParameterVisibilityCondition`, `ScenarioParameterPreset`
(+ revision/audit)
- **Governance** — `User`, `AccessToken`, `ApiCredential`, `AuditEntry`, `GlobalSetting`,
`Workspace`, `SimulatorLock`
- **Execution extensions** — `ExecutionAttribute`, `ScenarioExecutionFilter`,
`ScenarioValidation`, `ScenarioExecutionValidation`, `WorkflowDefinition`,
`WorkflowExecution`, `WorkflowStepExecution`, `EndpointBinding`, `CapturedMessage`,
`EndpointDeliveryClaim`
### 4.3 The three integration seams
Almost everything new hangs off one of three seams into upstream's runtime. If you want to
understand the fork quickly, understand these:
1. **Scenario resolution** — `ScenarioLookupService` / `DefaultScenarioExecutorService` consult a
`DynamicScenarioRegistry` before the Spring `ApplicationContext`. This is what makes runtime
bundles possible without a context restart.
2. **Log capture** — a Logback `TurboFilter` buffers log events per thread into an
`ExecutionLogRegistry`; `SimulatorStatusListener` flushes the buffer to
`ScenarioExecution.executionLog` on completion, and a STOMP broker streams the same events live.
3. **Configuration resolution** — `ConfigurationResolver` / `TypedConfigurationResolver` resolve
an effective JSON document (inheritance, overrides, decryption, placeholder resolution) that
config-driven starters read their parameter schemas and request templates from at run time.
---
## 5. What was built, and why
Each subsystem below states the pressure that produced it, what exists, and — the part that
matters for this conversation — how coupled it is to the rest of the fork.
---
### 5.1 Per-execution log capture and live streaming
**Pressure.** A failed run in the UI told you *that* it failed, and you then went to the pod
logs to find out why. With several teams sharing one simulator, the pod log is a shared,
interleaved mess.
**Built.** A three-phase capture:
1. `ExecutionLogCaptureFilter` (Logback `TurboFilter`) appends events to a per-thread
`ExecutionLogRegistry` buffer — O(1), capped at 1 MB.
2. `SimulatorStatusListener` stops capture at test end and persists the transcript to
`ScenarioExecution.executionLog`.
3. The same events are pushed over STOMP (`/ws`, broker `/topic`) to a live terminal in the UI,
with grep filter, autoscroll, and a 5,000-line ring buffer.
Scenario code prefixes its own lines with `§` so the UI can separate "what the test did" from
"what the simulator did"; the detail view has a *Show all logs* toggle on that distinction.
**Where.** `simulator-spring-boot/.../logging/`, `listener/SimulatorStatusListener`,
`config/WebSocketConfig`.
**Coupling.** **Low.** Four new classes, one field on `ScenarioExecution`, one listener hook.
This is the single most portable thing in the fork and, in my experience, the highest
value-per-line.
---
### 5.2 Runtime scenario bundles
**Pressure.** Scenarios lived in the simulator's own build. Every new scenario was a release of
the shared hub — which meant a queue, and which meant teams stopped writing scenarios.
**Built.** Scenario JARs are uploaded and activated at runtime, with no Spring context restart:
- **Upload** — JAR streamed to temp storage; an **ASM bytecode scan** (`JarAnnotationScanner`)
discovers `@Scenario` / `@Starter` / `@MessageMapping` classes and reads `MANIFEST.MF`
*without loading any class*; per-class SHA-256 checksums drive NEW/CHANGED/UNCHANGED
categorisation; JAR-level checksums make re-upload idempotent.
- **Activation** — an isolated `URLClassLoader` loads the class, Spring `autowireBean()`s it,
and it is registered in `DynamicScenarioRegistry`;
`ScenarioLookupService.evictAndReloadScenarioCache()` publishes `ScenariosReloadedEvent`.
- **Deactivation** — in-flight executions block it (HTTP 409), then unregister, close the
classloader, delete the temp JAR.
- **Persistence** — pluggable `JarStorageStrategy`: filesystem (`local` profile) or PostgreSQL
large objects; `ScenarioBundleSyncService` reactivates previously-active bundles on boot, with
an actuator health indicator, and synchronises across pods.
- **Governance** — per-bundle version history, release notes, audit entries, rollback,
optimistic locking.
**Where.** `simulator-spring-boot/.../plugin/`, `service/ScenarioBundleService`,
`web/rest/ScenarioBundleResource`.
**Coupling.** **Medium.** Self-contained as a package, but it needs the scenario-resolution
seam (§4.3.1) and, for the governance parts, the audit and workspace subsystems. The core
load/activate mechanism could be extracted without them.
---
### 5.3 Configuration Store and config-driven starters
**Pressure.** The one that generated the most work. Starters hard-code their parameter forms and
request bodies in Java. Changing a dropdown option, a default value, or a JSON field meant
recompiling and re-uploading a bundle. Multiply by four environments.
**Built — the store.** Named, typed, environment-scoped JSON documents with:
- Four types — `CONFIG` (parameter schemas, request templates), `ENVIRONMENT_PROFILE` (URLs,
credentials, flags — one per environment, shared by all starters), `BROKER`, `ENDPOINT`
- Type-scoped **inheritance** with override resolution and placeholder resolution
- **Encrypted secret paths** (`ENC(...)`), decrypted on read, masked everywhere else
- **Locked/protected paths**, full **version history** with change-impact analysis
- **Backup/restore**, including scheduled backups coordinated across pods by a DB lock
- **Git-friendly export/import** — folder trees and ZIP envelopes, plus bulk import/export
- Schema validation on save, JSON/YAML/properties formats, a proto-schema validator
- A "Configuration Studio" UI: visual tree editor, code editor, live form preview, request-body
preview, kind-aware starting examples
**Built — the starters.** `AbstractConfigDrivenStarter` loads its `{name}/params` document into a
parameter schema and its `{name}/template` document into a request template:
- `ParameterSchemaParser` supports all control types — `TEXTBOX`, `TEXTAREA`, `DROPDOWN`,
`MULTISELECT`, `KEY_VALUE`, `TYPEAHEAD`, `JSON_EDITOR`, `TABLE` (with nested columns) — plus
groups, ordering, optional/collapsed/skippable, and `visibleWhen` conditional visibility
- `RequestTemplateRenderer` supports `JSON`, `FORM_URLENCODED`, `RAW` output; `{{param}}`
placeholders; type-coercion prefixes (`bool:`, `int:`, `json:`, `array:`, `date:`, …);
`omitEmpty`; header and URL-suffix interpolation; warnings on unresolved placeholders
- `PROTO` support: protobuf encode for sends, decode for inspection, including registered `Any`
types
The net effect: a QA engineer adds a form field by editing a JSON document in the browser.
**Where.** `simulator-api/.../model/Configuration*`, `service/Configuration*`;
`simulator-spring-boot/.../config/`, `scenario/AbstractConfigDrivenStarter`,
`scenario/config/`, `web/rest/Configuration*Resource`.
**Coupling.** **High.** This is the fork's centre of gravity. It assumes identity (who locked
this path), audit (who changed it), encryption keys, and a UI. It is also, I think, the most
genuinely novel idea here — and the one most likely to be out of scope for you.
---
### 5.4 Message-driven scenario mapping
**Pressure.** Upstream's mapper chain composes single-dimension mappers
(`ContentBasedJsonPathScenarioMapper`, `ContentBasedXPathScenarioMapper`,
`HeaderValueScenarioMapper`) with no cross-dimension matching, no regex, and no explicit
precedence between overlapping rules. I needed one inbound queue feeding many scenarios chosen
by payload content.
**Built.** A `@MessageMapping` annotation and matching engine:
- Match by **XPath**, **JSONPath**, **regex**, or **headers**, with explicit **priority ordering**
for overlapping rules
- `MessageAnnotationScenarioMapper` resolves inbound messages; `MessageMappingMatcher` does the
matching against the parsed payload
- `JarAnnotationScanner` detects it too, so bundled scenarios participate in routing
- Mappings are persisted per bundle entry and visualised in the UI
- Tuned for high throughput; regex matching and priority ordering have their own specs
**Where.** `simulator-spring-boot/.../scenario/MessageMapping`, `scenario/mapper/`.
**Coupling.** **Low.** It plugs into the existing mapper chain and does not require anything
else from the fork. A strong upstream candidate.
---
### 5.5 Declarative message validations
**Pressure.** Assertions lived inside scenario Java. Test engineers who were not Java developers
could not add or adjust them.
**Built.** Validation rules stored as data and applied to a scenario or a single run,
delivered over six increments:
- Rules target messages, headers, SQL result columns, and PostgreSQL `RAISE NOTICE` output
- Async `AWAIT` semantics for eventually-consistent assertions, with a deferred last-receive
evaluator
- Depth/reach controls, value extraction from matched content
- Run-scoped rules apply to `launch-and-wait` and cannot leak into other runs
- Outcomes are persisted per execution (`ScenarioExecutionValidation`) and reported in the UI
- Exposed through REST, the SDK, and MCP
**Where.** `simulator-spring-boot/.../validation/`, `service/ScenarioValidationService`,
`web/rest/ScenarioValidationResource`.
**Coupling.** **Medium.** Depends on the execution model and on Citrus action interception, not
on the control plane.
---
### 5.6 Workflow engine (scenario chaining)
**Pressure.** Real test cases are sequences: provision, then trigger, then poll, then assert,
then clean up. Teams were doing this by hand, one starter launch at a time.
**Built.** A graph-based workflow engine over scenarios, delivered as ~13 "foundation" increments:
- **Graph model** — `ScenarioStepNode`, `ConditionNode`, `ForkNode`/`JoinNode` with join
strategies, `WorkflowEdge`, positions for the visual editor
- **Compiler** — `WorkflowCompiler` validates and compiles a graph into an `ExecutionPlan`
- **Executors** — linear and control-flow executors, async execution, cancellation propagation
via a `WorkflowCancellationRegistry`, retry with backoff policies
- **Data flow** — `WorkflowContext` with typed parameter mapping (static values, step outputs),
`OutputExtractor` with extraction types, context snapshots
- **Persistence & coordination** — definition lifecycle with locking, execution/step execution
records, PostgreSQL-backed multi-pod execution coordination, recovery service
- **Authorisation** — `WorkflowAuthorizationService`
- **UI** — catalog, authoring, validation, launch, execution detail, monitoring, cancellation,
delete/archive lifecycle
**Where.** `simulator-api/.../model/workflow/`, `simulator-spring-boot/.../workflow/`,
`web/rest/Workflow*Resource`, `simulator-ui/.../workflow/`.
**Coupling.** **High.** Depends on the execution model, context variables, authorisation, and
multi-pod coordination. This is the clearest example of the fork going further than a
simulator — arguably it is a small orchestration product living inside one.
---
### 5.7 Dynamic endpoint provisioning
**Pressure.** Adding a JMS queue to listen on meant editing Spring configuration and
redeploying. Operators needed to add destinations at run time, safely.
**Built.** (Landed September 2026 — the newest subsystem.)
- `BROKER` configurations hold protected connection details; `ENDPOINT` configurations hold
logical destinations with intake, capture, and send behaviour
- A transport-neutral seam (`EndpointTransportProvider`, `EndpointCapabilities`) with a JMS
implementation; generic JMS accepts any runtime `jakarta.jms.ConnectionFactory` with no
simulator allowlist
- Standing listeners bind to runtime bundle scenarios via a repeatable `@ListenOn`; shared or
fenced-exclusive modes; backlog is skipped by default; message/rate safety limits enforced
- **Non-destructive queue inspection** through REST, SDK, MCP, and UI
- Bounded, database-backed message capture with retention sweeping and duplicate-delivery claims
- `DistributedLockService` — reusable fenced database leases with monotonic fencing tokens,
renewal, expiry, and owner-safe release, for endpoint ownership and other singleton jobs
- Operational limits live in database-backed Global Settings and take effect without restart
- Metrics, health details, and payload-free audit events across the provision/own/capture/
consume/send lifecycle
Kafka is **specified but not implemented** — an approved SDD contract exists
(`docs/specs/scenario-workflows/dynamic-kafka-endpoint-provisioning/`), deliberately as a
separate transport rather than a conditional branch in the JMS code.
**Where.** `simulator-spring-boot/.../endpoint/dynamic/`, `endpoint/transport/`,
`web/rest/EndpointResource`.
**Coupling.** **High.** Built on the Configuration Store, global settings, and audit.
The `DistributedLockService` inside it is independently useful and independently portable.
---
### 5.8 Execution search, saved filters, export, and lifecycle
**Pressure.** Hundreds of runs a day across several teams. The flat execution list stopped working
at around run 200.
**Built.**
- `ScenarioExecutionQueryService` over a rich `ScenarioExecutionCriteria`
- `ScenarioExecutionFilter` — named, per-user saved filters, with a quick-filter bar, an advanced
filter panel, and column customisation
- `ScenarioExecutionExportService` with one `ExportGenerator` per format: **HTML, Markdown, PDF,
XLS**
- **Execution attributes** — arbitrary key/value tags on a run, queryable and shown in the list
- **Re-run** and **stop** for executions, execution timeouts, execution deletion
- **Context variables** persisted per execution; **typed result payloads**
(`ScenarioRunner.setResultPayload`) deserialisable by the SDK
- A dashboard summary API feeding the home page
**Where.** `simulator-api/.../service/criteria/`, `service/ScenarioExecutionExportService`,
`simulator-spring-boot/.../web/rest/ScenarioExecutionFilterResource`,
`simulator-ui/.../scenario-execution/`.
**Coupling.** **Medium.** Search/export are close to standalone; saved filters need users.
---
### 5.9 The control plane: identity, workspaces, audit, settings
**Pressure.** "Shared service" implies "who are you" and "what did you do".
This is the half of the fork that is unambiguously *not* simulator scope, and I flag it as such.
**Built.**
- **Users and roles** — upstream has a `SecurityConfiguration` but no user model; I added
`User` with `ROLE_ADMIN`/`ROLE_USER`, registration, password reset, account management, an
admin users screen, and a `dev`-profile bootstrap admin
- **Authentication** — form login with JDBC-backed Spring Session (multi-pod), persistent
"remember me" with token rotation tolerance, **GitHub OAuth2** with dynamic client
registration, bearer tokens (`AccessToken`), API credentials (`ApiCredential`, hashed client
secrets), and a single-API-key filter for machine bundle upload
- **Workspaces** — scenarios and starters carry `@Workspace(name=…)`; every execution is
assigned to one; the navbar picker filters scenarios and executions; workspaces are discovered
at startup and refreshed over WebSocket on bundle activation; never deleted (history references
them), with scenario counts and an empty-workspace toggle
- **Unified audit trail** — `AuditEntry` covering configuration changes, bundle lifecycle, preset
operations, endpoint lifecycle, and AI-triggered activity; searchable, exportable, with a
detail view
- **Global settings** — database-backed operational limits that take effect without restart
- **API error contract** — consistent problem responses and diagnostics across all controllers
**Where.** `simulator-ui/.../config/SecurityConfiguration`, `ui/security/`, `ui/filter/`;
`simulator-api/.../model/User|AccessToken|ApiCredential|AuditEntry|GlobalSetting|Workspace`;
`simulator-spring-boot/.../service/AuditService`, `UserService`, `GlobalSettingsService`.
**Coupling.** **Highest.** Nearly everything else references it. If upstream wants none of the
control plane, that is an entirely reasonable position and most of Section 6's Tier 1 and 2
items survive it.
---
### 5.10 The UI
**Pressure.** The generated JHipster UI was a good scaffold and a poor product surface for daily
use by non-developers.
**Built.** Effectively a rebuild of the Angular application on the same framework:
- A design system (`_design-system.scss`) with tokens, mixins, one typographic scale, one control
scale, one focus ring — enforced by **file-content contract Jest specs** that read stylesheets
as text and fail the build on drift
- New route groups: configuration studio, scenario bundles, workflows, endpoints, audit log,
API documentation, scenario source preview, live logs, presets, admin, welcome/setup
- Reworked upstream routes: home dashboard, scenario catalog and detail, run/launch flow with
optional preview, execution list and detail, results
- Parameter forms for every control type, including nested tables, JSON editors, and conditional
visibility
- Saved-parameter presets with revisions, import/export, and a global catalog
- ~3,530 Jest tests and a 20-spec Playwright + Serenity/JS Screenplay E2E suite with tiers
(`smoke` / `full` / `extended`)
- ~50 static HTML prototypes under `docs/prototypes/` used as the design contract before
implementation
**Coupling.** **N/A for porting** — this is Angular 19 against upstream's Angular 22. The
*ideas* port; the code does not.
---
### 5.11 Java client SDK (`citrus-test-hub-client-sdk`)
**Pressure.** Teams wanted to launch scenarios and read configuration from their own Citrus/JUnit
projects, not only from the UI.
**Built.** A fluent client depending only on `simulator-api` contracts:
- `CitrusTestHubClient` + builder; bearer-token and no-auth providers
- Discovery-based, immutable **catalog** handles for starters, scenarios, configurations
- **Async, offline-capable configuration snapshots**
- Fluent **query builders** for every queryable entity
- Fluent **launch builder** with typed result-payload deserialisation
- Structured HTTP request/wire logging; request interceptors; Spring `RestClient` transport
- `@EnableTestHubConfigurations` — auto-registers Test Hub configuration POJOs as Spring beans,
with a defined property-source precedence
- Saved-preset and configuration descriptors, starter abstractions
**Coupling.** **Low.** It is a client for a REST API. If upstream ever wants an official Java
client for the simulator's REST surface, the structure here transfers even where the endpoints
do not.
---
### 5.12 MCP server
**Pressure.** Once the hub had a complete REST surface, the obvious next question was whether an
AI agent could operate it — and the obvious next worry was what it could break.
**Built.** A standalone Spring Boot app (Spring AI) exposing **97 MCP tools** over the client SDK:
- Read/operate tools: list and launch scenarios, wait/stop/re-run executions, read messages,
results, validations, audit, statistics, health
- Authoring workflow: index bundle source context, plan changes, generate and diff configuration,
build a bundle, apply changes, triage the result
- Governance: `SecretMasker`, `McpResponseLimiter`, workspace and tool-group policy, HTTP filters
for credential handling and error sanitisation
- STDIO transport by default (local IDE/agent use), HTTP `/mcp` for deployed use
- Its own PostgreSQL state store for source-context indexing — it does **not** share the
simulator's store; multi-pod support; Helm chart and Dockerfile
**Coupling.** **Low to the runtime, high to the fork's REST surface.** It is a wrapper over the
SDK, not a reimplementation, so it stands or falls with whichever REST surface exists.
---
### 5.13 API documentation generator
**Pressure.** Teams asked "how do I call this scenario?" and the answer was "read the Java".
**Built.** `ApiDocumentationService` scans registered scenarios and generates usage examples in
three flavours — `CurlExampleGenerator`, `JavaExampleGenerator` (Spring `RestClient`), and
`SdkExampleGenerator` — surfaced in an API-documentation UI with presets and detail views, plus
message-driven-scenario enrichment and Swagger enrichment.
**Coupling.** **Low.** It reads the scenario registry and emits text.
---
## 6. What I think is worth upstreaming
Ranked by *(value to upstream ÷ coupling to this fork)*. I am not attached to any particular
item; this is a menu, not a plan.
### Tier 1 — small, self-contained, plausibly wanted
| # | Item | Size | Why it fits upstream |
|---|---|---|---|
| 1 | **Per-execution log capture** (§5.1) | ~4 classes + 1 entity field | Every simulator user eventually wants "why did *this* run fail" without grepping pod logs |
| 2 | **`@MessageMapping` scenario mapper** (§5.4) | ~6 classes | A natural extension of the existing mapper chain; content-based routing with priority is a common ask |
| 3 | **Live log streaming over STOMP** (§5.1) | ~3 classes + UI component | Small, and immediately visible in the UI |
| 4 | **`DistributedLockService`** (fenced DB leases, §5.7) | ~3 classes + 1 migration | Generic multi-replica coordination primitive; useful anywhere the simulator runs more than one pod |
### Tier 2 — larger, valuable, needs design agreement first
| # | Item | Open design question |
|---|---|---|
| 5 | **Runtime scenario bundles** (§5.2) | Does the simulator want to be a runtime host for third-party scenario JARs? Classloader isolation and storage strategy are the debatable parts |
| 6 | **Execution search + saved filters + export** (§5.8) | Which formats, and does export belong in core or an add-on? |
| 7 | **Java client SDK** (§5.11) | Does upstream want an official Java client, and should it be generated from OpenAPI instead? |
| 8 | **Declarative message validations** (§5.5) | Rules-as-data is a philosophy change from assertions-in-Java |
| 9 | **API documentation generator** (§5.13) | Overlaps with the existing Swagger/OpenAPI story |
| 10 | **MCP server** (§5.12) | Whether an AI-facing surface belongs in the project at all, and if so whether it ships separately |
### Tier 3 — the "admin" half: probably a separate distribution, not core
Configuration Store and config-driven starters (§5.3), the workflow engine (§5.6), dynamic
endpoint provisioning (§5.7), the control plane (§5.9), the UI rebuild (§5.10).
I am **not** proposing these for core. I describe them because they explain the shape of
everything else, and because if you ever wanted a "Citrus Simulator Server" distribution — a
hosted, multi-team simulator — this fork is a six-month prototype of what that looks like,
including the parts that did not work.
### Explicitly not proposed
Anything tied to one particular deployment: CI pipelines, Helm values, deploy scripts, and
domain-specific scenarios. None of it is in the modules above.
---
## 7. What would have to change before any of this lands upstream
I would rather state these plainly than have you find them.
| # | Obstacle | Severity | Notes |
|---|---|---|---|
| 1 | **No shared git history** | High | Squashed import; `merge-base` is empty. Every contribution is a hand-written patch against current upstream, reviewed on its own merits. There is no incremental merge path, ever |
| 2 | **Angular 19 vs upstream 22.1** | High for UI | Any UI contribution must be rewritten, not ported. Backend-only items are unaffected |
| 2b | **The fork is behind on Citrus (5.0.0 vs 5.0.1) and Java (25 vs 17)** | Medium | Contributions must be re-based onto upstream's platform and re-verified on Java 17 |
| 3 | **Schema ownership** | **High** | Upstream ships no migration tool and lets Hibernate manage the schema; I introduced Flyway with 56 migrations. Any contribution that adds a column or a table has to be expressible in upstream's model, which means it cannot assume a migration exists |
| 4 | **Module split (`simulator-api`)** | Medium | Several items assume contracts live in a separate module. Each can be re-inlined, at some cost to the SDK story |
| 5 | **Dependency footprint** | Medium | The fork pulls in Spring Security, Spring Session JDBC, Flyway, MapStruct, protobuf, PDF/XLS export libraries, and Spring AI. Tier 1 items need almost none of this; Tier 2 items each carry some |
| 6 | **Scope judgement** | Medium | Tier 3 is a product, not a library feature. I am not arguing it belongs in core |
| 7 | **Licensing / provenance** | Medium | Apache 2.0 headers preserved throughout, package root unchanged. Any contribution would need the usual CLA/provenance handling on my side, which I am prepared to do |
| 8 | **Residual deployment-specific references** | Low | Main sources are effectively brand-neutral already: one deployment-specific value in an MCP dev-profile YAML, and a handful of placeholder domain names used in javadoc and documentation examples. All trivially scrubbed |
| 9 | **Test conventions** | Low | I use JUnit Jupiter + Mockito + ArchUnit + Failsafe `*IT.java`, ~3,480 Java test methods. Should be compatible, but coverage-gate expectations differ |
| 10 | **Kafka is spec-only** | Low | The dynamic-endpoint work advertises a Kafka contract that is designed but unimplemented. Anything referencing it should say so |
---
## 8. Open questions for the maintainers
1. **Home.** The question underneath all the others: do you want any of this under the Citrus
organisation — as contributions into `citrus-simulator`, or as a separate distribution
alongside it — or would you rather it stayed an independent downstream fork under my own
account? Either answer is workable. Only the first needs the rest of this list.
2. **Scope.** Is a control plane (config store, identity, audit, workspaces) something Citrus
Simulator should ever grow, or is that firmly a downstream concern? Your answer determines
whether Tier 3 is worth any further conversation.
3. **Runtime extensibility.** Is hot-loading third-party scenario JARs (§5.2) a direction you
would take, or is "scenarios live in the simulator's build" a deliberate invariant?
4. **Configuration as data.** Would you consider a mechanism where a starter's parameter form and
request template come from stored documents rather than Java (§5.3)? Even a much smaller
version of this idea would be useful upstream.
5. **Client SDK.** Is an official Java client for the simulator's REST API desirable, and if so
would you prefer it hand-written (as mine is) or generated from an OpenAPI document?
6. **AI surface.** Any appetite for an MCP server in the project — as a module, a sibling repo, or
not at all?
7. **Sequencing.** If Tier 1 is interesting, would you prefer one PR per item, or a single
"observability" PR combining items 1 and 3?
---
## 9. Suggested review path
If you want to evaluate this without reading code, in roughly increasing depth:
1. **This document**, Sections 1–2 and 6–8.
2. **`CHANGELOG.md`** — dense but chronological; the six months read as a narrative and every
entry states its rationale.
3. **Concept pages** — `docs/specs/simulator-platform/concepts/` (workspace, configuration,
scenario-bundle, scenario-execution, scenario-parameter, authentication). Short,
user-facing, no code.
4. **Guides** — `docs/specs/simulator-platform/guides/` (`SCENARIO_VS_STARTER.md`,
`API_USAGE_GUIDE.md`, `AUTHENTICATION_GUIDE.md`, `UI_ROUTES_AND_DIALOGS.md`).
5. **UI prototypes** — `docs/prototypes/` — ~50 static HTML pages, openable directly in a browser,
showing the product surface with no build step.
6. **SDD specs** for anything in Section 6 that interests you. Every feature has a folder under
`docs/specs///sdd/` with `01-specification` → `05-documentation`; the
specification file alone usually answers "why does this exist and what does it promise".
Direct links for the Tier 1 candidates:
- `docs/specs/scenario-workflows/message-driven-mapping/`
- `docs/specs/scenario-workflows/dynamic-endpoint-provisioning/` (for the lock service)
7. **The reference manual** — `simulator-docs/src/main/asciidoc/` — upstream's manual plus my
added pages (`dynamic-endpoints.adoc`, `validation.adoc`, extended `starter.adoc`).
8. **Code**, last, and only for whatever survived the above.
---
## Appendix A — REST controllers
Upstream's controllers are still present and contract-compatible. Full list in this fork:
`ScenarioResource` · `ScenarioActionResource` · `ScenarioExecutionResource` ·
`ScenarioParameterResource` · `MessageResource` · `MessageHeaderResource` ·
`TestParameterResource` · `TestResultResource` — *(upstream, extended)*
`AdminScenarioParameterPresetResource` · `ApiDocumentationResource` · `AuditLogResource` ·
`ConfigurationBackupResource` · `ConfigurationClientResource` · `ConfigurationExportResource` ·
`ConfigurationResource` · `DashboardResource` · `EndpointResource` · `GlobalSettingResource` ·
`LogResource` · `ProtoSchemaValidationResource` · `ScenarioBundleResource` ·
`ScenarioExecutionFilterResource` · `ScenarioParameterPresetResource` ·
`ScenarioSourceResource` · `ScenarioValidationResource` · `WorkflowExecutionResource` ·
`WorkflowResource` · `WorkspaceResource` — *(added)*
## Appendix B — Schema evolution
56 Flyway migrations, `V1__initial_schema.sql` → `V56__add_endpoint_delivery_claim_table.sql`.
V1–V37 live in `simulator-api`, V38+ in `simulator-spring-boot`. Grouped by subsystem:
| Range | Subsystem |
|---|---|
| V1–V4 | Baseline schema, execution/parameter fixes |
| V5–V16 | Workspaces, global settings, parameter groups/options, execution attributes |
| V17–V22 | Configuration store, history, secret paths, inheritance; Spring Session |
| V23–V31 | Scenario bundles, audit, release notes, optimistic locking; result payloads; config backup |
| V32–V37 | Configuration types; target environment; GitHub auth; bundle entry types & message mapping; scenario type |
| V38–V43 | Saved execution filters; context variables; nested parameter columns; parameter presets & revisions |
| V44–V46 | Workflow foundation, execution coordination, definition lifecycle |
| V47–V49 | Scenario and execution validations, validation reporting |
| V50–V53 | Backup locking, persistent logins, distributed lock table |
| V54–V56 | Endpoint bindings (`listens_on`), captured messages, delivery claims |
## Appendix C — Specification index
675 markdown documents across 120 SDD feature folders, organised in six categories:
| Category | Covers |
|---|---|
| `simulator-platform` | Runtime, API, auth, UI shell, execution, audit, workspaces |
| `scenario-workflows` | Starters, message mapping, control types, validations, workflow engine, dynamic endpoints |
| `integrations-and-clients` | Client SDK, MCP server, external contracts |
| `operations-and-governance` | Configuration, secrets, import/export, backups, team collaboration |
| `developer-experience` | Build tooling, docs, the Citrus 5 / Boot 4 migration, E2E suite |
| `research-and-archive` | Roadmaps, research notes, retired approaches |
Each feature folder follows the same five-stage structure — specification, architecture, tasks,
implementation, documentation — so the *intent* of any subsystem is readable without the code.
---
## Appendix D — How the upstream comparison was made
Every upstream figure in this document was read from `citrusframework/citrus-simulator` `main`
on **2026-09-06**, not from memory:
```bash
git fetch --no-tags --depth=1 \
https://github.com/citrusframework/citrus-simulator.git main:refs/upstream-probe
git ls-tree -r --name-only refs/upstream-probe
git show refs/upstream-probe:pom.xml
```
`git merge-base HEAD refs/upstream-probe` returns nothing, which is the mechanical statement of
§2.1: there is no common ancestor to merge across.
---
*Prepared by the fork's maintainer. Corrections welcome — where this document and the code
disagree, the code is right and I would like to know.*
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with sections 5–7, then review the named modules and upstream classes such as ScenarioExecutionServiceImpl and SecurityConfiguration. Compare the fork’s control-plane features, Flyway schema, and platform versions with upstream main. Done means maintainers decide which components, if any, belong in citrus-simulator, a separate distribution, or the downstream fork.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- angular, java, spring-boot, typescript
- Domain
- backend-api-design, database, documentation, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100