Skip to main content

src/aion@testing.erl

-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).