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