//// Pure Gleam workflow test harness.
////
//// `aion/testing` is the recommended way to test workflow author code. It runs
//// under `gleam test` with no engine, beamr, store, external services, or Rust
//// NIFs. Test code initialises a process-scoped `TestEnv`; the test-only
//// Erlang module `test/aion_flow_ffi.erl` occupies the same production FFI
//// namespace so workflow code and `@external` declarations are byte-identical
//// in tests and production.
import aion/activity.{type Activity}
import aion/codec.{type Codec}
import aion/duration
import aion/error
import aion/internal/ffi
import aion/testing/clock
import aion/testing/mock
import aion/testing/replay
/// Process-scoped test environment handle.
///
/// The runtime state is held by the test FFI double under the current Erlang
/// process. The handle keeps test APIs explicit and prevents unrelated tests from
/// sharing state when gleeunit runs them concurrently in separate processes.
pub opaque type TestEnv {
TestEnv(process_key: String)
}
/// Build a fresh `TestEnv` for the current test process.
///
/// The simulated clock, activity mock registry, child/query/signal fixtures, and
/// observation capture are reset for the current process only.
pub fn new() -> Result(TestEnv, error.EngineError) {
case ffi.testing_reset() {
Ok(process_key) -> Ok(TestEnv(process_key: process_key))
Error(raw_error) -> Error(error.EngineFailure(raw_error))
}
}
/// Return the process key assigned by the test FFI double.
pub fn process_key(env: TestEnv) -> String {
env.process_key
}
/// Run a workflow thunk under a fresh process-scoped test environment.
pub fn run(workflow: fn(TestEnv) -> value) -> Result(value, error.EngineError) {
case new() {
Ok(env) -> Ok(workflow(env))
Error(engine_error) -> Error(engine_error)
}
}
/// Advance the simulated test clock by a canonical duration.
pub fn advance(
env: TestEnv,
by: duration.Duration,
) -> Result(TestEnv, error.EngineError) {
clock.advance(env, by)
}
/// Return the current simulated clock value in milliseconds.
pub fn current_time_milliseconds(
env: TestEnv,
) -> Result(Int, error.EngineError) {
clock.current_time_milliseconds(env)
}
/// Register a typed activity mock for the current test process.
pub fn mock_activity(
env: TestEnv,
activity_value: Activity(input, output),
handler: fn(input) -> Result(output, error.ActivityError),
) -> Result(TestEnv, error.EngineError) {
mock.activity(env, activity_value, handler)
}
/// Register a typed child-workflow double for the current test process.
///
/// `workflow.spawn_and_wait` calls with the same child name run `handler`
/// in-process and record its typed result as the child terminal. Register the
/// child module's real `execute` function to exercise full parent-child
/// composition under `gleam test`.
pub fn mock_child(
env: TestEnv,
name: String,
input_codec: Codec(input),
output_codec: Codec(output),
error_codec: Codec(workflow_error),
handler: fn(input) -> Result(output, workflow_error),
) -> Result(TestEnv, error.EngineError) {
mock.child(env, name, input_codec, output_codec, error_codec, handler)
}
/// Capture the current observation sequence emitted by the test FFI double.
pub fn observations(_env: TestEnv) -> Result(String, error.EngineError) {
case ffi.testing_observations() {
Ok(raw) -> Ok(raw)
Error(raw_error) -> Error(error.EngineFailure(raw_error))
}
}
/// Clear the observation sequence for the current process.
pub fn clear_observations(env: TestEnv) -> Result(TestEnv, error.EngineError) {
case ffi.testing_clear_observations() {
Ok(_) -> Ok(env)
Error(raw_error) -> Error(error.EngineFailure(raw_error))
}
}
/// Assert that a workflow emits the same observation sequence on a second run.
///
/// This mirrors AD's production non-determinism detection in a lightweight test
/// harness: if replay emits different observable commands, the helper returns a
/// clear `ReplayError` diagnostic instead of requiring a live engine.
pub fn assert_replay(
env: TestEnv,
workflow: fn() -> Result(value, workflow_error),
) -> Result(value, replay.ReplayError(workflow_error)) {
replay.assert_replay(env, workflow)
}