test/openagents_web/live/issue_start_work_live_test.exs

main at 58e6347eeb72 · 8 KB

defmodule OpenAgentsWeb.IssueStartWorkLiveTest do
  @moduledoc """
  Stage 2 of `#10`: start bounded agent work from the issue that describes it.

  The admission already existed and is not duplicated here. What these tests
  hold is the issue-side entry point: who is offered it, what it reads from the
  issue rather than from free text, and that every refusal the context returns
  reaches the person as itself.
  """
  use OpenAgentsWeb.ConnCase, async: false

  import Phoenix.LiveViewTest

  alias OpenAgents.Computer
  alias OpenAgents.Forge.Assignment
  alias OpenAgents.Issues
  alias OpenAgents.Machines
  alias OpenAgents.Repo

  setup %{conn: conn} do
    repository = OpenAgents.Repositories.get_by_path!("OpenAgentsInc", "openagents.com")
    user = github_user("issue-work-starter")
    {:ok, _membership} = OpenAgents.Repositories.add_member(repository, user, "owner")
    conn = Plug.Test.init_test_session(conn, %{"user_id" => user.id})

    {:ok, issue} =
      Issues.create_issue(repository, %{title: "Ship the join", body: "Bind the receipts."})

    %{conn: conn, user: user, repository: repository, issue: issue}
  end

  defp path(issue), do: ~p"/OpenAgentsInc/openagents.com/issues/#{issue.number}"

  describe "who is offered the control" do
    test "a maintainer with no connected computer is told why, not shown a form", %{
      conn: conn,
      issue: issue
    } do
      {:ok, view, _html} = live(conn, path(issue))

      assert has_element?(view, "#issue-work")
      assert has_element?(view, "#issue-work-unavailable")
      refute has_element?(view, "#issue-work-form")
    end

    test "a maintainer with a connected computer is offered the form", context do
      connected_computer(context.user)

      {:ok, view, _html} = live(context.conn, path(context.issue))

      assert has_element?(view, "#issue-work-form")
      assert has_element?(view, "#issue-work-start")
    end

    test "an anonymous reader is offered nothing and cannot reach the event", context do
      connected_computer(context.user)

      {:ok, view, _html} = live(build_conn(), path(context.issue))

      refute has_element?(view, "#issue-work")
      refute has_element?(view, "#issue-work-form")

      render_hook(view, "start_work", %{"work" => %{}})

      assert render(view) =~ "You can no longer start work on this issue."
      assert Repo.aggregate(Assignment, :count) == 0
    end

    test "a signed-in reader without write authority is refused", context do
      connected_computer(context.user)
      reader = github_user("issue-work-reader")
      reader_conn = Plug.Test.init_test_session(build_conn(), %{"user_id" => reader.id})

      {:ok, view, _html} = live(reader_conn, path(context.issue))

      refute has_element?(view, "#issue-work")

      render_hook(view, "start_work", %{"work" => %{}})

      assert Repo.aggregate(Assignment, :count) == 0
    end
  end

  describe "starting the work" do
    test "the attempt appears on the timeline without a reload", context do
      machine = connected_computer(context.user)

      {:ok, view, _html} = live(context.conn, path(context.issue))

      html =
        view
        |> form("#issue-work-form", %{
          "work" => %{
            "computer_id" => machine.id,
            "agent_id" => "codex",
            "cwd" => "/work",
            "branch" => "agent/issue-#{context.issue.number}"
          }
        })
        |> render_submit()

      assert html =~ "Work started on"
      assert html =~ "started work on a computer, on branch agent/issue-#{context.issue.number}"

      assert %Assignment{} =
               assignment = Repo.get_by(Assignment, issue_id: context.issue.id)

      assert assignment.branch == "agent/issue-#{context.issue.number}"
      assert assignment.repository_id == context.repository.id
      assert assignment.machine_id == machine.id
      assert assignment.requesting_principal["id"] == context.user.id
    end

    test "the objective is read from the issue rather than typed beside it", context do
      machine = connected_computer(context.user)

      {:ok, view, _html} = live(context.conn, path(context.issue))

      view
      |> form("#issue-work-form", %{
        "work" => %{
          "computer_id" => machine.id,
          "agent_id" => "codex",
          "cwd" => "/work",
          "branch" => "agent/objective"
        }
      })
      |> render_submit()

      assert %Assignment{work_job_id: job_id} =
               Repo.get_by(Assignment, issue_id: context.issue.id)

      assert job_id

      job = Repo.get!(OpenAgents.Work.Job, job_id)
      prompt = job.delegation["prompt"]

      assert prompt =~ "issue ##{context.issue.number}"
      assert prompt =~ "Ship the join"
      assert prompt =~ "Bind the receipts."
      assert prompt =~ "agent/objective"
    end

    test "a protected branch is refused by name and starts nothing", context do
      machine = connected_computer(context.user)

      {:ok, view, _html} = live(context.conn, path(context.issue))

      html =
        view
        |> form("#issue-work-form", %{
          "work" => %{
            "computer_id" => machine.id,
            "agent_id" => "codex",
            "cwd" => "/work",
            "branch" => "main"
          }
        })
        |> render_submit()

      assert html =~ "That branch is protected"
      assert Repo.aggregate(Assignment, :count) == 0
    end

    # The working directory and the agent are chosen from what the computer
    # itself declared, never taken from the request. A crafted event asking for
    # somewhere else does not reach the admission with that value at all.
    test "a working directory the computer never declared is never forwarded", context do
      machine = connected_computer(context.user)

      {:ok, view, _html} = live(context.conn, path(context.issue))

      render_hook(view, "start_work", %{
        "work" => %{
          "computer_id" => machine.id,
          "agent_id" => "impostor",
          "cwd" => "/elsewhere",
          "branch" => "agent/outside"
        }
      })

      assert %Assignment{work_job_id: job_id} =
               Repo.get_by(Assignment, issue_id: context.issue.id)

      job = Repo.get!(OpenAgents.Work.Job, job_id)

      assert job.delegation["cwd"] == "/work"
      assert job.delegation["agent_id"] == "codex"
    end
  end

  describe "one attempt at a time" do
    test "a live attempt replaces the form with the branch it is running on", context do
      machine = connected_computer(context.user)
      live_attempt(context, machine, "agent/already-running")

      {:ok, view, _html} = live(context.conn, path(context.issue))

      assert has_element?(view, "#issue-work-live")
      refute has_element?(view, "#issue-work-form")
      assert render(view) =~ "agent/already-running"
    end

    test "starting a second attempt is refused with the live one named", context do
      machine = connected_computer(context.user)

      {:ok, view, _html} = live(context.conn, path(context.issue))

      live_attempt(context, machine, "agent/already-running")

      render_hook(view, "start_work", %{
        "work" => %{
          "computer_id" => machine.id,
          "agent_id" => "codex",
          "cwd" => "/work",
          "branch" => "agent/second"
        }
      })

      html = render(view)
      assert html =~ "Work is already running on this issue"
      assert Repo.aggregate(Assignment, :count) == 1
    end
  end

  # ── fixtures ────────────────────────────────────────────────────────────

  defp connected_computer(user) do
    {:ok, %{code: code}} =
      Machines.start_pairing(%{
        "name" => "workstation",
        "tier" => "curated",
        "platform" => "linux-x64",
        "agent_version" => "0.1.0",
        "roots" => ["/work"]
      })

    {:ok, machine} = Machines.approve_pairing(user, code)
    {:ok, machine} = Machines.store_probe(machine, %{"acp_agents" => [%{"id" => "codex"}]})
    {:ok, _owner} = Computer.register(machine.id)
    on_exit(fn -> Computer.unregister(machine.id) end)
    machine
  end

  defp live_attempt(%{repository: repository, issue: issue, user: user}, machine, branch) do
    now = DateTime.utc_now() |> DateTime.truncate(:microsecond)

    %Assignment{}
    |> Assignment.changeset(%{
      target_kind: "computer",
      machine_id: machine.id,
      repository_id: repository.id,
      issue_id: issue.id,
      requesting_principal: %{"type" => "user", "id" => user.id},
      branch: branch,
      state: "running",
      admitted_at: now,
      started_at: now,
      deadline_at: DateTime.add(now, 3600, :second)
    })
    |> Repo.insert!()
  end
end