test/openagents/issue_dependencies_test.exs

main at 58e6347eeb72 · 8 KB

defmodule OpenAgents.IssueDependenciesTest do
  use OpenAgents.DataCase

  alias OpenAgents.Issues

  setup do
    {:ok, repository: repository_fixture()}
  end

  describe "add_dependencies/3" do
    test "records a prerequisite in both directions", %{repository: repository} do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Aim agents at the backlog"})
      {:ok, blocker} = Issues.create_issue(repository, %{title: "Deliver the work system"})

      assert :ok = Issues.add_dependencies(blocked, [blocker.number])

      assert %{blocked: true, blocked_by: [%{number: blocker_number, state: "open"}], blocks: []} =
               Issues.dependencies(blocked)

      assert blocker_number == blocker.number

      assert %{blocked: false, blocked_by: [], blocks: [%{number: blocked_number}]} =
               Issues.dependencies(blocker)

      assert blocked_number == blocked.number
    end

    test "recording the same prerequisite twice leaves one edge", %{repository: repository} do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Second"})
      {:ok, blocker} = Issues.create_issue(repository, %{title: "First"})

      assert :ok = Issues.add_dependencies(blocked, [blocker.number])
      assert :ok = Issues.add_dependencies(blocked, [blocker.number])

      assert %{blocked_by: [_one]} = Issues.dependencies(blocked)
    end

    test "one unknown number records none of the batch", %{repository: repository} do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Blocked"})
      {:ok, blocker} = Issues.create_issue(repository, %{title: "Blocker"})

      assert {:error, {:missing_issue, 4_242}} =
               Issues.add_dependencies(blocked, [blocker.number, 4_242])

      assert %{blocked: false, blocked_by: []} = Issues.dependencies(blocked)
    end

    test "an issue in another repository is not a prerequisite here", %{repository: repository} do
      other_repository = repository_fixture()
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Local"})
      {:ok, _first} = Issues.create_issue(other_repository, %{title: "First elsewhere"})
      {:ok, elsewhere} = Issues.create_issue(other_repository, %{title: "Elsewhere"})

      refute elsewhere.number == blocked.number

      assert {:error, {:missing_issue, number}} =
               Issues.add_dependencies(blocked, [elsewhere.number])

      assert number == elsewhere.number
    end

    test "an issue cannot be its own prerequisite", %{repository: repository} do
      {:ok, issue} = Issues.create_issue(repository, %{title: "Alone"})

      assert {:error, {:self_reference, number}} =
               Issues.add_dependencies(issue, [issue.number])

      assert number == issue.number
    end

    test "a value that is not an issue number is refused", %{repository: repository} do
      {:ok, issue} = Issues.create_issue(repository, %{title: "Alone"})

      assert {:error, {:invalid_number, "soon"}} = Issues.add_dependencies(issue, ["soon"])
    end

    test "an edge that would close a cycle is refused", %{repository: repository} do
      {:ok, first} = Issues.create_issue(repository, %{title: "First"})
      {:ok, second} = Issues.create_issue(repository, %{title: "Second"})
      {:ok, third} = Issues.create_issue(repository, %{title: "Third"})

      assert :ok = Issues.add_dependencies(second, [first.number])
      assert :ok = Issues.add_dependencies(third, [second.number])

      assert {:error, {:cycle, path}} = Issues.add_dependencies(first, [third.number])
      assert path == [first.number, third.number, second.number, first.number]

      assert %{blocked_by: []} = Issues.dependencies(first)
    end
  end

  describe "derived blocked state" do
    test "closing the last prerequisite unblocks the issue", %{repository: repository} do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Waiting"})
      {:ok, blocker} = Issues.create_issue(repository, %{title: "Prerequisite"})

      assert :ok = Issues.add_dependencies(blocked, [blocker.number])
      assert %{blocked: true} = Issues.dependencies(blocked)

      {:ok, blocker} = Issues.update_issue(blocker, %{"state" => "closed"})
      assert %{blocked: false, blocked_by: [%{state: "closed"}]} = Issues.dependencies(blocked)

      {:ok, _reopened} = Issues.update_issue(blocker, %{"state" => "open"})
      assert %{blocked: true} = Issues.dependencies(blocked)
    end

    test "one open prerequisite among closed ones still blocks", %{repository: repository} do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Waiting"})
      {:ok, done} = Issues.create_issue(repository, %{title: "Done", state: "closed"})
      {:ok, pending} = Issues.create_issue(repository, %{title: "Pending"})

      assert :ok = Issues.add_dependencies(blocked, [done.number, pending.number])
      assert %{blocked: true, blocked_by: [_first, _second]} = Issues.dependencies(blocked)
    end
  end

  describe "remove_dependency/2" do
    test "removes one edge and leaves the rest", %{repository: repository} do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Waiting"})
      {:ok, first} = Issues.create_issue(repository, %{title: "First"})
      {:ok, second} = Issues.create_issue(repository, %{title: "Second"})

      assert :ok = Issues.add_dependencies(blocked, [first.number, second.number])
      assert :ok = Issues.remove_dependency(blocked, first.number)

      assert %{blocked_by: [%{number: remaining}]} = Issues.dependencies(blocked)
      assert remaining == second.number
    end

    test "removing an edge that is not recorded reports the mismatch", %{
      repository: repository
    } do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Waiting"})
      {:ok, other} = Issues.create_issue(repository, %{title: "Unrelated"})

      assert {:error, {:missing_dependency, number}} =
               Issues.remove_dependency(blocked, other.number)

      assert number == other.number
    end
  end

  describe "list_issues_page/2 with the blocked filter" do
    test "blocked and unblocked partition the repository", %{repository: repository} do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Waiting"})
      {:ok, blocker} = Issues.create_issue(repository, %{title: "Prerequisite"})
      {:ok, free} = Issues.create_issue(repository, %{title: "Ready"})

      assert :ok = Issues.add_dependencies(blocked, [blocker.number])

      assert {[%{id: id}], 1} = Issues.list_issues_page(repository, blocked: true)
      assert id == blocked.id

      {unblocked, total} = Issues.list_issues_page(repository, blocked: false)
      assert total == 2
      assert Enum.map(unblocked, & &1.id) |> Enum.sort() == Enum.sort([blocker.id, free.id])
    end

    test "closing the prerequisite moves the issue between the two lists", %{
      repository: repository
    } do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Waiting"})
      {:ok, blocker} = Issues.create_issue(repository, %{title: "Prerequisite"})

      assert :ok = Issues.add_dependencies(blocked, [blocker.number])
      assert {_issues, 1} = Issues.list_issues_page(repository, blocked: true)

      {:ok, _closed} = Issues.update_issue(blocker, %{"state" => "closed"})

      assert {[], 0} = Issues.list_issues_page(repository, blocked: true)
      assert {[%{id: id}], 1} = Issues.list_issues_page(repository, blocked: false)
      assert id == blocked.id
    end

    test "the filter composes with the state filter", %{repository: repository} do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Waiting"})
      {:ok, blocker} = Issues.create_issue(repository, %{title: "Prerequisite"})
      assert :ok = Issues.add_dependencies(blocked, [blocker.number])

      {:ok, _closed_blocked} = Issues.update_issue(blocked, %{"state" => "closed"})

      assert {[], 0} = Issues.list_issues_page(repository, blocked: true, state: "open")
      assert {[_issue], 1} = Issues.list_issues_page(repository, blocked: true, state: "closed")
    end
  end

  describe "dependency_graph/1" do
    test "reads a whole page of edges without walking the graph per row", %{
      repository: repository
    } do
      {:ok, blocked} = Issues.create_issue(repository, %{title: "Waiting"})
      {:ok, blocker} = Issues.create_issue(repository, %{title: "Prerequisite"})
      {:ok, unrelated} = Issues.create_issue(repository, %{title: "Unrelated"})

      assert :ok = Issues.add_dependencies(blocked, [blocker.number])

      graph = Issues.dependency_graph([blocked, blocker, unrelated])

      assert %{blocked: true, blocked_by: [_one], blocks: []} = graph[blocked.id]
      assert %{blocked: false, blocked_by: [], blocks: [_one]} = graph[blocker.id]
      assert %{blocked: false, blocked_by: [], blocks: []} = graph[unrelated.id]
    end

    test "an empty list of issues reads no edges", %{repository: _repository} do
      assert Issues.dependency_graph([]) == %{}
    end
  end
end