// Go parity: golang/nats-server/server/jetstream_cluster_1_test.go // Covers: placement caps, cluster size variations, replica defaults, R1/R3/R5/R7 // placement, stepdown and info consistency, concurrent creation, long names, // subject overlap, re-create after delete, update without message loss. using System.Text; using NATS.Server.JetStream.Api; using NATS.Server.JetStream.Cluster; using NATS.Server.JetStream.Models; namespace NATS.Server.Tests.JetStream.Cluster; /// /// Tests covering JetStream cluster stream placement semantics: /// replica caps at cluster size, various cluster sizes, replica defaults, /// concurrent creation, leader stepdown, info consistency, and edge cases. /// Ported from Go jetstream_cluster_1_test.go. /// public class JsClusterStreamPlacementTests { // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public void Placement_planner_caps_five_replicas_in_three_node_cluster() { var planner = new AssetPlacementPlanner(nodes: 3); var placement = planner.PlanReplicas(replicas: 5); placement.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public void Placement_planner_allows_exact_cluster_size_replicas() { var planner = new AssetPlacementPlanner(nodes: 3); var placement = planner.PlanReplicas(replicas: 3); placement.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public void Placement_planner_zero_replicas_defaults_to_one() { var planner = new AssetPlacementPlanner(nodes: 3); var placement = planner.PlanReplicas(replicas: 0); placement.Count.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public void Placement_planner_negative_replicas_treated_as_one() { var planner = new AssetPlacementPlanner(nodes: 3); var placement = planner.PlanReplicas(replicas: -1); placement.Count.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterSingleReplicaStreams server/jetstream_cluster_1_test.go:223 // --------------------------------------------------------------- [Fact] public void Placement_planner_R1_in_single_node_cluster() { var planner = new AssetPlacementPlanner(nodes: 1); var placement = planner.PlanReplicas(replicas: 1); placement.Count.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterExpandCluster server/jetstream_cluster_1_test.go:86 // --------------------------------------------------------------- [Fact] public void Placement_planner_caps_to_single_node_in_one_node_cluster() { var planner = new AssetPlacementPlanner(nodes: 1); var placement = planner.PlanReplicas(replicas: 3); placement.Count.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public void Placement_planner_R1_in_three_node_cluster() { var planner = new AssetPlacementPlanner(nodes: 3); var placement = planner.PlanReplicas(replicas: 1); placement.Count.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public void Placement_planner_R3_in_five_node_cluster() { var planner = new AssetPlacementPlanner(nodes: 5); var placement = planner.PlanReplicas(replicas: 3); placement.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public void Placement_planner_R5_in_seven_node_cluster() { var planner = new AssetPlacementPlanner(nodes: 7); var placement = planner.PlanReplicas(replicas: 5); placement.Count.ShouldBe(5); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public void Placement_planner_R7_in_seven_node_cluster_exact_match() { var planner = new AssetPlacementPlanner(nodes: 7); var placement = planner.PlanReplicas(replicas: 7); placement.Count.ShouldBe(7); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public void Placement_planner_caps_R7_in_five_node_cluster_to_five() { var planner = new AssetPlacementPlanner(nodes: 5); var placement = planner.PlanReplicas(replicas: 7); placement.Count.ShouldBe(5); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamInfoList server/jetstream_cluster_1_test.go:1284 // --------------------------------------------------------------- [Fact] public async Task Multiple_streams_with_different_placements_coexist() { await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("P1", ["p1.>"], replicas: 1); await cluster.CreateStreamAsync("P3", ["p3.>"], replicas: 3); await cluster.CreateStreamAsync("P5", ["p5.>"], replicas: 5); var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}"); names.StreamNames.ShouldNotBeNull(); names.StreamNames!.Count.ShouldBe(3); names.StreamNames.ShouldContain("P1"); names.StreamNames.ShouldContain("P3"); names.StreamNames.ShouldContain("P5"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public async Task Stream_with_replicas_equal_to_cluster_size_succeeds() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = await cluster.CreateStreamAsync("FULL3", ["full3.>"], replicas: 3); resp.Error.ShouldBeNull(); var group = cluster.GetReplicaGroup("FULL3"); group.ShouldNotBeNull(); group!.Nodes.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamInfoList server/jetstream_cluster_1_test.go:1284 // --------------------------------------------------------------- [Fact] public async Task Stream_creation_after_another_stream_exists_succeeds() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("FIRST", ["first.>"], replicas: 3); var resp = await cluster.CreateStreamAsync("SECOND", ["second.>"], replicas: 3); resp.Error.ShouldBeNull(); resp.StreamInfo.ShouldNotBeNull(); resp.StreamInfo!.Config.Name.ShouldBe("SECOND"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMaxStreamsReached server/jetstream_cluster_1_test.go:3177 // --------------------------------------------------------------- [Fact] public async Task Ten_streams_in_same_cluster_all_exist() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var i = 0; i < 10; i++) await cluster.CreateStreamAsync($"PLACE{i}", [$"place{i}.>"], replicas: 3); var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}"); names.StreamNames.ShouldNotBeNull(); names.StreamNames!.Count.ShouldBe(10); for (var i = 0; i < 10; i++) names.StreamNames.ShouldContain($"PLACE{i}"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925 // --------------------------------------------------------------- [Fact] public async Task Replicated_stream_survives_meta_leader_stepdown() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("SURV", ["surv.>"], replicas: 3); for (var i = 0; i < 5; i++) await cluster.PublishAsync("surv.event", $"msg-{i}"); var metaBefore = cluster.GetMetaLeaderId(); cluster.StepDownMetaLeader(); var metaAfter = cluster.GetMetaLeaderId(); metaAfter.ShouldNotBe(metaBefore); // Stream still accessible after meta stepdown var state = await cluster.GetStreamStateAsync("SURV"); state.Messages.ShouldBe(5UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925 // --------------------------------------------------------------- [Fact] public async Task Stream_info_consistent_after_meta_stepdown() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("INFOSTEP", ["infostep.>"], replicas: 3); for (var i = 0; i < 7; i++) await cluster.PublishAsync("infostep.event", $"msg-{i}"); cluster.StepDownMetaLeader(); var info = await cluster.GetStreamInfoAsync("INFOSTEP"); info.Error.ShouldBeNull(); info.StreamInfo.ShouldNotBeNull(); info.StreamInfo!.Config.Name.ShouldBe("INFOSTEP"); info.StreamInfo.State.Messages.ShouldBe(7UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public void Placement_more_replicas_than_nodes_caps_not_errors() { // Verifies AssetPlacementPlanner silently caps rather than throwing var planner = new AssetPlacementPlanner(nodes: 3); var act = () => planner.PlanReplicas(replicas: 999); act.ShouldNotThrow(); var result = planner.PlanReplicas(replicas: 999); result.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterExpandCluster server/jetstream_cluster_1_test.go:86 // --------------------------------------------------------------- [Fact] public void Placement_cluster_size_one_always_returns_one_replica() { var planner = new AssetPlacementPlanner(nodes: 1); for (var r = 1; r <= 10; r++) planner.PlanReplicas(replicas: r).Count.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamNormalCatchup server/jetstream_cluster_1_test.go:1607 // --------------------------------------------------------------- [Fact] public async Task Stream_exists_after_remove_and_restart_node_simulation() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("NODEREMOVE", ["noderemove.>"], replicas: 3); for (var i = 0; i < 5; i++) await cluster.PublishAsync("noderemove.event", $"msg-{i}"); cluster.RemoveNode(2); cluster.SimulateNodeRestart(2); var state = await cluster.GetStreamStateAsync("NODEREMOVE"); state.Messages.ShouldBe(5UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamInfoList server/jetstream_cluster_1_test.go:1284 // --------------------------------------------------------------- [Fact] public async Task Concurrent_stream_creation_all_streams_verify_exist() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var tasks = Enumerable.Range(0, 5) .Select(i => cluster.CreateStreamAsync($"CONC{i}", [$"conc{i}.>"], replicas: 3)) .ToArray(); await Task.WhenAll(tasks); var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}"); names.StreamNames.ShouldNotBeNull(); names.StreamNames!.Count.ShouldBe(5); for (var i = 0; i < 5; i++) names.StreamNames.ShouldContain($"CONC{i}"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public async Task Stream_names_can_be_long_strings() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var longName = new string('A', 60); var resp = await cluster.CreateStreamAsync(longName, [$"{longName.ToLowerInvariant()}.>"], replicas: 3); resp.Error.ShouldBeNull(); resp.StreamInfo!.Config.Name.ShouldBe(longName); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamOverlapSubjects server/jetstream_cluster_1_test.go:1248 // --------------------------------------------------------------- [Fact] public async Task Stream_subjects_can_be_completely_distinct_from_others() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("DISTINCT1", ["ns1.>"], replicas: 3); await cluster.CreateStreamAsync("DISTINCT2", ["ns2.>"], replicas: 3); await cluster.CreateStreamAsync("DISTINCT3", ["ns3.>"], replicas: 3); var ack1 = await cluster.PublishAsync("ns1.event", "msg1"); ack1.Stream.ShouldBe("DISTINCT1"); var ack2 = await cluster.PublishAsync("ns2.event", "msg2"); ack2.Stream.ShouldBe("DISTINCT2"); var ack3 = await cluster.PublishAsync("ns3.event", "msg3"); ack3.Stream.ShouldBe("DISTINCT3"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamUpdate server/jetstream_cluster_1_test.go:1433 // --------------------------------------------------------------- [Fact] public async Task Re_creating_deleted_stream_with_same_placement_works() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("REDEL", ["redel.>"], replicas: 3); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}REDEL", "{}"); var resp = await cluster.CreateStreamAsync("REDEL", ["redel.>"], replicas: 3); resp.Error.ShouldBeNull(); resp.StreamInfo.ShouldNotBeNull(); resp.StreamInfo!.Config.Name.ShouldBe("REDEL"); resp.StreamInfo.Config.Replicas.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamUpdate server/jetstream_cluster_1_test.go:1433 // --------------------------------------------------------------- [Fact] public async Task Stream_update_does_not_lose_published_messages() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("NOLOSS", ["noloss.>"], replicas: 3); for (var i = 0; i < 15; i++) await cluster.PublishAsync("noloss.event", $"msg-{i}"); var update = cluster.UpdateStream("NOLOSS", ["noloss.>"], replicas: 3, maxMsgs: 100); update.Error.ShouldBeNull(); var state = await cluster.GetStreamStateAsync("NOLOSS"); state.Messages.ShouldBe(15UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925 // --------------------------------------------------------------- [Fact] public async Task R3_stream_leader_stepdown_elects_new_leader() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PLSTEP", ["plstep.>"], replicas: 3); var before = cluster.GetStreamLeaderId("PLSTEP"); before.ShouldNotBeNullOrWhiteSpace(); var resp = await cluster.StepDownStreamLeaderAsync("PLSTEP"); resp.Success.ShouldBeTrue(); var after = cluster.GetStreamLeaderId("PLSTEP"); after.ShouldNotBe(before); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925 // --------------------------------------------------------------- [Fact] public async Task Stream_info_consistent_after_R3_stream_leader_stepdown() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PLINFOSTEP", ["plinfostep.>"], replicas: 3); for (var i = 0; i < 5; i++) await cluster.PublishAsync("plinfostep.event", $"msg-{i}"); await cluster.StepDownStreamLeaderAsync("PLINFOSTEP"); var info = await cluster.GetStreamInfoAsync("PLINFOSTEP"); info.Error.ShouldBeNull(); info.StreamInfo.ShouldNotBeNull(); info.StreamInfo!.Config.Replicas.ShouldBe(3); info.StreamInfo.State.Messages.ShouldBe(5UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public async Task Placement_validation_replicas_capped_at_cluster_node_count() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); // StreamReplicaGroup internally caps replicas at cluster size var group = cluster.GetReplicaGroup("NOTEXIST"); group.ShouldBeNull(); // Creating with excess replicas should work (streamed to cluster-size) var resp = await cluster.CreateStreamAsync("CAPTEST", ["captest.>"], replicas: 3); resp.Error.ShouldBeNull(); var g = cluster.GetReplicaGroup("CAPTEST"); g.ShouldNotBeNull(); g!.Nodes.Count.ShouldBeLessThanOrEqualTo(cluster.NodeCount); } // --------------------------------------------------------------- // Go: TestJetStreamClusterExpandCluster server/jetstream_cluster_1_test.go:86 // --------------------------------------------------------------- [Fact] public void Placement_planner_cluster_size_reflected_correctly_for_different_sizes() { // 1-node cluster new AssetPlacementPlanner(1).PlanReplicas(3).Count.ShouldBe(1); // 3-node cluster new AssetPlacementPlanner(3).PlanReplicas(3).Count.ShouldBe(3); // 5-node cluster new AssetPlacementPlanner(5).PlanReplicas(3).Count.ShouldBe(3); // 7-node cluster new AssetPlacementPlanner(7).PlanReplicas(3).Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMetaSnapshotsAndCatchup server/jetstream_cluster_1_test.go:833 // --------------------------------------------------------------- [Fact] public async Task Meta_group_tracks_stream_placement_changes_through_stepdown() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("META_P1", ["meta_p1.>"], replicas: 1); await cluster.CreateStreamAsync("META_P3", ["meta_p3.>"], replicas: 3); var stateBefore = cluster.GetMetaState(); stateBefore.ShouldNotBeNull(); stateBefore!.Streams.ShouldContain("META_P1"); stateBefore.Streams.ShouldContain("META_P3"); cluster.StepDownMetaLeader(); var stateAfter = cluster.GetMetaState(); stateAfter.ShouldNotBeNull(); stateAfter!.Streams.ShouldContain("META_P1"); stateAfter.Streams.ShouldContain("META_P3"); stateAfter.LeadershipVersion.ShouldBeGreaterThan(stateBefore.LeadershipVersion); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamInfoList server/jetstream_cluster_1_test.go:1284 // --------------------------------------------------------------- [Fact] public async Task Stream_list_api_returns_all_streams_in_five_node_cluster() { await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("FL1", ["fl1.>"], replicas: 1); await cluster.CreateStreamAsync("FL3", ["fl3.>"], replicas: 3); await cluster.CreateStreamAsync("FL5", ["fl5.>"], replicas: 5); var list = await cluster.RequestAsync(JetStreamApiSubjects.StreamList, "{}"); list.StreamNames.ShouldNotBeNull(); list.StreamNames!.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterSingleReplicaStreams server/jetstream_cluster_1_test.go:223 // --------------------------------------------------------------- [Fact] public async Task R1_placement_in_five_node_cluster_creates_one_node_group() { await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("R1IN5", ["r1in5.>"], replicas: 1); var group = cluster.GetReplicaGroup("R1IN5"); group.ShouldNotBeNull(); group!.Nodes.Count.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public async Task R3_placement_in_five_node_cluster_creates_three_node_group() { await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("R3IN5", ["r3in5.>"], replicas: 3); var group = cluster.GetReplicaGroup("R3IN5"); group.ShouldNotBeNull(); group!.Nodes.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925 // --------------------------------------------------------------- [Fact] public async Task Consecutive_meta_stepdowns_preserve_stream_placements() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CONSEC1", ["consec1.>"], replicas: 3); await cluster.CreateStreamAsync("CONSEC2", ["consec2.>"], replicas: 1); // Perform multiple stepdowns cluster.StepDownMetaLeader(); cluster.StepDownMetaLeader(); cluster.StepDownMetaLeader(); var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}"); names.StreamNames.ShouldNotBeNull(); names.StreamNames!.ShouldContain("CONSEC1"); names.StreamNames.ShouldContain("CONSEC2"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamUpdate server/jetstream_cluster_1_test.go:1433 // --------------------------------------------------------------- [Fact] public async Task Publish_after_stream_update_works_correctly() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("POSTUPD", ["postupd.>"], replicas: 3); for (var i = 0; i < 5; i++) await cluster.PublishAsync("postupd.event", $"before-{i}"); cluster.UpdateStream("POSTUPD", ["postupd.>"], replicas: 3, maxMsgs: 100); for (var i = 0; i < 5; i++) await cluster.PublishAsync("postupd.event", $"after-{i}"); var state = await cluster.GetStreamStateAsync("POSTUPD"); state.Messages.ShouldBe(10UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamPurge server/jetstream_cluster_1_test.go:522 // --------------------------------------------------------------- [Fact] public async Task R3_stream_purge_after_stepdown_clears_messages() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PURGESTEP", ["purgestep.>"], replicas: 3); for (var i = 0; i < 10; i++) await cluster.PublishAsync("purgestep.event", $"msg-{i}"); await cluster.StepDownStreamLeaderAsync("PURGESTEP"); var purge = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}PURGESTEP", "{}"); purge.Success.ShouldBeTrue(); var state = await cluster.GetStreamStateAsync("PURGESTEP"); state.Messages.ShouldBe(0UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public async Task R3_stream_has_leader_with_naming_convention() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("LEADNM", ["leadnm.>"], replicas: 3); var group = cluster.GetReplicaGroup("LEADNM"); group.ShouldNotBeNull(); group!.Leader.Id.ShouldNotBeNullOrWhiteSpace(); group.Leader.IsLeader.ShouldBeTrue(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMaxStreamsReached server/jetstream_cluster_1_test.go:3177 // --------------------------------------------------------------- [Fact] public async Task Account_info_reflects_correct_stream_count_after_placements() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ACCP1", ["accp1.>"], replicas: 1); await cluster.CreateStreamAsync("ACCP3", ["accp3.>"], replicas: 3); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo.ShouldNotBeNull(); info.AccountInfo!.Streams.ShouldBe(2); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamNormalCatchup server/jetstream_cluster_1_test.go:1607 // --------------------------------------------------------------- [Fact] public async Task Wait_on_stream_leader_completes_for_newly_placed_stream() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("WAITPL", ["waitpl.>"], replicas: 3); await cluster.WaitOnStreamLeaderAsync("WAITPL", timeoutMs: 2000); var leaderId = cluster.GetStreamLeaderId("WAITPL"); leaderId.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterDelete server/jetstream_cluster_1_test.go:472 // --------------------------------------------------------------- [Fact] public async Task Stream_delete_reduces_account_stream_count() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("DEL_A", ["del_a.>"], replicas: 3); await cluster.CreateStreamAsync("DEL_B", ["del_b.>"], replicas: 3); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}DEL_A", "{}"); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo!.Streams.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamInfoList server/jetstream_cluster_1_test.go:1284 // --------------------------------------------------------------- [Fact] public async Task Stream_placement_info_accessible_via_api_router_subject() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("APIPLC", ["apiplc.>"], replicas: 3); var resp = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamInfo}APIPLC", "{}"); resp.Error.ShouldBeNull(); resp.StreamInfo.ShouldNotBeNull(); resp.StreamInfo!.Config.Name.ShouldBe("APIPLC"); resp.StreamInfo.Config.Replicas.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMemoryStore server/jetstream_cluster_1_test.go:423 // --------------------------------------------------------------- [Fact] public async Task Memory_store_placement_in_three_node_cluster_accepts_publishes() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("MEMPLACE", ["memplace.>"], replicas: 3, storage: StorageType.Memory); for (var i = 0; i < 20; i++) await cluster.PublishAsync("memplace.event", $"msg-{i}"); var state = await cluster.GetStreamStateAsync("MEMPLACE"); state.Messages.ShouldBe(20UL); cluster.GetStoreBackendType("MEMPLACE").ShouldBe("memory"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterLeader server/jetstream_cluster_1_test.go:73 // --------------------------------------------------------------- [Fact] public async Task Meta_leadership_version_increments_on_each_stepdown() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var initial = cluster.GetMetaState(); initial.ShouldNotBeNull(); initial!.LeadershipVersion.ShouldBe(1L); cluster.StepDownMetaLeader(); var v2 = cluster.GetMetaState()!.LeadershipVersion; v2.ShouldBe(2L); cluster.StepDownMetaLeader(); var v3 = cluster.GetMetaState()!.LeadershipVersion; v3.ShouldBe(3L); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLeaderStepDown server/jetstream_cluster_1_test.go:4925 // --------------------------------------------------------------- [Fact] public async Task Placement_group_leader_changes_on_stream_stepdown() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("STEPPL", ["steppl.>"], replicas: 3); var groupBefore = cluster.GetReplicaGroup("STEPPL"); groupBefore.ShouldNotBeNull(); var leaderBefore = groupBefore!.Leader.Id; await cluster.StepDownStreamLeaderAsync("STEPPL"); var groupAfter = cluster.GetReplicaGroup("STEPPL"); groupAfter.ShouldNotBeNull(); var leaderAfter = groupAfter!.Leader.Id; leaderAfter.ShouldNotBe(leaderBefore); groupAfter.Leader.IsLeader.ShouldBeTrue(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams server/jetstream_cluster_1_test.go:299 // --------------------------------------------------------------- [Fact] public async Task Placement_node_count_consistent_with_requested_replicas() { await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("NODECNT1", ["nc1.>"], replicas: 1); await cluster.CreateStreamAsync("NODECNT2", ["nc2.>"], replicas: 2); await cluster.CreateStreamAsync("NODECNT5", ["nc5.>"], replicas: 5); cluster.GetReplicaGroup("NODECNT1")!.Nodes.Count.ShouldBe(1); cluster.GetReplicaGroup("NODECNT2")!.Nodes.Count.ShouldBe(2); cluster.GetReplicaGroup("NODECNT5")!.Nodes.Count.ShouldBe(5); } }