disable external connections while testing with mimix
Some checks failed
CI/CD Pipeline / build (push) Failing after 3s
Some checks failed
CI/CD Pipeline / build (push) Failing after 3s
This commit is contained in:
@@ -36,7 +36,6 @@ defmodule ElixirAi.ChatUtils do
|
|||||||
%{
|
%{
|
||||||
name: name,
|
name: name,
|
||||||
definition: schema,
|
definition: schema,
|
||||||
# function: function,
|
|
||||||
run_function: run_function
|
run_function: run_function
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
defmodule ElixirAi.ChatRunner do
|
defmodule ElixirAi.ChatRunner do
|
||||||
require Logger
|
require Logger
|
||||||
use GenServer
|
use GenServer
|
||||||
import ElixirAi.ChatUtils
|
import ElixirAi.ChatUtils, only: [ai_tool: 1]
|
||||||
alias ElixirAi.{Conversation, Message}
|
alias ElixirAi.{Conversation, Message}
|
||||||
|
|
||||||
defp via(name), do: {:via, Horde.Registry, {ElixirAi.ChatRegistry, name}}
|
defp via(name), do: {:via, Horde.Registry, {ElixirAi.ChatRegistry, name}}
|
||||||
@@ -39,7 +39,7 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
"Last message role was #{last_message.role}, requesting AI response for conversation #{name}"
|
"Last message role was #{last_message.role}, requesting AI response for conversation #{name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
request_ai_response(self(), messages, tools(self(), name))
|
ElixirAi.ChatUtils.request_ai_response(self(), messages, tools(self(), name))
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
@@ -103,7 +103,7 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
store_message(state.name, new_message)
|
store_message(state.name, new_message)
|
||||||
new_state = %{state | messages: state.messages ++ [new_message]}
|
new_state = %{state | messages: state.messages ++ [new_message]}
|
||||||
|
|
||||||
request_ai_response(self(), new_state.messages, state.tools)
|
ElixirAi.ChatUtils.request_ai_response(self(), new_state.messages, state.tools)
|
||||||
{:noreply, new_state}
|
{:noreply, new_state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ defmodule ElixirAi.ChatRunner do
|
|||||||
|
|
||||||
if new_pending_tool_calls == [] do
|
if new_pending_tool_calls == [] do
|
||||||
broadcast_ui(state.name, :tool_calls_finished)
|
broadcast_ui(state.name, :tool_calls_finished)
|
||||||
request_ai_response(self(), state.messages ++ [new_message], state.tools)
|
ElixirAi.ChatUtils.request_ai_response(self(), state.messages ++ [new_message], state.tools)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
|
|||||||
@@ -83,3 +83,9 @@ defmodule ElixirAi.Data.DbHelpers do
|
|||||||
{positional_sql, ordered_values}
|
{positional_sql, ordered_values}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defmodule ElixirAi.Repo do
|
||||||
|
use Ecto.Repo,
|
||||||
|
otp_app: :elixir_ai,
|
||||||
|
adapter: Ecto.Adapters.Postgres
|
||||||
|
end
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
defmodule ElixirAi.Repo do
|
|
||||||
use Ecto.Repo,
|
|
||||||
otp_app: :elixir_ai,
|
|
||||||
adapter: Ecto.Adapters.Postgres
|
|
||||||
end
|
|
||||||
1
mix.exs
1
mix.exs
@@ -60,6 +60,7 @@ defmodule ElixirAi.MixProject do
|
|||||||
{:postgrex, ">= 0.0.0"},
|
{:postgrex, ">= 0.0.0"},
|
||||||
{:horde, "~> 0.9"},
|
{:horde, "~> 0.9"},
|
||||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
|
{:mimic, "~> 2.3.0"},
|
||||||
{:zoi, "~> 0.17"}
|
{:zoi, "~> 0.17"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|||||||
2
mix.lock
2
mix.lock
@@ -22,6 +22,7 @@
|
|||||||
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
||||||
"gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"},
|
"gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"},
|
||||||
"grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"},
|
"grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"},
|
||||||
|
"ham": {:hex, :ham, "0.3.2", "02ae195f49970ef667faf9d01bc454fb80909a83d6c775bcac724ca567aeb7b3", [:mix], [], "hexpm", "b71cc684c0e5a3d32b5f94b186770551509e93a9ae44ca1c1a313700f2f6a69a"},
|
||||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
|
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
|
||||||
"horde": {:hex, :horde, "0.10.0", "31c6a633057c3ec4e73064d7b11ba409c9f3c518aa185377d76bee441b76ceb0", [:mix], [{:delta_crdt, "~> 0.6.2", [hex: :delta_crdt, repo: "hexpm", optional: false]}, {:libring, "~> 1.7", [hex: :libring, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 0.5.0 or ~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "0b51c435cb698cac9bf9c17391dce3ebb1376ae6154c81f077fc61db771b9432"},
|
"horde": {:hex, :horde, "0.10.0", "31c6a633057c3ec4e73064d7b11ba409c9f3c518aa185377d76bee441b76ceb0", [:mix], [{:delta_crdt, "~> 0.6.2", [hex: :delta_crdt, repo: "hexpm", optional: false]}, {:libring, "~> 1.7", [hex: :libring, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 0.5.0 or ~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "0b51c435cb698cac9bf9c17391dce3ebb1376ae6154c81f077fc61db771b9432"},
|
||||||
"hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"},
|
"hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"},
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"libring": {:hex, :libring, "1.7.0", "4f245d2f1476cd7ed8f03740f6431acba815401e40299208c7f5c640e1883bda", [:mix], [], "hexpm", "070e3593cb572e04f2c8470dd0c119bc1817a7a0a7f88229f43cf0345268ec42"},
|
"libring": {:hex, :libring, "1.7.0", "4f245d2f1476cd7ed8f03740f6431acba815401e40299208c7f5c640e1883bda", [:mix], [], "hexpm", "070e3593cb572e04f2c8470dd0c119bc1817a7a0a7f88229f43cf0345268ec42"},
|
||||||
"merkle_map": {:hex, :merkle_map, "0.2.2", "f36ff730cca1f2658e317a3c73406f50bbf5ac8aff54cf837d7ca2069a6e251c", [:mix], [], "hexpm", "383107f0503f230ac9175e0631647c424efd027e89ea65ab5ea12eeb54257aaf"},
|
"merkle_map": {:hex, :merkle_map, "0.2.2", "f36ff730cca1f2658e317a3c73406f50bbf5ac8aff54cf837d7ca2069a6e251c", [:mix], [], "hexpm", "383107f0503f230ac9175e0631647c424efd027e89ea65ab5ea12eeb54257aaf"},
|
||||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||||
|
"mimic": {:hex, :mimic, "2.3.0", "88b1d13c285e57df6ea57204317bb56e49e7329668006cdcb80a9aafc73a9616", [:mix], [{:ham, "~> 0.3", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "52771f23689398c5d41c7d05e91c2c28e10df273b784f40ca8b02e35e46850d3"},
|
||||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||||
"mochiweb": {:hex, :mochiweb, "3.3.0", "2898ad0bfeee234e4cbae623c7052abc3ff0d73d499ba6e6ffef445b13ffd07a", [:rebar3], [], "hexpm", "aa85b777fb23e9972ebc424e40b5d35106f19bc998873e026dedd876df8ee50c"},
|
"mochiweb": {:hex, :mochiweb, "3.3.0", "2898ad0bfeee234e4cbae623c7052abc3ff0d73d499ba6e6ffef445b13ffd07a", [:rebar3], [], "hexpm", "aa85b777fb23e9972ebc424e40b5d35106f19bc998873e026dedd876df8ee50c"},
|
||||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
defmodule SQLTest do
|
defmodule SQLTest do
|
||||||
use ExUnit.Case
|
use ElixirAi.TestCase
|
||||||
alias ElixirAi.Data.DbHelpers
|
alias ElixirAi.Data.DbHelpers
|
||||||
|
|
||||||
test "converts simple named parameters" do
|
test "converts simple named parameters" do
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
defmodule ElixirAiWeb.ErrorHTMLTest do
|
defmodule ElixirAiWeb.ErrorHTMLTest do
|
||||||
use ElixirAiWeb.ConnCase, async: true
|
use ElixirAiWeb.ConnCase, async: false
|
||||||
|
|
||||||
# Bring render_to_string/4 for testing custom views
|
# Bring render_to_string/4 for testing custom views
|
||||||
import Phoenix.Template
|
import Phoenix.Template
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
defmodule ElixirAiWeb.ErrorJSONTest do
|
defmodule ElixirAiWeb.ErrorJSONTest do
|
||||||
use ElixirAiWeb.ConnCase, async: true
|
use ElixirAiWeb.ConnCase, async: false
|
||||||
|
|
||||||
test "renders 404" do
|
test "renders 404" do
|
||||||
assert ElixirAiWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
|
assert ElixirAiWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
|
||||||
|
|||||||
112
test/message_storage_test.exs
Normal file
112
test/message_storage_test.exs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
defmodule ElixirAi.MessageStorageTest do
|
||||||
|
use ElixirAi.TestCase
|
||||||
|
|
||||||
|
setup do
|
||||||
|
# Default run_sql and request_ai_response stubs are set by TestCase.
|
||||||
|
# Start ConversationManager AFTER stubs are active so its :load_conversations
|
||||||
|
# handler sees the stub rather than hitting the real (absent) DB.
|
||||||
|
case Horde.DynamicSupervisor.start_child(
|
||||||
|
ElixirAi.ChatRunnerSupervisor,
|
||||||
|
ElixirAi.ConversationManager
|
||||||
|
) do
|
||||||
|
{:ok, _pid} -> :ok
|
||||||
|
{:error, {:already_started, _pid}} -> :ok
|
||||||
|
{:error, :already_present} -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
# Stubs run_sql for all infrastructure calls (conversation lookup, message load,
|
||||||
|
# conversation insert) and notifies the test pid whenever a message INSERT occurs.
|
||||||
|
defp setup_conversation do
|
||||||
|
conv_name = "test_conv_#{System.unique_integer([:positive])}"
|
||||||
|
conv_id = :crypto.strong_rand_bytes(16)
|
||||||
|
test_pid = self()
|
||||||
|
|
||||||
|
stub(ElixirAi.Data.DbHelpers, :run_sql, fn sql, params, _topic ->
|
||||||
|
cond do
|
||||||
|
String.contains?(sql, "SELECT id FROM conversations") ->
|
||||||
|
[%{"id" => conv_id}]
|
||||||
|
|
||||||
|
String.contains?(sql, "INSERT INTO messages") ->
|
||||||
|
send(test_pid, {:insert_message, params})
|
||||||
|
[]
|
||||||
|
|
||||||
|
true ->
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# 4-arity version used by Conversation.all_names/0
|
||||||
|
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic, _schema -> [] end)
|
||||||
|
|
||||||
|
provider_id = Ecto.UUID.generate()
|
||||||
|
{:ok, _pid} = ElixirAi.ConversationManager.create_conversation(conv_name, provider_id)
|
||||||
|
conv_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run_sql is called with user message params" do
|
||||||
|
conv_name = setup_conversation()
|
||||||
|
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools -> :ok end)
|
||||||
|
|
||||||
|
ElixirAi.ChatRunner.new_user_message(conv_name, "hello world")
|
||||||
|
|
||||||
|
assert_receive {:insert_message, params}, 2000
|
||||||
|
assert params["role"] == "user"
|
||||||
|
assert params["content"] == "hello world"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run_sql is called with assistant message params" do
|
||||||
|
conv_name = setup_conversation()
|
||||||
|
|
||||||
|
stub(ElixirAi.ChatUtils, :request_ai_response, fn server, _messages, _tools ->
|
||||||
|
id = make_ref()
|
||||||
|
send(server, {:start_new_ai_response, id})
|
||||||
|
send(server, {:ai_text_chunk, id, "Hello from AI"})
|
||||||
|
send(server, {:ai_text_stream_finish, id})
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
ElixirAi.ChatRunner.new_user_message(conv_name, "hi")
|
||||||
|
|
||||||
|
assert_receive {:insert_message, %{"role" => "user"}}, 2000
|
||||||
|
assert_receive {:insert_message, params}, 2000
|
||||||
|
assert params["role"] == "assistant"
|
||||||
|
assert params["content"] == "Hello from AI"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "run_sql is called with tool request and tool result message params" do
|
||||||
|
conv_name = setup_conversation()
|
||||||
|
|
||||||
|
# First AI call triggers the tool; subsequent calls (after tool completes) are no-ops.
|
||||||
|
expect(ElixirAi.ChatUtils, :request_ai_response, fn server, _messages, _tools ->
|
||||||
|
id = make_ref()
|
||||||
|
send(server, {:start_new_ai_response, id})
|
||||||
|
|
||||||
|
send(
|
||||||
|
server,
|
||||||
|
{:ai_tool_call_start, id, {"store_thing", ~s({"name":"k","value":"v"}), 0, "tc_1"}}
|
||||||
|
)
|
||||||
|
|
||||||
|
send(server, {:ai_tool_call_end, id})
|
||||||
|
:ok
|
||||||
|
end)
|
||||||
|
|
||||||
|
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools -> :ok end)
|
||||||
|
|
||||||
|
ElixirAi.ChatRunner.new_user_message(conv_name, "store something")
|
||||||
|
|
||||||
|
assert_receive {:insert_message, %{"role" => "user"}}, 2000
|
||||||
|
|
||||||
|
# Assistant message that carries the tool_calls list
|
||||||
|
assert_receive {:insert_message, params}, 2000
|
||||||
|
assert params["role"] == "assistant"
|
||||||
|
refute is_nil(params["tool_calls"])
|
||||||
|
|
||||||
|
# Tool result message
|
||||||
|
assert_receive {:insert_message, params}, 2000
|
||||||
|
assert params["role"] == "tool"
|
||||||
|
assert params["tool_call_id"] == "tc_1"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
defmodule ElixirAi.AiUtils.StreamLineUtilsTest do
|
defmodule ElixirAi.AiUtils.StreamLineUtilsTest do
|
||||||
use ExUnit.Case
|
use ElixirAi.TestCase
|
||||||
|
@moduletag capture_log: true
|
||||||
import ElixirAi.StreamChunkHelpers
|
import ElixirAi.StreamChunkHelpers
|
||||||
alias ElixirAi.AiUtils.StreamLineUtils
|
alias ElixirAi.AiUtils.StreamLineUtils
|
||||||
|
|
||||||
|
|||||||
19
test/support/test_case.ex
Normal file
19
test/support/test_case.ex
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
defmodule ElixirAi.TestCase do
|
||||||
|
use ExUnit.CaseTemplate
|
||||||
|
use Mimic
|
||||||
|
|
||||||
|
using do
|
||||||
|
quote do
|
||||||
|
use Mimic
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
setup :set_mimic_global
|
||||||
|
|
||||||
|
setup do
|
||||||
|
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic -> [] end)
|
||||||
|
stub(ElixirAi.Data.DbHelpers, :run_sql, fn _sql, _params, _topic, _schema -> [] end)
|
||||||
|
stub(ElixirAi.ChatUtils, :request_ai_response, fn _server, _messages, _tools -> :ok end)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1 +1,3 @@
|
|||||||
ExUnit.start()
|
ExUnit.start()
|
||||||
|
Mimic.copy(ElixirAi.Data.DbHelpers)
|
||||||
|
Mimic.copy(ElixirAi.ChatUtils)
|
||||||
|
|||||||
Reference in New Issue
Block a user