Skip to main content

Testing

View Markdown

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
  • FAIL and USE_EXISTING on 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.