test/openagents/threads_test.exs

58e6347eeb72 · 29 KB

defmodule OpenAgents.ThreadsTest do
  use OpenAgents.DataCase, async: false

  import OpenAgentsWeb.ConnCase, only: [github_user: 1]

  alias OpenAgents.Conversations
  alias OpenAgents.Inference
  alias OpenAgents.Inference.Credit
  alias OpenAgents.Inference.Grant
  alias OpenAgents.Inference.{Models, Pricing}
  alias OpenAgents.Repo
  alias OpenAgents.Threads
  alias OpenAgents.Threads.Event
  alias OpenAgents.Threads.Thread
  alias OpenAgents.UnpricedLane

  defp owner(key), do: github_user("thread-#{key}")

  describe "open/3" do
    test "a thread is account-scoped and needs no conversation" do
      user = owner("scoped")

      assert {:ok, thread} = Threads.open(user, "Land the thread substrate")

      assert thread.status == "open"
      assert thread.generation == 0
      assert thread.event_count == 1
      assert thread.completed_at == nil
      assert is_binary(thread.owner_visitor_id)

      # The visitor root exists; the account's one conversation does not, and
      # nothing about the thread asked for it (DATA-002 is untouched).
      assert Conversations.get_conversation_for_user(user) == nil

      assert [%Event{event_type: "thread.opened", schema: "openagents.thread.event.v1"}] =
               Threads.list_events(thread)
    end

    test "two threads live side by side for one account" do
      user = owner("plural")

      assert {:ok, first} = Threads.open(user, "First objective")
      assert {:ok, second} = Threads.open(user, "Second objective")

      refute first.id == second.id
      assert length(Threads.list_for_user(user)) == 2
    end

    test "another account's thread is not reachable" do
      mine = owner("mine")
      theirs = owner("theirs")

      {:ok, thread} = Threads.open(mine, "Private objective")

      assert %Thread{} = Threads.get_for_user(mine, thread.id)
      assert Threads.get_for_user(theirs, thread.id) == nil
      assert Threads.get_for_user(mine, "not-a-uuid") == nil
    end

    test "the objective is bounded and the execution shape is admitted vocabulary" do
      user = owner("bounds")

      assert {:error, changeset} = Threads.open(user, "")
      assert %{objective: _} = errors_on(changeset)

      assert {:error, changeset} = Threads.open(user, String.duplicate("a", 32_769))
      assert %{objective: _} = errors_on(changeset)

      assert {:error, changeset} =
               Threads.open(user, "Objective", permission_profile: "danger_full_access")

      assert %{permission_profile: _} = errors_on(changeset)

      assert {:ok, narrowed} = Threads.open(user, "Objective", reasoning: "low")
      assert narrowed.reasoning_effort == "low"
      assert narrowed.permission_profile == "read_only"
    end
  end

  describe "record_event/3" do
    test "appends bounded transcript entries and advances the counter" do
      user = owner("events")
      {:ok, thread} = Threads.open(user, "Record something")

      assert {:ok, thread} = Threads.record_event(thread, "thread.turn.started", %{"turn" => 1})
      assert thread.event_count == 2

      assert ["thread.opened", "thread.turn.started"] =
               thread |> Threads.list_events() |> Enum.map(& &1.event_type)
    end

    test "a payload larger than a projection would allow is recorded whole" do
      user = owner("event-bound")
      {:ok, thread} = Threads.open(user, "Record something large")

      # Past the 16 KB ceiling this table inherited from `scv_run_events`, whose
      # payloads are a minimal projection of work stored elsewhere. This table
      # is the work: a single reasoning block observed in a live session is
      # 38,791 characters, and a transcript that cannot hold it cannot
      # reproduce the session.
      blob = String.duplicate("a", 40_000)

      assert {:ok, _updated} =
               Threads.record_event(thread, "thread.turn.started", %{"blob" => blob})

      stored =
        thread
        |> Threads.list_events()
        |> Enum.find(&(&1.payload["blob"] != nil))

      assert stored.payload["blob"] == blob
    end

    test "a terminal thread accepts no further transcript" do
      user = owner("event-terminal")
      {:ok, thread} = Threads.open(user, "Finish then append")
      {:ok, thread} = Threads.finish(thread, %{report: "Done."})

      assert {:error, :thread_terminal} = Threads.record_event(thread, "late", %{"a" => 1})
    end
  end

  describe "finish/2 and cancel/2" do
    test "a terminal thread carries its report and its digest" do
      user = owner("finish")
      {:ok, thread} = Threads.open(user, "Report at the end")

      assert {:ok, finished} = Threads.finish(thread, %{report: "It worked."})
      assert finished.status == "succeeded"
      assert finished.completed_at != nil

      assert finished.report_digest ==
               "sha256:" <> Base.encode16(:crypto.hash(:sha256, "It worked."), case: :lower)

      assert {:error, :thread_terminal} = Threads.finish(finished, %{report: "Again."})
    end

    test "a cancelled thread is terminal with a reason" do
      user = owner("cancel")
      {:ok, thread} = Threads.open(user, "Cancel me")

      assert {:ok, cancelled} = Threads.cancel(thread)
      assert cancelled.status == "cancelled"
      assert cancelled.error_code == "cancelled"
    end

    test "a report that names an outcome carries it, error code and all" do
      user = owner("finish-failed")
      {:ok, thread} = Threads.open(user, "Run out of steps")

      assert {:ok, failed} =
               Threads.finish(thread, %{
                 status: "failed",
                 report: "The turn budget ran out before an answer.",
                 report_type: "failure",
                 error_code: "max_steps"
               })

      assert failed.status == "failed"
      assert failed.error_code == "max_steps"
    end

    test "a success cannot carry an error code" do
      user = owner("finish-incoherent-success")
      {:ok, thread} = Threads.open(user, "Claim both")

      assert {:error, %Ecto.Changeset{} = changeset} =
               Threads.finish(thread, %{
                 status: "succeeded",
                 report: "It worked.",
                 error_code: "max_steps"
               })

      assert %{error_code: [_ | _]} = errors_on(changeset)
      assert Threads.get_for_user(user, thread.id).status == "open"
    end

    test "a failure has to name why, so nothing ends unexplained" do
      user = owner("finish-incoherent-failure")
      {:ok, thread} = Threads.open(user, "Fail silently")

      assert {:error, %Ecto.Changeset{} = changeset} =
               Threads.finish(thread, %{status: "failed", report: "It did not work."})

      assert %{error_code: [_ | _]} = errors_on(changeset)
      assert Threads.get_for_user(user, thread.id).status == "open"
    end

    test "the coherence rule is the database's too, not only the changeset's" do
      user = owner("finish-coherence-db")
      {:ok, thread} = Threads.open(user, "Write around the changeset")

      assert_raise Postgrex.Error, fn ->
        Repo.update_all(
          from(t in Thread, where: t.id == ^thread.id),
          set: [
            status: "succeeded",
            report: "It worked.",
            report_digest: "sha256:" <> String.duplicate("0", 64),
            report_type: "outcome",
            error_code: "max_steps",
            completed_at: DateTime.utc_now()
          ]
        )
      end
    end
  end

  describe "mint_grant/1 — the thread fence" do
    test "model authority names the thread and no conversation" do
      user = owner("mint")
      {:ok, thread} = Threads.open(user, "Reach a model")

      assert {:ok, fenced, grant, token} = Threads.mint_grant(thread)

      assert grant.thread_id == thread.id
      assert grant.conversation_id == nil
      assert grant.machine_id == nil
      assert grant.status == "active"
      assert String.starts_with?(token, "sig_")
      assert fenced.generation == 1

      assert {:ok, %Grant{status: "active"}} = Inference.resolve(token)
      assert Conversations.get_conversation_for_user(user) == nil
    end

    test "a new mint revokes the last one and bumps the generation" do
      user = owner("fence")
      {:ok, thread} = Threads.open(user, "Fence the authority")

      {:ok, _thread, _first, first_token} = Threads.mint_grant(thread)
      {:ok, thread, _second, second_token} = Threads.mint_grant(thread)

      assert thread.generation == 2
      assert {:error, :grant_revoked} = Inference.resolve(first_token)
      assert {:ok, %Grant{}} = Inference.resolve(second_token)
      assert length(Threads.active_grants(thread)) == 1
    end

    test "authority does not outlive the thread" do
      user = owner("outlive")
      {:ok, thread} = Threads.open(user, "End with authority live")
      {:ok, thread, _grant, token} = Threads.mint_grant(thread)

      {:ok, finished} = Threads.finish(thread, %{report: "Done."})

      assert Threads.active_grants(finished) == []
      assert {:error, :grant_revoked} = Inference.resolve(token)
    end

    test "a cancelled thread is refused: cancelling is the end that means it" do
      user = owner("cancelled-no-remint")
      {:ok, thread} = Threads.open(user, "Cancel then resume")
      {:ok, thread, _grant, _token} = Threads.mint_grant(thread)
      {:ok, cancelled} = Threads.cancel(thread)

      assert {:error, :thread_terminal} = Threads.mint_grant(cancelled)
      assert Threads.get_for_user(user, thread.id).status == "cancelled"
    end

    test "a thread that reported is re-granted by reopening it, and keeps its report" do
      user = owner("remint-reported")
      {:ok, thread} = Threads.open(user, "Report then resume")
      {:ok, thread, _grant, first_token} = Threads.mint_grant(thread)
      {:ok, finished} = Threads.finish(thread, %{report: "It worked.", report_type: "outcome"})

      assert {:ok, resumed, grant, token} = Threads.mint_grant(finished)

      assert resumed.status == "open"
      assert resumed.report == nil
      assert resumed.report_digest == nil
      assert resumed.report_type == nil
      assert resumed.error_code == nil
      assert resumed.completed_at == nil
      assert resumed.generation == 2

      assert grant.thread_id == thread.id
      assert {:ok, %Grant{status: "active"}} = Inference.resolve(token)
      assert {:error, :grant_revoked} = Inference.resolve(first_token)

      # Nothing is lost by reopening: the report the thread carried moves into
      # the transcript, which is the durable record either way.
      reopened =
        resumed |> Threads.list_events() |> Enum.find(&(&1.event_type == "thread.reopened"))

      assert reopened.payload["status"] == "succeeded"
      assert reopened.payload["report"] == "It worked."
      assert reopened.payload["error_code"] == nil

      # And the transcript accepts the resumed session's turns.
      assert {:ok, _appended} = Threads.record_event(resumed, "turn.user", %{"text" => "again"})
    end

    test "a failed thread resumes too: failing is a state of the work, not a disposal" do
      user = owner("remint-failed")
      {:ok, thread} = Threads.open(user, "Fail then resume")
      {:ok, thread, _grant, _token} = Threads.mint_grant(thread)

      {:ok, failed} =
        Threads.finish(thread, %{
          status: "failed",
          report: "The turn budget ran out.",
          error_code: "max_steps"
        })

      assert {:ok, resumed, _grant, _token} = Threads.mint_grant(failed)
      assert resumed.status == "open"
      assert resumed.error_code == nil
    end

    test "deleting a thread deletes its authority" do
      user = owner("cascade")
      {:ok, thread} = Threads.open(user, "Delete me")
      {:ok, thread, grant, _token} = Threads.mint_grant(thread)

      Repo.delete!(thread)

      assert Repo.get(Grant, grant.id) == nil
    end
  end

  describe "the admission cap" do
    test "an account cannot hold more open threads than the configured maximum" do
      cap(2)
      user = owner("capped")

      assert {:ok, _first} = Threads.open(user, "First")
      assert {:ok, second} = Threads.open(user, "Second")
      assert {:error, :thread_quota_reached} = Threads.open(user, "Third")

      # The cap counts what is open, so ending one admits the next.
      {:ok, _finished} = Threads.finish(second, %{report: "Done."})
      assert {:ok, _third} = Threads.open(user, "Third")
    end

    test "the cap counts one account's threads, never another's" do
      cap(1)

      assert {:ok, _mine} = Threads.open(owner("cap-mine"), "Mine")
      assert {:ok, _yours} = Threads.open(owner("cap-yours"), "Yours")
    end

    test "a refused open writes nothing" do
      cap(1)
      user = owner("cap-clean")

      {:ok, _first} = Threads.open(user, "First")
      assert {:error, :thread_quota_reached} = Threads.open(user, "Second")
      assert Threads.open_count(user) == 1
      assert length(Threads.list_for_user(user)) == 1
    end
  end

  describe "reap_expired/1" do
    test "a thread's authority has no clock, so waiting does not end it" do
      # The behaviour this replaces: an hour passed, the grant expired, the
      # open thread it fenced was closed as `authority_expired`, and a coding
      # session that was mid-sentence was told to start a new one.
      user = owner("no-clock")
      {:ok, thread} = Threads.open(user, "Still working")
      {:ok, thread, grant, token} = Threads.mint_grant(thread)

      assert is_nil(Repo.get!(Grant, grant.id).expires_at)

      # However long the reaper is run, and whenever.
      assert {0, 0} = Threads.reap_expired(user)
      assert {0, 0} = Threads.reap_expired(user)

      assert Repo.get!(Grant, grant.id).status == "active"
      assert Repo.get!(Thread, thread.id).status == "open"
      assert {:ok, _resolved} = Inference.resolve(token)
    end

    test "a grant that does carry a deadline is still retired when it passes" do
      # Not every grant is a thread's. A computer-bound delegation keeps its
      # clock, where the deadline is a security bound rather than a
      # convenience, and this is the reader that enforces it.
      user = owner("reaped")
      elapsed_ttl()
      {:ok, thread} = Threads.open(user, "Deadline")
      {:ok, _thread, grant, token} = Threads.mint_grant(thread)

      assert {1, 1} = Threads.reap_expired(user)
      assert Repo.get!(Grant, grant.id).status == "expired"
      assert {:error, :grant_expired} = Inference.resolve(token)
    end

    test "a thread left holding no authority is closed as spent, never as expired" do
      # The slot has to come back: nothing is coming to renew a grant, and an
      # open thread that can never work again would hold the ceiling forever.
      # What changed is the reason it is closed for — the budget ran out, which
      # is true, rather than a clock, which no longer exists.
      user = owner("left-open")
      elapsed_ttl()
      {:ok, thread} = Threads.open(user, "Deadline")
      {:ok, thread, _grant, _token} = Threads.mint_grant(thread)

      assert {1, 1} = Threads.reap_expired(user)

      reaped = Repo.get!(Thread, thread.id)
      assert reaped.status == "failed"
      assert reaped.error_code == "authority_spent"
      refute reaped.report =~ "expired"
    end

    test "reaping is idempotent" do
      user = owner("reap-twice")
      elapsed_ttl()
      {:ok, thread} = Threads.open(user, "Deadline")
      {:ok, _thread, _grant, _token} = Threads.mint_grant(thread)

      assert {1, 1} = Threads.reap_expired(user)
      assert {0, 0} = Threads.reap_expired(user)
    end
  end

  describe "ceilings/0" do
    test "a thread's grant carries the thread ceilings, not the delegation ceilings" do
      user = owner("ceilings")
      {:ok, thread} = Threads.open(user, "Measure me")
      {:ok, _thread, grant, _token} = Threads.mint_grant(thread)

      ceilings = Threads.ceilings()

      assert grant.max_total_tokens == ceilings.max_total_tokens
      assert grant.max_calls == ceilings.max_calls

      # Money is the account's, not the thread's: the cost ceiling is what the
      # account has left of its credit, so a second thread cannot mint itself a
      # fresh allowance (`OpenAgents.Inference.Credit`).
      assert grant.max_cost_microusd == Credit.remaining(grant.owner_visitor_id)

      delegation = Inference.delegation_ceilings()

      refute {grant.max_total_tokens, grant.max_calls, grant.max_cost_microusd} ==
               {delegation.max_total_tokens, delegation.max_calls, delegation.max_cost_microusd}
    end

    test "a delegation grant is unchanged by the thread ceilings" do
      user = owner("delegation-untouched")
      {:ok, conversation} = Conversations.ensure_conversation(user)

      {:ok, grant, _token} =
        Inference.mint(%{
          owner_visitor_id: conversation.visitor_id,
          conversation_id: conversation.id
        })

      delegation = Inference.delegation_ceilings()

      assert grant.max_total_tokens == delegation.max_total_tokens
      assert grant.max_calls == delegation.max_calls
      assert grant.max_cost_microusd == delegation.max_cost_microusd
    end
  end

  defp set_config(key, value) do
    previous = Application.get_env(:openagents, key)
    Application.put_env(:openagents, key, value)

    on_exit(fn ->
      Application.put_env(:openagents, key, previous)
    end)
  end

  describe "delegated child threads" do
    test "a child thread names its parent and stays on the same account" do
      user = owner("child-parent")
      {:ok, parent, _grant, _token} = Threads.open_and_mint(user, "Parent")
      {:ok, child} = Threads.open(user, "Child", parent_thread_id: parent.id)

      assert child.parent_thread_id == parent.id
      assert child.owner_visitor_id == parent.owner_visitor_id
    end

    test "a child cannot name another account's parent" do
      mine = owner("child-owner-mine")
      theirs = owner("child-owner-theirs")
      {:ok, parent, _grant, _token} = Threads.open_and_mint(mine, "Parent")

      assert {:error, changeset} =
               Threads.open(theirs, "Child", parent_thread_id: parent.id)

      assert %{parent_thread_id: _} = errors_on(changeset)
    end

    test "a child cannot name a terminal parent" do
      user = owner("child-terminal-parent")
      {:ok, parent, _grant, _token} = Threads.open_and_mint(user, "Parent")
      {:ok, parent} = Threads.finish(parent, %{report: "Done."})

      assert {:error, changeset} =
               Threads.open(user, "Child", parent_thread_id: parent.id)

      assert %{parent_thread_id: _} = errors_on(changeset)
    end

    test "a child is refused when the parent holds no active grant" do
      user = owner("child-no-grant")
      {:ok, parent} = Threads.open(user, "Parent")

      assert {:error, :parent_authority_exhausted} =
               Threads.open_and_mint(user, "Child", parent_thread_id: parent.id)
    end

    test "a child inherits its parent's visibility" do
      user = owner("child-visibility-inherit")
      {:ok, parent} = Threads.open(user, "Parent", visibility: "ledger")
      {:ok, child} = Threads.open(user, "Child", parent_thread_id: parent.id)

      assert child.visibility == "ledger"
    end

    test "a child cannot be opened wider than its parent" do
      user = owner("child-visibility-wide")
      {:ok, parent} = Threads.open(user, "Parent", visibility: "dark")

      assert {:error, changeset} =
               Threads.open(user, "Child",
                 parent_thread_id: parent.id,
                 visibility: "ledger"
               )

      assert %{visibility: _} = errors_on(changeset)
    end

    test "a child counts toward the admission cap" do
      cap(2)
      user = owner("child-cap")
      {:ok, parent} = Threads.open(user, "Parent")
      assert {:ok, _child} = Threads.open(user, "Child", parent_thread_id: parent.id)
      assert {:error, :thread_quota_reached} = Threads.open(user, "Third")
    end

    test "spawning a child appends a thread.spawn event to the parent transcript" do
      user = owner("child-spawn")
      {:ok, parent, _grant, _token} = Threads.open_and_mint(user, "Parent")
      {:ok, child} = Threads.open(user, "Child", parent_thread_id: parent.id)

      parent = Threads.get_for_user(user, parent.id)
      events = Threads.list_events(parent)

      assert [%Event{event_type: "thread.opened"}, %Event{event_type: "thread.spawn"}] = events
      spawn = List.last(events)
      assert spawn.payload["child_thread_id"] == child.id
      assert parent.event_count == 2
    end
  end

  describe "child thread ceilings" do
    test "a child grant is ceiled at the parent's remaining calls" do
      set_config(:thread_grant_max_calls, 5)
      set_config(:thread_grant_max_total_tokens, nil)
      user = owner("child-calls")
      {:ok, parent, grant, _token} = Threads.open_and_mint(user, "Parent")
      {:ok, spent} = Inference.record_usage(grant, %{"output_tokens" => 1})
      {:ok, spent} = Inference.record_usage(spent, %{"output_tokens" => 1})
      assert spent.call_count == 2

      {:ok, _child, child_grant, _token} =
        Threads.open_and_mint(user, "Child",
          parent_thread_id: parent.id,
          model: "gemini-3.7-flash"
        )

      assert child_grant.max_calls == 3
    end

    test "a child grant is ceiled at the parent's remaining tokens" do
      set_config(:thread_grant_max_total_tokens, 100)
      set_config(:thread_grant_max_calls, nil)
      user = owner("child-tokens")
      {:ok, parent, grant, _token} = Threads.open_and_mint(user, "Parent")
      {:ok, spent} = Inference.record_usage(grant, %{"output_tokens" => 30})
      assert spent.usage["total_tokens"] == 30

      {:ok, _child, child_grant, _token} =
        Threads.open_and_mint(user, "Child", parent_thread_id: parent.id)

      assert child_grant.max_total_tokens == 70
    end

    test "a child grant is ceiled at the parent's remaining cost" do
      set_config(:account_credit_microusd, 100_000)
      set_config(:visitor_credit_microusd, 100_000)
      set_config(:thread_grant_max_calls, nil)
      set_config(:thread_grant_max_total_tokens, nil)
      user = owner("child-cost")

      {:ok, parent, grant, _token} =
        Threads.open_and_mint(user, "Parent", model: "gemini-3.7-flash")

      # Output tokens priced at the paid lane's own output rate, so the
      # arithmetic below follows the catalog rather than restating it.
      {:ok, paid_model} = Models.fetch("gemini-3.7-flash")
      rate = Pricing.effective_pricing(paid_model).output_per_million_tokens
      output = 5_000
      cost = div(output * rate, 1_000_000)

      {:ok, spent} = Inference.record_usage(grant, %{"output_tokens" => output})
      assert spent.usage["estimated_cost_microusd"] == cost

      {:ok, _child, child_grant, _token} =
        Threads.open_and_mint(user, "Child",
          parent_thread_id: parent.id,
          model: "gemini-3.7-flash"
        )

      assert child_grant.max_cost_microusd == 100_000 - cost
    end
  end

  describe "typed thread reports" do
    test "completing a thread records the requested report type" do
      user = owner("typed-report")
      {:ok, thread} = Threads.open(user, "Objective")
      {:ok, finished} = Threads.finish(thread, %{report: "Done.", report_type: "result"})

      assert finished.report_type == "result"
    end

    test "finish defaults the report type when the caller names none" do
      user = owner("typed-report-default")
      {:ok, thread} = Threads.open(user, "Objective")
      {:ok, finished} = Threads.finish(thread, %{report: "Done."})

      assert finished.report_type == "outcome"
    end
  end

  defp cap(limit) do
    previous = Application.get_env(:openagents, :maximum_open_threads_per_account)
    Application.put_env(:openagents, :maximum_open_threads_per_account, limit)

    on_exit(fn ->
      Application.put_env(:openagents, :maximum_open_threads_per_account, previous)
    end)
  end

  # A grant's expiry is immutable once minted, so the TTL is set before the
  # mint rather than the row edited after it.
  defp elapsed_ttl do
    previous = Application.get_env(:openagents, :thread_grant_ttl_seconds)
    Application.put_env(:openagents, :thread_grant_ttl_seconds, -1)
    on_exit(fn -> Application.put_env(:openagents, :thread_grant_ttl_seconds, previous) end)
  end

  describe "what a thread spent" do
    test "sums calls and usage across every grant the thread has held" do
      user = owner("spend-sums")
      {:ok, thread, grant, _token} = Threads.open_and_mint(user, "Spend some")

      {:ok, _} = Inference.record_usage(grant, %{"input_tokens" => 100, "output_tokens" => 10})

      # Resuming re-mints: the second grant is where later spend lands, and a
      # reader asking what the session cost must see both.
      {:ok, resumed, second, _token} = Threads.mint_grant(thread)
      {:ok, _} = Inference.record_usage(second, %{"input_tokens" => 40, "output_tokens" => 5})

      spend = Threads.spend(resumed)
      assert spend.grants == 2
      assert spend.calls == 2
      assert spend.usage["input_tokens"] == 140
      assert spend.usage["output_tokens"] == 15
    end

    test "a dimension no provider reported is absent rather than zero" do
      user = owner("spend-partial")
      {:ok, thread, grant, _token} = Threads.open_and_mint(user, "Partial usage")
      {:ok, _} = Inference.record_usage(grant, %{"input_tokens" => 7})

      spend = Threads.spend(thread)
      assert spend.usage["input_tokens"] == 7
      refute Map.has_key?(spend.usage, "cache_read_input_tokens")
    end

    test "a thread that never spent reports nothing spent" do
      user = owner("spend-none")
      {:ok, thread} = Threads.open(user, "Never spent")

      assert %{calls: 0, grants: 0, usage: %{}, cost: cost} = Threads.spend(thread)

      # Nothing was bought, so there is nothing to price. `absent` is not
      # `unpriced`: one says no calls were made, the other says calls were made
      # and this deployment cannot say what they cost.
      assert cost.basis == "absent"
      assert cost.microusd == nil
      assert cost.unpriced_calls == 0
      assert cost.unpriced_models == []
    end
  end

  # METER-001. Every assertion here is about the same wrong number: a session
  # on a lane with no declared rates reporting `$0.00`, which reads as a
  # measurement rather than as the absence of one.
  describe "what a thread spent, when the deployment has no price for it" do
    test "a thread on an unpriced model reports an unknown cost, never a zero" do
      unpriced = admit_unpriced_lane()
      user = owner("spend-unpriced")
      {:ok, thread} = Threads.open(user, "Run the coder's own lane", model: unpriced)
      {:ok, _fenced, grant, _token} = Threads.mint_grant(thread)

      {:ok, metered} =
        Inference.record_usage(grant, %{"input_tokens" => 40_000, "output_tokens" => 9_000})

      # The grant itself refuses to invent the figure.
      refute Map.has_key?(metered.usage, "estimated_cost_microusd")
      assert metered.usage["pricing_id"] == "unpriced"

      spend = Threads.spend(thread)

      assert spend.calls == 1
      assert spend.usage["input_tokens"] == 40_000
      # The one that matters: not zero.
      refute spend.cost.microusd == 0
      assert spend.cost.microusd == nil
      assert spend.cost.basis == "unpriced"
      assert spend.cost.unpriced_calls == 1
      assert spend.cost.unpriced_models == [unpriced]
    end

    test "a thread on a priced model reports a total, labelled by its basis" do
      user = owner("spend-priced")
      {:ok, thread, grant, _token} = Threads.open_and_mint(user, "Priced lane")
      {:ok, _} = Inference.record_usage(grant, %{"input_tokens" => 1_000_000})

      spend = Threads.spend(thread)

      pricing = Pricing.effective_pricing(Models.default())
      rate = pricing.input_per_million_tokens

      assert spend.cost.microusd == rate
      assert spend.cost.priced_microusd == rate
      assert spend.cost.basis == Pricing.basis_of(pricing)
      assert spend.cost.unpriced_models == []
    end

    test "one unpriced grant makes the whole session's total unknown, and names why" do
      unpriced = admit_unpriced_lane()
      user = owner("spend-mixed")
      {:ok, thread, first, _token} = Threads.open_and_mint(user, "Start priced")
      {:ok, _} = Inference.record_usage(first, %{"input_tokens" => 1_000_000})

      # A thread re-mints on resume, and a grant pins its own model, so a
      # session whose grants ran on different lanes is exactly the case a total
      # has to survive honestly.
      {:ok, _revoked} = Inference.revoke(first)
      {:ok, second, _token} = unpriced_grant_for(thread, unpriced)
      {:ok, _} = Inference.record_usage(second, %{"input_tokens" => 50_000})

      spend = Threads.spend(thread)

      assert spend.cost.microusd == nil
      # Nothing measured is thrown away — the priced half is still reported,
      # just not as the answer to "what did this cost".
      assert spend.cost.priced_microusd ==
               Pricing.effective_pricing(Models.default()).input_per_million_tokens

      assert spend.cost.basis == "unpriced"
      assert spend.cost.unpriced_models == [unpriced]
    end

    test "a grant that was minted and never called does not make the total unknown" do
      unpriced = admit_unpriced_lane()
      user = owner("spend-idle-unpriced")
      {:ok, thread, first, _token} = Threads.open_and_mint(user, "Priced work")
      {:ok, _} = Inference.record_usage(first, %{"input_tokens" => 1_000_000})

      {:ok, _revoked} = Inference.revoke(first)
      {:ok, _idle, _token} = unpriced_grant_for(thread, unpriced)

      spend = Threads.spend(thread)

      assert spend.grants == 2

      assert spend.cost.microusd ==
               Pricing.effective_pricing(Models.default()).input_per_million_tokens

      assert spend.cost.unpriced_calls == 0
    end

    # `gpt-5.6-luna` was the shipped unpriced lane until it was withdrawn. The
    # invariant it demonstrated did not go with it, so the lane is admitted
    # here for the length of one test instead.
    defp admit_unpriced_lane do
      previous = UnpricedLane.admit!()
      on_exit(fn -> UnpricedLane.restore(previous) end)
      UnpricedLane.id()
    end

    # A thread holds at most one active grant, so a second lane is reached the
    # way a resume reaches it: revoke, then mint again against the same fence.
    defp unpriced_grant_for(thread, model_id) do
      Inference.mint(%{
        owner_visitor_id: thread.owner_visitor_id,
        thread_id: thread.id,
        model_id: model_id
      })
    end
  end
end