// Go parity: golang/nats-server/server/jetstream_cluster.go // Covers: RaftGroup member management, peer add/remove/preferred, // factory method via PlacementEngine, replication health properties, // and quorum size calculation. using NATS.Server.JetStream.Cluster; namespace NATS.Server.JetStream.Tests.JetStream.Cluster; /// /// Tests for RaftGroup lifecycle: membership helpers, factory method, /// replication status properties, and quorum size. /// Go reference: jetstream_cluster.go:154-163 raftGroup struct and peer management. /// public class RaftGroupLifecycleTests { // --------------------------------------------------------------- // IsMember — membership check // Go reference: jetstream_cluster.go isMember helper // --------------------------------------------------------------- [Fact] public void IsMember_returns_true_for_existing_peer() { // Go reference: jetstream_cluster.go isMember — checks rg.Peers contains id var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2", "peer-3"] }; group.IsMember("peer-2").ShouldBeTrue(); } [Fact] public void IsMember_returns_false_for_non_member() { // Go reference: jetstream_cluster.go isMember — returns false when not in Peers var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2"] }; group.IsMember("peer-9").ShouldBeFalse(); } // --------------------------------------------------------------- // SetPreferred — assign preferred peer // Go reference: jetstream_cluster.go setPreferred / rg.Preferred // --------------------------------------------------------------- [Fact] public void SetPreferred_sets_preferred_peer() { // Go reference: jetstream_cluster.go setPreferred — assigns rg.Preferred when member var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2", "peer-3"] }; group.SetPreferred("peer-3"); group.Preferred.ShouldBe("peer-3"); } [Fact] public void SetPreferred_throws_for_non_member() { // Go reference: jetstream_cluster.go setPreferred — validates membership before setting var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2"] }; Should.Throw(() => group.SetPreferred("peer-99")); } // --------------------------------------------------------------- // RemovePeer — remove a peer from the group // Go reference: jetstream_cluster.go removePeer // --------------------------------------------------------------- [Fact] public void RemovePeer_removes_existing_peer() { // Go reference: jetstream_cluster.go removePeer — removes peer from rg.Peers var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2", "peer-3"] }; var removed = group.RemovePeer("peer-2"); removed.ShouldBeTrue(); group.Peers.ShouldNotContain("peer-2"); group.Peers.Count.ShouldBe(2); } [Fact] public void RemovePeer_clears_preferred_when_removing_preferred() { // Go reference: jetstream_cluster.go removePeer — clears rg.Preferred if it matches removed peer var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2", "peer-3"], Preferred = "peer-2" }; group.RemovePeer("peer-2"); group.Preferred.ShouldBe(string.Empty); } [Fact] public void RemovePeer_returns_false_for_non_member() { // Go reference: jetstream_cluster.go removePeer — returns false when peer not found var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2"] }; var removed = group.RemovePeer("peer-99"); removed.ShouldBeFalse(); group.Peers.Count.ShouldBe(2); } // --------------------------------------------------------------- // AddPeer — add a new peer to the group // Go reference: jetstream_cluster.go addPeer / expandGroup // --------------------------------------------------------------- [Fact] public void AddPeer_adds_new_peer() { // Go reference: jetstream_cluster.go addPeer — appends peer to rg.Peers var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2"] }; var added = group.AddPeer("peer-3"); added.ShouldBeTrue(); group.Peers.ShouldContain("peer-3"); group.Peers.Count.ShouldBe(3); } [Fact] public void AddPeer_returns_false_for_existing_peer() { // Go reference: jetstream_cluster.go addPeer — skips duplicate, returns false var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2"] }; var added = group.AddPeer("peer-1"); added.ShouldBeFalse(); group.Peers.Count.ShouldBe(2); } // --------------------------------------------------------------- // CreateRaftGroup factory — uses PlacementEngine // Go reference: jetstream_cluster.go:7212 selectPeerGroup called from createGroupForStream // --------------------------------------------------------------- [Fact] public void CreateRaftGroup_uses_placement_engine() { // Go reference: jetstream_cluster.go createGroupForStream — calls selectPeerGroup var peers = new List { new() { PeerId = "peer-A", Available = true, AvailableStorage = 9000 }, new() { PeerId = "peer-B", Available = true, AvailableStorage = 8000 }, new() { PeerId = "peer-C", Available = true, AvailableStorage = 7000 }, }; var group = RaftGroup.CreateRaftGroup("my-stream", 3, peers); group.Name.ShouldBe("my-stream"); group.Peers.Count.ShouldBe(3); group.Peers.ShouldContain("peer-A"); group.Peers.ShouldContain("peer-B"); group.Peers.ShouldContain("peer-C"); } [Fact] public void CreateRaftGroup_sets_desired_replicas() { // Go reference: jetstream_cluster.go rg.DesiredReplicas = replicas after group creation var peers = new List { new() { PeerId = "peer-X", Available = true }, new() { PeerId = "peer-Y", Available = true }, new() { PeerId = "peer-Z", Available = true }, }; var group = RaftGroup.CreateRaftGroup("replicated-stream", 3, peers); group.DesiredReplicas.ShouldBe(3); group.HasDesiredReplicas.ShouldBeTrue(); } // --------------------------------------------------------------- // IsUnderReplicated — replication health // Go reference: jetstream_cluster.go missingPeers — len(Peers) < DesiredReplicas // --------------------------------------------------------------- [Fact] public void IsUnderReplicated_true_when_peers_less_than_desired() { // Go reference: jetstream_cluster.go:2284 sa.missingPeers() var group = new RaftGroup { Name = "test", Peers = ["peer-1"], DesiredReplicas = 3 }; group.IsUnderReplicated.ShouldBeTrue(); } [Fact] public void IsUnderReplicated_false_when_peers_equal_desired() { // Go reference: jetstream_cluster.go:2284 sa.missingPeers() — no deficit when equal var group = new RaftGroup { Name = "test", Peers = ["peer-1", "peer-2", "peer-3"], DesiredReplicas = 3 }; group.IsUnderReplicated.ShouldBeFalse(); } [Fact] public void IsUnderReplicated_false_when_no_desired_replicas_set() { // Go reference: jetstream_cluster.go — without DesiredReplicas set, no under-replication var group = new RaftGroup { Name = "test", Peers = ["peer-1"] }; group.IsUnderReplicated.ShouldBeFalse(); } // --------------------------------------------------------------- // IsOverReplicated — excess replication detection // Go reference: jetstream_cluster.go extraPeers — len(Peers) > DesiredReplicas // --------------------------------------------------------------- [Fact] public void IsOverReplicated_true_when_peers_more_than_desired() { // Go reference: jetstream_cluster.go extraPeers detection for scale-down var group = new RaftGroup { Name = "test", Peers = ["p1", "p2", "p3", "p4"], DesiredReplicas = 3 }; group.IsOverReplicated.ShouldBeTrue(); } [Fact] public void IsOverReplicated_false_when_peers_equal_desired() { // Go reference: jetstream_cluster.go — no excess when equal var group = new RaftGroup { Name = "test", Peers = ["p1", "p2", "p3"], DesiredReplicas = 3 }; group.IsOverReplicated.ShouldBeFalse(); } [Fact] public void IsOverReplicated_false_when_no_desired_replicas_set() { // Go reference: jetstream_cluster.go — without DesiredReplicas set, no over-replication var group = new RaftGroup { Name = "test", Peers = ["p1", "p2", "p3", "p4"] }; group.IsOverReplicated.ShouldBeFalse(); } // --------------------------------------------------------------- // QuorumSize — majority quorum calculation // Go reference: jetstream_cluster.go quorumNeeded — (n/2)+1 // --------------------------------------------------------------- [Theory] [InlineData(1, 1)] // R=1 → quorum=1 [InlineData(3, 2)] // R=3 → quorum=2 [InlineData(5, 3)] // R=5 → quorum=3 [InlineData(2, 2)] // R=2 → quorum=2 (degenerate, but formula consistent) [InlineData(4, 3)] // R=4 → quorum=3 public void QuorumSize_correct_for_various_counts(int peerCount, int expectedQuorum) { // Go reference: jetstream_cluster.go quorumNeeded — (n/2)+1 var peers = Enumerable.Range(1, peerCount).Select(i => $"peer-{i}").ToList(); var group = new RaftGroup { Name = "quorum-test", Peers = peers }; group.QuorumSize.ShouldBe(expectedQuorum); } }