Enforce merged local and cluster coverage

2b52b624f2d7 · Christopher David · · parent f34649b29f0a

Enforce merged local and cluster coverage

Add an owned coverage command with clean exports, remote peer collection, direct Ra bootstrap decision tests, and an initial 83 percent floor. Record the 83.58 percent baseline in Gate 0.

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 docs/2026-08-20-integration-hardening-and-staging-readiness-recommendations.md
  • modified lib/openagents/cluster/ra_bootstrap.ex
  • modified mix.exs
  • added ops/ci/coverage.sh
  • modified test/openagents/cluster/chaos_test.exs
  • modified test/openagents/cluster/drain_test.exs
  • added test/openagents/cluster/ra_bootstrap_test.exs
  • modified test/openagents/cluster/ra_cluster_test.exs
  • modified test/openagents/cluster_test.exs
  • modified test/openagents/work/handoff_test.exs
  • added test/support/openagents/test/remote_cover.ex

Diff

11 files changed, +147 -19

docs/2026-08-20-integration-hardening-and-staging-readiness-recommendations.md modified +16 -6

@@ -94,11 +94,19 @@ Completed on 2026-08-20:

94 94
- Replaced Ra's captured session-query functions with stable
95 95
  module-function-argument descriptors. The cluster suite now runs under
96 96
  coverage without a peer crashing on an instrumented function identity.
97
- Merged the default and cluster coverage exports locally. The combined result
98
  is 83.37%; the exact-SHA receipt and enforced floor remain pending.
99
100
Gate 0 still requires an enforced coverage floor, release startup proof against
101
a disposable database, and an exact-SHA gate receipt.
97
- Merged the default and cluster coverage exports locally. The current combined
98
  result is 83.58% from 1,222 default tests and all 9 cluster tests.
99
- Added `ops/ci/coverage.sh` to discard stale exports, run both suites with
100
  warnings as errors, collect execution from distributed peers, merge both
101
  exports, and enforce an initial 83% floor. Raise the floor as direct recovery
102
  and release-path tests land. Do not lower it to admit a candidate.
103
- Added coverage-aware peer shutdown so peer execution flushes back to the main
104
  coverage node before a test stops the peer.
105
- Added direct `RaBootstrap` decision tests for healthy, phantom, join, form,
106
  and wait outcomes instead of treating cluster execution as indirect proof.
107
108
Gate 0 still requires release startup proof against a disposable database and
109
an exact-SHA gate receipt.
102 110
103 111
Do not use the current green suite as evidence for untested code. The updated
104 112
coverage audit records strong Issues and Projects coverage and the defects it

@@ -1082,7 +1090,9 @@ through `mix assets.test`.

1082 1090
The first merged coverage attempt also exposed an instrumented anonymous Ra
1083 1091
query that crashed on a peer with `badfun`. Session-registry queries now use
1084 1092
stable module-function-argument descriptors, and all 9 cluster tests complete
1085
under coverage. The merged default and cluster result is 83.37%.
1093
under coverage. Coverage-aware peer shutdown now collects remote execution, and
1094
direct `RaBootstrap` decision tests cover the previously untested worker. The
1095
merged default and cluster result is 83.58% and passes the enforced 83% floor.
1086 1096
1087 1097
## A2. Blocker: staging is not isolated from production today
1088 1098
lib/openagents/cluster/ra_bootstrap.ex modified +33 -13

@@ -78,25 +78,22 @@ defmodule OpenAgents.Cluster.RaBootstrap do

78 78
    # The cluster's member set as seen by a peer (survives our local server
79 79
    # being down — the phantom-member case after an ungraceful restart).
80 80
    peer_members = discover_members(connected -- [node()])
81
    members = if local_members != [], do: local_members, else: peer_members
82 81
83
    cond do
84
      # Healthy: our local server is up and we are a member.
85
      node() in local_members ->
82
    case convergence_action(node(), connected, local_members, peer_members, expected) do
83
      # Healthy: the local server is up and is a member.
84
      :healthy ->
86 85
        :ok
87 86
88 87
      # Phantom: peers still list us as a member but our local Raft server is
89 88
      # not running (ungraceful restart lost the tmpfs data dir). Restart it so
90 89
      # it rejoins and catches up, instead of sitting as a dead config entry.
91
      node() in peer_members ->
92
        _ = Ra.ensure_local_server(peer_members)
90
      {:restart_local, known_members} ->
91
        _ = Ra.ensure_local_server(known_members)
93 92
        Logger.info("ra_bootstrap: restarted phantom local server (#{node()})")
94 93
95 94
      # Cluster is formed elsewhere but we are not in it yet: join through an
96 95
      # existing member (add_member + start our local server).
97
      members != [] ->
98
        via = Enum.find(members, &(&1 in connected)) || hd(members)
99
96
      {:join, via} ->
100 97
        case Ra.join(via) do
101 98
          {:ok, _, _} -> Logger.info("ra_bootstrap: joined cluster via #{via} (#{node()})")
102 99
          :ok -> Logger.info("ra_bootstrap: joined cluster via #{via} (#{node()})")

@@ -104,8 +101,8 @@ defmodule OpenAgents.Cluster.RaBootstrap do

104 101
        end
105 102
106 103
      # No cluster yet. The coordinator forms it once a majority is present.
107
      coordinator?(connected) and majority?(length(connected), expected) ->
108
        case Ra.start_cluster(connected) do
104
      {:form, formation_nodes} ->
105
        case Ra.start_cluster(formation_nodes) do
109 106
          {:ok, started} ->
110 107
            Logger.info("ra_bootstrap: formed cluster across #{inspect(started)}")
111 108

@@ -113,7 +110,7 @@ defmodule OpenAgents.Cluster.RaBootstrap do

113 110
            :ok
114 111
        end
115 112
116
      true ->
113
      :wait ->
117 114
        :ok
118 115
    end
119 116
  rescue

@@ -122,9 +119,32 @@ defmodule OpenAgents.Cluster.RaBootstrap do

122 119
      :ok
123 120
  end
124 121
122
  @doc false
123
  def convergence_action(local_node, connected, local_members, peer_members, expected) do
124
    members = if local_members != [], do: local_members, else: peer_members
125
126
    cond do
127
      local_node in local_members ->
128
        :healthy
129
130
      local_node in peer_members ->
131
        {:restart_local, peer_members}
132
133
      members != [] ->
134
        via = Enum.find(members, &(&1 in connected)) || hd(members)
135
        {:join, via}
136
137
      coordinator?(local_node, connected) and majority?(length(connected), expected) ->
138
        {:form, connected}
139
140
      true ->
141
        :wait
142
    end
143
  end
144
125 145
  # We are the coordinator iff we are the lowest-named connected node — a stable,
126 146
  # coordinator-free way to pick exactly one former.
127
  defp coordinator?(connected), do: node() == Enum.min(connected)
147
  defp coordinator?(local_node, connected), do: local_node == Enum.min(connected)
128 148
129 149
  defp majority?(present, expected), do: present * 2 > expected
130 150
mix.exs modified +1

@@ -9,6 +9,7 @@ defmodule OpenAgents.MixProject do

9 9
      elixirc_paths: elixirc_paths(Mix.env()),
10 10
      start_permanent: Mix.env() == :prod,
11 11
      aliases: aliases(),
12
      test_coverage: [summary: [threshold: 83.0], local_only: false],
12 13
      deps: deps(),
13 14
      compilers: [:appup, :phoenix_live_view] ++ Mix.compilers(),
14 15
      appup: "rel/openagents.appup.exs",
ops/ci/coverage.sh added +29

@@ -0,0 +1,29 @@

1
#!/bin/sh
2
set -eu
3
4
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
repo_root=$(CDPATH= cd -- "$script_dir/../.." && pwd)
6
coverage_dir="$repo_root/cover"
7
8
if [ ! -d "$repo_root/.git" ]; then
9
  echo "coverage gate must run from a Git worktree" >&2
10
  exit 1
11
fi
12
13
case "$coverage_dir" in
14
  "$repo_root/cover") ;;
15
  *)
16
    echo "refusing to clean an unexpected coverage path" >&2
17
    exit 1
18
    ;;
19
esac
20
21
# Coverage exports are cumulative. Remove the generated directory before both
22
# runs so an old partition cannot make the current report look better.
23
rm -rf -- "$coverage_dir"
24
25
cd "$repo_root"
26
27
MIX_ENV=test mix test --warnings-as-errors --cover --export-coverage default
28
MIX_ENV=test mix test --warnings-as-errors --only cluster --cover --export-coverage cluster
29
MIX_ENV=test mix test.coverage
test/openagents/cluster/chaos_test.exs modified +1

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

105 105
      :peer.start_link(%{
106 106
        name: unique_peer_name(name),
107 107
        host: ~c"127.0.0.1",
108
        shutdown: OpenAgents.Test.RemoteCover.shutdown(),
108 109
        args: [~c"-setcookie", Atom.to_charlist(cookie)]
109 110
      })
110 111
test/openagents/cluster/drain_test.exs modified +1

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

60 60
      :peer.start_link(%{
61 61
        name: unique_peer_name(name),
62 62
        host: ~c"127.0.0.1",
63
        shutdown: OpenAgents.Test.RemoteCover.shutdown(),
63 64
        args: [~c"-setcookie", Atom.to_charlist(cookie)]
64 65
      })
65 66
test/openagents/cluster/ra_bootstrap_test.exs added +53

@@ -0,0 +1,53 @@

1
defmodule OpenAgents.Cluster.RaBootstrapTest do
2
  use ExUnit.Case, async: true
3
4
  alias OpenAgents.Cluster.RaBootstrap
5
6
  test "keeps a healthy local member unchanged" do
7
    assert :healthy =
8
             RaBootstrap.convergence_action(
9
               :node1@host,
10
               [:node1@host, :node2@host],
11
               [:node1@host, :node2@host],
12
               [:node1@host, :node2@host],
13
               3
14
             )
15
  end
16
17
  test "restarts a phantom local member from the peer membership" do
18
    peer_members = [:node1@host, :node2@host, :node3@host]
19
20
    assert {:restart_local, ^peer_members} =
21
             RaBootstrap.convergence_action(
22
               :node1@host,
23
               peer_members,
24
               [],
25
               peer_members,
26
               3
27
             )
28
  end
29
30
  test "joins through a reachable member of an existing cluster" do
31
    assert {:join, :node2@host} =
32
             RaBootstrap.convergence_action(
33
               :node1@host,
34
               [:node1@host, :node2@host],
35
               [],
36
               [:node3@host, :node2@host],
37
               3
38
             )
39
  end
40
41
  test "forms only when the local node is coordinator and a majority is present" do
42
    connected = [:node1@host, :node2@host]
43
44
    assert {:form, ^connected} =
45
             RaBootstrap.convergence_action(:node1@host, connected, [], [], 3)
46
47
    assert :wait =
48
             RaBootstrap.convergence_action(:node2@host, connected, [], [], 3)
49
50
    assert :wait =
51
             RaBootstrap.convergence_action(:node1@host, [:node1@host], [], [], 3)
52
  end
53
end
test/openagents/cluster/ra_cluster_test.exs modified +1

@@ -236,6 +236,7 @@ defmodule OpenAgents.Cluster.RaClusterTest do

236 236
    base = %{
237 237
      name: unique_peer_name(name),
238 238
      host: ~c"127.0.0.1",
239
      shutdown: OpenAgents.Test.RemoteCover.shutdown(),
239 240
      args: [~c"-setcookie", Atom.to_charlist(cookie)]
240 241
    }
241 242
test/openagents/cluster_test.exs modified +1

@@ -109,6 +109,7 @@ defmodule OpenAgents.ClusterTest do

109 109
          name: short_name,
110 110
          host: ~c"127.0.0.1",
111 111
          user: %{},
112
          shutdown: OpenAgents.Test.RemoteCover.shutdown(),
112 113
          args:
113 114
            [
114 115
              ~c"-setcookie",
test/openagents/work/handoff_test.exs modified +1

@@ -121,6 +121,7 @@ defmodule OpenAgents.Work.HandoffTest do

121 121
      :peer.start_link(%{
122 122
        name: unique_peer_name(name),
123 123
        host: ~c"127.0.0.1",
124
        shutdown: OpenAgents.Test.RemoteCover.shutdown(),
124 125
        args: [~c"-setcookie", Atom.to_charlist(cookie)]
125 126
      })
126 127
test/support/openagents/test/remote_cover.ex added +10

@@ -0,0 +1,10 @@

1
defmodule OpenAgents.Test.RemoteCover do
2
  @moduledoc false
3
4
  def shutdown do
5
    case Process.whereis(:cover_server) do
6
      nil -> 5_000
7
      _pid -> {10_000, node()}
8
    end
9
  end
10
end

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