-module(aion@testing). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/aion/testing.gleam"). -export([new/0, process_key/1, run/1, advance/2, current_time_milliseconds/1, mock_activity/3, mock_child/6, observations/1, clear_observations/1, assert_replay/2]). -export_type([test_env/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Pure Gleam workflow test harness.\n" "\n" " `aion/testing` is the recommended way to test workflow author code. It runs\n" " under `gleam test` with no engine, beamr, store, external services, or Rust\n" " NIFs. Test code initialises a process-scoped `TestEnv`; the test-only\n" " Erlang module `test/aion_flow_ffi.erl` occupies the same production FFI\n" " namespace so workflow code and `@external` declarations are byte-identical\n" " in tests and production.\n" ). -opaque test_env() :: {test_env, binary()}. -file("src/aion/testing.gleam", 32). ?DOC( " Build a fresh `TestEnv` for the current test process.\n" "\n" " The simulated clock, activity mock registry, child/query/signal fixtures, and\n" " observation capture are reset for the current process only.\n" ). -spec new() -> {ok, test_env()} | {error, aion@error:engine_error()}. new() -> case aion_flow_ffi:testing_reset() of {ok, Process_key} -> {ok, {test_env, Process_key}}; {error, Raw_error} -> {error, {engine_failure, Raw_error}} end. -file("src/aion/testing.gleam", 40). ?DOC(" Return the process key assigned by the test FFI double.\n"). -spec process_key(test_env()) -> binary(). process_key(Env) -> erlang:element(2, Env). -file("src/aion/testing.gleam", 45). ?DOC(" Run a workflow thunk under a fresh process-scoped test environment.\n"). -spec run(fun((test_env()) -> ERE)) -> {ok, ERE} | {error, aion@error:engine_error()}. run(Workflow) -> case new() of {ok, Env} -> {ok, Workflow(Env)}; {error, Engine_error} -> {error, Engine_error} end. -file("src/aion/testing.gleam", 53). ?DOC(" Advance the simulated test clock by a canonical duration.\n"). -spec advance(test_env(), aion@duration:duration()) -> {ok, test_env()} | {error, aion@error:engine_error()}. advance(Env, By) -> aion@testing@clock:advance(Env, By). -file("src/aion/testing.gleam", 61). ?DOC(" Return the current simulated clock value in milliseconds.\n"). -spec current_time_milliseconds(test_env()) -> {ok, integer()} | {error, aion@error:engine_error()}. current_time_milliseconds(Env) -> aion@testing@clock:current_time_milliseconds(Env). -file("src/aion/testing.gleam", 68). ?DOC(" Register a typed activity mock for the current test process.\n"). -spec mock_activity( test_env(), aion@activity:activity(ERL, ERM), fun((ERL) -> {ok, ERM} | {error, aion@error:activity_error()}) ) -> {ok, test_env()} | {error, aion@error:engine_error()}. mock_activity(Env, Activity_value, Handler) -> aion@testing@mock:activity(Env, Activity_value, Handler). -file("src/aion/testing.gleam", 82). ?DOC( " Register a typed child-workflow double for the current test process.\n" "\n" " `workflow.spawn_and_wait` calls with the same child name run `handler`\n" " in-process and record its typed result as the child terminal. Register the\n" " child module's real `execute` function to exercise full parent-child\n" " composition under `gleam test`.\n" ). -spec mock_child( test_env(), binary(), aion@codec:codec(ERT), aion@codec:codec(ERV), aion@codec:codec(ERX), fun((ERT) -> {ok, ERV} | {error, ERX}) ) -> {ok, test_env()} | {error, aion@error:engine_error()}. mock_child(Env, Name, Input_codec, Output_codec, Error_codec, Handler) -> aion@testing@mock:child( Env, Name, Input_codec, Output_codec, Error_codec, Handler ). -file("src/aion/testing.gleam", 94). ?DOC(" Capture the current observation sequence emitted by the test FFI double.\n"). -spec observations(test_env()) -> {ok, binary()} | {error, aion@error:engine_error()}. observations(_) -> case aion_flow_ffi:testing_observations() of {ok, Raw} -> {ok, Raw}; {error, Raw_error} -> {error, {engine_failure, Raw_error}} end. -file("src/aion/testing.gleam", 102). ?DOC(" Clear the observation sequence for the current process.\n"). -spec clear_observations(test_env()) -> {ok, test_env()} | {error, aion@error:engine_error()}. clear_observations(Env) -> case aion_flow_ffi:testing_clear_observations() of {ok, _} -> {ok, Env}; {error, Raw_error} -> {error, {engine_failure, Raw_error}} end. -file("src/aion/testing.gleam", 114). ?DOC( " Assert that a workflow emits the same observation sequence on a second run.\n" "\n" " This mirrors AD's production non-determinism detection in a lightweight test\n" " harness: if replay emits different observable commands, the helper returns a\n" " clear `ReplayError` diagnostic instead of requiring a live engine.\n" ). -spec assert_replay(test_env(), fun(() -> {ok, ESH} | {error, ESI})) -> {ok, ESH} | {error, aion@testing@replay:replay_error(ESI)}. assert_replay(Env, Workflow) -> aion@testing@replay:assert_replay(Env, Workflow).