-module(telega@testing@conversation). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/telega/testing/conversation.gleam"). -export([conversation_test/0, send/2, send_callback/2, send_photo/1, send_photo_with/2, send_video/1, send_video_with/2, send_audio/1, send_audio_with/2, send_voice/1, send_voice_with/2, send_message/2, expect_reply/2, expect_reply_containing/2, expect_keyboard/2, expect_reply_with_keyboard/3, expect_api_call/3, run_with_client_with_dependencies/6, run_with_client/5, run_with/3, run/3, run_with_mock_with_dependencies/6, run_with_mock/5, run_with_dependencies/4]). -export_type([conversation_test/0, step/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( " Declarative conversation testing DSL for integration testing bot flows.\n" "\n" " ```gleam\n" " import telega/testing/conversation\n" "\n" " conversation.conversation_test()\n" " |> conversation.send(\"/start\")\n" " |> conversation.expect_reply_containing(\"Hello\")\n" " |> conversation.send(\"/set_name\")\n" " |> conversation.expect_reply(\"What's your name?\")\n" " |> conversation.send(\"Alice\")\n" " |> conversation.expect_reply_containing(\"Alice\")\n" " |> conversation.run(router, fn() { Nil })\n" " ```\n" ). -opaque conversation_test() :: {conversation_test, list(step())}. -type step() :: {send_text, binary()} | {send_command, binary(), gleam@option:option(binary())} | {send_callback, binary()} | {send_photo, list(telega@model@types:photo_size())} | {send_video, telega@model@types:video()} | {send_audio, telega@model@types:audio()} | {send_voice, telega@model@types:voice()} | {send_message_update, telega@model@types:message()} | {expect_reply, binary()} | {expect_reply_containing, binary()} | {expect_keyboard, list(binary())} | {expect_reply_with_keyboard, binary(), list(binary())} | {expect_api_call, binary(), binary()}. -file("src/telega/testing/conversation.gleam", 57). ?DOC(" Creates a new empty conversation test.\n"). -spec conversation_test() -> conversation_test(). conversation_test() -> {conversation_test, []}. -file("src/telega/testing/conversation.gleam", 63). ?DOC( " Sends a message. Text starting with `/` is auto-detected as a command.\n" " Command payloads are supported: `/cmd payload here`.\n" ). -spec send(conversation_test(), binary()) -> conversation_test(). send(Ct, Text) -> Step = case gleam_stdlib:string_starts_with(Text, <<"/"/utf8>>) of true -> Without_slash = gleam@string:drop_start(Text, 1), case gleam@string:split_once(Without_slash, <<" "/utf8>>) of {ok, {Cmd, Payload}} -> {send_command, Cmd, {some, Payload}}; {error, _} -> {send_command, Without_slash, none} end; false -> {send_text, Text} end, {conversation_test, lists:append(erlang:element(2, Ct), [Step])}. -file("src/telega/testing/conversation.gleam", 78). ?DOC(" Sends a callback query with the given data.\n"). -spec send_callback(conversation_test(), binary()) -> conversation_test(). send_callback(Ct, Data) -> {conversation_test, lists:append(erlang:element(2, Ct), [{send_callback, Data}])}. -file("src/telega/testing/conversation.gleam", 83). ?DOC(" Sends a photo message with a single default photo.\n"). -spec send_photo(conversation_test()) -> conversation_test(). send_photo(Ct) -> {conversation_test, lists:append( erlang:element(2, Ct), [{send_photo, [telega@testing@factory:photo_size()]}] )}. -file("src/telega/testing/conversation.gleam", 90). ?DOC(" Sends a photo message with custom photos.\n"). -spec send_photo_with( conversation_test(), list(telega@model@types:photo_size()) ) -> conversation_test(). send_photo_with(Ct, Photos) -> {conversation_test, lists:append(erlang:element(2, Ct), [{send_photo, Photos}])}. -file("src/telega/testing/conversation.gleam", 98). ?DOC(" Sends a video message with a default video.\n"). -spec send_video(conversation_test()) -> conversation_test(). send_video(Ct) -> {conversation_test, lists:append( erlang:element(2, Ct), [{send_video, telega@testing@factory:video()}] )}. -file("src/telega/testing/conversation.gleam", 105). ?DOC(" Sends a video message with a custom video.\n"). -spec send_video_with(conversation_test(), telega@model@types:video()) -> conversation_test(). send_video_with(Ct, Video) -> {conversation_test, lists:append(erlang:element(2, Ct), [{send_video, Video}])}. -file("src/telega/testing/conversation.gleam", 113). ?DOC(" Sends an audio message with a default audio.\n"). -spec send_audio(conversation_test()) -> conversation_test(). send_audio(Ct) -> {conversation_test, lists:append( erlang:element(2, Ct), [{send_audio, telega@testing@factory:audio()}] )}. -file("src/telega/testing/conversation.gleam", 120). ?DOC(" Sends an audio message with custom audio.\n"). -spec send_audio_with(conversation_test(), telega@model@types:audio()) -> conversation_test(). send_audio_with(Ct, Audio) -> {conversation_test, lists:append(erlang:element(2, Ct), [{send_audio, Audio}])}. -file("src/telega/testing/conversation.gleam", 128). ?DOC(" Sends a voice message with a default voice.\n"). -spec send_voice(conversation_test()) -> conversation_test(). send_voice(Ct) -> {conversation_test, lists:append( erlang:element(2, Ct), [{send_voice, telega@testing@factory:voice()}] )}. -file("src/telega/testing/conversation.gleam", 135). ?DOC(" Sends a voice message with custom voice.\n"). -spec send_voice_with(conversation_test(), telega@model@types:voice()) -> conversation_test(). send_voice_with(Ct, Voice) -> {conversation_test, lists:append(erlang:element(2, Ct), [{send_voice, Voice}])}. -file("src/telega/testing/conversation.gleam", 143). ?DOC(" Sends a raw message update with a user-provided message.\n"). -spec send_message(conversation_test(), telega@model@types:message()) -> conversation_test(). send_message(Ct, Message) -> {conversation_test, lists:append(erlang:element(2, Ct), [{send_message_update, Message}])}. -file("src/telega/testing/conversation.gleam", 151). ?DOC(" Expects the next reply to exactly match the given text.\n"). -spec expect_reply(conversation_test(), binary()) -> conversation_test(). expect_reply(Ct, Text) -> {conversation_test, lists:append(erlang:element(2, Ct), [{expect_reply, Text}])}. -file("src/telega/testing/conversation.gleam", 156). ?DOC(" Expects the next reply to contain the given substring.\n"). -spec expect_reply_containing(conversation_test(), binary()) -> conversation_test(). expect_reply_containing(Ct, Substring) -> {conversation_test, lists:append( erlang:element(2, Ct), [{expect_reply_containing, Substring}] )}. -file("src/telega/testing/conversation.gleam", 166). ?DOC(" Expects the next reply to include an inline keyboard containing all specified button texts.\n"). -spec expect_keyboard(conversation_test(), list(binary())) -> conversation_test(). expect_keyboard(Ct, Buttons) -> {conversation_test, lists:append(erlang:element(2, Ct), [{expect_keyboard, Buttons}])}. -file("src/telega/testing/conversation.gleam", 174). ?DOC(" Expects the next reply to contain the given substring AND include a keyboard with specified buttons.\n"). -spec expect_reply_with_keyboard(conversation_test(), binary(), list(binary())) -> conversation_test(). expect_reply_with_keyboard(Ct, Substring, Buttons) -> {conversation_test, lists:append( erlang:element(2, Ct), [{expect_reply_with_keyboard, Substring, Buttons}] )}. -file("src/telega/testing/conversation.gleam", 188). ?DOC( " Expects the next API call to have a path containing `path_contains`\n" " and a body containing `body_contains`.\n" ). -spec expect_api_call(conversation_test(), binary(), binary()) -> conversation_test(). expect_api_call(Ct, Path_contains, Body_contains) -> {conversation_test, lists:append( erlang:element(2, Ct), [{expect_api_call, Path_contains, Body_contains}] )}. -file("src/telega/testing/conversation.gleam", 576). -spec receive_next_call_matching( gleam@erlang@process:subject(telega@testing@mock:api_call()), binary(), integer() ) -> telega@testing@mock:api_call(). receive_next_call_matching(Calls, Path_contains, Attempts) -> case Attempts > 20 of true -> erlang:error(#{gleam_error => panic, message => (<<<<"No API call with path containing '"/utf8, Path_contains/binary>>/binary, "' received after waiting"/utf8>>), file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"receive_next_call_matching"/utf8>>, line => 583}); false -> case gleam@erlang@process:'receive'(Calls, 100) of {ok, Call} -> case gleam_stdlib:contains_string( erlang:element(8, erlang:element(2, Call)), Path_contains ) of true -> Call; false -> receive_next_call_matching( Calls, Path_contains, Attempts + 1 ) end; {error, _} -> receive_next_call_matching( Calls, Path_contains, Attempts + 1 ) end end. -file("src/telega/testing/conversation.gleam", 558). -spec assert_next_api_call( gleam@erlang@process:subject(telega@testing@mock:api_call()), binary(), binary() ) -> nil. assert_next_api_call(Calls, Path_contains, Body_contains) -> Call = receive_next_call_matching(Calls, Path_contains, 0), case gleam_stdlib:contains_string( erlang:element(4, erlang:element(2, Call)), Body_contains ) of true -> nil; false -> erlang:error(#{gleam_error => panic, message => (<<<<<<"Expected API call body to contain '"/utf8, Body_contains/binary>>/binary, "', got: "/utf8>>/binary, (erlang:element(4, erlang:element(2, Call)))/binary>>), file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"assert_next_api_call"/utf8>>, line => 567}) end. -file("src/telega/testing/conversation.gleam", 524). -spec assert_body_keyboard(binary(), list(binary())) -> nil. assert_body_keyboard(Body, Expected_buttons) -> case gleam_stdlib:contains_string(Body, <<"inline_keyboard"/utf8>>) of false -> erlang:error(#{gleam_error => panic, message => (<<"Expected reply to contain an inline keyboard, but none found in: "/utf8, Body/binary>>), file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"assert_body_keyboard"/utf8>>, line => 529}); true -> gleam@list:each( Expected_buttons, fun(Button_text) -> case gleam_stdlib:contains_string(Body, Button_text) of true -> nil; false -> erlang:error(#{gleam_error => panic, message => (<<<<<<"Expected keyboard button '"/utf8, Button_text/binary>>/binary, "' not found in reply body: "/utf8>>/binary, Body/binary>>), file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"assert_body_keyboard"/utf8>>, line => 538}) end end ) end. -file("src/telega/testing/conversation.gleam", 511). -spec assert_body_text(binary(), fun((binary()) -> nil)) -> nil. assert_body_text(Body, Check) -> Text_result = gleam@json:parse( Body, begin gleam@dynamic@decode:field( <<"text"/utf8>>, {decoder, fun gleam@dynamic@decode:decode_string/1}, fun(Text) -> gleam@dynamic@decode:success(Text) end ) end ), case Text_result of {ok, Text@1} -> Check(Text@1); {error, _} -> erlang:error(#{gleam_error => panic, message => (<<"Could not extract 'text' from API call body: "/utf8, Body/binary>>), file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"assert_body_text"/utf8>>, line => 520}) end. -file("src/telega/testing/conversation.gleam", 478). -spec receive_next_send_message( gleam@erlang@process:subject(telega@testing@mock:api_call()), integer() ) -> telega@testing@mock:api_call(). receive_next_send_message(Calls, Attempts) -> case Attempts > 20 of true -> erlang:error(#{gleam_error => panic, message => <<"Expected a reply but no sendMessage API call was received after waiting"/utf8>>, file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"receive_next_send_message"/utf8>>, line => 484}); false -> case gleam@erlang@process:'receive'(Calls, 100) of {ok, Call} -> Is_message_call = gleam_stdlib:contains_string( erlang:element(8, erlang:element(2, Call)), <<"sendMessage"/utf8>> ) orelse gleam_stdlib:contains_string( erlang:element(8, erlang:element(2, Call)), <<"editMessageText"/utf8>> ), case Is_message_call of true -> Call; false -> receive_next_send_message(Calls, Attempts + 1) end; {error, _} -> receive_next_send_message(Calls, Attempts + 1) end end. -file("src/telega/testing/conversation.gleam", 550). -spec assert_next_reply_keyboard( gleam@erlang@process:subject(telega@testing@mock:api_call()), list(binary()) ) -> nil. assert_next_reply_keyboard(Calls, Expected_buttons) -> Call = receive_next_send_message(Calls, 0), assert_body_keyboard( erlang:element(4, erlang:element(2, Call)), Expected_buttons ). -file("src/telega/testing/conversation.gleam", 503). -spec assert_next_reply( gleam@erlang@process:subject(telega@testing@mock:api_call()), fun((binary()) -> nil) ) -> nil. assert_next_reply(Calls, Check) -> Call = receive_next_send_message(Calls, 0), assert_body_text(erlang:element(4, erlang:element(2, Call)), Check). -file("src/telega/testing/conversation.gleam", 347). -spec execute_steps( list(step()), gleam@erlang@process:subject(telega@bot:bot_message()), gleam@erlang@process:subject(telega@testing@mock:api_call()) ) -> nil. execute_steps(Steps, Bot_subject, Calls) -> case Steps of [] -> nil; [Step | Rest] -> case Step of {send_text, Text} -> Update = telega@testing@factory:text_update(Text), telega@bot:handle_update(Bot_subject, Update), nil; {send_command, Command, Payload} -> Update@1 = telega@testing@factory:command_update_with( Command, Payload, 987654321, 123456789 ), telega@bot:handle_update(Bot_subject, Update@1), nil; {send_callback, Data} -> Update@2 = telega@testing@factory:callback_query_update( Data ), telega@bot:handle_update(Bot_subject, Update@2), nil; {send_photo, Photos} -> Update@3 = telega@testing@factory:photo_update_with( Photos, 987654321, 123456789 ), telega@bot:handle_update(Bot_subject, Update@3), nil; {send_video, Video} -> Update@4 = telega@testing@factory:video_update_with( Video, 987654321, 123456789 ), telega@bot:handle_update(Bot_subject, Update@4), nil; {send_audio, Audio} -> Update@5 = telega@testing@factory:audio_update_with( Audio, 987654321, 123456789 ), telega@bot:handle_update(Bot_subject, Update@5), nil; {send_voice, Voice} -> Update@6 = telega@testing@factory:voice_update_with( Voice, 987654321, 123456789 ), telega@bot:handle_update(Bot_subject, Update@6), nil; {send_message_update, Message} -> Update@7 = telega@testing@factory:message_update(Message), telega@bot:handle_update(Bot_subject, Update@7), nil; {expect_reply, Text@1} -> assert_next_reply( Calls, fun(Reply_text) -> case Reply_text =:= Text@1 of true -> nil; false -> erlang:error(#{gleam_error => panic, message => (<<<<<<<<"Expected reply '"/utf8, Text@1/binary>>/binary, "', got '"/utf8>>/binary, Reply_text/binary>>/binary, "'"/utf8>>), file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"execute_steps"/utf8>>, line => 427}) end end ); {expect_reply_containing, Substring} -> assert_next_reply( Calls, fun(Reply_text@1) -> case gleam_stdlib:contains_string( Reply_text@1, Substring ) of true -> nil; false -> erlang:error(#{gleam_error => panic, message => (<<<<<<<<"Expected reply containing '"/utf8, Substring/binary>>/binary, "', got '"/utf8>>/binary, Reply_text@1/binary>>/binary, "'"/utf8>>), file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"execute_steps"/utf8>>, line => 438}) end end ); {expect_keyboard, Buttons} -> assert_next_reply_keyboard(Calls, Buttons); {expect_reply_with_keyboard, Substring@1, Buttons@1} -> Call = receive_next_send_message(Calls, 0), Body = erlang:element(4, erlang:element(2, Call)), assert_body_text( Body, fun(Reply_text@2) -> case gleam_stdlib:contains_string( Reply_text@2, Substring@1 ) of true -> nil; false -> erlang:error(#{gleam_error => panic, message => (<<<<<<<<"Expected reply containing '"/utf8, Substring@1/binary>>/binary, "', got '"/utf8>>/binary, Reply_text@2/binary>>/binary, "'"/utf8>>), file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"execute_steps"/utf8>>, line => 458}) end end ), assert_body_keyboard(Body, Buttons@1); {expect_api_call, Path_contains, Body_contains} -> assert_next_api_call(Calls, Path_contains, Body_contains) end, execute_steps(Rest, Bot_subject, Calls) end. -file("src/telega/testing/conversation.gleam", 311). ?DOC( " Lowest-level dependencies-aware runner: custom client, calls subject, router handler,\n" " session settings, and injected `dependencies`. All the other `run_*` helpers funnel\n" " through this — use it directly when you need full control over every input.\n" ). -spec run_with_client_with_dependencies( conversation_test(), telega@client:telegram_client(), gleam@erlang@process:subject(telega@testing@mock:api_call()), fun((telega@bot:context(BOXY, BOXZ, BOYA), telega@update:update()) -> {ok, telega@bot:context(BOXY, BOXZ, BOYA)} | {error, BOXZ}), telega@bot:session_settings(BOXY, BOXZ), BOYA ) -> nil. run_with_client_with_dependencies( Ct, Client, Calls, Router_handler, Session_settings, Dependencies ) -> Config = telega@testing@context:config_with_client(Client), Reg@1 = case telega@internal@registry:start(<<"test_conversation"/utf8>>) of {ok, Reg} -> Reg; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"run_with_client_with_dependencies"/utf8>>, line => 322, value => _assert_fail, start => 10116, 'end' => 10172, pattern_start => 10127, pattern_end => 10134}) end, Chat_factory_started@1 = case begin _pipe = gleam@otp@factory_supervisor:worker_child( fun telega@bot:start_chat_instance/1 ), _pipe@1 = gleam@otp@factory_supervisor:restart_strategy( _pipe, transient ), gleam@otp@factory_supervisor:start(_pipe@1) end of {ok, Chat_factory_started} -> Chat_factory_started; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"run_with_client_with_dependencies"/utf8>>, line => 323, value => _assert_fail@1, start => 10175, 'end' => 10329, pattern_start => 10186, pattern_end => 10210}) end, Started@1 = case telega@bot:start( Reg@1, Config, telega@testing@factory:bot_user(), Router_handler, [], Session_settings, fun(_, _) -> {ok, nil} end, Dependencies, erlang:element(3, Chat_factory_started@1), none ) of {ok, Started} -> Started; _assert_fail@2 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"telega/testing/conversation"/utf8>>, function => <<"run_with_client_with_dependencies"/utf8>>, line => 328, value => _assert_fail@2, start => 10333, 'end' => 10657, pattern_start => 10344, pattern_end => 10355}) end, execute_steps(erlang:element(2, Ct), erlang:element(3, Started@1), Calls), _ = telega@internal@registry:stop(Reg@1), nil. -file("src/telega/testing/conversation.gleam", 263). ?DOC(" Runs the test with a custom client, calls subject, router handler, and session settings.\n"). -spec run_with_client( conversation_test(), telega@client:telegram_client(), gleam@erlang@process:subject(telega@testing@mock:api_call()), fun((telega@bot:context(BOXF, BOXG, nil), telega@update:update()) -> {ok, telega@bot:context(BOXF, BOXG, nil)} | {error, BOXG}), telega@bot:session_settings(BOXF, BOXG) ) -> nil. run_with_client(Ct, Client, Calls, Router_handler, Session_settings) -> run_with_client_with_dependencies( Ct, Client, Calls, Router_handler, Session_settings, nil ). -file("src/telega/testing/conversation.gleam", 214). ?DOC(" Lower-level variant: runs the test with a custom router handler and session settings.\n"). -spec run_with( conversation_test(), fun((telega@bot:context(BOWF, BOWG, nil), telega@update:update()) -> {ok, telega@bot:context(BOWF, BOWG, nil)} | {error, BOWG}), telega@bot:session_settings(BOWF, BOWG) ) -> nil. run_with(Ct, Router_handler, Session_settings) -> {Client, Calls} = telega@testing@mock:message_client(), run_with_client(Ct, Client, Calls, Router_handler, Session_settings). -file("src/telega/testing/conversation.gleam", 201). ?DOC(" Runs the conversation test against a router with the given default session factory.\n"). -spec run( conversation_test(), telega@router:router(BOWA, any(), nil), fun(() -> BOWA) ) -> nil. run(Ct, Router, Default_session) -> run_with( Ct, fun(Ctx, Update) -> telega@router:handle(Router, Ctx, Update) end, telega@testing@context:session_settings(Default_session) ). -file("src/telega/testing/conversation.gleam", 244). ?DOC( " Like `run_with_mock`, but for a router whose handlers read services from\n" " `ctx.dependencies`. Combines a custom mock client with injected `dependencies`.\n" ). -spec run_with_mock_with_dependencies( conversation_test(), telega@router:router(BOWX, any(), BOWZ), fun(() -> BOWX), telega@client:telegram_client(), gleam@erlang@process:subject(telega@testing@mock:api_call()), BOWZ ) -> nil. run_with_mock_with_dependencies( Ct, Router, Default_session, Client, Calls, Dependencies ) -> run_with_client_with_dependencies( Ct, Client, Calls, fun(Ctx, Update) -> telega@router:handle(Router, Ctx, Update) end, telega@testing@context:session_settings(Default_session), Dependencies ). -file("src/telega/testing/conversation.gleam", 225). ?DOC(" Runs the test with a custom mock client (e.g. from `mock.routed_client`).\n"). -spec run_with_mock( conversation_test(), telega@router:router(BOWR, any(), nil), fun(() -> BOWR), telega@client:telegram_client(), gleam@erlang@process:subject(telega@testing@mock:api_call()) ) -> nil. run_with_mock(Ct, Router, Default_session, Client, Calls) -> run_with_mock_with_dependencies( Ct, Router, Default_session, Client, Calls, nil ). -file("src/telega/testing/conversation.gleam", 291). ?DOC( " Runs the conversation test against a router that needs injected dependencies\n" " (`dependencies`). Use this to drive a bot whose handlers read services from\n" " `ctx.dependencies` through the conversation DSL.\n" "\n" " ```gleam\n" " conversation.conversation_test()\n" " |> conversation.send(\"/my_bookings\")\n" " |> conversation.expect_reply_containing(\"No bookings\")\n" " |> conversation.run_with_dependencies(build_router(), fn() { Nil }, Dependencies(db:, catalog:))\n" " ```\n" ). -spec run_with_dependencies( conversation_test(), telega@router:router(BOXR, any(), BOXT), fun(() -> BOXR), BOXT ) -> nil. run_with_dependencies(Ct, Router, Default_session, Dependencies) -> {Client, Calls} = telega@testing@mock:message_client(), run_with_client_with_dependencies( Ct, Client, Calls, fun(Ctx, Update) -> telega@router:handle(Router, Ctx, Update) end, telega@testing@context:session_settings(Default_session), Dependencies ).