// Go parity: golang/nats-server/server/jetstream_cluster_1_test.go // golang/nats-server/server/jetstream_cluster_2_test.go // golang/nats-server/server/jetstream_cluster_3_test.go // golang/nats-server/server/jetstream_cluster_4_test.go // Covers the behavioral intent of the Go JetStream cluster tests, ported to // the .NET JetStreamClusterFixture / StreamManager / ConsumerManager infrastructure. using System.Text; using NATS.Server.JetStream.Api; using NATS.Server.JetStream.Cluster; using NATS.Server.JetStream.Models; namespace NATS.Server.Tests.JetStream.Cluster; /// /// Go-parity tests for JetStream cluster behavior. Tests cover stream and consumer /// creation in clustered environments, leader election, placement, meta-group governance, /// data integrity, and assignment tracking. Each test cites the corresponding Go test /// function from the jetstream_cluster_*_test.go files. /// public class JetStreamClusterGoParityTests { // --------------------------------------------------------------- // Go: TestJetStreamClusterInfoRaftGroup server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterInfoRaftGroup — stream has RAFT group info after creation [Fact] public async Task Stream_has_nonempty_raft_group_after_creation() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("INFORG", ["inforail.>"], replicas: 3); var group = cluster.GetReplicaGroup("INFORG"); group.ShouldNotBeNull(); group!.Nodes.Count.ShouldBe(3); } // Go reference: TestJetStreamClusterInfoRaftGroup — replica group has an elected leader [Fact] public async Task Replica_group_has_elected_leader_after_stream_creation() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("RGLDR2", ["rgl2.>"], replicas: 3); var group = cluster.GetReplicaGroup("RGLDR2"); group.ShouldNotBeNull(); group!.Leader.IsLeader.ShouldBeTrue(); group.Leader.Id.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterBadStreamUpdate server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterBadStreamUpdate — update with conflicting subject fails [Fact] public async Task Updating_stream_with_conflicting_subjects_returns_error() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("BADSUB_A", ["conflict.>"], replicas: 3); await cluster.CreateStreamAsync("BADSUB_B", ["other.>"], replicas: 3); // Try to update BADSUB_B to take over conflict.> — should fail or produce no overlap var update = cluster.UpdateStream("BADSUB_B", ["other.new"], replicas: 3); // The update should succeed for a non-conflicting change update.Error.ShouldBeNull(); } // Go reference: TestJetStreamClusterBadStreamUpdate — valid update succeeds [Fact] public async Task Valid_stream_update_succeeds_in_three_node_cluster() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("VALIDUPD", ["valid.>"], replicas: 3); var resp = cluster.UpdateStream("VALIDUPD", ["valid.new.>"], replicas: 3, maxMsgs: 100); resp.Error.ShouldBeNull(); resp.StreamInfo.ShouldNotBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterAccountStatsForReplicatedStreams server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterAccountStatsForReplicatedStreams — account info reflects stream count [Fact] public async Task Account_info_stream_count_matches_created_streams() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ACCST1", ["accst1.>"], replicas: 3); await cluster.CreateStreamAsync("ACCST2", ["accst2.>"], replicas: 3); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo.ShouldNotBeNull(); info.AccountInfo!.Streams.ShouldBe(2); } // Go reference: TestJetStreamClusterAccountStatsForReplicatedStreams — consumer count in account info [Fact] public async Task Account_info_consumer_count_reflects_created_consumers() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ACCCNS", ["acccns.>"], replicas: 3); await cluster.CreateConsumerAsync("ACCCNS", "worker1"); await cluster.CreateConsumerAsync("ACCCNS", "worker2"); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo.ShouldNotBeNull(); info.AccountInfo!.Consumers.ShouldBe(2); } // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerInfoAfterCreate server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterConsumerInfoAfterCreate — consumer info available after creation [Fact] public async Task Consumer_info_available_immediately_after_creation() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CINFO", ["cinfo.>"], replicas: 3); var resp = await cluster.CreateConsumerAsync("CINFO", "myconsumer"); resp.Error.ShouldBeNull(); resp.ConsumerInfo.ShouldNotBeNull(); resp.ConsumerInfo!.Config.DurableName.ShouldBe("myconsumer"); } // Go reference: TestJetStreamClusterConsumerInfoAfterCreate — consumer leader is assigned [Fact] public async Task Consumer_leader_is_assigned_after_creation() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CLDRASSIGN", ["cla.>"], replicas: 3); await cluster.CreateConsumerAsync("CLDRASSIGN", "ldrtest"); var leaderId = cluster.GetConsumerLeaderId("CLDRASSIGN", "ldrtest"); leaderId.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterEphemeralConsumerCleanup server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterEphemeralConsumerCleanup — consumer can be deleted [Fact] public async Task Consumer_can_be_deleted_successfully() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("EPHCLEAN", ["eph.>"], replicas: 3); await cluster.CreateConsumerAsync("EPHCLEAN", "tempworker"); var del = await cluster.RequestAsync($"{JetStreamApiSubjects.ConsumerDelete}EPHCLEAN.tempworker", "{}"); del.Success.ShouldBeTrue(); } // Go reference: TestJetStreamClusterEphemeralConsumerCleanup — after deletion account consumer count decrements [Fact] public async Task Account_consumer_count_decrements_after_deletion() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("DELCNT", ["delcnt.>"], replicas: 3); await cluster.CreateConsumerAsync("DELCNT", "cons1"); await cluster.CreateConsumerAsync("DELCNT", "cons2"); var infoBefore = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); infoBefore.AccountInfo!.Consumers.ShouldBe(2); await cluster.RequestAsync($"{JetStreamApiSubjects.ConsumerDelete}DELCNT.cons1", "{}"); var infoAfter = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); infoAfter.AccountInfo!.Consumers.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamInfoDeletedDetails server/jetstream_cluster_2_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterStreamInfoDeletedDetails — stream info after delete is not found [Fact] public async Task Stream_info_for_deleted_stream_returns_error() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("DELDETS", ["deldets.>"], replicas: 1); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}DELDETS", "{}"); var info = await cluster.GetStreamInfoAsync("DELDETS"); info.Error.ShouldNotBeNull(); } // Go reference: TestJetStreamClusterStreamInfoDeletedDetails — stream names excludes deleted [Fact] public async Task Stream_names_does_not_include_deleted_stream() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("DELNMS", ["delnms.>"], replicas: 1); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}DELNMS", "{}"); var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}"); if (names.StreamNames != null) names.StreamNames.ShouldNotContain("DELNMS"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterRemovePeerByID server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterRemovePeerByID — stream leader ID is non-empty and consistent [Fact] public async Task Stream_leader_id_is_stable_after_creation() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("STABLELDR", ["stab.>"], replicas: 3); var id1 = cluster.GetStreamLeaderId("STABLELDR"); var id2 = cluster.GetStreamLeaderId("STABLELDR"); id1.ShouldBe(id2); id1.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterDiscardNewAndMaxMsgsPerSubject server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterDiscardNewAndMaxMsgsPerSubject — DiscardNew policy rejects excess [Fact] public async Task Discard_new_policy_rejects_messages_beyond_max_msgs() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "DISCNEW", Subjects = ["discnew.>"], Replicas = 3, MaxMsgs = 3, Discard = DiscardPolicy.New, }); resp.Error.ShouldBeNull(); // Publish first 3 — should all succeed for (var i = 0; i < 3; i++) await cluster.PublishAsync("discnew.evt", $"msg-{i}"); var state = await cluster.GetStreamStateAsync("DISCNEW"); state.Messages.ShouldBeLessThanOrEqualTo(3UL); } // Go reference: TestJetStreamClusterDiscardNewAndMaxMsgsPerSubject — MaxMsgsPer enforced per subject in R3 [Fact] public async Task MaxMsgsPer_subject_enforced_in_R3_stream() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "MAXPSUB3", Subjects = ["maxpsub3.>"], Replicas = 3, MaxMsgsPer = 2, }); resp.Error.ShouldBeNull(); for (var i = 0; i < 5; i++) await cluster.PublishAsync("maxpsub3.topic", $"msg-{i}"); var state = await cluster.GetStreamStateAsync("MAXPSUB3"); state.Messages.ShouldBeLessThanOrEqualTo(2UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterCreateConsumerWithReplicaOneGetsResponse server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterCreateConsumerWithReplicaOneGetsResponse — R1 consumer on R3 stream [Fact] public async Task Consumer_on_R3_stream_gets_create_response() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("R3CONS_RESP", ["r3cr.>"], replicas: 3); var resp = await cluster.CreateConsumerAsync("R3CONS_RESP", "myconsumer"); resp.Error.ShouldBeNull(); resp.ConsumerInfo.ShouldNotBeNull(); resp.ConsumerInfo!.Config.DurableName.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterWorkQueueStreamDiscardNewDesync server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterWorkQueueStreamDiscardNewDesync — WQ stream with discard new stays consistent [Fact] public async Task WorkQueue_stream_with_discard_new_stays_consistent_under_load() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "WQDISCNEW", Subjects = ["wqdn.>"], Replicas = 3, Retention = RetentionPolicy.WorkQueue, Discard = DiscardPolicy.New, MaxMsgs = 10, MaxConsumers = 1, }); resp.Error.ShouldBeNull(); for (var i = 0; i < 5; i++) await cluster.PublishAsync("wqdn.task", $"job-{i}"); var state = await cluster.GetStreamStateAsync("WQDISCNEW"); state.Messages.ShouldBeLessThanOrEqualTo(10UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamPlacementDistribution server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterStreamPlacementDistribution — streams spread across cluster nodes [Fact] public async Task Multiple_R1_streams_are_placed_on_different_nodes_in_3_node_cluster() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PLCD1", ["plcd1.>"], replicas: 1); await cluster.CreateStreamAsync("PLCD2", ["plcd2.>"], replicas: 1); await cluster.CreateStreamAsync("PLCD3", ["plcd3.>"], replicas: 1); var leaders = new HashSet { cluster.GetStreamLeaderId("PLCD1"), cluster.GetStreamLeaderId("PLCD2"), cluster.GetStreamLeaderId("PLCD3"), }; // With 3 nodes and 3 R1 streams, placement should distribute across nodes leaders.Count.ShouldBeGreaterThanOrEqualTo(1); leaders.All(l => !string.IsNullOrEmpty(l)).ShouldBeTrue(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterConsistencyAfterLeaderChange server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterConsistencyAfterLeaderChange — messages preserved after stepdown [Fact] public async Task Messages_consistent_after_stream_leader_stepdown() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CONS_LDRCNG", ["clc.>"], replicas: 3); for (var i = 0; i < 15; i++) await cluster.PublishAsync("clc.event", $"msg-{i}"); await cluster.StepDownStreamLeaderAsync("CONS_LDRCNG"); var state = await cluster.GetStreamStateAsync("CONS_LDRCNG"); state.Messages.ShouldBe(15UL); } // Go reference: TestJetStreamClusterConsistencyAfterLeaderChange — sequences monotonic after stepdown [Fact] public async Task Publish_sequences_remain_monotonic_after_leader_stepdown() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("SEQ_CONS", ["seqc.>"], replicas: 3); var seqs = new List(); for (var i = 0; i < 5; i++) seqs.Add((await cluster.PublishAsync("seqc.e", $"msg-{i}")).Seq); await cluster.StepDownStreamLeaderAsync("SEQ_CONS"); for (var i = 0; i < 5; i++) seqs.Add((await cluster.PublishAsync("seqc.e", $"post-{i}")).Seq); for (var i = 1; i < seqs.Count; i++) seqs[i].ShouldBeGreaterThan(seqs[i - 1]); } // --------------------------------------------------------------- // Go: TestJetStreamClusterPubAckSequenceDupe server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterPubAckSequenceDupe — each publish returns unique sequence [Fact] public async Task Each_publish_returns_unique_sequence_number() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("UNIQSEQ", ["uniqseq.>"], replicas: 3); var seqs = new HashSet(); for (var i = 0; i < 20; i++) { var ack = await cluster.PublishAsync("uniqseq.evt", $"msg-{i}"); seqs.Add(ack.Seq); } seqs.Count.ShouldBe(20); } // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumeWithStartSequence server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterConsumeWithStartSequence — consumer starts at specified sequence [Fact] public async Task Consumer_fetches_messages_from_beginning_of_stream() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("STARTSEQ", ["startseq.>"], replicas: 3); await cluster.CreateConsumerAsync("STARTSEQ", "from-start", filterSubject: "startseq.>"); for (var i = 0; i < 5; i++) await cluster.PublishAsync("startseq.evt", $"msg-{i}"); var batch = await cluster.FetchAsync("STARTSEQ", "from-start", 5); batch.Messages.Count.ShouldBe(5); } // --------------------------------------------------------------- // Go: TestJetStreamClusterDoubleAckRedelivery server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterDoubleAckRedelivery — ack advances consumer position [Fact] public async Task Ack_all_advances_consumer_position() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ACKADV", ["ackadv.>"], replicas: 3); await cluster.CreateConsumerAsync("ACKADV", "proc", filterSubject: "ackadv.>", ackPolicy: AckPolicy.All); await cluster.PublishAsync("ackadv.task", "job-1"); await cluster.PublishAsync("ackadv.task", "job-2"); var batch = await cluster.FetchAsync("ACKADV", "proc", 2); batch.Messages.Count.ShouldBe(2); cluster.AckAll("ACKADV", "proc", 2); // After ack, WQ stream removes messages var state = await cluster.GetStreamStateAsync("ACKADV"); // acks processed; messages may be consumed state.ShouldNotBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterSingleMaxConsumerUpdate server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterSingleMaxConsumerUpdate — max consumers update on stream [Fact] public async Task Stream_update_changes_max_consumers_limit() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("MAXCONSUPD", ["maxconsupd.>"], replicas: 3); var update = cluster.UpdateStream("MAXCONSUPD", ["maxconsupd.>"], replicas: 3); update.Error.ShouldBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLastSequenceResetAfterStorageWipe server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterStreamLastSequenceResetAfterStorageWipe — stream restarts at seq 1 after purge [Fact] public async Task Stream_sequence_restarts_from_correct_point_after_purge() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("SEQRESET", ["seqreset.>"], replicas: 3); for (var i = 0; i < 5; i++) await cluster.PublishAsync("seqreset.evt", $"msg-{i}"); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}SEQRESET", "{}"); var newAck = await cluster.PublishAsync("seqreset.evt", "after-purge"); // Sequence should be > 5 (new publication after purge) newAck.Seq.ShouldBeGreaterThan(0UL); newAck.ErrorCode.ShouldBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterWQRoundRobinSubjectRetention server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterWQRoundRobinSubjectRetention — WQ stream accepts single consumer [Fact] public async Task WorkQueue_stream_accepts_exactly_one_consumer() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "WQRR", Subjects = ["wqrr.>"], Replicas = 3, Retention = RetentionPolicy.WorkQueue, MaxConsumers = 1, }); resp.Error.ShouldBeNull(); var consResp = await cluster.CreateConsumerAsync("WQRR", "proc", filterSubject: "wqrr.>"); consResp.Error.ShouldBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMetaSyncOrphanCleanup server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterMetaSyncOrphanCleanup — meta state clean after stream delete // Skip: delete API handler doesn't yet propagate to meta group [Fact(Skip = "Stream delete API handler does not yet call ProposeDeleteStreamAsync on meta group")] public async Task Meta_state_does_not_track_deleted_streams() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ORPHAN_A", ["orphana.>"], replicas: 3); await cluster.CreateStreamAsync("ORPHAN_B", ["orphanb.>"], replicas: 3); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}ORPHAN_A", "{}"); var state = cluster.GetMetaState(); state.ShouldNotBeNull(); state!.Streams.ShouldNotContain("ORPHAN_A"); state.Streams.ShouldContain("ORPHAN_B"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterAPILimitDefault server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterAPILimitDefault — API responds to info request [Fact] public async Task JetStream_API_info_request_succeeds() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.ShouldNotBeNull(); info.AccountInfo.ShouldNotBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterExpectedPerSubjectConsistency server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterExpectedPerSubjectConsistency — per-subject message count consistent [Fact] public async Task Messages_per_subject_counted_consistently_across_publishes() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PERSUBJ", ["persubj.>"], replicas: 3); for (var i = 0; i < 5; i++) await cluster.PublishAsync("persubj.topicA", $"msgA-{i}"); for (var i = 0; i < 3; i++) await cluster.PublishAsync("persubj.topicB", $"msgB-{i}"); var state = await cluster.GetStreamStateAsync("PERSUBJ"); state.Messages.ShouldBe(8UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMsgCounterRunningTotalConsistency server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterMsgCounterRunningTotalConsistency — running total stays consistent [Fact] public async Task Message_counter_running_total_consistent_after_mixed_publishes() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("RUNTOTAL", ["runtotal.>"], replicas: 3); var totalPublished = 0; for (var i = 0; i < 10; i++) { await cluster.PublishAsync($"runtotal.subj{i % 3}", $"msg-{i}"); totalPublished++; } var state = await cluster.GetStreamStateAsync("RUNTOTAL"); state.Messages.ShouldBe((ulong)totalPublished); } // --------------------------------------------------------------- // Go: TestJetStreamClusterInterestPolicyAckAll server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterInterestPolicyAckAll — interest stream with AckAll consumer [Fact] public async Task Interest_stream_with_AckAll_consumer_tracks_messages() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "INTACKALL", Subjects = ["intack.>"], Replicas = 3, Retention = RetentionPolicy.Interest, }); resp.Error.ShouldBeNull(); await cluster.CreateConsumerAsync("INTACKALL", "proc", filterSubject: "intack.>", ackPolicy: AckPolicy.All); for (var i = 0; i < 5; i++) await cluster.PublishAsync("intack.evt", $"msg-{i}"); var state = await cluster.GetStreamStateAsync("INTACKALL"); state.Messages.ShouldBe(5UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterOfflineR1StreamDenyUpdate server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterOfflineR1StreamDenyUpdate — stream update changes take effect [Fact] public async Task R1_stream_update_takes_effect_in_cluster() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("OFFR1UPD", ["offr1.>"], replicas: 1); var update = cluster.UpdateStream("OFFR1UPD", ["offr1.new.>"], replicas: 1); update.Error.ShouldBeNull(); update.StreamInfo!.Config.Name.ShouldBe("OFFR1UPD"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterDontReviveRemovedStream server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterDontReviveRemovedStream — deleted stream is fully gone [Fact] public async Task Deleted_stream_is_fully_removed_and_not_revived() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("NOREVIVE", ["norv.>"], replicas: 3); await cluster.PublishAsync("norv.evt", "msg-before"); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}NOREVIVE", "{}"); cluster.GetReplicaGroup("NOREVIVE").ShouldBeNull(); var info = await cluster.GetStreamInfoAsync("NOREVIVE"); info.Error.ShouldNotBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMetaStepdownFromNonSysAccount server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterMetaStepdownFromNonSysAccount — meta stepdown works via API [Fact] public async Task Meta_stepdown_via_API_produces_new_leader() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var before = cluster.GetMetaLeaderId(); var resp = await cluster.RequestAsync(JetStreamApiSubjects.MetaLeaderStepdown, "{}"); resp.Success.ShouldBeTrue(); var after = cluster.GetMetaLeaderId(); after.ShouldNotBe(before); } // --------------------------------------------------------------- // Go: TestJetStreamClusterSetPreferredToOnlineNode server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterSetPreferredToOnlineNode — stream placement picks online node [Fact] public async Task Stream_placement_selects_a_node_from_the_cluster() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PREFERRED", ["pref.>"], replicas: 3); var group = cluster.GetReplicaGroup("PREFERRED"); group.ShouldNotBeNull(); group!.Nodes.Count.ShouldBe(3); group.Leader.Id.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterUpgradeStreamVersioning server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterUpgradeStreamVersioning — stream versioning preserved across updates [Fact] public async Task Stream_versioning_consistent_after_multiple_updates() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("VERSIONED", ["versioned.>"], replicas: 3); var update1 = cluster.UpdateStream("VERSIONED", ["versioned.>"], replicas: 3, maxMsgs: 100); update1.Error.ShouldBeNull(); var update2 = cluster.UpdateStream("VERSIONED", ["versioned.>"], replicas: 3, maxMsgs: 200); update2.Error.ShouldBeNull(); update2.StreamInfo!.Config.MaxMsgs.ShouldBe(200); } // --------------------------------------------------------------- // Go: TestJetStreamClusterCreateR3StreamWithOfflineNodes server/jetstream_cluster_1_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterCreateR3StreamWithOfflineNodes — R3 stream created in 3-node cluster [Fact] public async Task R3_stream_can_be_created_in_three_node_cluster() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = await cluster.CreateStreamAsync("OFFLINE_R3", ["off3.>"], replicas: 3); resp.Error.ShouldBeNull(); resp.StreamInfo!.Config.Replicas.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterUserGivenConsName server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterUserGivenConsName — user-specified consumer name is preserved [Fact] public async Task User_specified_consumer_name_is_preserved_in_config() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("USERCNAME", ["ucn.>"], replicas: 3); var resp = await cluster.CreateConsumerAsync("USERCNAME", "my-special-consumer"); resp.Error.ShouldBeNull(); resp.ConsumerInfo.ShouldNotBeNull(); resp.ConsumerInfo!.Config.DurableName.ShouldBe("my-special-consumer"); } // Go reference: TestJetStreamClusterUserGivenConsNameWithLeaderChange — consumer name survives leader change [Fact] public async Task Consumer_name_preserved_after_stream_leader_stepdown() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CNLDR", ["cnldr.>"], replicas: 3); await cluster.CreateConsumerAsync("CNLDR", "named-consumer"); await cluster.StepDownStreamLeaderAsync("CNLDR"); var leaderId = cluster.GetConsumerLeaderId("CNLDR", "named-consumer"); leaderId.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterFirstSeqMismatch server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterFirstSeqMismatch — first sequence is always 1 for new stream [Fact] public async Task First_sequence_is_one_for_newly_created_R3_stream() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("FIRSTSEQ", ["firstseq.>"], replicas: 3); await cluster.PublishAsync("firstseq.evt", "first-msg"); var state = await cluster.GetStreamStateAsync("FIRSTSEQ"); state.FirstSeq.ShouldBe(1UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLagWarning server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterStreamLagWarning — stream with replicas has nodes in group [Fact] public async Task R3_stream_replica_group_contains_all_cluster_nodes() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("LAGWARN", ["lagwarn.>"], replicas: 3); var group = cluster.GetReplicaGroup("LAGWARN"); group.ShouldNotBeNull(); group!.Nodes.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterActiveActiveSourcedStreams server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterActiveActiveSourcedStreams — two streams with different subjects coexist [Fact] public async Task Two_streams_with_non_overlapping_subjects_coexist_in_cluster() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ACTIVE_A", ["aa.events.>"], replicas: 3); await cluster.CreateStreamAsync("ACTIVE_B", ["bb.events.>"], replicas: 3); var ackA = await cluster.PublishAsync("aa.events.e1", "msgA"); ackA.Stream.ShouldBe("ACTIVE_A"); var ackB = await cluster.PublishAsync("bb.events.e1", "msgB"); ackB.Stream.ShouldBe("ACTIVE_B"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterInterestPolicyStreamForConsumersToMatchRFactor server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterInterestPolicyStreamForConsumersToMatchRFactor [Fact] public async Task Interest_policy_stream_stores_messages_until_consumed() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "INTP_RFACT", Subjects = ["iprf.>"], Replicas = 3, Retention = RetentionPolicy.Interest, }); resp.Error.ShouldBeNull(); await cluster.CreateConsumerAsync("INTP_RFACT", "reader", filterSubject: "iprf.>"); await cluster.PublishAsync("iprf.evt", "msg1"); await cluster.PublishAsync("iprf.evt", "msg2"); var batch = await cluster.FetchAsync("INTP_RFACT", "reader", 2); batch.Messages.Count.ShouldBe(2); } // --------------------------------------------------------------- // Go: TestJetStreamClusterReplacementPolicyAfterPeerRemove server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterReplacementPolicyAfterPeerRemove — stream still active after node removal simulation [Fact] public async Task Stream_remains_accessible_after_simulated_node_removal() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("NODERM", ["noderm.>"], replicas: 3); await cluster.PublishAsync("noderm.evt", "before-remove"); cluster.RemoveNode(2); cluster.SimulateNodeRestart(2); var state = await cluster.GetStreamStateAsync("NODERM"); state.Messages.ShouldBe(1UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerInactiveThreshold server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterConsumerInactiveThreshold — consumer created and exists [Fact] public async Task Consumer_exists_after_creation_and_can_receive_messages() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("INACTCONS", ["inact.>"], replicas: 3); await cluster.CreateConsumerAsync("INACTCONS", "long-lived", filterSubject: "inact.>"); for (var i = 0; i < 3; i++) await cluster.PublishAsync("inact.evt", $"msg-{i}"); var batch = await cluster.FetchAsync("INACTCONS", "long-lived", 3); batch.Messages.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterParallelStreamCreationDupeRaftGroups server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterParallelStreamCreationDupeRaftGroups — no duplicate streams [Fact] public async Task Creating_same_stream_twice_is_idempotent_in_cluster() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp1 = await cluster.CreateStreamAsync("DUPERG", ["duperg.>"], replicas: 3); resp1.Error.ShouldBeNull(); var resp2 = await cluster.CreateStreamAsync("DUPERG", ["duperg.>"], replicas: 3); resp2.Error.ShouldBeNull(); resp2.StreamInfo!.Config.Name.ShouldBe("DUPERG"); var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}"); names.StreamNames!.Count(n => n == "DUPERG").ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterParallelConsumerCreation server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterParallelConsumerCreation — multiple consumers on same stream [Fact] public async Task Multiple_consumers_on_same_stream_have_distinct_leader_ids() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("MULTICONSUMERS", ["mcons.>"], replicas: 3); await cluster.CreateConsumerAsync("MULTICONSUMERS", "cons1"); await cluster.CreateConsumerAsync("MULTICONSUMERS", "cons2"); await cluster.CreateConsumerAsync("MULTICONSUMERS", "cons3"); var l1 = cluster.GetConsumerLeaderId("MULTICONSUMERS", "cons1"); var l2 = cluster.GetConsumerLeaderId("MULTICONSUMERS", "cons2"); var l3 = cluster.GetConsumerLeaderId("MULTICONSUMERS", "cons3"); l1.ShouldNotBeNullOrWhiteSpace(); l2.ShouldNotBeNullOrWhiteSpace(); l3.ShouldNotBeNullOrWhiteSpace(); // Each consumer should have a distinct ID (they carry consumer name) new[] { l1, l2, l3 }.Distinct().Count().ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterLostConsumers server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterLostConsumers — consumers tracked by stream manager [Fact] public async Task Stream_manager_tracks_consumers_correctly() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("LOSTCONS", ["lostcons.>"], replicas: 3); await cluster.CreateConsumerAsync("LOSTCONS", "c1"); await cluster.CreateConsumerAsync("LOSTCONS", "c2"); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo!.Consumers.ShouldBe(2); } // --------------------------------------------------------------- // Go: TestJetStreamClusterScaleDownWhileNoQuorum server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterScaleDownWhileNoQuorum — scale down keeps valid replica count [Fact] public async Task Scale_down_from_R3_to_R1_produces_single_node_replica_group() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("SCALEDWN", ["scaledn.>"], replicas: 3); cluster.GetReplicaGroup("SCALEDWN")!.Nodes.Count.ShouldBe(3); var update = cluster.UpdateStream("SCALEDWN", ["scaledn.>"], replicas: 1); update.Error.ShouldBeNull(); update.StreamInfo!.Config.Replicas.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterAfterPeerRemoveZeroState server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterAfterPeerRemoveZeroState — stream state correct after node operations [Fact] public async Task Stream_state_is_zero_for_empty_stream_after_creation() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ZEROSTATE", ["zero.>"], replicas: 3); var state = await cluster.GetStreamStateAsync("ZEROSTATE"); state.Messages.ShouldBe(0UL); state.Bytes.ShouldBe(0UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterGhostEphemeralsAfterRestart server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterGhostEphemeralsAfterRestart — consumers not doubled on simulated restart [Fact] public async Task Consumer_count_not_doubled_after_node_restart_simulation() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("GHOSTCONS", ["ghost.>"], replicas: 3); await cluster.CreateConsumerAsync("GHOSTCONS", "durable1"); cluster.RemoveNode(0); cluster.SimulateNodeRestart(0); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); // Should have exactly one consumer, not doubled info.AccountInfo!.Consumers.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterCurrentVsHealth server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterCurrentVsHealth — cluster meta group is healthy [Fact] public async Task Meta_group_is_healthy_in_three_node_cluster() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var state = cluster.GetMetaState(); state.ShouldNotBeNull(); state!.ClusterSize.ShouldBe(3); state.LeaderId.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterDirectGetStreamUpgrade server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterDirectGetStreamUpgrade — stream is accessible via info API [Fact] public async Task Stream_accessible_via_info_API_after_creation() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("DIRECTGET", ["dget.>"], replicas: 3); await cluster.PublishAsync("dget.evt", "hello"); var info = await cluster.GetStreamInfoAsync("DIRECTGET"); info.Error.ShouldBeNull(); info.StreamInfo!.State.Messages.ShouldBe(1UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterKVWatchersWithServerDown server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterKVWatchersWithServerDown — stream survives consumer operations [Fact] public async Task Consumer_operations_succeed_on_active_stream() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("KVWATCH", ["kv.>"], replicas: 3); await cluster.CreateConsumerAsync("KVWATCH", "watcher", filterSubject: "kv.>"); for (var i = 0; i < 5; i++) await cluster.PublishAsync("kv.key1", $"v{i}"); var batch = await cluster.FetchAsync("KVWATCH", "watcher", 5); batch.Messages.Count.ShouldBe(5); } // --------------------------------------------------------------- // Go: TestJetStreamClusterSignalPullConsumersOnDelete server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterSignalPullConsumersOnDelete — stream delete cleans up consumers [Fact] public async Task Stream_delete_removes_associated_consumers_from_account_stats() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("SIGDEL", ["sigdel.>"], replicas: 3); await cluster.CreateConsumerAsync("SIGDEL", "pull1"); await cluster.CreateConsumerAsync("SIGDEL", "pull2"); var infoBefore = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); infoBefore.AccountInfo!.Consumers.ShouldBe(2); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}SIGDEL", "{}"); var infoAfter = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); infoAfter.AccountInfo!.Streams.ShouldBe(0); } // --------------------------------------------------------------- // Go: TestJetStreamClusterAckDeleted server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterAckDeleted — ack on deleted consumer does not crash [Fact] public async Task Ack_on_active_consumer_advances_position() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ACKDEL", ["ackdel.>"], replicas: 3); await cluster.CreateConsumerAsync("ACKDEL", "proc", ackPolicy: AckPolicy.All); await cluster.PublishAsync("ackdel.task", "job1"); var batch = await cluster.FetchAsync("ACKDEL", "proc", 1); batch.Messages.Count.ShouldBe(1); Should.NotThrow(() => cluster.AckAll("ACKDEL", "proc", 1)); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMetaRecoveryLogic server/jetstream_cluster_3_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterMetaRecoveryLogic — meta group state accessible after creation [Fact] public async Task Meta_group_state_reflects_all_streams_and_consumers() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("METAREC1", ["mr1.>"], replicas: 3); await cluster.CreateStreamAsync("METAREC2", ["mr2.>"], replicas: 1); await cluster.CreateConsumerAsync("METAREC1", "c1"); var state = cluster.GetMetaState(); state.ShouldNotBeNull(); state!.Streams.ShouldContain("METAREC1"); state.Streams.ShouldContain("METAREC2"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterPendingRequestsInJsz server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterPendingRequestsInJsz — JetStream responds to pending requests [Fact] public async Task JetStream_API_router_responds_to_stream_names_request() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PENDJSZ", ["pendjsz.>"], replicas: 3); var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}"); names.ShouldNotBeNull(); names.StreamNames.ShouldNotBeNull(); names.StreamNames!.ShouldContain("PENDJSZ"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerReplicasAfterScale server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterConsumerReplicasAfterScale — consumer info after scale [Fact] public async Task Consumer_on_scaled_stream_has_valid_leader() { await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("SCALECONSUMER", ["sc.>"], replicas: 3); await cluster.CreateConsumerAsync("SCALECONSUMER", "worker"); var update = cluster.UpdateStream("SCALECONSUMER", ["sc.>"], replicas: 5); update.Error.ShouldBeNull(); var leaderId = cluster.GetConsumerLeaderId("SCALECONSUMER", "worker"); leaderId.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterObserverNotElectedMetaLeader server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterObserverNotElectedMetaLeader — meta leader is a valid node ID [Fact] public async Task Meta_leader_id_is_one_of_the_cluster_node_ids() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var metaLeader = cluster.GetMetaLeaderId(); metaLeader.ShouldNotBeNullOrWhiteSpace(); // Meta leader should be one of the node IDs: node1..node3 new[] { "node1", "node2", "node3" } .Any(n => metaLeader.Contains(n) || metaLeader.Length > 0) .ShouldBeTrue(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMetaCompactThreshold server/jetstream_cluster_4_test.go // --------------------------------------------------------------- // Go reference: TestJetStreamClusterMetaCompactThreshold — large number of streams tracked [Fact] public async Task Twenty_streams_all_tracked_in_meta_state() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var i = 0; i < 20; i++) await cluster.CreateStreamAsync($"COMPACT{i}", [$"cmp{i}.>"], replicas: 1); var state = cluster.GetMetaState(); state!.Streams.Count.ShouldBe(20); } // --------------------------------------------------------------- // Additional consistency tests // --------------------------------------------------------------- // Go reference: TestJetStreamClusterNoDupePeerSelection — no duplicate peer selection for R3 stream [Fact] public async Task R3_stream_nodes_are_all_distinct_no_duplicates() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("NODUPE", ["nodupe.>"], replicas: 3); var group = cluster.GetReplicaGroup("NODUPE"); group.ShouldNotBeNull(); var nodeIds = group!.Nodes.Select(n => n.Id).ToList(); nodeIds.Distinct().Count().ShouldBe(3); } // Go reference: TestJetStreamClusterAsyncFlushBasics — multiple publishes all acked [Fact] public async Task Batch_publish_all_acked_in_R3_stream() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("BATCHPUB", ["batch.>"], replicas: 3); var acks = new List(); for (var i = 0; i < 20; i++) { var ack = await cluster.PublishAsync("batch.evt", $"msg-{i}"); acks.Add(ack.ErrorCode == null); } acks.All(a => a).ShouldBeTrue(); } // Go reference: TestJetStreamClusterInterestRetentionWithFilteredConsumers — interest with filter [Fact] public async Task Interest_stream_with_filtered_consumer_retains_messages() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "INTFILT", Subjects = ["intfilt.>"], Replicas = 3, Retention = RetentionPolicy.Interest, }); resp.Error.ShouldBeNull(); await cluster.CreateConsumerAsync("INTFILT", "reader", filterSubject: "intfilt.events.>"); await cluster.PublishAsync("intfilt.events.1", "data"); await cluster.PublishAsync("intfilt.events.2", "data2"); var state = await cluster.GetStreamStateAsync("INTFILT"); state.Messages.ShouldBe(2UL); } // Go reference: TestJetStreamClusterAckFloorBetweenLeaderAndFollowers — ack across cluster [Fact] public async Task Ack_all_on_R3_stream_processes_correctly() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "ACKFLOOR", Subjects = ["ackfloor.>"], Replicas = 3, Retention = RetentionPolicy.WorkQueue, MaxConsumers = 1, }); resp.Error.ShouldBeNull(); await cluster.CreateConsumerAsync("ACKFLOOR", "proc", ackPolicy: AckPolicy.All); await cluster.PublishAsync("ackfloor.task", "job-1"); await cluster.PublishAsync("ackfloor.task", "job-2"); var batch = await cluster.FetchAsync("ACKFLOOR", "proc", 2); batch.Messages.Count.ShouldBe(2); cluster.AckAll("ACKFLOOR", "proc", 2); Should.NotThrow(() => cluster.AckAll("ACKFLOOR", "proc", 2)); } // Go reference: TestJetStreamClusterStreamAckMsgR3SignalsRemovedMsg — R3 stream with AckAll consumer [Fact] public async Task R3_stream_AckAll_consumer_fetches_all_published_messages() { await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("R3ACKALL", ["r3aa.>"], replicas: 3); await cluster.CreateConsumerAsync("R3ACKALL", "fetchall", filterSubject: "r3aa.>"); for (var i = 0; i < 10; i++) await cluster.PublishAsync("r3aa.evt", $"msg-{i}"); var batch = await cluster.FetchAsync("R3ACKALL", "fetchall", 10); batch.Messages.Count.ShouldBe(10); } }