test/openagents/forge/independence_test.exs

58e6347eeb72 · 38 KB

defmodule OpenAgents.Forge.IndependenceTest do
  @moduledoc """
  EXIT-002, EXIT-003, and EXIT-004: what a user can check, recover, and leave
  with when they do not take the operator's word for anything.

  Every test drives the real git client over real HTTP, so the WAL entries
  under examination are genuine `receive-pack` requests and the clones are
  genuine clones. A verifier that only ever saw synthetic entries would prove
  nothing about the forge people actually push to.
  """

  use OpenAgents.DataCase, async: false

  alias OpenAgents.Forge.{
    Anchor,
    Independence,
    PushReceipt,
    Pushes,
    Repos,
    Sync,
    Verification,
    WAL
  }

  alias OpenAgents.Repo

  defmodule TestPipeline do
    @moduledoc false
    use Plug.Builder

    plug OpenAgentsWeb.Plugs.ForgeGitAuth
    plug OpenAgents.Forge.GitHTTP
  end

  setup do
    Ecto.Adapters.SQL.Sandbox.mode(OpenAgents.Repo, {:shared, self()})

    base =
      Path.join(System.tmp_dir!(), "forge-independence-#{System.unique_integer([:positive])}")

    File.mkdir_p!(base)
    previous_data = Application.get_env(:openagents, :forge_data_dir)
    previous_wal = Application.get_env(:openagents, :forge_wal_dir)
    previous_mirrors = Application.get_env(:openagents, :forge_mirror_urls)
    Application.put_env(:openagents, :forge_data_dir, Path.join(base, "data"))
    Application.put_env(:openagents, :forge_wal_dir, Path.join(base, "wal"))
    OpenAgents.Forge.CacheReadiness.reset()

    user = OpenAgents.AccountsFixtures.repository_user_fixture("exit-owner")

    {:ok, repository, :created} =
      OpenAgents.Repositories.create_user_repository(user, %{name: "demo"}, "exit-demo")

    repository =
      repository
      |> Ecto.Changeset.change(lifecycle_state: "ready", ready_at: DateTime.utc_now())
      |> OpenAgents.Repo.update!()

    {:ok, _api_token, plaintext} =
      OpenAgents.ApiTokens.create(user, %{
        name: "forge independence test",
        scopes: ["forge:write"],
        lifetime_days: 1
      })

    port = free_port()
    start_supervised!({Bandit, plug: TestPipeline, port: port, ip: {127, 0, 0, 1}})

    on_exit(fn ->
      Application.put_env(:openagents, :forge_data_dir, previous_data)
      Application.put_env(:openagents, :forge_wal_dir, previous_wal)

      if previous_mirrors do
        Application.put_env(:openagents, :forge_mirror_urls, previous_mirrors)
      else
        Application.delete_env(:openagents, :forge_mirror_urls)
      end

      OpenAgents.Forge.CacheReadiness.reset()
      File.rm_rf(base)
    end)

    %{
      base: base,
      repo: repository.storage_key,
      repository: repository,
      url: "http://x:#{plaintext}@127.0.0.1:#{port}/exit-owner/demo.git"
    }
  end

  ## ── EXIT-002: independent verification ─────────────────────────────────

  describe "independent verification" do
    test "an untampered forge verifies clean", context do
      seed_history!(context)

      assert {:ok, %{findings: []} = report} = Verification.verify(context.repo)
      assert report.entries >= 2
      assert report.repo == context.repo
    end

    test "a ref moved without a push is reported", context do
      seed_history!(context)
      path = Repos.bare_path(context.repo)

      {parent, 0} = Repos.git(path, ["rev-parse", "refs/heads/main^"])

      {_output, 0} =
        Repos.git(path, ["update-ref", "refs/heads/main", String.trim(parent)])

      assert {:error, %{findings: findings}} = Verification.verify(context.repo)
      assert %{"ref" => "refs/heads/main"} = detail(findings, "served_refs_diverged")
    end

    test "a ref added without a push is reported", context do
      seed_history!(context)
      path = Repos.bare_path(context.repo)
      {head, 0} = Repos.git(path, ["rev-parse", "refs/heads/main"])

      {_output, 0} =
        Repos.git(path, ["update-ref", "refs/heads/smuggled", String.trim(head)])

      assert {:error, %{findings: findings}} = Verification.verify(context.repo)

      assert %{"ref" => "refs/heads/smuggled", "recorded" => nil} =
               detail(findings, "served_refs_diverged")
    end

    test "a rewritten entry no longer matches the key the index recorded", context do
      seed_history!(context)
      {:ok, _generation, index} = WAL.read_index(context.repo)
      [entry | _rest] = WAL.entries(index)

      # The key is a digest of the payload, so replacing the payload leaves the
      # index naming a key its own contents no longer produce.
      overwrite_entry!(context, entry["object"], "tampered")

      assert {:error, %{findings: findings}} = Verification.verify(context.repo)
      assert %{"seq" => 0} = detail(findings, "entry_digest_mismatch")
    end

    test "a removed entry breaks the recorded sequence", context do
      seed_history!(context)
      {:ok, generation, index} = WAL.read_index(context.repo)
      [_dropped | kept] = WAL.entries(index)

      {:ok, _generation} =
        WAL.cas_index(context.repo, generation, Map.put(index, "entries", kept))

      assert {:error, %{findings: findings}} = Verification.verify(context.repo)
      assert detail(findings, "entry_sequence_broken") != nil
    end

    test "an entry object the store cannot produce is reported", context do
      seed_history!(context)
      {:ok, _generation, index} = WAL.read_index(context.repo)
      [entry | _rest] = WAL.entries(index)

      File.rm!(wal_object_path(context, entry["object"]))

      assert {:error, %{findings: findings}} = Verification.verify(context.repo)
      assert %{"seq" => 0} = detail(findings, "entry_object_missing")
    end

    test "a lost cache is reported as an empty projection, not as a disagreement", context do
      seed_history!(context)
      {:ok, %{status: :current, head_seq: head_seq}} = Verification.verify(context.repo)
      File.rm_rf!(Repos.bare_path(context.repo))

      # A cache that was deleted and a node that has not replayed yet are the
      # same observation from the WAL and the repository alone, and the second
      # is every node's ordinary state (issue #251). Both are reported as an
      # empty projection at sequence -1 rather than as tampering, which is what
      # `REPOSITORY-003` says a disposable projection is. It is still not
      # reported as current: the report says how far from the log it is.
      assert {:ok, report} = Verification.verify(context.repo)
      assert report.findings == []
      assert report.status == :behind
      assert report.position == -1
      assert report.behind == head_seq + 1
    end

    test "verification reaches no database", _context do
      # Independence is structural, not a promise: a verifier that queried
      # PostgreSQL would be asking the operator to confirm the operator.
      for module <- external_calls(Verification) do
        refute module == OpenAgents.Repo
        refute match?("Ecto." <> _rest, inspect(module))
        refute match?("Postgrex" <> _rest, inspect(module))
      end
    end

    test "an owner/name path resolves to the same report as its storage key", context do
      seed_history!(context)

      assert {:ok, by_key} = Verification.verify(context.repo)
      assert {:ok, by_name} = Verification.verify("exit-owner/demo")

      assert by_name.repo == "exit-owner/demo"
      assert by_name.storage_key == context.repo
      assert by_key.entries == by_name.entries
      assert by_key.head == by_name.head
      assert by_key.findings == []
      assert by_name.findings == []
    end

    test "an unknown owner/name returns a repository_not_found finding", _context do
      {:error, result} = Verification.verify("exit-owner/no-such-repo")

      assert result.repo == "exit-owner/no-such-repo"
      assert result.storage_key == nil
      assert result.entries == 0

      assert [%{code: "repository_not_found", detail: %{"repo" => "exit-owner/no-such-repo"}}] =
               result.findings
    end
  end

  ## ── EXIT-005: each entry commits to the entry before it ────────────────

  describe "chained entries" do
    test "every accepted push links to the push before it", context do
      seed_history!(context)

      assert {:ok, report} = Verification.verify(context.repo)
      assert report.chained_from == 0
      assert %{seq: seq, link: link} = report.head
      assert seq == report.entries - 1
      assert link =~ ~r/^[0-9a-f]{64}$/

      {:ok, _generation, index} = WAL.read_index(context.repo)

      index
      |> WAL.entries()
      |> Enum.reduce("", fn entry, previous ->
        assert {:ok, derived} = WAL.chain_link(previous, Map.delete(entry, "link"))
        assert derived == WAL.entry_link(entry)
        derived
      end)
    end

    test "a rewritten entry that leaves the chain alone is reported", context do
      seed_history!(context)
      {:ok, generation, index} = WAL.read_index(context.repo)
      [first | rest] = WAL.entries(index)

      File.rm!(wal_object_path(context, first["object"]))
      {:ok, key} = WAL.put_entry(context.repo, 0, "a payload the pusher never sent")

      {:ok, _generation} =
        WAL.cas_index(
          context.repo,
          generation,
          Map.put(index, "entries", [%{first | "object" => key} | rest])
        )

      assert {:error, %{findings: findings}} = Verification.verify(context.repo)
      assert %{"seq" => 0} = detail(findings, "chain_link_mismatch")
    end

    test "a link removed from the middle of the log is reported", context do
      seed_history!(context)
      {:ok, generation, index} = WAL.read_index(context.repo)
      [first | rest] = WAL.entries(index)

      {:ok, _generation} =
        WAL.cas_index(
          context.repo,
          generation,
          Map.put(index, "entries", [first | Enum.map(rest, &Map.delete(&1, "link"))])
        )

      assert {:error, %{findings: findings}} = Verification.verify(context.repo)
      assert %{"seq" => 1} = detail(findings, "chain_link_missing")
    end

    test "entries written before the chain are a boundary, not a finding", context do
      seed_history!(context)
      {:ok, generation, index} = WAL.read_index(context.repo)
      entries = WAL.entries(index)

      # The production shape on the day this ships: every entry already in the
      # log carries no link, and the first push after it binds to the chain
      # start because there is no predecessor link to name.
      {legacy, [newest]} = Enum.split(entries, length(entries) - 1)
      {:ok, link} = WAL.chain_link("", Map.delete(newest, "link"))

      relinked = Enum.map(legacy, &Map.delete(&1, "link")) ++ [Map.put(newest, "link", link)]

      {:ok, _generation} =
        WAL.cas_index(context.repo, generation, Map.put(index, "entries", relinked))

      assert {:ok, report} = Verification.verify(context.repo)
      assert report.findings == []
      assert report.chained_from == newest["seq"]
    end

    test "a push that changes no ref appends nothing and leaves the chain alone", context do
      seed_history!(context)
      assert {:ok, %{head: head, entries: count}} = Verification.verify(context.repo)

      sh!(work_dir(context), "git", ["push", "origin", "HEAD:main"])

      assert {:ok, %{head: ^head, entries: ^count}} = Verification.verify(context.repo)
    end

    test "a consistent rewrite verifies clean, and only an anchor reports it", context do
      seed_history!(context)
      assert {:ok, %{findings: [], head: head}} = Verification.verify(context.repo)

      rewrite_first_entry_consistently!(context, "a payload the pusher never sent")

      # This is the limit, stated as a test rather than as a caveat: an
      # operator who rewrites the entry, its content-addressed key, the index,
      # and every link after it leaves nothing inside their own storage that
      # disagrees with anything else inside it.
      assert {:ok, %{findings: []}} = Verification.verify(context.repo)

      # A link remembered before the rewrite is outside that storage, and it
      # disagrees. One remembered link covers the whole prefix, because the
      # chain makes a rewrite non-local.
      assert {:error, %{findings: findings}} = Verification.verify(context.repo, anchor: head)
      assert %{"seq" => _seq} = detail(findings, "anchor_mismatch")
    end

    test "an anchor for a sequence the log does not have is reported", context do
      seed_history!(context)

      anchor = %{seq: 99, link: String.duplicate("a", 64)}

      assert {:error, %{findings: findings}} = Verification.verify(context.repo, anchor: anchor)
      assert %{"seq" => 99} = detail(findings, "anchor_unreachable")
    end

    test "an unreadable anchor is ignored rather than failing verification", context do
      seed_history!(context)

      assert {:ok, %{findings: []}} = Verification.verify(context.repo, anchor: "nonsense")
      assert {:ok, %{findings: []}} = Verification.verify(context.repo, anchor: nil)
    end
  end

  ## ── EXIT-005: the receipt returned to the pusher ───────────────────────

  describe "the link git push hands back" do
    test "a push prints the sequence and link it produced", context do
      seed_history!(context)
      output = commit_and_push!(work_dir(context), "three.txt", "three\n", "three")

      # What a person sees, verbatim: git prefixes a side-band band-2 message
      # with `remote: `, so the receipt arrives in the push's own output.
      assert output =~ ~r/remote: *openagents wal-receipt seq=\d+ link=[0-9a-f]{64}/

      {seq, link} = wal_receipt!(output)

      {:ok, _generation, index} = WAL.read_index(context.repo)
      head = List.last(WAL.entries(index))

      assert seq == head["seq"]
      assert link == WAL.entry_link(head)

      # And where to read it again, so the line is self-describing.
      assert output =~ "/api/v1/repos/exit-owner/demo/pushes/#{seq}"
    end

    test "the link the pusher kept reports a rewrite the log agrees with", context do
      seed_history!(context)
      output = commit_and_push!(work_dir(context), "four.txt", "four\n", "four")
      {seq, link} = wal_receipt!(output)

      rewrite_first_entry_consistently!(context, "a payload the pusher never sent")

      # Nothing inside the operator's storage disagrees with anything else in
      # it, and the value the pusher wrote down does.
      assert {:ok, %{findings: []}} = Verification.verify(context.repo)

      assert {:error, %{findings: findings}} =
               Verification.verify(context.repo, anchor: %{seq: seq, link: link})

      assert %{"seq" => ^seq} = detail(findings, "anchor_mismatch")
    end

    test "a rejected push stays a rejected push and carries no receipt", context do
      seed_history!(context)
      work = work_dir(context)

      # Rewrite history behind the remote, so `receive-pack` refuses the
      # update. The report-status the client parses has to survive whatever
      # this change does to the stream, and there is no accepted entry to
      # hand back.
      sh!(work, "git", ["reset", "--hard", "HEAD~1"])
      File.write!(Path.join(work, "divergent.txt"), "divergent\n")
      sh!(work, "git", ["add", "."])
      sh!(work, "git", ["commit", "-m", "divergent"])

      {output, status} =
        System.cmd("git", ["-c", "credential.helper=", "push", "origin", "HEAD:main"],
          cd: work,
          stderr_to_stdout: true
        )

      assert status != 0, "the non-fast-forward push was accepted:\n#{output}"
      assert output =~ "rejected"
      refute output =~ "wal-receipt"
    end
  end

  ## ── EXIT-005: the anchor published to a stranger ───────────────────────

  describe "the anchor published at a well-known path" do
    test "a stranger who kept the document reports a consistent rewrite", context do
      publish_repository!(context)
      seed_history!(context)

      {:ok, published} = Anchor.publish()

      # The anchor a stranger holds is the bytes they fetched, so the head
      # comes out of the served document and not out of the log it commits to.
      head = document_head!(published, context)

      rewrite_first_entry_consistently!(context, "a payload the pusher never sent")

      # Nothing inside the operator's storage disagrees with anything else in
      # it. This is the case `EXIT-002` cannot catch, and #167's receipt only
      # catches for the one person who pushed.
      assert {:ok, %{findings: []}} = Verification.verify(context.repo)

      assert {:error, %{findings: findings}} = Verification.verify(context.repo, anchor: head)
      assert %{"seq" => _seq} = detail(findings, "anchor_mismatch")
    end

    test "the served bytes are what the digest and the next anchor name", context do
      publish_repository!(context)
      seed_history!(context)

      {:ok, first} = Anchor.publish()
      {:ok, second} = Anchor.publish()

      # A reader hashes the file they fetched, so the stored digest has to be
      # a digest of the stored bytes and nothing re-rendered from columns.
      assert Anchor.digest(first.body) == first.digest
      assert Anchor.digest(second.body) == second.digest

      # The published sequence is itself a chain: one archived anchor pins
      # every anchor before it, the way an entry link pins every entry.
      assert first.anchor_seq == 0
      assert first.previous_digest == nil
      assert second.anchor_seq == 1
      assert second.previous_digest == first.digest
    end

    test "a private repository is anchored for nobody", context do
      seed_history!(context)

      # The repository is private in this setup, and the anchor is anonymous.
      # Naming it here would publish its existence and its push count on a
      # surface `TRANSPARENCY-001` keeps a dark repository off entirely.
      {:ok, private_anchor} = Anchor.publish()
      assert repository_section(private_anchor, context) == nil

      publish_repository!(context)
      {:ok, public_anchor} = Anchor.publish()
      assert %{"entries" => entries} = repository_section(public_anchor, context)
      assert entries > 0
    end

    test "the document states that nobody witnesses it", context do
      publish_repository!(context)
      seed_history!(context)

      {:ok, published} = Anchor.publish()
      document = Jason.decode!(published.body)

      # ADR 0008: the operator serves this, so it proves nothing on its own.
      # The claim has to travel with the artifact, not only with the docs.
      refute document["witnessed"]
      refute document["signed"]
      assert document["trust"] =~ "witnessed by nobody"
      assert document["decision"] =~ "0008"
    end

    test "each axis of the disclosure decides degraded on its own", _context do
      clean_export = %{"gaps" => []}

      # #178 split the private-data axis in two: an export encrypted to a
      # recipient-held key, and a store that is still plaintext. Both have to
      # be clean before the axis is, so the "private" side of this table
      # varies each of them.
      private_clean = %{
        "export_recipient_encryption" => true,
        "encrypted_at_rest" => true
      }

      unencrypted_export = %{private_clean | "export_recipient_encryption" => false}
      plaintext_store = %{private_clean | "encrypted_at_rest" => false}
      published_unwitnessed = %{"anchor_published" => true, "anchor_witnessed" => false}
      witnessed = %{"anchor_published" => true, "anchor_witnessed" => true}
      unpublished = %{"anchor_published" => false, "anchor_witnessed" => false}

      # Publishing the anchor is not witnessing it, and the disclosure has to
      # keep reporting degraded on the second. Every other axis is clean here,
      # so this asserts the witness disjunct rather than riding on a constant.
      assert Independence.degraded?(clean_export, published_unwitnessed, private_clean)
      assert Independence.degraded?(clean_export, unpublished, private_clean)
      assert Independence.degraded?(clean_export, witnessed, unencrypted_export)
      assert Independence.degraded?(clean_export, witnessed, plaintext_store)
      assert Independence.degraded?(%{"gaps" => [%{}]}, witnessed, private_clean)
      refute Independence.degraded?(clean_export, witnessed, private_clean)
    end
  end

  ## ── EXIT-003: recovery from the WAL, never from the mirror ─────────────

  describe "the mirror contract" do
    # `EXIT-003` and `CLAUDE.md` both stated that no mirror was configured and
    # that none could be, while production had one configured through
    # `OPENAGENTS_FORGE_MIRROR_URLS_JSON`. Since `mirror_now/1` force-pushes
    # every ref, that made both documents promise the destructive thing was
    # switched off while it was switched on. Neither claim can return while
    # runtime still reads that variable.
    #
    # `docs/forge-operator-independence.md` joined the guarded set with #188:
    # it is where the mirror contract is written in prose, it carried the same
    # sentence, and the first pass corrected the two files a test named while
    # leaving the third to be found by hand a second time.
    test "does not claim a mirror is unconfigured while runtime configures one" do
      assert File.read!("config/runtime.exs") =~ "OPENAGENTS_FORGE_MIRROR_URLS_JSON",
             "the guard below is only meaningful while runtime reads this variable"

      for path <- ["INVARIANTS.md", "CLAUDE.md", "docs/forge-operator-independence.md"] do
        text = File.read!(path)

        for claim <- [
              "set by no environment",
              "no mirror runs today",
              "Automatic mirroring to GitHub is not configured"
            ] do
          refute String.contains?(text, claim),
                 "#{path} claims #{inspect(claim)}, which production contradicts"
        end
      end
    end
  end

  describe "recovery" do
    test "the WAL rebuilds the repository and re-derives receipts the database lost", context do
      seed_history!(context)
      {:ok, _generation, index} = WAL.read_index(context.repo)
      expected_refs = WAL.refs(index)
      entries = WAL.entries(index)

      Repo.delete_all(PushReceipt)
      File.rm_rf!(Repos.bare_path(context.repo))

      assert :ok = Sync.ensure_fresh(context.repo, "main")
      assert Repos.refs(context.repo) == expected_refs
      assert {:ok, %{findings: []}} = Verification.verify(context.repo)

      assert Pushes.reconcile_receipts(context.repo) == length(entries)

      recovered =
        PushReceipt
        |> Repo.all()
        |> Enum.sort_by(& &1.wal_seq)

      assert Enum.map(recovered, & &1.wal_seq) == Enum.map(entries, & &1["seq"])
      assert Enum.map(recovered, & &1.principal) == Enum.map(entries, & &1["principal"])

      # The chain link comes back with the row, taken from the entry it
      # derives from rather than from anything the emptied table held.
      links = Enum.map(entries, &WAL.entry_link/1)
      refute Enum.any?(links, &is_nil/1)
      assert Enum.map(recovered, & &1.link) == links
    end

    test "the mirror restores source and cannot restore the push record", context do
      seed_history!(context)
      {:ok, _generation, index} = WAL.read_index(context.repo)
      exportable = Verification.exportable_refs(WAL.refs(index))

      mirror = Path.join(context.base, "mirror.git")
      sh!(context.base, "git", ["init", "--bare", "--initial-branch=main", mirror])
      Application.put_env(:openagents, :forge_mirror_urls, %{context.repo => mirror})

      assert :ok = Pushes.mirror_now(context.repo)
      assert Repos.refs_at(mirror) == exportable

      # Now lose the forge: the WAL is gone and the derived receipts with it.
      # The mirror still holds every commit, and that is all it holds.
      File.rm_rf!(Application.get_env(:openagents, :forge_wal_dir))
      File.rm_rf!(Repos.bare_path(context.repo))
      Repo.delete_all(PushReceipt)

      assert {:error, :not_found} = WAL.read_index(context.repo)
      assert Pushes.reconcile_receipts(context.repo) == 0
      assert Repo.aggregate(PushReceipt, :count) == 0

      restored = Path.join(context.base, "restored.git")
      sh!(context.base, "git", ["clone", "--mirror", mirror, restored])
      assert Repos.refs_at(restored) == exportable

      # Source recovered, evidence not: no sequence, no principal, no push time
      # survives a mirror, because a mirror is a ref map and a pack.
      assert Repos.applied_seq_at(restored) == -1

      mirror_bytes =
        mirror
        |> Path.join("**")
        |> Path.wildcard(match_dot: true)
        |> Enum.filter(&File.regular?/1)
        |> Enum.map_join("\n", &File.read!/1)

      for entry <- WAL.entries(index) do
        refute String.contains?(mirror_bytes, entry["principal"]),
               "the mirror carries the push principal #{entry["principal"]}"

        refute String.contains?(mirror_bytes, entry["pushed_at"])
      end
    end

    test "the rebuild path never reads the mirror", _context do
      # GitHub is a mirror and never authority. That direction holds only while
      # nothing on the recovery path can consult it.
      for module <- [Sync, Repos] do
        for {called, function, _arity} <- external_calls_with_functions(module) do
          refute called == OpenAgents.Forge.MirrorWatch,
                 "#{inspect(module)} reaches the mirror watcher"

          refute called == Pushes and function in [:mirror_url, :mirror_now, :mirror_storage_key],
                 "#{inspect(module)} reads the mirror through #{function}"
        end
      end
    end
  end

  ## ── EXIT-003: the receipt carries the link and decides nothing ─────────

  describe "the derived receipt's chain link" do
    test "every receipt carries the link of the entry it derives from", context do
      seed_history!(context)
      {:ok, _generation, index} = WAL.read_index(context.repo)

      expected =
        index
        |> WAL.entries()
        |> Map.new(fn entry -> {entry["seq"], WAL.entry_link(entry)} end)

      refute expected == %{}
      refute Enum.any?(expected, fn {_seq, link} -> is_nil(link) end)

      recorded =
        PushReceipt
        |> Repo.all()
        |> Map.new(fn receipt -> {receipt.wal_seq, receipt.link} end)

      assert recorded == expected
    end

    test "a rewritten receipt link decides nothing, because the verifier reads the WAL",
         context do
      seed_history!(context)
      {:ok, _generation, index} = WAL.read_index(context.repo)
      head = List.last(WAL.entries(index))

      # The receipt is a projection. Edit every stored link to a value the log
      # never produced: verification is unmoved, because it recomputes the
      # chain from the WAL and never asks PostgreSQL what the chain is.
      {3, _returning} = Repo.update_all(PushReceipt, set: [link: String.duplicate("0", 64)])

      assert {:ok, report} = Verification.verify(context.repo)
      assert report.findings == []
      assert report.head == %{seq: head["seq"], link: WAL.entry_link(head)}

      assert {:ok, %{findings: []}} =
               Verification.verify(context.repo,
                 anchor: %{seq: head["seq"], link: WAL.entry_link(head)}
               )
    end
  end

  ## ── EXIT-004: a clone is complete and self-hosting ─────────────────────

  describe "clone completeness" do
    test "a clone carries every advertised ref and every object behind it", context do
      seed_history!(context)
      sh!(context.base, "git", ["-C", work_dir(context), "tag", "v1"])
      sh!(work_dir(context), "git", ["push", "origin", "v1"])

      {:ok, _generation, index} = WAL.read_index(context.repo)
      exportable = Verification.exportable_refs(WAL.refs(index))
      assert Map.has_key?(exportable, "refs/tags/v1")
      assert Map.has_key?(exportable, "refs/heads/feature")

      clone = Path.join(context.base, "exit.git")
      sh!(context.base, "git", ["clone", "--mirror", context.url, clone])

      assert Repos.refs_at(clone) == exportable

      for {_name, sha} <- exportable do
        assert {_output, 0} = Repos.git(clone, ["cat-file", "-e", sha])
      end

      assert {_output, 0} = Repos.git(clone, ["fsck", "--no-progress"])
    end

    test "the clone re-serves the same history with the forge gone", context do
      seed_history!(context)
      clone = Path.join(context.base, "self-hosted.git")
      sh!(context.base, "git", ["clone", "--mirror", context.url, clone])

      # Stop trusting the forge entirely: delete its cache and its WAL, then
      # clone from the copy the user took.
      File.rm_rf!(Repos.bare_path(context.repo))
      File.rm_rf!(Application.get_env(:openagents, :forge_wal_dir))

      elsewhere = Path.join(context.base, "elsewhere")
      sh!(context.base, "git", ["clone", clone, elsewhere])

      assert File.read!(Path.join(elsewhere, "one.txt")) == "one\n"
      assert File.read!(Path.join(elsewhere, "two.txt")) == "two\n"

      sh!(elsewhere, "git", ["checkout", "feature"])
      assert File.read!(Path.join(elsewhere, "feature.txt")) == "feature\n"
    end

    test "only the internal bookkeeping namespace is withheld", context do
      seed_history!(context)
      path = Repos.bare_path(context.repo)

      assert {"refs/internal/\n", 0} = Repos.git(path, ["config", "--get", "transfer.hideRefs"])

      # Through the git plane, so the ref is recorded in the WAL and survives
      # the convergence a clone triggers.
      {:ok, head} = OpenAgents.Forge.GitPlane.resolve_commit(context.repo, "refs/heads/main")

      {:ok, _result} =
        OpenAgents.Forge.GitPlane.batch_update_refs(
          context.repo,
          [%{ref: "refs/internal/boundary", expected_old: :absent, new: head}],
          "exit-test"
        )

      clone = Path.join(context.base, "hidden.git")
      sh!(context.base, "git", ["clone", "--mirror", context.url, clone])

      refute Map.has_key?(Repos.refs_at(clone), "refs/internal/boundary")

      served = Repos.refs(context.repo)
      assert Map.has_key?(served, "refs/internal/boundary")
      assert Repos.refs_at(clone) == Verification.exportable_refs(served)
    end

    test "a repository seeded from a shallow fetch still serves a full clone", context do
      # The shape this forge's own repository is in, and the one #179 named:
      # seq 0 is a bundle from a `--depth=1` fetch, written before WAL entries
      # carried a `shallow` key, so the log states no boundary and the
      # projection holds a commit whose parent it does not have. Every ref tip
      # resolves, so every tip-shaped check passes; a clone still aborts.
      seed_unrecorded_shallow_import!(context)
      :ok = Sync.ensure_fresh(context.repo)

      # A real push on top, so what is cloned is the production shape rather
      # than a bare seed: pushes layered over an unrecorded shallow boundary.
      work = Path.join(context.base, "shallow-work")
      sh!(context.base, "git", ["clone", "--depth=1", context.url, work])
      sh!(work, "git", ["config", "user.email", "test@example.com"])
      sh!(work, "git", ["config", "user.name", "Forge Test"])
      commit_and_push!(work, "after.txt", "after\n", "after")

      clone = Path.join(context.base, "seeded")
      sh!(context.base, "git", ["clone", context.url, clone])

      assert File.read!(Path.join(clone, "after.txt")) == "after\n"
      assert File.read!(Path.join(clone, "history-3.txt")) == "history 3\n"
      sh!(clone, "git", ["fsck", "--no-progress"])
      assert {:ok, %{findings: []}} = Verification.verify(context.repo)
    end

    test "a history a clone cannot walk is reported even when every tip resolves", context do
      seed_unrecorded_shallow_import!(context)
      :ok = Sync.ensure_fresh(context.repo)

      path = Repos.bare_path(context.repo)
      {:ok, _generation, index} = WAL.read_index(context.repo)
      head = index |> WAL.refs() |> Map.fetch!("refs/heads/main")

      # Every tip is present, which is all `object_missing` asks.
      assert {_output, 0} = Repos.git(path, ["cat-file", "-e", head])

      # Ungraft the projection the way a replay written before `REPOSITORY-003`
      # did, without touching a single object.
      File.rm(Path.join(path, "shallow"))

      assert {:error, %{findings: findings}} = Verification.verify(context.repo)
      assert %{"object" => object} = detail(findings, "object_unreachable")
      assert object =~ ~r/^[0-9a-f]{40,64}$/
      refute Enum.any?(findings, &(&1.code == "object_missing"))
    end
  end

  ## ── helpers ────────────────────────────────────────────────────────────

  # The one line a pusher keeps. Parsed the way a pusher would grep for it.
  defp wal_receipt!(output) do
    case Regex.run(~r/openagents wal-receipt seq=(\d+) link=([0-9a-f]{64})/, output) do
      [_line, seq, link] -> {String.to_integer(seq), link}
      nil -> flunk("git push printed no WAL receipt line:\n#{output}")
    end
  end

  defp detail(findings, code) do
    Enum.find_value(findings, fn
      %{code: ^code, detail: detail} -> detail
      _other -> nil
    end)
  end

  defp external_calls(module) do
    module |> external_calls_with_functions() |> Enum.map(&elem(&1, 0)) |> Enum.uniq()
  end

  defp external_calls_with_functions(module) do
    {:ok, {^module, [imports: imports]}} = :beam_lib.chunks(:code.which(module), [:imports])
    imports
  end

  defp wal_object_path(context, object_key) do
    Path.join([Application.get_env(:openagents, :forge_wal_dir), context.repo, object_key])
  end

  defp overwrite_entry!(context, object_key, contents) do
    File.write!(wal_object_path(context, object_key), contents)
  end

  defp work_dir(context), do: Path.join(context.base, "work")

  # The setup's repository is private, which is what a repository is by
  # default. The anchor's population is what an anonymous reader can already
  # see, so a test about publication has to publish first.
  defp publish_repository!(context) do
    context.repository
    |> Ecto.Changeset.change(visibility: "public")
    |> Repo.update!()
  end

  defp repository_section(anchor, context) do
    path = "#{context.repository.owner}/#{context.repository.name}"

    anchor.body
    |> Jason.decode!()
    |> Map.fetch!("repositories")
    |> Enum.find(&(&1["repo"] == path))
  end

  defp document_head!(anchor, context) do
    case repository_section(anchor, context) do
      %{"head_seq" => seq, "head_link" => link} when is_integer(seq) and is_binary(link) ->
        %{seq: seq, link: link}

      other ->
        flunk("the published anchor carried no head for the repository: #{inspect(other)}")
    end
  end

  # What an operator with write access to their own object storage can do:
  # replace an accepted entry, re-derive its content-addressed key, and
  # recompute every link after it so the log agrees with itself.
  defp rewrite_first_entry_consistently!(context, replacement) do
    {:ok, generation, index} = WAL.read_index(context.repo)
    [first | rest] = WAL.entries(index)

    File.rm!(wal_object_path(context, first["object"]))
    {:ok, key} = WAL.put_entry(context.repo, first["seq"], replacement)

    entries =
      [%{first | "object" => key} | rest]
      |> Enum.map_reduce("", fn entry, previous ->
        {:ok, link} = WAL.chain_link(previous, Map.delete(entry, "link"))
        {Map.put(entry, "link", link), link}
      end)
      |> elem(0)

    {:ok, _generation} =
      WAL.cas_index(context.repo, generation, Map.put(index, "entries", entries))

    :ok
  end

  # Two commits on main and one on a branch, all through the real push path,
  # so the WAL holds three genuine `receive-pack` entries.
  defp seed_history!(context) do
    work = work_dir(context)

    unless File.exists?(work) do
      sh!(context.base, "git", ["clone", context.url, work])
      sh!(work, "git", ["config", "user.email", "test@example.com"])
      sh!(work, "git", ["config", "user.name", "Forge Test"])
      commit_and_push!(work, "one.txt", "one\n", "one")
      commit_and_push!(work, "two.txt", "two\n", "two")
      sh!(work, "git", ["checkout", "-b", "feature"])
      commit_and_push!(work, "feature.txt", "feature\n", "feature", "feature")
      sh!(work, "git", ["checkout", "main"])
    end

    :ok
  end

  # A `--depth=1` fetch bundled with `--all` and recorded as the seq 0 entry
  # *without* a `shallow` key, which is what every entry written before that
  # key existed looks like. `OpenAgents.Repositories.Importer` records the key
  # now, and nothing backfills a log that predates it, so this is not a
  # hypothetical shape.
  defp seed_unrecorded_shallow_import!(context) do
    source = Path.join(context.base, "seed-source")
    File.mkdir_p!(source)
    sh!(source, "git", ["init", "--initial-branch=main", "."])
    sh!(source, "git", ["config", "user.email", "test@example.com"])
    sh!(source, "git", ["config", "user.name", "Forge Test"])

    Enum.each(1..3, fn n ->
      File.write!(Path.join(source, "history-#{n}.txt"), "history #{n}\n")
      sh!(source, "git", ["add", "."])
      sh!(source, "git", ["commit", "-m", "history #{n}"])
    end)

    snapshot = Path.join(context.base, "seed-snapshot.git")
    sh!(context.base, "git", ["init", "--bare", "--initial-branch=main", snapshot])

    sh!(context.base, "git", [
      "--git-dir",
      snapshot,
      "fetch",
      "--depth=1",
      source,
      "refs/heads/main:refs/heads/main"
    ])

    bundle = Path.join(context.base, "seed-snapshot.bundle")
    sh!(context.base, "git", ["--git-dir", snapshot, "bundle", "create", bundle, "--all"])

    head =
      context.base
      |> sh!("git", ["--git-dir", snapshot, "rev-parse", "refs/heads/main"])
      |> String.trim()

    {expected, index} =
      case WAL.read_index(context.repo) do
        {:ok, generation, index} -> {generation, index}
        {:error, :not_found} -> {:none, WAL.new_index()}
      end

    seq = WAL.next_seq(index)
    {:ok, object} = WAL.put_entry_file(context.repo, seq, bundle)

    entry = %{
      "seq" => seq,
      "object" => object,
      "format" => "git_bundle",
      "refs" => %{"refs/heads/main" => head},
      "principal" => "bootstrap:test",
      "pushed_at" => DateTime.to_iso8601(DateTime.utc_now())
    }

    {:ok, _generation} = WAL.cas_index(context.repo, expected, WAL.append_entry(index, entry))

    :ok
  end

  defp commit_and_push!(work, filename, contents, message, branch \\ "main") do
    File.write!(Path.join(work, filename), contents)
    sh!(work, "git", ["add", "."])
    sh!(work, "git", ["commit", "-m", message])
    sh!(work, "git", ["push", "origin", "HEAD:#{branch}"])
  end

  defp free_port do
    {:ok, socket} = :gen_tcp.listen(0, [])
    {:ok, port} = :inet.port(socket)
    :gen_tcp.close(socket)
    port
  end

  defp sh!(dir, "git", args), do: sh_raw!(dir, "git", ["-c", "credential.helper="] ++ args)
  defp sh!(dir, command, args), do: sh_raw!(dir, command, args)

  defp sh_raw!(dir, command, args) do
    {output, status} = System.cmd(command, args, cd: dir, stderr_to_stdout: true)
    if status != 0, do: flunk("#{command} #{Enum.join(args, " ")} failed:\n#{output}")
    output
  end
end