Repair the port's stale app atoms, forge_builds shape, and cluster gate.

72c644f107bc · AtlantisPleb · · parent 54c2acc6a0ed

Repair the port's stale app atoms, forge_builds shape, and cluster gate.

Four unrelated buckets, sorted by who was actually wrong.

The app code was wrong about its own OTP application name. Four
`Application.get_env/fetch_env!` calls still read `:sarah` after the rename,
so they silently returned defaults (or raised) against config that is written
under `:openagents`. `InferenceProxyController` raised outright; the tool
selector's `top_k`, the tool-embedding provider, and the forge git operator
token quietly ignored their configuration. `config :openagents, :provider`
was already correct in `config/test.exs`; only the reader was stale.

The migrations were wrong about `forge_builds`. `OpenAgents.Forge.BuildReceipt`
declares the upstream receipt shape (`repo`, `sha`, `modules`, binary id), but
223c6f6 created that table with an unrelated Phase-3 shape (bigint key,
`source_sha`, `module_changes`) that no module in the tree binds to. The cost
was not cosmetic: `Changelog.receipt_index/1` selects `forge_builds.repo`, so
every query raised `UndefinedColumnError` — and `Changelog.build/1`'s
`safely/2` rescue swallowed it, dropping the push, build AND deploy receipts
for the whole timeline. That is why a receipted deploy never surfaced as a
`:receipt` row and no entry ever carried `receipt_ids`. Reconciled by a
guarded migration that renames the divergent table aside rather than dropping
it, so no row is lost. NOTE for the forge lane: if that lane rewrites
223229-31 in place, this migration becomes a no-op and can be dropped.

The router was wrong about `/machines`. Upstream answers the legacy Computers
path with a redirect controller so the canonical URL is reached without its
query string — an OAuth `code=` must not survive the hop. The port pointed it
at `ComputersController, :index`, which renders a second copy of the surface
and 200s. Ported the missing `LegacyMachinesController`. (The route line
itself landed in d2018fb, swept in from this worktree by the concurrent
agent working here.)

The cluster tests were wrong in three ways at once, all of them port damage:

  * Upstream gates every distribution test behind `:cluster` and runs them as
    their own stage, because they leave node-global state behind. The tag came
    across, but a blanket `@moduletag :skip` was added on top and the helper's
    exclude was changed from `:cluster` to `:skip` — so `--include skip`
    dragged them back into the shared BEAM, where `OpenAgents.ClusterTest`
    duly failed on `net_kernel already started`. Restored the gate: excluded
    unconditionally, and the `:cluster` modules carry `:cluster` only, never
    `:skip` too, or the include flag would defeat the exclusion again. This
    is upstream's own two-stage design, not a new skip: `mix test --only
    cluster` runs all nine and they pass.
  * `ra_cluster_test` still stopped `{:sarah_sessions, node}`, but the cluster
    atom is `:openagents_sessions` here. `:ra.stop_server` answers `:ok` for a
    server id it does not know, so the stop did nothing, peer2 stayed up, and
    the phantom-member test failed on a lie. It now asks `Ra.server_id/1`.
  * Every module hardcoded the node name `sarah_test@127.0.0.1` and fixed peer
    names. Any run that leaves a name registered with epmd — a killed peer, a
    crashed run, a second worktree on the same host — makes net_kernel refuse
    to start ever after, and the whole stage flunks "distribution
    unavailable". Unique per run now, and OpenAgents-named. The stage also
    starts epmd itself instead of depending on whichever module ran first.

`ClusterTest` additionally stopped distribution in a shared `setup` and again
after its peer test, stranding every module scheduled behind it; it now leaves
net_kernel up, like every sibling.

All ten target tests pass: 19 in the default suite, 9 in `--only cluster`
(green on three consecutive runs).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149rBWy7br1Z7bbz9NrQhEr
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.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents/tools/embeddings.ex
  • modified lib/openagents/tools/selector.ex
  • modified lib/openagents_web/controllers/inference_proxy_controller.ex
  • added lib/openagents_web/controllers/legacy_machines_controller.ex
  • modified lib/openagents_web/plugs/forge_git_auth.ex
  • added priv/repo/migrations/20260820120000_reconcile_forge_builds_receipt_shape.exs
  • modified test/openagents/cluster/chaos_test.exs
  • modified test/openagents/cluster/drain_test.exs
  • modified test/openagents/cluster/ra_cluster_test.exs
  • modified test/openagents/cluster_test.exs
  • modified test/openagents/sarah_cluster_test.exs
  • modified test/openagents/work/handoff_test.exs
  • modified test/openagents_web/live/computers_navigation_test.exs
  • modified test/test_helper.exs

Diff

14 files changed, +222 -44

lib/openagents/tools/embeddings.ex modified +1 -1

@@ -124,7 +124,7 @@ defmodule OpenAgents.Tools.Embeddings do

124 124
  end
125 125
126 126
  defp provider do
127
    :sarah
127
    :openagents
128 128
    |> Application.get_env(:tool_discovery, [])
129 129
    |> Keyword.get(:provider)
130 130
  end
lib/openagents/tools/selector.ex modified +1 -1

@@ -150,7 +150,7 @@ defmodule OpenAgents.Tools.Selector do

150 150
  end
151 151
152 152
  defp default_top_k do
153
    :sarah
153
    :openagents
154 154
    |> Application.get_env(:tool_discovery, [])
155 155
    |> Keyword.get(:top_k, 12)
156 156
  end
lib/openagents_web/controllers/inference_proxy_controller.ex modified +1 -1

@@ -101,7 +101,7 @@ defmodule OpenAgentsWeb.InferenceProxyController do

101 101
  # ── run + translate ─────────────────────────────────────────────────────
102 102
103 103
  defp run(conn, grant, request) do
104
    provider = Application.fetch_env!(:sarah, :provider)
104
    provider = Application.fetch_env!(:openagents, :provider)
105 105
    parent = self()
106 106
107 107
    # The provider pushes events synchronously; capture them to this process's
lib/openagents_web/controllers/legacy_machines_controller.ex added +9

@@ -0,0 +1,9 @@

1
defmodule OpenAgentsWeb.LegacyMachinesController do
2
  @moduledoc false
3
4
  use OpenAgentsWeb, :controller
5
6
  def show(conn, _params) do
7
    redirect(conn, to: ~p"/computers")
8
  end
9
end
lib/openagents_web/plugs/forge_git_auth.ex modified +1 -1

@@ -49,7 +49,7 @@ defmodule OpenAgentsWeb.Plugs.ForgeGitAuth do

49 49
  end
50 50
51 51
  defp principal_for(token) when is_binary(token) and token != "" do
52
    operator_token = Application.get_env(:sarah, :forge_operator_token)
52
    operator_token = Application.get_env(:openagents, :forge_operator_token)
53 53
54 54
    if is_binary(operator_token) and operator_token != "" and
55 55
         Plug.Crypto.secure_compare(token, operator_token) do
priv/repo/migrations/20260820120000_reconcile_forge_builds_receipt_shape.exs added +75

@@ -0,0 +1,75 @@

1
defmodule OpenAgents.Repo.Migrations.ReconcileForgeBuildsReceiptShape do
2
  @moduledoc """
3
  Port fix: `forge_builds` was created (223c6f6, "Phase 3 target and receipt
4
  persistence") with a shape that no module in this repo declares — a bigint
5
  key plus `source_sha`/`module_changes`/`artifact_path`. The only schema
6
  bound to that table is `OpenAgents.Forge.BuildReceipt`, which declares the
7
  upstream receipt shape (`repo`, `sha`, `modules`, `warnings`, `tests`,
8
  `artifact`, binary id).
9
10
  The mismatch is not merely cosmetic: `OpenAgents.Changelog.receipt_index/1`
11
  selects `forge_builds.repo`, so every query raised `UndefinedColumnError`,
12
  and `Changelog.build/1`'s `safely/2` rescue swallowed it — dropping the
13
  push, build **and** deploy receipts for the whole timeline. That is why a
14
  receipted deploy never appeared as a `:receipt` row and no entry ever
15
  carried `receipt_ids`.
16
17
  Guarded and non-destructive: it acts only when the divergent shape is
18
  present, and preserves any existing rows by renaming the old table aside
19
  rather than dropping it.
20
  """
21
22
  use Ecto.Migration
23
24
  def up do
25
    if divergent_shape?() do
26
      rename(table(:forge_builds), to: table(:forge_builds_phase3_legacy))
27
28
      create table(:forge_builds, primary_key: false) do
29
        add :id, :binary_id, primary_key: true
30
        add :repo, :string, null: false
31
        add :sha, :string, null: false
32
        add :target_id, :binary_id, null: false
33
        add :modules, {:array, :string}, null: false, default: []
34
        add :warnings, :text
35
        add :tests, :text
36
        add :duration_ms, :integer
37
        add :artifact, :string
38
        timestamps(type: :utc_datetime_usec, updated_at: false)
39
      end
40
41
      create unique_index(:forge_builds, [:repo, :sha, :target_id])
42
      create index(:forge_builds, [:repo, :inserted_at])
43
    end
44
  end
45
46
  def down do
47
    if receipt_shape?() do
48
      drop table(:forge_builds)
49
      rename(table(:forge_builds_phase3_legacy), to: table(:forge_builds))
50
    end
51
  end
52
53
  defp divergent_shape?, do: not has_column?("forge_builds", "repo")
54
55
  defp receipt_shape?,
56
    do: has_column?("forge_builds", "repo") and table?("forge_builds_phase3_legacy")
57
58
  defp has_column?(table, column) do
59
    query?("""
60
    SELECT 1 FROM information_schema.columns
61
    WHERE table_name = '#{table}' AND column_name = '#{column}'
62
    """)
63
  end
64
65
  defp table?(table) do
66
    query?("SELECT 1 FROM information_schema.tables WHERE table_name = '#{table}'")
67
  end
68
69
  defp query?(sql) do
70
    case repo().query(sql) do
71
      {:ok, %{num_rows: n}} -> n > 0
72
      _ -> false
73
    end
74
  end
75
end
test/openagents/cluster/chaos_test.exs modified +18 -5

@@ -13,8 +13,6 @@ defmodule OpenAgents.Cluster.ChaosTest do

13 13
  Tagged `:cluster`; run with `mix test --include cluster` (needs epmd).
14 14
  """
15 15
  use ExUnit.Case, async: false
16
  @moduletag :skip
17
18 16
  @moduletag :cluster
19 17
  @moduletag timeout: 180_000
20 18

@@ -105,7 +103,7 @@ defmodule OpenAgents.Cluster.ChaosTest do

105 103
  defp start_peer(name, cookie) do
106 104
    {:ok, peer, node} =
107 105
      :peer.start_link(%{
108
        name: name,
106
        name: unique_peer_name(name),
109 107
        host: ~c"127.0.0.1",
110 108
        args: [~c"-setcookie", Atom.to_charlist(cookie)]
111 109
      })

@@ -130,8 +128,12 @@ defmodule OpenAgents.Cluster.ChaosTest do

130 128
      Node.self() != :nonode@nohost ->
131 129
        :ok
132 130
133
      match?({:ok, _}, :net_kernel.start([:"sarah_test@127.0.0.1", :longnames])) ->
134
        :erlang.set_cookie(Node.self(), :sarah_cluster_test_cookie)
131
      # A fixed node name wedges this whole stage on any machine where an
132
      # earlier run left that name registered with epmd: net_kernel then
133
      # refuses to start and every distribution test flunks "unavailable".
134
      # Unique per run, and OpenAgents-named now that this is not Sarah's BEAM.
135
      match?({:ok, _}, :net_kernel.start([unique_test_node(), :longnames])) ->
136
        :erlang.set_cookie(Node.self(), :openagents_cluster_test_cookie)
135 137
        :ok
136 138
137 139
      true ->

@@ -146,4 +148,15 @@ defmodule OpenAgents.Cluster.ChaosTest do

146 148
      true -> Process.sleep(100) && eventually(fun, attempts - 1)
147 149
    end
148 150
  end
151
152
  defp unique_test_node do
153
    :erlang.list_to_atom(~c"openagents_test_#{:erlang.unique_integer([:positive])}@127.0.0.1")
154
  end
155
156
  # Peer node names register with epmd too. A fixed name that a killed peer
157
  # left behind makes the next run's :peer.start_link fail, so the gate is
158
  # green once and wedged thereafter. Suffix every peer uniquely.
159
  defp unique_peer_name(base) do
160
    :erlang.list_to_atom(~c"#{base}_#{:erlang.unique_integer([:positive])}")
161
  end
149 162
end
test/openagents/cluster/drain_test.exs modified +18 -5

@@ -7,8 +7,6 @@ defmodule OpenAgents.Cluster.DrainTest do

7 7
  Tagged `:cluster`; run with `mix test --include cluster` (needs epmd).
8 8
  """
9 9
  use ExUnit.Case, async: false
10
  @moduletag :skip
11
12 10
  @moduletag :cluster
13 11
14 12
  alias OpenAgents.Cluster.{Drain, Ra}

@@ -60,7 +58,7 @@ defmodule OpenAgents.Cluster.DrainTest do

60 58
  defp start_peer(name, cookie) do
61 59
    {:ok, peer, node} =
62 60
      :peer.start_link(%{
63
        name: name,
61
        name: unique_peer_name(name),
64 62
        host: ~c"127.0.0.1",
65 63
        args: [~c"-setcookie", Atom.to_charlist(cookie)]
66 64
      })

@@ -84,8 +82,12 @@ defmodule OpenAgents.Cluster.DrainTest do

84 82
      Node.self() != :nonode@nohost ->
85 83
        :ok
86 84
87
      match?({:ok, _}, :net_kernel.start([:"sarah_test@127.0.0.1", :longnames])) ->
88
        :erlang.set_cookie(Node.self(), :sarah_cluster_test_cookie)
85
      # A fixed node name wedges this whole stage on any machine where an
86
      # earlier run left that name registered with epmd: net_kernel then
87
      # refuses to start and every distribution test flunks "unavailable".
88
      # Unique per run, and OpenAgents-named now that this is not Sarah's BEAM.
89
      match?({:ok, _}, :net_kernel.start([unique_test_node(), :longnames])) ->
90
        :erlang.set_cookie(Node.self(), :openagents_cluster_test_cookie)
89 91
        :ok
90 92
91 93
      true ->

@@ -100,4 +102,15 @@ defmodule OpenAgents.Cluster.DrainTest do

100 102
      true -> Process.sleep(100) && eventually(fun, attempts - 1)
101 103
    end
102 104
  end
105
106
  defp unique_test_node do
107
    :erlang.list_to_atom(~c"openagents_test_#{:erlang.unique_integer([:positive])}@127.0.0.1")
108
  end
109
110
  # Peer node names register with epmd too. A fixed name that a killed peer
111
  # left behind makes the next run's :peer.start_link fail, so the gate is
112
  # green once and wedged thereafter. Suffix every peer uniquely.
113
  defp unique_peer_name(base) do
114
    :erlang.list_to_atom(~c"#{base}_#{:erlang.unique_integer([:positive])}")
115
  end
103 116
end
test/openagents/cluster/ra_cluster_test.exs modified +24 -6

@@ -9,8 +9,6 @@ defmodule OpenAgents.Cluster.RaClusterTest do

9 9
  (needs epmd).
10 10
  """
11 11
  use ExUnit.Case, async: false
12
  @moduletag :skip
13
14 12
  @moduletag :cluster
15 13
16 14
  alias OpenAgents.Cluster.Ra

@@ -117,7 +115,12 @@ defmodule OpenAgents.Cluster.RaClusterTest do

117 115
118 116
    # Stop peer2's local Raft server, leaving it in the cluster config but not
119 117
    # running here — the phantom-member state an ungraceful restart produces.
120
    :ok = :erpc.call(node2, :ra, :stop_server, [:default, {:sarah_sessions, node2}])
118
    # The Ra cluster atom was renamed `:sarah_sessions` -> `:openagents_sessions`
119
    # in `OpenAgents.Cluster.Ra`, but this line kept the old name. `:ra.stop_server`
120
    # answers `:ok` for a server id it does not know, so the stop silently did
121
    # nothing and peer2's real server stayed up. Ask the module for the id so the
122
    # test can never drift from the cluster name again.
123
    :ok = :erpc.call(node2, :ra, :stop_server, [:default, Ra.server_id(node2)])
121 124
122 125
    assert eventually(fn -> :erpc.call(node2, Ra, :members, [node2]) == [] end),
123 126
           "peer2 local server did not stop"

@@ -231,7 +234,7 @@ defmodule OpenAgents.Cluster.RaClusterTest do

231 234
232 235
  defp start_peer(name, cookie, opts \\ []) do
233 236
    base = %{
234
      name: name,
237
      name: unique_peer_name(name),
235 238
      host: ~c"127.0.0.1",
236 239
      args: [~c"-setcookie", Atom.to_charlist(cookie)]
237 240
    }

@@ -265,8 +268,12 @@ defmodule OpenAgents.Cluster.RaClusterTest do

265 268
      Node.self() != :nonode@nohost ->
266 269
        :ok
267 270
268
      match?({:ok, _}, :net_kernel.start([:"sarah_test@127.0.0.1", :longnames])) ->
269
        :erlang.set_cookie(Node.self(), :sarah_cluster_test_cookie)
271
      # A fixed node name wedges this whole stage on any machine where an
272
      # earlier run left that name registered with epmd: net_kernel then
273
      # refuses to start and every distribution test flunks "unavailable".
274
      # Unique per run, and OpenAgents-named now that this is not Sarah's BEAM.
275
      match?({:ok, _}, :net_kernel.start([unique_test_node(), :longnames])) ->
276
        :erlang.set_cookie(Node.self(), :openagents_cluster_test_cookie)
270 277
        :ok
271 278
272 279
      true ->

@@ -281,4 +288,15 @@ defmodule OpenAgents.Cluster.RaClusterTest do

281 288
      true -> Process.sleep(100) && eventually(fun, attempts - 1)
282 289
    end
283 290
  end
291
292
  defp unique_test_node do
293
    :erlang.list_to_atom(~c"openagents_test_#{:erlang.unique_integer([:positive])}@127.0.0.1")
294
  end
295
296
  # Peer node names register with epmd too. A fixed name that a killed peer
297
  # left behind makes the next run's :peer.start_link fail, so the gate is
298
  # green once and wedged thereafter. Suffix every peer uniquely.
299
  defp unique_peer_name(base) do
300
    :erlang.list_to_atom(~c"#{base}_#{:erlang.unique_integer([:positive])}")
301
  end
284 302
end
test/openagents/cluster_test.exs modified +15 -15

@@ -1,14 +1,9 @@

1 1
defmodule OpenAgents.ClusterTest do
2
  use ExUnit.Case
3
  @moduletag :skip
2
  # Not async: the last test manipulates node-global distribution state.
3
  use ExUnit.Case, async: false
4 4
5 5
  alias OpenAgents.Cluster
6 6
7
  setup do
8
    on_exit(fn -> _ = Node.stop() end)
9
    :ok
10
  end
11
12 7
  test "single-node health report is well-formed" do
13 8
    report = Cluster.local_report()
14 9

@@ -33,8 +28,12 @@ defmodule OpenAgents.ClusterTest do

33 28
    assert snapshot["size"] == 1
34 29
  end
35 30
31
  # Real peer nodes: gated to the `mix test --only cluster` stage, like every
32
  # other distribution test here. Left in the default suite it both needs epmd
33
  # and inherits whatever node-global state a previous cluster module left
34
  # behind — which is exactly how it came to fail on `net_kernel already started`.
35
  @tag :cluster
36 36
  test "three local nodes form a cluster, report a consistent revision, and detect a missing node" do
37
    start_epmd()
38 37
    start_distribution!()
39 38
    peers = start_peers(3)
40 39

@@ -61,11 +60,17 @@ defmodule OpenAgents.ClusterTest do

61 60
      assert report2["missing"] == []
62 61
      refute to_string(down_node) in Enum.map(Cluster.members(), &to_string/1)
63 62
    after
63
      # Stop this test's peers, but leave net_kernel up. Stopping it stranded
64
      # every cluster module scheduled after this one: their
65
      # `ensure_distributed/0` then saw an undistributed node whose net_kernel
66
      # would no longer restart, and flunked "distribution unavailable".
64 67
      for {pid, _node} <- peers, do: safe_stop_peer(pid)
65
      _ = Node.stop()
68
      assert wait_for(fn -> Node.list() == [] end)
66 69
    end
67 70
  end
68 71
72
  # Idempotent: an already-distributed node is a legitimate state in the cluster
73
  # stage (another module got here first), not a reason to fail.
69 74
  defp start_distribution! do
70 75
    suffix = :erlang.unique_integer([:positive])
71 76
    name = :erlang.list_to_atom(~c"openagents_test_#{suffix}@127.0.0.1")

@@ -74,6 +79,7 @@ defmodule OpenAgents.ClusterTest do

74 79
      :ok -> :ok
75 80
      true -> :ok
76 81
      {:ok, _pid} -> :ok
82
      {:error, {:already_started, _pid}} -> :ok
77 83
      other -> raise "Failed to start net_kernel: #{inspect(other)}"
78 84
    end
79 85

@@ -81,12 +87,6 @@ defmodule OpenAgents.ClusterTest do

81 87
    :ok
82 88
  end
83 89
84
  defp start_epmd do
85
    # Start the Erlang port mapper if it is not already running.
86
    _ = System.cmd("epmd", ["-daemon"], stderr_to_stdout: true)
87
    :ok
88
  end
89
90 90
  defp start_peers(n) do
91 91
    ebins = [
92 92
      Application.app_dir(:openagents, "ebin"),
test/openagents/sarah_cluster_test.exs modified +14 -4

@@ -1,7 +1,6 @@

1 1
defmodule OpenAgents.SarahClusterTest do
2 2
  # Not async: it manipulates node-global distribution state.
3 3
  use ExUnit.Case, async: false
4
  @moduletag :skip
5 4
  alias OpenAgents.Cluster
6 5
7 6
  describe "single-node (undistributed or one member)" do

@@ -31,7 +30,7 @@ defmodule OpenAgents.SarahClusterTest do

31 30
32 31
      {:ok, peer, node} =
33 32
        :peer.start_link(%{
34
          name: :sarah_cluster_peer,
33
          name: unique_peer_name("openagents_cluster_peer"),
35 34
          host: ~c"127.0.0.1",
36 35
          args: [~c"-setcookie", Atom.to_charlist(cookie)]
37 36
        })

@@ -57,8 +56,11 @@ defmodule OpenAgents.SarahClusterTest do

57 56
      Node.self() != :nonode@nohost ->
58 57
        :ok
59 58
60
      match?({:ok, _}, :net_kernel.start([:"sarah_test@127.0.0.1", :longnames])) ->
61
        :erlang.set_cookie(Node.self(), :sarah_cluster_test_cookie)
59
      # Unique per run: a fixed name that a crashed run left registered with
60
      # epmd makes net_kernel refuse to start ever after. OpenAgents-named now
61
      # that this is not Sarah's BEAM.
62
      match?({:ok, _}, :net_kernel.start([unique_test_node(), :longnames])) ->
63
        :erlang.set_cookie(Node.self(), :openagents_cluster_test_cookie)
62 64
        :ok
63 65
64 66
      true ->

@@ -73,4 +75,12 @@ defmodule OpenAgents.SarahClusterTest do

73 75
      true -> Process.sleep(50) && eventually(fun, attempts - 1)
74 76
    end
75 77
  end
78
79
  defp unique_test_node do
80
    :erlang.list_to_atom(~c"openagents_test_#{:erlang.unique_integer([:positive])}@127.0.0.1")
81
  end
82
83
  defp unique_peer_name(base) do
84
    :erlang.list_to_atom(~c"#{base}_#{:erlang.unique_integer([:positive])}")
85
  end
76 86
end
test/openagents/work/handoff_test.exs modified +18 -3

@@ -119,7 +119,7 @@ defmodule OpenAgents.Work.HandoffTest do

119 119
  defp start_peer(name, cookie) do
120 120
    {:ok, peer, node} =
121 121
      :peer.start_link(%{
122
        name: name,
122
        name: unique_peer_name(name),
123 123
        host: ~c"127.0.0.1",
124 124
        args: [~c"-setcookie", Atom.to_charlist(cookie)]
125 125
      })

@@ -149,8 +149,12 @@ defmodule OpenAgents.Work.HandoffTest do

149 149
      Node.self() != :nonode@nohost ->
150 150
        :ok
151 151
152
      match?({:ok, _}, :net_kernel.start([:"sarah_test@127.0.0.1", :longnames])) ->
153
        :erlang.set_cookie(Node.self(), :sarah_cluster_test_cookie)
152
      # A fixed node name wedges this whole stage on any machine where an
153
      # earlier run left that name registered with epmd: net_kernel then
154
      # refuses to start and every distribution test flunks "unavailable".
155
      # Unique per run, and OpenAgents-named now that this is not Sarah's BEAM.
156
      match?({:ok, _}, :net_kernel.start([unique_test_node(), :longnames])) ->
157
        :erlang.set_cookie(Node.self(), :openagents_cluster_test_cookie)
154 158
        :ok
155 159
156 160
      true ->

@@ -165,4 +169,15 @@ defmodule OpenAgents.Work.HandoffTest do

165 169
      true -> Process.sleep(100) && eventually(fun, attempts - 1)
166 170
    end
167 171
  end
172
173
  defp unique_test_node do
174
    :erlang.list_to_atom(~c"openagents_test_#{:erlang.unique_integer([:positive])}@127.0.0.1")
175
  end
176
177
  # Peer node names register with epmd too. A fixed name that a killed peer
178
  # left behind makes the next run's :peer.start_link fail, so the gate is
179
  # green once and wedged thereafter. Suffix every peer uniquely.
180
  defp unique_peer_name(base) do
181
    :erlang.list_to_atom(~c"#{base}_#{:erlang.unique_integer([:positive])}")
182
  end
168 183
end
test/openagents_web/live/computers_navigation_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgentsWeb.ComputersNavigationTest do
2 2
  use OpenAgentsWeb.SarahConnCase
3
  @moduletag :skip
4 3
  import Phoenix.LiveViewTest
5 4
6 5
  test "the canonical Computers surface carries authenticated application chrome", %{conn: conn} do
test/test_helper.exs modified +27 -1

@@ -1,2 +1,28 @@

1
ExUnit.start(exclude: [:skip])
1
# The :cluster tag spins up real distributed peer nodes (needs epmd) and leaves
2
# node-global distribution state behind, so those tests cannot share a BEAM with
3
# the rest of the suite — a module that expects an undistributed node sees
4
# net_kernel already running. Upstream runs them as their own gate stage
5
# (`mix test --only cluster`); this excludes them here for the same reason.
6
# Note the exclusion is unconditional: `--include skip` must not drag them back
7
# in, so a :cluster module carries the :cluster tag only, never :skip too.
8
ExUnit.start(exclude: [:skip, :cluster])
9
10
# The cluster stage needs the Erlang port mapper up before its first peer node.
11
# Leaving that to whichever module happened to run first made the stage
12
# order-dependent: modules scheduled ahead of it flunked "distribution
13
# unavailable". Idempotent, and only paid for when that stage is selected.
14
cluster_stage? =
15
  ExUnit.configuration()
16
  |> Keyword.get(:include, [])
17
  |> Enum.any?(&(&1 == :cluster or match?({:cluster, _}, &1)))
18
19
if cluster_stage? do
20
  try do
21
    System.cmd("epmd", ["-daemon"], stderr_to_stdout: true)
22
  rescue
23
    # No epmd on PATH: the cluster tests flunk with their own clear message.
24
    _ -> :ok
25
  end
26
end
27
2 28
Ecto.Adapters.SQL.Sandbox.mode(OpenAgents.Repo, :manual)

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