Testing
Application tests: the in-memory engine
InMemoryOrchestration implements both Orchestrator and Runner in one object, with no
server. Retries, id policies, cancellation at heartbeats, timeouts and signals all behave as the
port specifies, so a service that takes an Orchestrator is tested with the real semantics and
none of the infrastructure.
val engine = InMemoryOrchestration().apply {
register(GreetingTask)
register(AgentRun)
start()
}
val service = OnboardingService(orchestrator = engine)
Flows run their body on a plain thread and block on it, which is what an engine does too, minus the durability. Cancellation and timeouts reach the body as thread interruption.
Engine tests: the contract
OrchestratorContract (tasks, 10 tests) and FlowContract (flows, 10 tests) live in the api
module's test fixtures. An adapter passes them or it is not an adapter. They cover:
- typed results, status after completion, re-attaching by id
- retry until success, retry ceiling, non-retryable stop
FAILandUSE_EXISTINGon a duplicate id- cancel observed at the next heartbeat, attempt timeout
- sequence, fan-out, signals (including ones sent before the body asks), receive with timeout, catching a failed task, a body that throws, sleep on engine time, cancel while waiting
The engine adapter's contract spawns a real headless dev orchestrator per test class. With
LERTHA_FLOW_ADDRESS=host:port set, the same suite runs against a live orchestrator instead:
that is the acceptance test for every orchestrator upgrade.
LERTHA_FLOW_ADDRESS=127.0.0.1:7233 ./gradlew test --rerun-tasks
One JUnit trap
A Kotlin test whose last expression returns a value has a non-Unit return type, and JUnit
silently skips it. End such tests with Unit.