104 lines
3.0 KiB
Elixir
104 lines
3.0 KiB
Elixir
defmodule Symbiont.LedgerTest do
|
|
use ExUnit.Case, async: false
|
|
import Symbiont.TestHelpers
|
|
|
|
@moduletag :capture_log
|
|
|
|
setup do
|
|
tmp_dir = Path.join(System.tmp_dir!(), "symbiont_test_#{:rand.uniform(999_999)}")
|
|
File.mkdir_p!(tmp_dir)
|
|
|
|
safe_stop(Symbiont.Ledger)
|
|
{:ok, _pid} = Symbiont.Ledger.start_link(data_dir: tmp_dir)
|
|
|
|
on_exit(fn ->
|
|
safe_stop(Symbiont.Ledger)
|
|
File.rm_rf!(tmp_dir)
|
|
end)
|
|
|
|
%{tmp_dir: tmp_dir}
|
|
end
|
|
|
|
test "starts with empty ledger" do
|
|
assert Symbiont.Ledger.recent() == []
|
|
assert Symbiont.Ledger.stats() == %{
|
|
"total_calls" => 0,
|
|
"total_cost_estimated_usd" => 0.0,
|
|
"by_model" => %{},
|
|
"by_date" => %{}
|
|
}
|
|
end
|
|
|
|
test "appending entries and reading them back" do
|
|
Symbiont.Ledger.append(%{
|
|
model: "haiku",
|
|
success: true,
|
|
input_tokens: 100,
|
|
output_tokens: 50,
|
|
estimated_cost_usd: 0.008,
|
|
elapsed_seconds: 1.2
|
|
})
|
|
|
|
Process.sleep(50)
|
|
|
|
entries = Symbiont.Ledger.recent()
|
|
assert length(entries) == 1
|
|
|
|
[entry] = entries
|
|
assert entry["model"] == "haiku"
|
|
assert entry["success"] == true
|
|
assert entry["input_tokens"] == 100
|
|
assert entry["estimated_cost_usd"] == 0.008
|
|
assert entry["timestamp"] != nil
|
|
end
|
|
|
|
test "stats aggregate correctly across multiple entries" do
|
|
entries = [
|
|
%{model: "haiku", estimated_cost_usd: 0.008, success: true, input_tokens: 50, output_tokens: 25},
|
|
%{model: "haiku", estimated_cost_usd: 0.006, success: true, input_tokens: 40, output_tokens: 20},
|
|
%{model: "sonnet", estimated_cost_usd: 0.04, success: true, input_tokens: 200, output_tokens: 100},
|
|
%{model: "opus", estimated_cost_usd: 0.15, success: true, input_tokens: 500, output_tokens: 300}
|
|
]
|
|
|
|
Enum.each(entries, &Symbiont.Ledger.append/1)
|
|
# Ensure all async casts are processed
|
|
_ = Symbiont.Ledger.stats()
|
|
Process.sleep(50)
|
|
|
|
stats = Symbiont.Ledger.stats()
|
|
assert stats["total_calls"] == 4
|
|
assert stats["total_cost_estimated_usd"] == 0.204
|
|
|
|
assert stats["by_model"]["haiku"]["calls"] == 2
|
|
assert stats["by_model"]["sonnet"]["calls"] == 1
|
|
assert stats["by_model"]["opus"]["calls"] == 1
|
|
end
|
|
|
|
test "recent limits the number of entries returned" do
|
|
for i <- 1..10 do
|
|
Symbiont.Ledger.append(%{model: "haiku", estimated_cost_usd: 0.001 * i, success: true})
|
|
end
|
|
|
|
Process.sleep(100)
|
|
|
|
assert length(Symbiont.Ledger.recent(3)) == 3
|
|
assert length(Symbiont.Ledger.recent(50)) == 10
|
|
end
|
|
|
|
test "ledger persists to JSONL file", %{tmp_dir: tmp_dir} do
|
|
Symbiont.Ledger.append(%{model: "sonnet", estimated_cost_usd: 0.04, success: true})
|
|
Process.sleep(50)
|
|
|
|
path = Path.join(tmp_dir, "ledger.jsonl")
|
|
content = File.read!(path)
|
|
assert String.contains?(content, "sonnet")
|
|
|
|
lines = content |> String.split("\n", trim: true)
|
|
assert length(lines) == 1
|
|
|
|
{:ok, decoded} = Jason.decode(hd(lines))
|
|
assert decoded["model"] == "sonnet"
|
|
end
|
|
|
|
end
|