test/openagents_web/live/thread_show_live_test.exs

58e6347eeb72 · 6 KB

defmodule OpenAgentsWeb.ThreadShowLiveTest do
  # `async: false` because the unpriced-lane test below rewrites
  # `:model_catalog`, which every process in the node reads.
  use OpenAgentsWeb.ConnCase, async: false

  import Phoenix.LiveViewTest

  alias OpenAgents.Inference.Models
  alias OpenAgents.Threads
  alias OpenAgents.UnpricedLane

  defp signed_in(conn, user), do: Plug.Test.init_test_session(conn, %{"user_id" => user.id})

  defp event_id(thread, event_type) do
    thread
    |> Threads.list_events()
    |> Enum.find(&(&1.event_type == event_type))
    |> Map.fetch!(:id)
  end

  test "renders each event kind of the vocabulary typed", %{conn: conn} do
    owner = github_user("thread-show-kinds")
    {:ok, thread} = Threads.open(owner, "Render the vocabulary")

    {:ok, thread} =
      Threads.record_event(thread, "turn.user", %{"text" => "Fix the bug", "steered" => true})

    {:ok, thread} =
      Threads.record_event(thread, "turn.reasoning", %{"text" => "The bug is in the parser."})

    {:ok, thread} =
      Threads.record_event(thread, "tool.ran", %{
        "tool" => "read_file",
        "status" => "ok",
        "arguments" => %{"path" => "lib/parser.ex"},
        "result" => "defmodule Parser do"
      })

    {:ok, thread} =
      Threads.record_event(thread, "turn.assistant", %{
        "text" => "Fixed. The parser **now** handles it.",
        "usage" => %{"input_tokens" => 100, "output_tokens" => 25, "total_tokens" => 125}
      })

    {:ok, view, _html} = live(signed_in(conn, owner), ~p"/threads/#{thread.id}")

    user_id = event_id(thread, "turn.user")

    assert view |> element("#events-#{user_id}[data-kind='turn.user']") |> render() =~
             "Fix the bug"

    assert view |> element("#events-#{user_id}") |> render() =~ "steered"

    reasoning_id = event_id(thread, "turn.reasoning")

    assert view |> element("#events-#{reasoning_id}[data-kind='turn.reasoning']") |> render() =~
             "The bug is in the parser."

    assert has_element?(view, "#events-#{reasoning_id} details")

    tool_id = event_id(thread, "tool.ran")
    tool_row = view |> element("#events-#{tool_id}[data-kind='tool.ran']") |> render()
    assert tool_row =~ "read_file"
    assert tool_row =~ "lib/parser.ex"
    assert tool_row =~ "defmodule Parser do"
    assert tool_row =~ "ok"

    assistant_id = event_id(thread, "turn.assistant")

    assistant_row =
      view |> element("#events-#{assistant_id}[data-kind='turn.assistant']") |> render()

    assert assistant_row =~ "<strong>now</strong>"
    assert assistant_row =~ "125 total"
  end

  test "an unknown event type renders as a neutral raw row", %{conn: conn} do
    owner = github_user("thread-show-unknown")
    {:ok, thread} = Threads.open(owner, "Survive the unknown")

    {:ok, thread} =
      Threads.record_event(thread, "plugin.custom", %{"whatever" => ["shape", 1, true]})

    # A typed event whose payload misses its text degrades the same way.
    {:ok, thread} = Threads.record_event(thread, "turn.user", %{"no_text" => true})

    {:ok, view, _html} = live(signed_in(conn, owner), ~p"/threads/#{thread.id}")

    unknown_id = event_id(thread, "plugin.custom")

    assert view |> element("#events-#{unknown_id}[data-kind='plugin.custom']") |> render() =~
             "plugin.custom"

    textless_id = event_id(thread, "turn.user")
    assert view |> element("#events-#{textless_id}") |> render() =~ "no_text"
  end

  test "another account's thread id is a plain 404", %{conn: conn} do
    owner = github_user("thread-show-owner")
    intruder = github_user("thread-show-intruder")
    {:ok, thread} = Threads.open(owner, "Private work")

    assert_raise OpenAgentsWeb.PublicNotFoundError, fn ->
      live(signed_in(conn, intruder), ~p"/threads/#{thread.id}")
    end
  end

  test "an unknown id is the same 404", %{conn: conn} do
    viewer = github_user("thread-show-unknown-id")

    assert_raise OpenAgentsWeb.PublicNotFoundError, fn ->
      live(signed_in(conn, viewer), ~p"/threads/#{Ecto.UUID.generate()}")
    end

    assert_raise OpenAgentsWeb.PublicNotFoundError, fn ->
      live(signed_in(conn, viewer), ~p"/threads/not-a-uuid")
    end
  end

  # METER-001. This cell used to read "$0.00 / $100.00" for a session on the
  # lane the coder actually runs on, which is the most confident wrong number
  # the product could show.
  describe "the budget card's cost cell" do
    test "an unpriced lane shows the word, never a dollar figure", %{conn: conn} do
      owner = github_user("thread-show-unpriced")

      # `gpt-5.6-luna` was the shipped unpriced lane until it was withdrawn.
      previous = UnpricedLane.admit!()
      on_exit(fn -> UnpricedLane.restore(previous) end)
      unpriced = UnpricedLane.id()

      {:ok, thread} = Threads.open(owner, "Run the unpriced lane", model: unpriced)
      {:ok, _fenced, grant, _token} = Threads.mint_grant(thread)

      {:ok, _metered} =
        OpenAgents.Inference.record_usage(grant, %{"input_tokens" => 900, "output_tokens" => 80})

      {:ok, view, _html} = live(signed_in(conn, owner), ~p"/threads/#{thread.id}")

      cost = view |> element("#thread-budget-cost") |> render()

      assert cost =~ "Unpriced"
      refute cost =~ "$0.00"
      assert view |> element("#thread-budget-cost-note") |> render() =~ "unknown rather than zero"
    end

    test "a declared priced lane shows the billable figure", %{conn: conn} do
      owner = github_user("thread-show-priced")

      {:ok, thread} = Threads.open(owner, "Run a lane with rates")
      {:ok, _fenced, grant, _token} = Threads.mint_grant(thread)
      {:ok, _metered} = OpenAgents.Inference.record_usage(grant, %{"input_tokens" => 1_000_000})

      {:ok, view, _html} = live(signed_in(conn, owner), ~p"/threads/#{thread.id}")

      # A million input tokens costs exactly the default lane's per-million
      # rate, rendered in dollars.
      dollars = Models.default().pricing.input_per_million_tokens / 1_000_000

      assert view |> element("#thread-budget-cost") |> render() =~
               "$#{:erlang.float_to_binary(dollars, decimals: 2)}"

      assert has_element?(view, ~s(#thread-budget-cost[data-basis="declared"]))
      refute has_element?(view, "#thread-budget-cost-note")
    end

    test "an unbounded ceiling reads as unbounded rather than blank", %{conn: conn} do
      owner = github_user("thread-show-unbounded")

      {:ok, thread} = Threads.open(owner, "No ceilings but the account's")
      {:ok, _fenced, _grant, _token} = Threads.mint_grant(thread)

      {:ok, view, _html} = live(signed_in(conn, owner), ~p"/threads/#{thread.id}")

      assert view |> element("#thread-budget") |> render() =~ "\u221e"
    end
  end
end