test/openagents/tools/incident_lookup_test.exs

58e6347eeb72 · 6 KB

defmodule OpenAgents.Tools.IncidentLookupTest do
  use OpenAgents.DataCase

  alias OpenAgents.Incidents
  alias OpenAgents.Tools.{ExecutionContext, Registry, Runner}
  alias OpenAgents.{Accounts, Conversations, Machines, Repo}

  setup do
    assert {:ok, snapshot} = Registry.build([OpenAgents.Tools.IncidentLookup])
    %{snapshot: snapshot}
  end

  test "returns the owner's own incidents for this conversation", %{snapshot: snapshot} do
    scope = owner_scope("incident-tool")

    {:ok, _} =
      Incidents.record(%{
        conversation_id: scope.conversation.id,
        owner_user_id: scope.user.id,
        owner_visitor_id: scope.owner.id,
        surface: "text",
        origin: "turn_server",
        correlation_ref: "turn-x",
        code: "transport:provider_task_exited",
        summary: "Text turn failed"
      })

    assert {:ok, outcome} =
             Runner.run(snapshot, call("incident_lookup", %{}), context(scope))

    assert outcome["status"] == "succeeded"
    assert outcome["result"]["status"] == "matches"
    assert outcome["result"]["primary"] == "incident"
    assert outcome["result"]["jobs"] == []
    assert [listed] = outcome["result"]["incidents"]
    assert listed["code"] == "transport:provider_task_exited"
    assert listed["severity"] == "degraded"
    assert listed["recurrence_count"] >= 1
  end

  test "never returns another owner's incidents", %{snapshot: snapshot} do
    scope = owner_scope("incident-tool-scoped")
    other = owner_scope("incident-tool-foreign")

    {:ok, _} =
      Incidents.record(%{
        conversation_id: other.conversation.id,
        owner_user_id: other.user.id,
        owner_visitor_id: other.owner.id,
        surface: "text",
        origin: "turn_server",
        code: "leak_me"
      })

    assert {:ok, outcome} =
             Runner.run(snapshot, call("incident_lookup", %{"scope" => "owner"}), context(scope))

    assert outcome["result"]["status"] == "none"
    assert outcome["result"]["primary"] == "none"
    assert outcome["result"]["incidents"] == []
    assert outcome["result"]["jobs"] == []
  end

  test "a newer job report is primary over a stale incident", %{snapshot: snapshot} do
    scope = owner_scope("incident-tool-stale")
    machine = machine_for(scope.user)

    {:ok, _} =
      Incidents.record(%{
        conversation_id: scope.conversation.id,
        owner_user_id: scope.user.id,
        owner_visitor_id: scope.owner.id,
        surface: "text",
        origin: "turn_server",
        code: "unknown",
        summary: "Text turn failed: unknown"
      })

    {:ok, job} =
      OpenAgents.Work.create_job(%{
        conversation_id: scope.conversation.id,
        owner_visitor_id: scope.owner.id,
        machine_id: machine.id,
        surface: "text",
        kind: "delegation",
        goal: "Delegate to claude: input-bar refactor",
        delegation: %{
          "agent_id" => "claude",
          "machine_id" => machine.id,
          "machine_name" => machine.name,
          "prompt" => "input-bar refactor",
          "cwd" => "/tmp/openagents-incidents",
          "timeout_ms" => 3_600_000
        },
        authority_snapshot: %{
          "machine_tier" => machine.tier,
          "roots" => machine.roots,
          "cwd" => "/tmp/openagents-incidents",
          "agent_id" => "claude",
          "machine_name" => machine.name
        },
        budget_snapshot: %{
          "wall_clock_ms" => 3_600_000,
          "maximum_prompt_bytes" => 8_000,
          "maximum_report_bytes" => 8_000
        }
      })

    {:ok, running} = OpenAgents.Work.mark_job_running(job, %{})

    {:ok, _} =
      OpenAgents.Work.append_report_delta(
        running,
        "Delegation to claude on devin-test — timed out.\n\nTerminal: git status"
      )

    {:ok, finished} =
      OpenAgents.Work.finish_job(running.id, "failed", error_code: "delegation_timeout")

    assert {:ok, outcome} =
             Runner.run(snapshot, call("incident_lookup", %{}), context(scope))

    assert outcome["result"]["status"] == "matches"
    assert outcome["result"]["primary"] == "job"
    assert outcome["result"]["guidance"] =~ "jobs[]"
    assert [listed_job] = outcome["result"]["jobs"]
    assert listed_job["id"] == finished.id
    assert listed_job["kind"] == "delegation"
    assert listed_job["report_excerpt"] =~ "timed out"
    assert listed_job["error_code"] == "delegation_timeout"
  end

  test "correlation_ref filters incidents and jobs to that id", %{snapshot: snapshot} do
    scope = owner_scope("incident-tool-ref")

    {:ok, kept} =
      Incidents.record(%{
        conversation_id: scope.conversation.id,
        owner_user_id: scope.user.id,
        owner_visitor_id: scope.owner.id,
        surface: "text",
        origin: "turn_server",
        correlation_ref: "turn-keep",
        code: "turn_timeout"
      })

    {:ok, _} =
      Incidents.record(%{
        conversation_id: scope.conversation.id,
        owner_user_id: scope.user.id,
        owner_visitor_id: scope.owner.id,
        surface: "text",
        origin: "turn_server",
        correlation_ref: "turn-drop",
        code: "cancelled"
      })

    assert {:ok, outcome} =
             Runner.run(
               snapshot,
               call("incident_lookup", %{"correlation_ref" => "turn-keep"}),
               context(scope)
             )

    assert [listed] = outcome["result"]["incidents"]
    assert listed["id"] == kept.id
    assert listed["code"] == "turn_timeout"
  end

  defp call(name, arguments) do
    %{
      call_id: "call_#{System.unique_integer([:positive])}",
      name: name,
      version: 1,
      raw_arguments: Jason.encode!(arguments)
    }
  end

  defp context(scope) do
    %ExecutionContext{
      scope: "browser_conversation",
      scope_ref: "conversation:#{scope.conversation.id}",
      authorities: MapSet.new(["conversation.read"]),
      conversation_id: scope.conversation.id,
      owner_visitor_id: scope.owner.id
    }
  end

  defp owner_scope(login) do
    {:ok, user} =
      Accounts.upsert_github_user(%{
        github_id: System.unique_integer([:positive]),
        github_login: login,
        github_avatar_url: "https://avatars.githubusercontent.com/u/1?v=4"
      })

    {:ok, conversation} = Conversations.ensure_conversation(user)
    owner = Repo.get!(OpenAgents.Conversations.Visitor, conversation.visitor_id)
    %{user: user, owner: owner, conversation: conversation}
  end

  defp machine_for(user) do
    {:ok, %{code: code}} =
      Machines.start_pairing(%{
        "name" => "incident-test-machine",
        "tier" => "curated",
        "platform" => "linux-x64",
        "agent_version" => "0.1.0",
        "roots" => ["/tmp/openagents-incidents"]
      })

    {:ok, machine} = Machines.approve_pairing(user, code)
    machine
  end
end