Skip to main content

src/aion@testing@mock.erl

-module(aion@testing@mock).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aion/testing/mock.gleam").
-export([activity/3, child/6]).

-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(
    " Typed activity and child-workflow mock registries for `aion/testing`.\n"
    "\n"
    " Tests register activity mocks against an `Activity(i, o)` value so the\n"
    " handler is statically checked against the same input and output types that\n"
    " `workflow.run` will use, and child doubles against the input/output/error\n"
    " codecs that `workflow.spawn_and_wait` will use. The test-only FFI double\n"
    " stores a type-erased wrapper in process-scoped state and intercepts\n"
    " activity dispatch and child spawn by name.\n"
).

-file("src/aion/testing/mock.gleam", 82).
-spec activity_error_to_raw(aion@error:activity_error()) -> binary().
activity_error_to_raw(Activity_error) ->
    case Activity_error of
        {retryable, Message, _} ->
            <<"retryable:"/utf8, Message/binary>>;

        {terminal, Message@1, _} ->
            <<"terminal:"/utf8, Message@1/binary>>;

        {activity_decode_failed, Decode_error} ->
            <<"terminal:activity decode failed: "/utf8,
                (erlang:element(2, Decode_error))/binary>>;

        {activity_timed_out, {timed_out, Message@2}} ->
            <<"timeout:"/utf8, Message@2/binary>>;

        {activity_cancelled, {cancelled, Reason}} ->
            <<"cancelled:"/utf8, Reason/binary>>;

        {activity_non_deterministic, {non_determinism_violation, Message@3}} ->
            <<"non_determinism:"/utf8, Message@3/binary>>;

        {activity_engine_failure, Message@4} ->
            Message@4
    end.

-file("src/aion/testing/mock.gleam", 19).
?DOC(
    " Register a typed activity mock for the current process.\n"
    "\n"
    " A handler whose input or output type does not match the supplied activity will\n"
    " fail at `gleam build`, before the workflow test can run.\n"
).
-spec activity(
    EKA,
    aion@activity:activity(EKB, EKC),
    fun((EKB) -> {ok, EKC} | {error, aion@error:activity_error()})
) -> {ok, EKA} | {error, aion@error:engine_error()}.
activity(Env, Activity_value, Handler) ->
    Input_codec = aion@activity:input_codec(Activity_value),
    Output_codec = aion@activity:output_codec(Activity_value),
    Name = aion@activity:name(Activity_value),
    Raw_handler = fun(Raw_input) ->
        case (erlang:element(3, Input_codec))(Raw_input) of
            {ok, Typed_input} ->
                case Handler(Typed_input) of
                    {ok, Typed_output} ->
                        {ok, (erlang:element(2, Output_codec))(Typed_output)};

                    {error, Activity_error} ->
                        {error, activity_error_to_raw(Activity_error)}
                end;

            {error, Decode_error} ->
                {error,
                    <<"terminal:mock input decode failed: "/utf8,
                        (erlang:element(2, Decode_error))/binary>>}
        end
    end,
    case aion_flow_ffi:testing_register_activity_mock(Name, Raw_handler) of
        {ok, _} ->
            {ok, Env};

        {error, Raw_error} ->
            {error, {engine_failure, Raw_error}}
    end.

-file("src/aion/testing/mock.gleam", 54).
?DOC(
    " Register a typed child-workflow double for the current test process.\n"
    "\n"
    " `workflow.spawn_and_wait(name, ...)` calls with the same `name` execute\n"
    " `handler` synchronously and record its typed result as the child terminal:\n"
    " `Ok` is decoded by the parent's output codec and a typed `Error` surfaces\n"
    " as `error.ChildWorkflowFailed`. Registering the child module's real\n"
    " `execute` function as the handler runs the full child workflow body —\n"
    " including its own activity dispatches against this process's activity\n"
    " mocks — inside the parent test.\n"
).
-spec child(
    EKJ,
    binary(),
    aion@codec:codec(EKK),
    aion@codec:codec(EKM),
    aion@codec:codec(EKO),
    fun((EKK) -> {ok, EKM} | {error, EKO})
) -> {ok, EKJ} | {error, aion@error:engine_error()}.
child(
    Env,
    Name,
    Child_input_codec,
    Child_output_codec,
    Child_error_codec,
    Handler
) ->
    Raw_handler = fun(Raw_input) ->
        case (erlang:element(3, Child_input_codec))(Raw_input) of
            {ok, Typed_input} ->
                case Handler(Typed_input) of
                    {ok, Typed_output} ->
                        {ok,
                            <<"ok:"/utf8,
                                ((erlang:element(2, Child_output_codec))(
                                    Typed_output
                                ))/binary>>};

                    {error, Workflow_error} ->
                        {ok,
                            <<"error:"/utf8,
                                ((erlang:element(2, Child_error_codec))(
                                    Workflow_error
                                ))/binary>>}
                end;

            {error, Decode_error} ->
                {error,
                    <<"child mock input decode failed: "/utf8,
                        (erlang:element(2, Decode_error))/binary>>}
        end
    end,
    case aion_flow_ffi:testing_register_child_mock(Name, Raw_handler) of
        {ok, _} ->
            {ok, Env};

        {error, Raw_error} ->
            {error, {engine_failure, Raw_error}}
    end.