Let a repository import the history that predates its seed

e4e6d71049aa · AtlantisPleb · · parent 14232e02caf6

Let a repository import the history that predates its seed

A repository seeded from a shallow fetch records a truthful boundary and
serves every ref tip, so nothing tip-shaped reports that it holds none of
the history behind its seed. Where a mirror holds that history and the log
does not, the forge's own authority holds less than its copy does.

The import writes the missing objects into the log as an ordinary
git_bundle entry carrying the ref map unchanged and an empty shallow
boundary set, so replay from seq 0 reproduces the full history and no read
path consults a mirror. An append-only log cannot retract a bad entry, so
the bundle is materialized against a throwaway repository that shares the
projection's objects, and the import refuses to touch the log unless each
boundary's recorded parents then resolve. Requiring the parents rather
than only a clean walk is what stops the proof passing vacuously on a
scratch copy that never received the refs.

Also corrects EXIT-003 and CLAUDE.md, which both stated that no mirror was
configured and none could be, while production has mirrored openagents.com
through OPENAGENTS_FORGE_MIRROR_URLS_JSON. Since mirror_now/1 force-pushes
every ref, the documents promised the destructive thing was switched off
while it was switched on. A test now refuses either claim while runtime
still reads that variable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnhfrafYx5ZGaMbzZEJQ2d
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 379 · 2026-08-25T14:29:43.265825Z

Changed files

  • modified AGENTS.md
  • modified INVARIANTS.md
  • added lib/openagents/forge/backfill.ex
  • added test/openagents/forge/backfill_test.exs
  • modified test/openagents/forge/independence_test.exs

Diff

5 files changed, +501 -15

AGENTS.md modified +3 -2

@@ -31,8 +31,9 @@ The `openagents` remote is the forge at `openagents.com`, which records every

31 31
push in the durable WAL and serves it. The `origin` remote is the GitHub
32 32
mirror; pushing to it directly leaves the forge behind a mirror it does not
33 33
know about, and nothing reports the divergence until a clone disagrees with
34
the site. Automatic mirroring to GitHub is not configured today, so GitHub
35
stays at whatever was last pushed to it. `ops/ci/push-remote-check.sh` refuses
34
the site. Production mirrors to GitHub with a force push of every ref, so a
35
direct GitHub push is not merged later — it is overwritten. GitHub
36
holds what the forge last mirrored there. `ops/ci/push-remote-check.sh` refuses
36 37
a non-forge push, and `mix precommit` installs it into the clone you are
37 38
working in, so running precommit before you push is enough. To install it by
38 39
hand:
INVARIANTS.md modified +19 -13

@@ -4170,14 +4170,20 @@ GitHub ID. A push sent straight to GitHub inverts that: the WAL never sees the

4170 4170
objects, and nothing reports the divergence until a clone disagrees with the
4171 4171
site.
4172 4172
4173
The mirror that would keep GitHub current is not running. `mirror_url/1` reads
4174
`:forge_mirror_urls`, which is empty in `config/config.exs` and set by no
4175
environment, so `MirrorWatch` reports `off` and GitHub receives only what
4176
someone pushes to it. `mirror_now/1` is a `git push --mirror`, a force push of
4177
every ref, so configuring a mirror overwrites whatever direct pushes left on
4178
GitHub rather than merging with it. Configuring that mirror is what makes this
4179
contract complete; until then it keeps the forge authoritative and lets GitHub
4180
go stale, which is the honest trade and not an accident.
4173
A mirror is running. `mirror_url/1` reads `:forge_mirror_urls`, which is empty
4174
in `config/config.exs` but is set from `OPENAGENTS_FORGE_MIRROR_URLS_JSON` in
4175
`config/runtime.exs`, and production sets it for `openagents.com`, so
4176
`MirrorWatch` publishes a per-repo state rather than `off`. `mirror_now/1` is a
4177
`git push --mirror`, a force push of every ref, so the mirror overwrites
4178
whatever a direct push left on GitHub rather than merging with it. That is the
4179
reason this contract admits no direct GitHub push: a push that lands there is
4180
not merged later, it is erased.
4181
4182
This paragraph read the other way — that no mirror was configured — until
4183
2026-08-25, while production had one. A contract that says the destructive
4184
thing is switched off, while it is switched on, is worse than one that says
4185
nothing, so what is configured is checked here against the running node rather
4186
than against `config/config.exs` alone.
4181 4187
4182 4188
`ops/ci/push-remote-check.sh` admits only forge hosts and refuses every other
4183 4189
remote, whatever URL form it takes. `.githooks/pre-push` runs it before the

@@ -4487,11 +4493,11 @@ which rebuilds this table from the WAL alone. The absence is the invariant

4487 4493
holding, not an omission.
4488 4494
4489 4495
Two operational facts bound the claim. `:forge_mirror_urls` is empty in
4490
`config/config.exs` and set by no environment, so no mirror runs today and
4491
GitHub holds whatever was last pushed to it directly, which is the trade
4492
`REPOSITORY-002` records. And `mirror_now/1` is a force push of every ref, so
4493
configuring a mirror overwrites what direct pushes left there rather than
4494
merging with it.
4496
`config/config.exs` but set from the environment in `config/runtime.exs`, and
4497
production sets it for `openagents.com`, so a mirror does run and GitHub holds
4498
what the forge last pushed there, which is the trade `REPOSITORY-002` records.
4499
And `mirror_now/1` is a force push of every ref, so the mirror overwrites what a
4500
direct push left there rather than merging with it.
4495 4501
4496 4502
Evidence: `OpenAgents.Forge.Sync`, `OpenAgents.Forge.Pushes`,
4497 4503
`OpenAgents.Forge.PushReceipt`, `OpenAgents.Forge.Verification`, and
lib/openagents/forge/backfill.ex added +234

@@ -0,0 +1,234 @@

1
defmodule OpenAgents.Forge.Backfill do
2
  @moduledoc """
3
  Import the history that predates a repository's seed into that repository's
4
  own write-ahead log.
5
6
  A repository seeded from a shallow fetch records a truthful boundary: the
7
  seed commit's parents were never fetched, so they were never written to the
8
  log, and `REPOSITORY-003`'s replay reproduces a grafted history from seq 0
9
  exactly as recorded. Every ref tip resolves and every clone succeeds, so
10
  nothing tip-shaped reports a problem. What is missing is everything behind
11
  the seed.
12
13
  Where a mirror holds that history and the log does not, the repository's own
14
  authority holds less than its copy does, and `EXIT-003`'s claim that the
15
  forge is the authority is weaker than it reads. This closes that gap the only
16
  way an append-only log can: by writing the missing objects into the log.
17
18
  The bundle is read once, at an operator's instruction, and appended as an
19
  ordinary `git_bundle` entry that carries the ref map unchanged and an empty
20
  shallow boundary set — the same entry shape `OpenAgents.Repositories.Importer`
21
  writes at seq 0, which is why replay needs no new case to apply it. The
22
  authority boundary is unchanged after it: a mirror supplied bytes on this one
23
  occasion, the log now holds them, and no read path consults a mirror.
24
25
  ## The entry is proven before it is written
26
27
  An append-only log cannot retract a bad entry. So the bundle is materialized
28
  against a throwaway repository that shares the current projection's objects,
29
  and the import refuses to touch the log unless that union can then be walked
30
  the way `git upload-pack` walks it — the same check
31
  `OpenAgents.Forge.Sync` uses to decide a projection is servable. A bundle
32
  that would leave the repository grafted, or that closes no boundary at all,
33
  is rejected while the log is still untouched.
34
  """
35
36
  alias OpenAgents.Forge.{Repos, Sync, WAL}
37
38
  require Logger
39
40
  @doc """
41
  Append `bundle_path` to `storage_key`'s log as a history import.
42
43
  Returns `{:ok, summary}` with the sequence written and the boundary commits
44
  the import closed, or `{:error, reason}` with the log unchanged.
45
46
  `principal` records who authorized the import, and is written to the entry
47
  the same way a push records its pusher.
48
  """
49
  @spec import_history(String.t(), String.t(), String.t()) ::
50
          {:ok, %{seq: non_neg_integer(), closed: [String.t()]}} | {:error, term()}
51
  def import_history(storage_key, bundle_path, principal)
52
      when is_binary(storage_key) and is_binary(bundle_path) and is_binary(principal) do
53
    with :ok <- validate_principal(principal),
54
         {:ok, generation, index} <- WAL.read_index(storage_key),
55
         projection = Repos.bare_path(storage_key),
56
         {:ok, closed} <- prove_bundle(projection, bundle_path),
57
         seq = WAL.next_seq(index),
58
         {:ok, object} <- WAL.put_entry_file(storage_key, seq, bundle_path) do
59
      entry = %{
60
        "seq" => seq,
61
        "object" => object,
62
        "format" => "git_bundle",
63
        "refs" => WAL.refs(index),
64
        "shallow" => [],
65
        "principal" => principal,
66
        "pushed_at" => DateTime.to_iso8601(DateTime.utc_now())
67
      }
68
69
      case WAL.cas_index(storage_key, generation, WAL.append_entry(index, entry)) do
70
        {:ok, _generation} ->
71
          Logger.info(
72
            "forge_backfill_imported repo=#{storage_key} seq=#{seq} closed=#{length(closed)}"
73
          )
74
75
          Sync.ensure_fresh(storage_key)
76
          {:ok, %{seq: seq, closed: closed}}
77
78
        {:error, reason} ->
79
          {:error, {:cas_failed, reason}}
80
      end
81
    end
82
  end
83
84
  # The boundary this import must close: the commits the projection holds whose
85
  # parents it does not. Derived from the objects on disk rather than from what
86
  # an entry recorded, because a commit whose parent is absent *is* a boundary
87
  # whatever the log says — the same derivation `OpenAgents.Forge.Sync` uses.
88
  @doc """
89
  The commits `storage_key`'s projection holds whose parents it does not.
90
91
  An empty list means the projection is already complete and an import has
92
  nothing to close.
93
  """
94
  @spec open_boundaries(String.t()) :: [String.t()]
95
  def open_boundaries(storage_key) do
96
    storage_key |> Repos.bare_path() |> boundaries_at()
97
  end
98
99
  defp boundaries_at(path) do
100
    case Repos.git(path, ["rev-list", "--all", "--max-parents=0"]) do
101
      {output, 0} ->
102
        output
103
        |> String.split("\n", trim: true)
104
        |> Enum.filter(&Regex.match?(~r/\A[0-9a-f]{40}\z/, &1))
105
        |> Enum.filter(&grafted?(path, &1))
106
107
      {_output, _status} ->
108
        []
109
    end
110
  end
111
112
  # A root commit reported by `--max-parents=0` is either a genuine root or a
113
  # graft boundary, and the two are told apart by the commit object itself:
114
  # the object records its parents whether or not the repository holds them.
115
  defp grafted?(path, sha), do: parents_of(path, sha) != []
116
117
  # The parents a commit object records, which is not the same question as the
118
  # parents a repository holds: the object is immutable and names them whether
119
  # or not they were ever fetched.
120
  defp parents_of(path, sha) do
121
    case Repos.git(path, ["cat-file", "commit", sha]) do
122
      {output, 0} ->
123
        output
124
        |> String.split("\n")
125
        |> Enum.take_while(&(&1 != ""))
126
        |> Enum.filter(&String.starts_with?(&1, "parent "))
127
        |> Enum.map(&(&1 |> String.replace_prefix("parent ", "") |> String.trim()))
128
129
      {_output, _status} ->
130
        []
131
    end
132
  end
133
134
  defp prove_bundle(projection, bundle_path) do
135
    cond do
136
      not File.dir?(projection) ->
137
        {:error, :projection_absent}
138
139
      not match?({:ok, %File.Stat{type: :regular}}, File.stat(bundle_path)) ->
140
        {:error, :bundle_unreadable}
141
142
      true ->
143
        boundaries = boundaries_at(projection)
144
145
        if boundaries == [] do
146
          {:error, :no_open_boundary}
147
        else
148
          prove_against_scratch(projection, bundle_path, boundaries)
149
        end
150
    end
151
  end
152
153
  # Shares the projection's objects through `objects/info/alternates` rather
154
  # than copying them, so proving a large repository costs the bundle's own
155
  # size and nothing more. Nothing here writes to the projection: an alternate
156
  # is read-only to the borrower, and every new object lands in the scratch
157
  # directory.
158
  defp prove_against_scratch(projection, bundle_path, boundaries) do
159
    scratch =
160
      Path.join(
161
        System.tmp_dir!(),
162
        "openagents-backfill-#{System.unique_integer([:positive, :monotonic])}.git"
163
      )
164
165
    try do
166
      with :ok <- init_scratch(scratch, projection),
167
           :ok <- copy_refs(projection, scratch),
168
           {_output, 0} <- Repos.git(scratch, ["bundle", "unbundle", bundle_path]) do
169
        # The import claims the boundaries are closed, so prove the walk with
170
        # no graft in place at all rather than with the old one still excusing
171
        # it.
172
        File.rm(Path.join(scratch, "shallow"))
173
174
        # Name what must be true rather than inferring it from an exit status.
175
        # A repository with no refs walks clean, so a walk alone would pass
176
        # vacuously on a scratch copy that never received them; requiring each
177
        # boundary's recorded parents to resolve cannot.
178
        unresolved =
179
          boundaries
180
          |> Enum.flat_map(&parents_of(projection, &1))
181
          |> Enum.uniq()
182
          |> Enum.reject(&match?({_output, 0}, Repos.git(scratch, ["cat-file", "-e", &1])))
183
184
        cond do
185
          unresolved != [] ->
186
            {:error, {:still_grafted, unresolved}}
187
188
          not match?(
189
            {_output, 0},
190
            Repos.git(scratch, ["rev-list", "--objects", "--quiet", "--all"])
191
          ) ->
192
            {:error, {:unwalkable, boundaries}}
193
194
          true ->
195
            {:ok, boundaries}
196
        end
197
      else
198
        {:error, reason} -> {:error, reason}
199
        {_output, _status} -> {:error, :bundle_unreadable}
200
      end
201
    after
202
      File.rm_rf(scratch)
203
    end
204
  end
205
206
  defp init_scratch(scratch, projection) do
207
    case Repos.git(scratch, ["init", "--bare", "--quiet", scratch]) do
208
      {_output, 0} ->
209
        alternates = Path.join([scratch, "objects", "info", "alternates"])
210
        File.mkdir_p!(Path.dirname(alternates))
211
        File.write(alternates, Path.join(projection, "objects") <> "\n")
212
213
      {_output, _status} ->
214
        {:error, :scratch_unavailable}
215
    end
216
  end
217
218
  defp copy_refs(projection, scratch) do
219
    Enum.reduce_while(Repos.refs_at(projection), :ok, fn {name, sha}, :ok ->
220
      case Repos.git(scratch, ["update-ref", name, sha]) do
221
        {_output, 0} -> {:cont, :ok}
222
        {_output, _status} -> {:halt, {:error, {:scratch_ref_failed, name}}}
223
      end
224
    end)
225
  end
226
227
  defp validate_principal(principal) do
228
    if String.trim(principal) == "" do
229
      {:error, :invalid_principal}
230
    else
231
      :ok
232
    end
233
  end
234
end
test/openagents/forge/backfill_test.exs added +219

@@ -0,0 +1,219 @@

1
defmodule OpenAgents.Forge.BackfillTest do
2
  @moduledoc """
3
  Importing pre-seed history into the log.
4
5
  A repository seeded from a shallow fetch serves every ref tip and clones
6
  without error while holding none of the history behind its seed. These tests
7
  build exactly that repository, prove it is grafted, import the missing
8
  history, and then destroy the cache and rebuild from seq 0 — because an
9
  import that only repairs the projection has repaired a cache, not the
10
  authority.
11
  """
12
13
  use OpenAgents.DataCase, async: false
14
15
  alias OpenAgents.Forge.{Backfill, Repos, Sync, WAL}
16
17
  setup do
18
    base = Path.join(System.tmp_dir!(), "forge-backfill-#{System.unique_integer([:positive])}")
19
    File.mkdir_p!(base)
20
    previous_data = Application.get_env(:openagents, :forge_data_dir)
21
    previous_wal = Application.get_env(:openagents, :forge_wal_dir)
22
    Application.put_env(:openagents, :forge_data_dir, Path.join(base, "data"))
23
    Application.put_env(:openagents, :forge_wal_dir, Path.join(base, "wal"))
24
25
    on_exit(fn ->
26
      Application.put_env(:openagents, :forge_data_dir, previous_data)
27
      Application.put_env(:openagents, :forge_wal_dir, previous_wal)
28
      File.rm_rf(base)
29
    end)
30
31
    storage_key = "backfill-demo-#{System.unique_integer([:positive])}"
32
    source = build_source!(base)
33
    seed_shallow_import!(base, storage_key, source)
34
35
    %{base: base, storage_key: storage_key, source: source}
36
  end
37
38
  test "a shallow-seeded repository is grafted and holds none of its own history",
39
       %{storage_key: storage_key, source: source} do
40
    :ok = Sync.ensure_fresh!(storage_key)
41
    path = Repos.bare_path(storage_key)
42
43
    assert [boundary] = Backfill.open_boundaries(storage_key)
44
    assert boundary == rev(source, "HEAD")
45
46
    assert count_commits(path) == 1
47
    assert File.exists?(Path.join(path, "shallow"))
48
  end
49
50
  test "importing the pre-seed bundle closes the boundary and survives a rebuild from seq 0",
51
       %{base: base, storage_key: storage_key, source: source} do
52
    :ok = Sync.ensure_fresh!(storage_key)
53
    path = Repos.bare_path(storage_key)
54
    total = count_worktree_commits(source)
55
    assert total > 1
56
57
    bundle = pre_seed_bundle!(base, source)
58
59
    assert {:ok, %{seq: 1, closed: [_boundary]}} =
60
             Backfill.import_history(storage_key, bundle, "operator:test")
61
62
    assert Backfill.open_boundaries(storage_key) == []
63
    refute File.exists?(Path.join(path, "shallow"))
64
    assert count_commits(path) == total
65
66
    # The point of the import: the authority now holds the history, so a node
67
    # that loses its cache rebuilds the whole repository rather than the seed.
68
    :ok = Sync.rebuild(storage_key)
69
    rebuilt = Repos.bare_path(storage_key)
70
71
    assert Backfill.open_boundaries(storage_key) == []
72
    assert count_commits(rebuilt) == total
73
    assert {_output, 0} = Repos.git(rebuilt, ["rev-list", "--objects", "--quiet", "--all"])
74
  end
75
76
  test "the import leaves the ref map exactly as it found it",
77
       %{base: base, storage_key: storage_key, source: source} do
78
    :ok = Sync.ensure_fresh!(storage_key)
79
    {:ok, _generation, before_index} = WAL.read_index(storage_key)
80
    before_refs = WAL.refs(before_index)
81
82
    bundle = pre_seed_bundle!(base, source)
83
84
    assert {:ok, _summary} = Backfill.import_history(storage_key, bundle, "operator:test")
85
86
    {:ok, _generation, after_index} = WAL.read_index(storage_key)
87
    assert WAL.refs(after_index) == before_refs
88
    assert Repos.refs_at(Repos.bare_path(storage_key)) == before_refs
89
  end
90
91
  test "a bundle that closes nothing is refused with the log untouched",
92
       %{base: base, storage_key: storage_key} do
93
    :ok = Sync.ensure_fresh!(storage_key)
94
    {:ok, _generation, before_index} = WAL.read_index(storage_key)
95
96
    # A bundle of an unrelated repository: real, readable, and no help.
97
    unrelated = build_source!(base, "unrelated")
98
    bundle = Path.join(base, "unrelated.bundle")
99
    sh!(unrelated, "git", ["bundle", "create", bundle, "--all"])
100
101
    assert {:error, {:still_grafted, [_boundary]}} =
102
             Backfill.import_history(storage_key, bundle, "operator:test")
103
104
    {:ok, _generation, after_index} = WAL.read_index(storage_key)
105
    assert WAL.entries(after_index) == WAL.entries(before_index)
106
  end
107
108
  test "an unreadable bundle is refused with the log untouched",
109
       %{storage_key: storage_key} do
110
    :ok = Sync.ensure_fresh!(storage_key)
111
    {:ok, _generation, before_index} = WAL.read_index(storage_key)
112
113
    assert {:error, :bundle_unreadable} =
114
             Backfill.import_history(storage_key, "/nonexistent/backfill.bundle", "operator:test")
115
116
    {:ok, _generation, after_index} = WAL.read_index(storage_key)
117
    assert WAL.entries(after_index) == WAL.entries(before_index)
118
  end
119
120
  test "an import needs a principal", %{base: base, storage_key: storage_key, source: source} do
121
    :ok = Sync.ensure_fresh!(storage_key)
122
    bundle = pre_seed_bundle!(base, source)
123
124
    assert {:error, :invalid_principal} = Backfill.import_history(storage_key, bundle, "   ")
125
  end
126
127
  defp build_source!(base, name \\ "source") do
128
    source = Path.join(base, name)
129
    File.mkdir_p!(source)
130
    sh!(source, "git", ["init", "--initial-branch=main", "."])
131
    sh!(source, "git", ["config", "user.email", "test@example.com"])
132
    sh!(source, "git", ["config", "user.name", "Forge Test"])
133
134
    Enum.each(1..4, fn n ->
135
      File.write!(Path.join(source, "history-#{n}.txt"), "history #{n}\n")
136
      sh!(source, "git", ["add", "."])
137
      sh!(source, "git", ["commit", "-m", "history #{n}"])
138
    end)
139
140
    source
141
  end
142
143
  # Everything behind the seed: the seed's parent and all of its ancestors.
144
  defp pre_seed_bundle!(base, source) do
145
    bundle = Path.join(base, "pre-seed-#{System.unique_integer([:positive])}.bundle")
146
    sh!(source, "git", ["branch", "--force", "pre-seed", "HEAD~1"])
147
    sh!(source, "git", ["bundle", "create", bundle, "pre-seed"])
148
    sh!(source, "git", ["branch", "--delete", "--force", "pre-seed"])
149
    bundle
150
  end
151
152
  # Mirrors `OpenAgents.Repositories.Importer`: a `--depth=1` fetch bundled
153
  # with `--all`, recorded as the seq 0 `git_bundle` entry with the shallow
154
  # boundaries the fetch produced.
155
  defp seed_shallow_import!(base, storage_key, source) do
156
    snapshot = Path.join(base, "snapshot-#{storage_key}.git")
157
    sh!(base, "git", ["init", "--bare", "--initial-branch=main", snapshot])
158
159
    sh!(base, "git", [
160
      "--git-dir",
161
      snapshot,
162
      "fetch",
163
      "--depth=1",
164
      source,
165
      "refs/heads/main:refs/heads/main"
166
    ])
167
168
    bundle = Path.join(base, "snapshot-#{storage_key}.bundle")
169
    sh!(base, "git", ["--git-dir", snapshot, "bundle", "create", bundle, "--all"])
170
171
    shallow =
172
      snapshot
173
      |> Path.join("shallow")
174
      |> File.read!()
175
      |> String.split("\n", trim: true)
176
177
    assert shallow != []
178
179
    head =
180
      base
181
      |> sh!("git", ["--git-dir", snapshot, "rev-parse", "refs/heads/main"])
182
      |> String.trim()
183
184
    {:ok, object} = WAL.put_entry_file(storage_key, 0, bundle)
185
186
    entry = %{
187
      "seq" => 0,
188
      "object" => object,
189
      "format" => "git_bundle",
190
      "refs" => %{"refs/heads/main" => head},
191
      "shallow" => shallow,
192
      "principal" => "github-import:test",
193
      "pushed_at" => DateTime.to_iso8601(DateTime.utc_now())
194
    }
195
196
    {:ok, _generation} =
197
      WAL.cas_index(storage_key, :none, WAL.append_entry(WAL.new_index(), entry))
198
199
    :ok
200
  end
201
202
  defp count_worktree_commits(source) do
203
    source |> sh!("git", ["rev-list", "--count", "--all"]) |> String.trim() |> String.to_integer()
204
  end
205
206
  defp count_commits(path) do
207
    {output, 0} = Repos.git(path, ["rev-list", "--count", "--all"])
208
    output |> String.trim() |> String.to_integer()
209
  end
210
211
  defp rev(source, ref) do
212
    source |> sh!("git", ["rev-parse", ref]) |> String.trim()
213
  end
214
215
  defp sh!(dir, command, args) do
216
    {output, 0} = System.cmd(command, args, cd: dir, stderr_to_stdout: true)
217
    output
218
  end
219
end
test/openagents/forge/independence_test.exs modified +26

@@ -502,6 +502,32 @@ defmodule OpenAgents.Forge.IndependenceTest do

502 502
503 503
  ## ── EXIT-003: recovery from the WAL, never from the mirror ─────────────
504 504
505
  describe "the mirror contract" do
506
    # `EXIT-003` and `CLAUDE.md` both stated that no mirror was configured and
507
    # that none could be, while production had one configured through
508
    # `OPENAGENTS_FORGE_MIRROR_URLS_JSON`. Since `mirror_now/1` force-pushes
509
    # every ref, that made both documents promise the destructive thing was
510
    # switched off while it was switched on. Neither claim can return while
511
    # runtime still reads that variable.
512
    test "does not claim a mirror is unconfigured while runtime configures one" do
513
      assert File.read!("config/runtime.exs") =~ "OPENAGENTS_FORGE_MIRROR_URLS_JSON",
514
             "the guard below is only meaningful while runtime reads this variable"
515
516
      for path <- ["INVARIANTS.md", "CLAUDE.md"] do
517
        text = File.read!(path)
518
519
        for claim <- [
520
              "set by no environment",
521
              "no mirror runs today",
522
              "Automatic mirroring to GitHub is not configured"
523
            ] do
524
          refute String.contains?(text, claim),
525
                 "#{path} claims #{inspect(claim)}, which production contradicts"
526
        end
527
      end
528
    end
529
  end
530
505 531
  describe "recovery" do
506 532
    test "the WAL rebuilds the repository and re-derives receipts the database lost", context do
507 533
      seed_history!(context)

This page updates live while a promote is in flight · changelog