test/openagents/threads/local_lane_test.exs

58e6347eeb72 · 5 KB

defmodule OpenAgents.Threads.LocalLaneTest do
  @moduledoc """
  The transcript-only local lane (THREAD-001, issue #243).

  A local-lane thread records a run whose model calls never touch this server:
  its model is the vendor string a local runtime serves, and it holds no
  authority, ever — `mint_grant/1` refuses it the way it refuses a terminal
  thread. Everything else about it is an ordinary thread: the transcript
  appends and broadcasts, the open-thread cap counts it, and cancelling ends
  it. Both halves are proved here, because the lane's contract is exactly that
  pair.
  """
  use OpenAgents.DataCase, async: false

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

  alias OpenAgents.Threads
  alias OpenAgents.Threads.Event
  alias OpenAgents.Threads.Thread

  @vendor_model "ollama:qwen3.8:27b-mtp-q8_0"

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

  defp open_local(user, objective) do
    Threads.open(user, objective, lane: "local", model: @vendor_model)
  end

  describe "open/3 on the local lane" do
    test "records the vendor model string and mints nothing" do
      user = owner("open")

      assert {:ok, thread} = open_local(user, "Record a local run")

      assert thread.lane == "local"
      assert thread.status == "open"
      assert thread.model == @vendor_model
      # No mint means no fence bump and no grant row: the thread has never
      # held authority, not merely lost it.
      assert thread.generation == 0
      assert Threads.active_grants(thread) == []
      assert Threads.latest_grant(thread) == nil
    end

    test "the opened event names the lane" do
      user = owner("opened-event")

      {:ok, thread} = open_local(user, "Name the lane up front")

      assert [%Event{event_type: "thread.opened", payload: payload}] = Threads.list_events(thread)
      assert payload["lane"] == "local"
    end

    test "a thread-lane open records no lane in its opened event" do
      user = owner("granted-event")

      {:ok, thread} = Threads.open(user, "The granted lane is unmarked")

      assert thread.lane == Thread.default_lane()
      assert [%Event{event_type: "thread.opened", payload: payload}] = Threads.list_events(thread)
      refute Map.has_key?(payload, "lane")
    end

    test "a lane outside the admitted pair is refused" do
      user = owner("unknown")

      assert {:error, changeset} = Threads.open(user, "Objective", lane: "gym")
      assert %{lane: _} = errors_on(changeset)
    end

    test "the model string is bounded like every thread's model column" do
      user = owner("bound")

      assert {:error, changeset} =
               Threads.open(user, "Objective",
                 lane: "local",
                 model: String.duplicate("a", 201)
               )

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

  describe "mint_grant/1" do
    test "refuses a local-lane thread with :thread_local_lane" do
      user = owner("mint")
      {:ok, thread} = open_local(user, "Ask for authority the lane forbids")

      assert {:error, :thread_local_lane} = Threads.mint_grant(thread)

      # The refusal leaves no trace of an attempt: no fence bump, no grant.
      refused = Threads.get_for_user(user, thread.id)
      assert refused.generation == 0
      assert Threads.active_grants(refused) == []
    end
  end

  describe "the transcript" do
    test "events record and broadcast exactly as thread-lane events do" do
      user = owner("transcript")
      {:ok, thread} = open_local(user, "Stream the local run")

      :ok = Threads.subscribe(thread)

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

      assert updated.event_count == 2

      assert_receive {:thread_event,
                      %Event{event_type: "turn.user", payload: %{"text" => "Fix the bug"}}}
    end
  end

  describe "admission and the reaper" do
    test "a local-lane thread counts against the open-thread cap" do
      user = owner("cap")
      previous = Application.get_env(:openagents, :maximum_open_threads_per_account)
      Application.put_env(:openagents, :maximum_open_threads_per_account, 1)

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

      {:ok, _local} = open_local(user, "Hold the one slot")

      assert {:error, :thread_quota_reached} = Threads.open(user, "One too many")
    end

    test "the reaper leaves an open local-lane thread open" do
      user = owner("reap")
      {:ok, thread} = open_local(user, "Nothing to spend, nothing to reap")

      # The authority-spent sweep closes threads that minted and hold nothing.
      # A local thread never minted, so it is not abandoned authority — it is
      # a transcript still being written.
      _reaped = Threads.reap_expired(user)

      assert Threads.get_for_user(user, thread.id).status == "open"
    end
  end

  test "cancelling ends a local-lane thread like any other" do
    user = owner("cancel")
    {:ok, thread} = open_local(user, "End the record")

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

    assert {:error, :thread_terminal} =
             Threads.record_event(cancelled, "turn.user", %{"text" => "late"})
  end
end