// Go ref: TestJetStreamClusterXxx — jetstream_cluster_4_test.go // Covers: large clusters, many-subject streams, wildcard streams, high-message-count // publishes, multi-stream mixed replica counts, create/delete/recreate cycles, // consumer on high-message streams, purge/republish, stream delete cascades, // node removal and restart lifecycle markers. using System.Text; using NATS.Server.JetStream.Api; using NATS.Server.JetStream.Cluster; using NATS.Server.JetStream.Models; namespace NATS.Server.Tests.JetStream.Cluster; /// /// Advanced JetStream cluster tests covering high-load scenarios, large clusters, /// many-subject streams, wildcard subjects, multi-stream environments, consumer /// lifecycle edge cases, purge/republish cycles, and node lifecycle markers. /// Ported from Go jetstream_cluster_4_test.go. /// public class JsClusterAdvancedTests { // --------------------------------------------------------------- // Go ref: TestJetStreamClusterLargeClusterR5 — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Large_seven_node_cluster_with_R5_stream_accepts_publishes() { // Go ref: TestJetStreamClusterLargeClusterR5 — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(7); cluster.NodeCount.ShouldBe(7); var resp = await cluster.CreateStreamAsync("R5LARGE", ["r5.>"], replicas: 5); resp.Error.ShouldBeNull(); resp.StreamInfo.ShouldNotBeNull(); resp.StreamInfo!.Config.Replicas.ShouldBe(5); for (var i = 0; i < 20; i++) { var ack = await cluster.PublishAsync("r5.event", $"msg-{i}"); ack.Stream.ShouldBe("R5LARGE"); ack.Seq.ShouldBe((ulong)(i + 1)); } var state = await cluster.GetStreamStateAsync("R5LARGE"); state.Messages.ShouldBe(20UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterStreamWithManySubjects — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Stream_with_twenty_subjects_routes_all_correctly() { // Go ref: TestJetStreamClusterStreamWithManySubjects — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); var subjects = Enumerable.Range(1, 20).Select(i => $"topic.{i}").ToArray(); var resp = await cluster.CreateStreamAsync("MANYSUBJ", subjects, replicas: 3); resp.Error.ShouldBeNull(); resp.StreamInfo!.Config.Subjects.Count.ShouldBe(20); // Publish to each subject for (var i = 1; i <= 20; i++) { var ack = await cluster.PublishAsync($"topic.{i}", $"payload-{i}"); ack.Stream.ShouldBe("MANYSUBJ"); } var state = await cluster.GetStreamStateAsync("MANYSUBJ"); state.Messages.ShouldBe(20UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterWildcardSubjectStream — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Stream_with_wildcard_gt_subject_captures_all_sub_subjects() { // Go ref: TestJetStreamClusterWildcardSubjectStream — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = await cluster.CreateStreamAsync("WILDCARD", [">"], replicas: 3); resp.Error.ShouldBeNull(); await cluster.PublishAsync("any.subject.here", "msg1"); await cluster.PublishAsync("totally.different", "msg2"); await cluster.PublishAsync("nested.deep.path.to.leaf", "msg3"); var state = await cluster.GetStreamStateAsync("WILDCARD"); state.Messages.ShouldBe(3UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterPublish1000MessagesToReplicatedStream — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Publish_1000_messages_to_R3_stream_all_acknowledged() { // Go ref: TestJetStreamClusterPublish1000MessagesToReplicatedStream — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("BIG3", ["big.>"], replicas: 3); var lastSeq = 0UL; for (var i = 0; i < 1000; i++) { var ack = await cluster.PublishAsync("big.event", $"msg-{i}"); ack.Stream.ShouldBe("BIG3"); ack.ErrorCode.ShouldBeNull(); lastSeq = ack.Seq; } lastSeq.ShouldBe(1000UL); var state = await cluster.GetStreamStateAsync("BIG3"); state.Messages.ShouldBe(1000UL); state.FirstSeq.ShouldBe(1UL); state.LastSeq.ShouldBe(1000UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterPublish1000MessagesToR1Stream — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Publish_1000_messages_to_R1_stream_all_acknowledged() { // Go ref: TestJetStreamClusterPublish1000MessagesToR1Stream — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("BIG1", ["b1.>"], replicas: 1); for (var i = 0; i < 1000; i++) { var ack = await cluster.PublishAsync("b1.event", $"msg-{i}"); ack.Stream.ShouldBe("BIG1"); ack.ErrorCode.ShouldBeNull(); } var state = await cluster.GetStreamStateAsync("BIG1"); state.Messages.ShouldBe(1000UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterStreamStateAfter1000Messages — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Stream_state_accurate_after_1000_messages() { // Go ref: TestJetStreamClusterStreamStateAfter1000Messages — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("STATE1K", ["s1k.>"], replicas: 3); for (var i = 0; i < 1000; i++) await cluster.PublishAsync("s1k.data", $"payload-{i}"); var state = await cluster.GetStreamStateAsync("STATE1K"); state.Messages.ShouldBe(1000UL); state.FirstSeq.ShouldBe(1UL); state.LastSeq.ShouldBe(1000UL); state.Bytes.ShouldBeGreaterThan(0UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterMultipleStreamsMixedReplicas — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Ten_streams_with_mixed_replica_counts_all_independent() { // Go ref: TestJetStreamClusterMultipleStreamsMixedReplicas — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var i = 0; i < 10; i++) { var replicas = (i % 3) + 1; var resp = await cluster.CreateStreamAsync($"MIX{i}", [$"mix{i}.>"], replicas: replicas); resp.Error.ShouldBeNull(); } // Publish to each stream independently for (var i = 0; i < 10; i++) { var ack = await cluster.PublishAsync($"mix{i}.event", $"stream-{i}-msg"); ack.Stream.ShouldBe($"MIX{i}"); } // Verify each stream has exactly 1 message for (var i = 0; i < 10; i++) { var state = await cluster.GetStreamStateAsync($"MIX{i}"); state.Messages.ShouldBe(1UL); } } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterCreatePublishDeleteRecreate — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Create_publish_delete_recreate_cycle_three_times() { // Go ref: TestJetStreamClusterCreatePublishDeleteRecreate — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var cycle = 0; cycle < 3; cycle++) { // Create stream var create = await cluster.CreateStreamAsync("CYCLE", ["cyc.>"], replicas: 3); create.Error.ShouldBeNull(); // Publish messages for (var i = 0; i < 5; i++) await cluster.PublishAsync("cyc.event", $"cycle-{cycle}-msg-{i}"); var state = await cluster.GetStreamStateAsync("CYCLE"); state.Messages.ShouldBe(5UL); // Delete stream var del = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}CYCLE", "{}"); del.Success.ShouldBeTrue(); } } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterConsumerOn1000MessageStream — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Consumer_on_stream_with_1000_messages_fetches_correctly() { // Go ref: TestJetStreamClusterConsumerOn1000MessageStream — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("FETCH1K", ["f1k.>"], replicas: 3); for (var i = 0; i < 1000; i++) await cluster.PublishAsync("f1k.event", $"msg-{i}"); await cluster.CreateConsumerAsync("FETCH1K", "fetcher", filterSubject: "f1k.>"); var batch = await cluster.FetchAsync("FETCH1K", "fetcher", 100); batch.Messages.Count.ShouldBe(100); batch.Messages[0].Sequence.ShouldBe(1UL); batch.Messages[99].Sequence.ShouldBe(100UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterAckAllFor1000Messages — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task AckAll_for_1000_messages_reduces_pending_to_zero() { // Go ref: TestJetStreamClusterAckAllFor1000Messages — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ACKBIG", ["ab.>"], replicas: 3); await cluster.CreateConsumerAsync("ACKBIG", "acker", filterSubject: "ab.>", ackPolicy: AckPolicy.All); for (var i = 0; i < 1000; i++) await cluster.PublishAsync("ab.event", $"msg-{i}"); var batch = await cluster.FetchAsync("ACKBIG", "acker", 1000); batch.Messages.Count.ShouldBe(1000); // AckAll up to last sequence cluster.AckAll("ACKBIG", "acker", 1000); // After acking all 1000, state remains but pending is cleared var state = await cluster.GetStreamStateAsync("ACKBIG"); state.Messages.ShouldBe(1000UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterStreamInfoConsistentAfterManyOps — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Stream_info_consistent_after_many_operations() { // Go ref: TestJetStreamClusterStreamInfoConsistentAfterManyOps — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("INFOCONSIST", ["ic.>"], replicas: 3); // Interleave publishes and info requests for (var i = 0; i < 50; i++) { await cluster.PublishAsync("ic.event", $"msg-{i}"); var info = await cluster.GetStreamInfoAsync("INFOCONSIST"); info.StreamInfo.ShouldNotBeNull(); info.StreamInfo!.State.Messages.ShouldBe((ulong)(i + 1)); } var finalInfo = await cluster.GetStreamInfoAsync("INFOCONSIST"); finalInfo.StreamInfo!.Config.Name.ShouldBe("INFOCONSIST"); finalInfo.StreamInfo.Config.Replicas.ShouldBe(3); finalInfo.StreamInfo.State.Messages.ShouldBe(50UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterMetaStateAfter10StreamOps — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Meta_state_after_creating_and_deleting_ten_streams() { // Go ref: TestJetStreamClusterMetaStateAfter10StreamOps — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var i = 0; i < 10; i++) await cluster.CreateStreamAsync($"META{i}", [$"meta{i}.>"], replicas: 3); // Delete half for (var i = 0; i < 5; i++) { var del = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}META{i}", "{}"); del.Success.ShouldBeTrue(); } var metaState = cluster.GetMetaState(); metaState.ShouldNotBeNull(); var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}"); names.StreamNames.ShouldNotBeNull(); names.StreamNames!.Count.ShouldBe(5); for (var i = 5; i < 10; i++) names.StreamNames.ShouldContain($"META{i}"); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterMultipleConsumersIndependentPending — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Five_consumers_on_same_stream_have_independent_pending() { // Go ref: TestJetStreamClusterMultipleConsumersIndependentPending — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("MULTIDUP", ["md.>"], replicas: 3); for (var i = 0; i < 10; i++) await cluster.PublishAsync("md.event", $"msg-{i}"); for (var c = 0; c < 5; c++) await cluster.CreateConsumerAsync("MULTIDUP", $"consumer{c}", filterSubject: "md.>"); // Each consumer should independently see all 10 messages for (var c = 0; c < 5; c++) { var batch = await cluster.FetchAsync("MULTIDUP", $"consumer{c}", 10); batch.Messages.Count.ShouldBe(10); batch.Messages[0].Sequence.ShouldBe(1UL); } } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterConsumerWildcardFilter — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Consumer_with_wildcard_filter_delivers_only_matching_messages() { // Go ref: TestJetStreamClusterConsumerWildcardFilter — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("WFILT", ["wf.>"], replicas: 3); await cluster.CreateConsumerAsync("WFILT", "wildcons", filterSubject: "wf.alpha.>"); await cluster.PublishAsync("wf.alpha.one", "match1"); await cluster.PublishAsync("wf.beta.two", "no-match"); await cluster.PublishAsync("wf.alpha.three", "match2"); await cluster.PublishAsync("wf.gamma.four", "no-match2"); await cluster.PublishAsync("wf.alpha.five", "match3"); var batch = await cluster.FetchAsync("WFILT", "wildcons", 10); batch.Messages.Count.ShouldBe(3); foreach (var msg in batch.Messages) msg.Subject.ShouldStartWith("wf.alpha."); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterStreamUpdateAddSubjectsAfterPublish — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Stream_update_adding_subjects_after_publishes_works() { // Go ref: TestJetStreamClusterStreamUpdateAddSubjectsAfterPublish — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ADDSUB", ["as.alpha"], replicas: 3); for (var i = 0; i < 5; i++) await cluster.PublishAsync("as.alpha", $"msg-{i}"); var state = await cluster.GetStreamStateAsync("ADDSUB"); state.Messages.ShouldBe(5UL); // Add more subjects via update var update = cluster.UpdateStream("ADDSUB", ["as.alpha", "as.beta", "as.gamma"], replicas: 3); update.Error.ShouldBeNull(); update.StreamInfo!.Config.Subjects.Count.ShouldBe(3); update.StreamInfo.Config.Subjects.ShouldContain("as.beta"); // Now publish to new subjects await cluster.PublishAsync("as.beta", "beta-msg"); await cluster.PublishAsync("as.gamma", "gamma-msg"); var finalState = await cluster.GetStreamStateAsync("ADDSUB"); finalState.Messages.ShouldBe(7UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterStreamPurgeAndRepublish — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Stream_purge_in_cluster_then_republish_works_correctly() { // Go ref: TestJetStreamClusterStreamPurgeAndRepublish — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PURGEREP", ["pr.>"], replicas: 3); for (var i = 0; i < 100; i++) await cluster.PublishAsync("pr.data", $"msg-{i}"); var before = await cluster.GetStreamStateAsync("PURGEREP"); before.Messages.ShouldBe(100UL); // Purge var purge = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}PURGEREP", "{}"); purge.Success.ShouldBeTrue(); var afterPurge = await cluster.GetStreamStateAsync("PURGEREP"); afterPurge.Messages.ShouldBe(0UL); // Re-publish for (var i = 0; i < 50; i++) { var ack = await cluster.PublishAsync("pr.data", $"new-msg-{i}"); ack.ErrorCode.ShouldBeNull(); } var final = await cluster.GetStreamStateAsync("PURGEREP"); final.Messages.ShouldBe(50UL); // Sequences restart after purge final.FirstSeq.ShouldBeGreaterThan(0UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterFetchEmptyAfterPurge — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Fetch_empty_after_stream_purge() { // Go ref: TestJetStreamClusterFetchEmptyAfterPurge — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PURGEDRAIN", ["pd.>"], replicas: 3); await cluster.CreateConsumerAsync("PURGEDRAIN", "reader", filterSubject: "pd.>"); for (var i = 0; i < 20; i++) await cluster.PublishAsync("pd.event", $"msg-{i}"); // Fetch to advance the consumer var pre = await cluster.FetchAsync("PURGEDRAIN", "reader", 20); pre.Messages.Count.ShouldBe(20); // Purge the stream (await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}PURGEDRAIN", "{}")).Success.ShouldBeTrue(); // Fetch should now return empty var post = await cluster.FetchAsync("PURGEDRAIN", "reader", 20); post.Messages.Count.ShouldBe(0); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterStreamDeleteCascadesConsumers — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Stream_delete_cascades_consumer_removal() { // Go ref: TestJetStreamClusterStreamDeleteCascadesConsumers — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CASCADE", ["cas.>"], replicas: 3); await cluster.CreateConsumerAsync("CASCADE", "c1"); await cluster.CreateConsumerAsync("CASCADE", "c2"); await cluster.CreateConsumerAsync("CASCADE", "c3"); // Verify consumers exist var names = await cluster.RequestAsync($"{JetStreamApiSubjects.ConsumerNames}CASCADE", "{}"); names.ConsumerNames!.Count.ShouldBe(3); // Delete the stream (await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}CASCADE", "{}")).Success.ShouldBeTrue(); // Stream no longer exists var info = await cluster.GetStreamInfoAsync("CASCADE"); info.Error.ShouldNotBeNull(); info.Error!.Code.ShouldBe(404); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterNodeRemovalPreservesDataReads — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Node_removal_does_not_affect_stream_data_reads() { // Go ref: TestJetStreamClusterNodeRemovalPreservesDataReads — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("NODEREM", ["nr.>"], replicas: 3); for (var i = 0; i < 30; i++) await cluster.PublishAsync("nr.event", $"msg-{i}"); var before = await cluster.GetStreamStateAsync("NODEREM"); before.Messages.ShouldBe(30UL); // Simulate removing a node cluster.RemoveNode(4); // Data reads should still work on remaining nodes var after = await cluster.GetStreamStateAsync("NODEREM"); after.Messages.ShouldBe(30UL); after.LastSeq.ShouldBe(30UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterNodeRestartPreservesLifecycleMarkers — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Node_restart_records_lifecycle_markers_correctly() { // Go ref: TestJetStreamClusterNodeRestartPreservesLifecycleMarkers — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("RESTART", ["rs.>"], replicas: 3); for (var i = 0; i < 10; i++) await cluster.PublishAsync("rs.event", $"msg-{i}"); // Simulate node removal cluster.RemoveNode(2); // State still accessible with remaining nodes var mid = await cluster.GetStreamStateAsync("RESTART"); mid.Messages.ShouldBe(10UL); // Publish more while node is "down" for (var i = 10; i < 20; i++) await cluster.PublishAsync("rs.event", $"msg-{i}"); // Simulate node restart cluster.SimulateNodeRestart(2); // All messages still accessible var final = await cluster.GetStreamStateAsync("RESTART"); final.Messages.ShouldBe(20UL); final.LastSeq.ShouldBe(20UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterLeaderStepdownDuringPublish — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Leader_stepdown_during_publish_sequence_is_monotonic() { // Go ref: TestJetStreamClusterLeaderStepdownDuringPublish — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("SEQSTEP", ["seq.>"], replicas: 3); var seqs = new List(); for (var i = 0; i < 10; i++) { var ack = await cluster.PublishAsync("seq.event", $"msg-{i}"); seqs.Add(ack.Seq); } // Step down leader (await cluster.StepDownStreamLeaderAsync("SEQSTEP")).Success.ShouldBeTrue(); for (var i = 10; i < 20; i++) { var ack = await cluster.PublishAsync("seq.event", $"msg-{i}"); seqs.Add(ack.Seq); } // All sequences must be strictly increasing for (var i = 1; i < seqs.Count; i++) seqs[i].ShouldBeGreaterThan(seqs[i - 1]); seqs[^1].ShouldBe(20UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterStreamInfoAfterStepdown — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Stream_info_accurate_after_leader_stepdown_with_many_messages() { // Go ref: TestJetStreamClusterStreamInfoAfterStepdown — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("INFOSD1K", ["isd.>"], replicas: 3); for (var i = 0; i < 500; i++) await cluster.PublishAsync("isd.event", $"msg-{i}"); (await cluster.StepDownStreamLeaderAsync("INFOSD1K")).Success.ShouldBeTrue(); for (var i = 500; i < 1000; i++) await cluster.PublishAsync("isd.event", $"msg-{i}"); var info = await cluster.GetStreamInfoAsync("INFOSD1K"); info.StreamInfo.ShouldNotBeNull(); info.StreamInfo!.State.Messages.ShouldBe(1000UL); info.StreamInfo.State.FirstSeq.ShouldBe(1UL); info.StreamInfo.State.LastSeq.ShouldBe(1000UL); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterStreamReplicaGroupHasCorrectNodes — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Replica_group_for_stream_has_correct_node_count() { // Go ref: TestJetStreamClusterStreamReplicaGroupHasCorrectNodes — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("GRPCHECK", ["gc.>"], replicas: 3); var group = cluster.GetReplicaGroup("GRPCHECK"); group.ShouldNotBeNull(); group!.Nodes.Count.ShouldBe(3); group.Leader.ShouldNotBeNull(); group.Leader.IsLeader.ShouldBeTrue(); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterConsumerLeaderAfterStreamStepdown — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Consumer_leader_remains_valid_after_stream_stepdown() { // Go ref: TestJetStreamClusterConsumerLeaderAfterStreamStepdown — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CONSLEADER", ["cl.>"], replicas: 3); await cluster.CreateConsumerAsync("CONSLEADER", "durable1"); var leaderBefore = cluster.GetConsumerLeaderId("CONSLEADER", "durable1"); leaderBefore.ShouldNotBeNullOrWhiteSpace(); (await cluster.StepDownStreamLeaderAsync("CONSLEADER")).Success.ShouldBeTrue(); var leaderAfter = cluster.GetConsumerLeaderId("CONSLEADER", "durable1"); leaderAfter.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterWaitOnStreamLeaderAfterCreation — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task WaitOnStreamLeader_resolves_immediately_for_existing_stream() { // Go ref: TestJetStreamClusterWaitOnStreamLeaderAfterCreation — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("WLEADER", ["wl.>"], replicas: 3); // Should complete immediately, no timeout await cluster.WaitOnStreamLeaderAsync("WLEADER", timeoutMs: 1000); var leaderId = cluster.GetStreamLeaderId("WLEADER"); leaderId.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterConsumerWaitOnLeaderAfterCreation — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task WaitOnConsumerLeader_resolves_for_existing_consumer() { // Go ref: TestJetStreamClusterConsumerWaitOnLeaderAfterCreation — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("WCLEADER2", ["wcl2.>"], replicas: 3); await cluster.CreateConsumerAsync("WCLEADER2", "dur-wc"); await cluster.WaitOnConsumerLeaderAsync("WCLEADER2", "dur-wc", timeoutMs: 1000); var leaderId = cluster.GetConsumerLeaderId("WCLEADER2", "dur-wc"); leaderId.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go ref: TestJetStreamClusterAccountInfoAfterBatchDelete — jetstream_cluster_4_test.go // --------------------------------------------------------------- [Fact] public async Task Account_info_reflects_accurate_stream_count_after_batch_delete() { // Go ref: TestJetStreamClusterAccountInfoAfterBatchDelete — jetstream_cluster_4_test.go await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var i = 0; i < 8; i++) await cluster.CreateStreamAsync($"BATCH{i}", [$"batch{i}.>"], replicas: 3); var pre = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); pre.AccountInfo!.Streams.ShouldBe(8); // Delete 3 streams for (var i = 0; i < 3; i++) (await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}BATCH{i}", "{}")).Success.ShouldBeTrue(); var post = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); post.AccountInfo!.Streams.ShouldBe(5); } }