//// Typed activity and child-workflow mock registries for `aion/testing`. //// //// Tests register activity mocks against an `Activity(i, o)` value so the //// handler is statically checked against the same input and output types that //// `workflow.run` will use, and child doubles against the input/output/error //// codecs that `workflow.spawn_and_wait` will use. The test-only FFI double //// stores a type-erased wrapper in process-scoped state and intercepts //// activity dispatch and child spawn by name. import aion/activity.{type Activity, input_codec, name, output_codec} import aion/codec.{type Codec} import aion/error import aion/internal/ffi /// Register a typed activity mock for the current process. /// /// A handler whose input or output type does not match the supplied activity will /// fail at `gleam build`, before the workflow test can run. pub fn activity( env: env, activity_value: Activity(input, output), handler: fn(input) -> Result(output, error.ActivityError), ) -> Result(env, error.EngineError) { let input_codec = input_codec(activity_value) let output_codec = output_codec(activity_value) let name = name(activity_value) let raw_handler = fn(raw_input: String) { case input_codec.decode(raw_input) { Ok(typed_input) -> case handler(typed_input) { Ok(typed_output) -> Ok(output_codec.encode(typed_output)) Error(activity_error) -> Error(activity_error_to_raw(activity_error)) } Error(decode_error) -> Error("terminal:mock input decode failed: " <> decode_error.reason) } } case ffi.testing_register_activity_mock(name, raw_handler) { Ok(_) -> Ok(env) Error(raw_error) -> Error(error.EngineFailure(raw_error)) } } /// Register a typed child-workflow double for the current test process. /// /// `workflow.spawn_and_wait(name, ...)` calls with the same `name` execute /// `handler` synchronously and record its typed result as the child terminal: /// `Ok` is decoded by the parent's output codec and a typed `Error` surfaces /// as `error.ChildWorkflowFailed`. Registering the child module's real /// `execute` function as the handler runs the full child workflow body — /// including its own activity dispatches against this process's activity /// mocks — inside the parent test. pub fn child( env: env, name: String, child_input_codec: Codec(input), child_output_codec: Codec(output), child_error_codec: Codec(workflow_error), handler: fn(input) -> Result(output, workflow_error), ) -> Result(env, error.EngineError) { let raw_handler = fn(raw_input: String) { case child_input_codec.decode(raw_input) { Ok(typed_input) -> case handler(typed_input) { Ok(typed_output) -> Ok("ok:" <> child_output_codec.encode(typed_output)) Error(workflow_error) -> Ok("error:" <> child_error_codec.encode(workflow_error)) } Error(decode_error) -> Error("child mock input decode failed: " <> decode_error.reason) } } case ffi.testing_register_child_mock(name, raw_handler) { Ok(_) -> Ok(env) Error(raw_error) -> Error(error.EngineFailure(raw_error)) } } fn activity_error_to_raw(activity_error: error.ActivityError) -> String { case activity_error { error.Retryable(message: message, details: _) -> "retryable:" <> message error.Terminal(message: message, details: _) -> "terminal:" <> message error.ActivityDecodeFailed(decode_error) -> "terminal:activity decode failed: " <> decode_error.reason error.ActivityTimedOut(error.TimedOut(message: message)) -> "timeout:" <> message error.ActivityCancelled(error.Cancelled(reason: reason)) -> "cancelled:" <> reason error.ActivityNonDeterministic(error.NonDeterminismViolation( message: message, )) -> "non_determinism:" <> message error.ActivityEngineFailure(message: message) -> message } }