test/openagents/tools/repository_tools_test.exs

main at 58e6347eeb72 · 4 KB

defmodule OpenAgents.Tools.RepositoryToolsTest do
  @moduledoc """
  The repository read family (#122): reads over the baked source tree,
  bounded search and listing, the code_check gate, and the path/authority
  refusals that keep the family inside SELF-EDIT-001.
  """

  use OpenAgents.DataCase, async: false

  alias OpenAgents.Tools.{
    CodeCheck,
    ExecutionContext,
    Registry,
    RepoGrep,
    RepoList,
    RepoRead,
    Runner
  }

  setup do
    source = Path.join(System.tmp_dir!(), "repo-tools-#{System.unique_integer([:positive])}")
    File.mkdir_p!(Path.join(source, "lib"))
    File.write!(Path.join(source, "lib/sample.ex"), "defmodule RepoToolsFixtureSample do\nend\n")
    File.write!(Path.join(source, "README.md"), "hello forge sample\n")
    previous = Application.get_env(:openagents, :source_repo_dir)
    Application.put_env(:openagents, :source_repo_dir, source)

    on_exit(fn ->
      if previous,
        do: Application.put_env(:openagents, :source_repo_dir, previous),
        else: Application.delete_env(:openagents, :source_repo_dir)

      File.rm_rf(source)
    end)

    {:ok, snapshot} = Registry.build([RepoRead, RepoGrep, RepoList, CodeCheck])
    %{snapshot: snapshot}
  end

  defp context do
    %ExecutionContext{
      scope: "browser_conversation",
      scope_ref: "conversation:test",
      authorities: MapSet.new(["repository.read", "code.execute"])
    }
  end

  defp run(snapshot, name, arguments) do
    {:ok, outcome} =
      Runner.run(
        snapshot,
        %{
          call_id: "call-#{name}",
          name: name,
          version: 1,
          raw_arguments: Jason.encode!(arguments)
        },
        context()
      )

    outcome
  end

  test "repo_read reads a file from the baked source and refuses traversal", %{snapshot: snapshot} do
    outcome = run(snapshot, "repo_read", %{"path" => "README.md"})
    assert outcome["status"] == "succeeded"
    assert outcome["result"]["content"] =~ "hello forge"
    assert outcome["result"]["from"] == "image"

    refused = run(snapshot, "repo_read", %{"path" => "../outside.txt"})
    assert refused["status"] == "failed"
    assert refused["error"]["code"] == "invalid_repository_path"

    missing = run(snapshot, "repo_read", %{"path" => "nope.txt"})
    assert missing["error"]["code"] == "repository_file_not_found"
  end

  test "repo_grep finds bounded matches; repo_list lists the tree", %{snapshot: snapshot} do
    outcome = run(snapshot, "repo_grep", %{"pattern" => "FixtureSample"})
    assert outcome["status"] == "succeeded"
    assert [%{"path" => "lib/sample.ex", "line" => 1}] = outcome["result"]["matches"]

    bad = run(snapshot, "repo_grep", %{"pattern" => "([unclosed"})
    assert bad["error"]["code"] == "invalid_search_pattern"

    listing = run(snapshot, "repo_list", %{})
    names = Enum.map(listing["result"]["entries"], & &1["name"])
    assert "lib" in names and "README.md" in names
  end

  test "repo tools refuse without the repository authority", %{snapshot: snapshot} do
    bare_context = %ExecutionContext{
      scope: "browser_conversation",
      scope_ref: "conversation:test",
      authorities: MapSet.new(["conversation.read"])
    }

    {:ok, outcome} =
      Runner.run(
        snapshot,
        %{
          call_id: "call-refuse",
          name: "repo_read",
          version: 1,
          raw_arguments: Jason.encode!(%{"path" => "README.md"})
        },
        bare_context
      )

    assert outcome["status"] == "refused"
    assert outcome["error"]["code"] == "authority_refused"
  end

  test "code_check reports syntax health and skips compiling loaded modules", %{
    snapshot: snapshot
  } do
    ok = run(snapshot, "code_check", %{"content" => "defmodule CodeCheckFreshProbe do\nend\n"})
    assert ok["result"]["syntax"] == "ok"
    assert ok["result"]["compile"] == "ok"
    refute Code.ensure_loaded?(CodeCheckFreshProbe)

    broken =
      run(snapshot, "code_check", %{"content" => "defmodule Broken do\n  def x( do\nend\n"})

    assert broken["result"]["syntax"] == "error"
    assert broken["result"]["detail"] =~ "line"

    # A module the running system has loaded is never redefined by the probe.
    loaded =
      run(snapshot, "code_check", %{
        "content" => "defmodule OpenAgents.BuildInfo do\n  def revision, do: \"x\"\nend\n"
      })

    assert loaded["result"]["syntax"] == "ok"
    assert loaded["result"]["compile"] == "skipped"
    assert loaded["result"]["detail"] =~ "already loaded"
    assert OpenAgents.BuildInfo.revision() != "x"
  end
end