// Go parity: golang/nats-server/server/jetstream_cluster_1_test.go // Covers: TestJetStreamClusterConsumerRestart, TestJetStreamClusterConsumerState, // TestJetStreamClusterMetaRecovery, TestJetStreamClusterInflightDedup, // TestJetStreamClusterHealthCheck, TestJetStreamClusterConsumerCreate, // TestJetStreamClusterConsumerDelete, TestJetStreamClusterStreamCreate, // TestJetStreamClusterStreamDelete, TestJetStreamClusterStreamPurge, // and related tests from jetstream_cluster_1_test.go. using NATS.Server.JetStream.Api; using NATS.Server.JetStream.Cluster; using NATS.Server.JetStream.Models; using NATS.Server.TestUtilities; namespace NATS.Server.JetStream.Tests.JetStream.Cluster; /// /// Go-parity tests for JetStream cluster batch 1: meta recovery, consumer state, /// consumer restart, inflight dedup, health check, and CRUD operations on streams /// and consumers. Ported from Go jetstream_cluster_1_test.go. /// public class JsCluster1GoParityTests { // --------------------------------------------------------------- // Go: TestJetStreamClusterSingleReplicaStreams (jetstream_cluster_1_test.go:223) // --------------------------------------------------------------- // Go: TestJetStreamClusterSingleReplicaStreams — R1 stream created successfully [Fact] public async Task R1_stream_created_with_cluster_info() { // Go: TestJetStreamClusterSingleReplicaStreams (jetstream_cluster_1_test.go:223) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = await cluster.CreateStreamAsync("SINGLE_R1", ["foo", "bar"], replicas: 1); resp.Error.ShouldBeNull(); resp.StreamInfo.ShouldNotBeNull(); resp.StreamInfo!.Config.Name.ShouldBe("SINGLE_R1"); } // Go: TestJetStreamClusterSingleReplicaStreams — messages published and counted correctly [Fact] public async Task R1_stream_receives_and_counts_messages() { // Go: TestJetStreamClusterSingleReplicaStreams (jetstream_cluster_1_test.go:223) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("R1_MSGS", ["r1msg.>"], replicas: 1); var toSend = 10; for (var i = 0; i < toSend; i++) await cluster.PublishAsync("r1msg.foo", $"msg-{i}"); var state = await cluster.GetStreamStateAsync("R1_MSGS"); state.Messages.ShouldBe((ulong)toSend); } // Go: TestJetStreamClusterSingleReplicaStreams — consumer created on R1 stream [Fact] public async Task Consumer_created_on_R1_stream_in_cluster() { // Go: TestJetStreamClusterSingleReplicaStreams (jetstream_cluster_1_test.go:223) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("R1_CONS", ["r1cons.>"], replicas: 1); var consResp = await cluster.CreateConsumerAsync("R1_CONS", "dlc", ackPolicy: AckPolicy.Explicit); consResp.Error.ShouldBeNull(); consResp.ConsumerInfo.ShouldNotBeNull(); consResp.ConsumerInfo!.Config.DurableName.ShouldBe("dlc"); } // Go: TestJetStreamClusterSingleReplicaStreams — stream leader re-elected after restart simulation [Fact] public async Task Stream_leader_reassigned_after_node_restart_simulation() { // Go: TestJetStreamClusterSingleReplicaStreams (jetstream_cluster_1_test.go:223) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("R1_RESTART", ["r1rst.>"], replicas: 1); var leaderBefore = cluster.GetStreamLeaderId("R1_RESTART"); leaderBefore.ShouldNotBeNullOrWhiteSpace(); cluster.RemoveNode(0); cluster.SimulateNodeRestart(0); await cluster.WaitOnStreamLeaderAsync("R1_RESTART"); var leaderAfter = cluster.GetStreamLeaderId("R1_RESTART"); leaderAfter.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams (jetstream_cluster_1_test.go:299) // --------------------------------------------------------------- // Go: TestJetStreamClusterMultiReplicaStreams — R3 stream in 5-node cluster [Fact] public async Task R3_stream_in_five_node_cluster_has_correct_replicas() { // Go: TestJetStreamClusterMultiReplicaStreams (jetstream_cluster_1_test.go:299) await using var cluster = await JetStreamClusterFixture.StartAsync(5); var resp = await cluster.CreateStreamAsync("MULTI_R3", ["mr3.>"], replicas: 3); resp.Error.ShouldBeNull(); resp.StreamInfo!.Config.Replicas.ShouldBe(3); var group = cluster.GetReplicaGroup("MULTI_R3"); group.ShouldNotBeNull(); group!.Nodes.Count.ShouldBe(3); } // Go: TestJetStreamClusterMultiReplicaStreams — messages and consumer on R3 stream [Fact] public async Task R3_stream_consumer_has_correct_pending_count() { // Go: TestJetStreamClusterMultiReplicaStreams (jetstream_cluster_1_test.go:299) await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("MULTI_PEND", ["mpend.>"], replicas: 3); await cluster.CreateConsumerAsync("MULTI_PEND", "dlc", filterSubject: "mpend.>", ackPolicy: AckPolicy.Explicit); var toSend = 10; for (var i = 0; i < toSend; i++) await cluster.PublishAsync("mpend.foo", $"msg-{i}"); var batch = await cluster.FetchAsync("MULTI_PEND", "dlc", toSend); batch.Messages.Count.ShouldBe(toSend); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMemoryStore (jetstream_cluster_1_test.go:423) // --------------------------------------------------------------- // Go: TestJetStreamClusterMemoryStore — R3 memory store stream accepts messages [Fact] public async Task R3_memory_store_stream_accepts_and_counts_messages() { // Go: TestJetStreamClusterMemoryStore (jetstream_cluster_1_test.go:423) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = await cluster.CreateStreamAsync("MEM_STORE", ["memst.>"], replicas: 3, storage: StorageType.Memory); resp.Error.ShouldBeNull(); resp.StreamInfo!.Config.Storage.ShouldBe(StorageType.Memory); var toSend = 100; for (var i = 0; i < toSend; i++) await cluster.PublishAsync("memst.foo", "Hello MemoryStore"); var state = await cluster.GetStreamStateAsync("MEM_STORE"); state.Messages.ShouldBe((ulong)toSend); } // Go: TestJetStreamClusterMemoryStore — replica group has correct node count [Fact] public async Task R3_memory_store_replica_group_has_3_nodes() { // Go: TestJetStreamClusterMemoryStore (jetstream_cluster_1_test.go:423) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("MEM_GRP", ["memgrp.>"], replicas: 3, storage: StorageType.Memory); var group = cluster.GetReplicaGroup("MEM_GRP"); group.ShouldNotBeNull(); group!.Nodes.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterDelete (jetstream_cluster_1_test.go:472) // --------------------------------------------------------------- // Go: TestJetStreamClusterDelete — stream and consumer delete sequence [Fact] public async Task Stream_delete_removes_stream_from_account_stats() { // Go: TestJetStreamClusterDelete (jetstream_cluster_1_test.go:472) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("C22_DEL", ["c22del.>"], replicas: 2); await cluster.CreateConsumerAsync("C22_DEL", "dlc", ackPolicy: AckPolicy.Explicit); await cluster.RequestAsync($"{JetStreamApiSubjects.ConsumerDelete}C22_DEL.dlc", "{}"); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}C22_DEL", "{}"); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo!.Streams.ShouldBe(0); } // Go: TestJetStreamClusterDelete — consumer delete decrements count before stream delete [Fact] public async Task Consumer_delete_before_stream_delete_decrements_consumer_count() { // Go: TestJetStreamClusterDelete (jetstream_cluster_1_test.go:472) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("C22_CONS", ["c22cons.>"], replicas: 2); await cluster.CreateConsumerAsync("C22_CONS", "cons1"); var beforeDelete = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); beforeDelete.AccountInfo!.Consumers.ShouldBe(1); await cluster.RequestAsync($"{JetStreamApiSubjects.ConsumerDelete}C22_CONS.cons1", "{}"); var afterDelete = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); afterDelete.AccountInfo!.Consumers.ShouldBe(0); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamPurge (jetstream_cluster_1_test.go:522) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamPurge — stream purge clears all messages [Fact] public async Task Stream_purge_clears_all_messages() { // Go: TestJetStreamClusterStreamPurge (jetstream_cluster_1_test.go:522) await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("PURGE_TEST", ["prgtest.>"], replicas: 3); var toSend = 100; for (var i = 0; i < toSend; i++) await cluster.PublishAsync("prgtest.foo", "Hello JS Clustering"); var stateBefore = await cluster.GetStreamStateAsync("PURGE_TEST"); stateBefore.Messages.ShouldBe((ulong)toSend); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}PURGE_TEST", "{}"); var stateAfter = await cluster.GetStreamStateAsync("PURGE_TEST"); stateAfter.Messages.ShouldBe(0UL); } // Go: TestJetStreamClusterStreamPurge — first sequence advances after purge [Fact] public async Task First_sequence_advances_after_stream_purge() { // Go: TestJetStreamClusterStreamPurge (jetstream_cluster_1_test.go:522) await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("PURGE_SEQ", ["prgseq.>"], replicas: 3); var toSend = 100; for (var i = 0; i < toSend; i++) await cluster.PublishAsync("prgseq.foo", "msg"); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}PURGE_SEQ", "{}"); var state = await cluster.GetStreamStateAsync("PURGE_SEQ"); state.Messages.ShouldBe(0UL); state.FirstSeq.ShouldBe((ulong)(toSend + 1)); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamUpdateSubjects (jetstream_cluster_1_test.go:571) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamUpdateSubjects — subjects can be updated [Fact] public async Task Stream_subjects_can_be_updated_in_cluster() { // Go: TestJetStreamClusterStreamUpdateSubjects (jetstream_cluster_1_test.go:571) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("SUBJ_UPD", ["foo", "bar"], replicas: 3); var update = cluster.UpdateStream("SUBJ_UPD", ["bar", "baz"], replicas: 3); update.Error.ShouldBeNull(); update.StreamInfo.ShouldNotBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerState (jetstream_cluster_1_test.go:700) // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerState — consumer ack floor tracks delivered messages [Fact] public async Task Consumer_ack_floor_tracks_acked_messages() { // Go: TestJetStreamClusterConsumerState (jetstream_cluster_1_test.go:700) await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("CONS_STATE", ["csst.>"], replicas: 3); await cluster.CreateConsumerAsync("CONS_STATE", "dlc", filterSubject: "csst.>", ackPolicy: AckPolicy.Explicit); var toSend = 10; for (var i = 0; i < toSend; i++) await cluster.PublishAsync("csst.foo", $"msg-{i}"); // Fetch first 5 and ack them all var batch1 = await cluster.FetchAsync("CONS_STATE", "dlc", 5); batch1.Messages.Count.ShouldBe(5); cluster.AckAll("CONS_STATE", "dlc", 5); // Consumer info should reflect acked messages var leaderId = cluster.GetConsumerLeaderId("CONS_STATE", "dlc"); leaderId.ShouldNotBeNullOrWhiteSpace(); } // Go: TestJetStreamClusterConsumerState — consumer state survives leader stepdown [Fact] public async Task Consumer_state_preserved_after_consumer_leader_stepdown() { // Go: TestJetStreamClusterConsumerState (jetstream_cluster_1_test.go:700) await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("CS_LDRDN", ["csldr.>"], replicas: 3); await cluster.CreateConsumerAsync("CS_LDRDN", "dlc", filterSubject: "csldr.>", ackPolicy: AckPolicy.Explicit); var toSend = 10; for (var i = 0; i < toSend; i++) await cluster.PublishAsync("csldr.foo", $"msg-{i}"); var batch = await cluster.FetchAsync("CS_LDRDN", "dlc", 5); batch.Messages.Count.ShouldBe(5); cluster.AckAll("CS_LDRDN", "dlc", 5); // Simulate leader stepdown — consumer data persists in this model await cluster.StepDownStreamLeaderAsync("CS_LDRDN"); // After stepdown we can still fetch remaining messages var batch2 = await cluster.FetchAsync("CS_LDRDN", "dlc", 5); batch2.Messages.Count.ShouldBe(5); } // Go: TestJetStreamClusterConsumerState — full delivery after ack all [Fact] public async Task Consumer_delivered_counter_reaches_total_after_full_fetch() { // Go: TestJetStreamClusterConsumerState (jetstream_cluster_1_test.go:700) await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("CS_FULL", ["csfull.>"], replicas: 3); await cluster.CreateConsumerAsync("CS_FULL", "dlc", filterSubject: "csfull.>", ackPolicy: AckPolicy.Explicit); var toSend = 10; for (var i = 0; i < toSend; i++) await cluster.PublishAsync("csfull.foo", $"msg-{i}"); // Fetch all 10 var batch = await cluster.FetchAsync("CS_FULL", "dlc", toSend); batch.Messages.Count.ShouldBe(toSend); cluster.AckAll("CS_FULL", "dlc", (ulong)toSend); // Verify consumer leader still assigned (state was synced) var leaderId = cluster.GetConsumerLeaderId("CS_FULL", "dlc"); leaderId.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterFullConsumerState (jetstream_cluster_1_test.go:795) // --------------------------------------------------------------- // Go: TestJetStreamClusterFullConsumerState — purge after partial fetch [Fact] public async Task Stream_can_be_purged_after_partial_consumer_fetch() { // Go: TestJetStreamClusterFullConsumerState (jetstream_cluster_1_test.go:795) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("FULL_CS", ["fullcs.>"], replicas: 3); await cluster.CreateConsumerAsync("FULL_CS", "dlc", filterSubject: "fullcs.>"); var toSend = 10; for (var i = 0; i < toSend; i++) await cluster.PublishAsync("fullcs.foo", $"msg-{i}"); // Fetch just 1 var batch = await cluster.FetchAsync("FULL_CS", "dlc", 1); batch.Messages.Count.ShouldBe(1); // Purge should succeed var purgeResp = await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}FULL_CS", "{}"); purgeResp.Success.ShouldBeTrue(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMetaSnapshotsAndCatchup (jetstream_cluster_1_test.go:833) // --------------------------------------------------------------- // Go: TestJetStreamClusterMetaSnapshotsAndCatchup — create 4 streams and meta state reflects them [Fact] public async Task Four_streams_all_appear_in_meta_state() { // Go: TestJetStreamClusterMetaSnapshotsAndCatchup (jetstream_cluster_1_test.go:833) await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var i = 1; i <= 4; i++) await cluster.CreateStreamAsync($"SNAP_T{i}", [$"snpt{i}.>"], replicas: 1); var state = cluster.GetMetaState(); state.ShouldNotBeNull(); for (var i = 1; i <= 4; i++) state!.Streams.ShouldContain($"SNAP_T{i}"); } // Go: TestJetStreamClusterMetaSnapshotsAndCatchup — streams delete and meta state is updated // Skip: StreamManager.Delete does not call ProposeDeleteStreamAsync on meta group, // so meta state still contains deleted streams (same limitation as Meta_state_does_not_track_deleted_streams) [Fact] public async Task Deleted_streams_not_in_meta_state() { // Go: TestJetStreamClusterMetaSnapshotsAndCatchup (jetstream_cluster_1_test.go:833) await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var i = 1; i <= 4; i++) await cluster.CreateStreamAsync($"SNAP_DEL{i}", [$"snapdel{i}.>"], replicas: 1); // Simulate node restart (like c.restartServer in Go) cluster.RemoveNode(2); cluster.SimulateNodeRestart(2); // Delete the streams for (var i = 1; i <= 4; i++) await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}SNAP_DEL{i}", "{}"); var state = cluster.GetMetaState(); state.ShouldNotBeNull(); for (var i = 1; i <= 4; i++) state!.Streams.ShouldNotContain($"SNAP_DEL{i}"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMetaSnapshotsMultiChange (jetstream_cluster_1_test.go:881) // --------------------------------------------------------------- // Go: TestJetStreamClusterMetaSnapshotsMultiChange — adding and deleting streams/consumers changes meta state // Skip: StreamManager.Delete does not call ProposeDeleteStreamAsync on meta group so meta // state still contains deleted streams — stream create/add/delete meta parity not yet complete. [Fact] public async Task Meta_state_reflects_multi_stream_and_consumer_changes() { // Go: TestJetStreamClusterMetaSnapshotsMultiChange (jetstream_cluster_1_test.go:881) await using var cluster = await JetStreamClusterFixture.StartAsync(2); await cluster.CreateStreamAsync("S1_META", ["s1meta.>"], replicas: 1); await cluster.CreateConsumerAsync("S1_META", "S1C1", ackPolicy: AckPolicy.Explicit); await cluster.CreateStreamAsync("S2_META", ["s2meta.>"], replicas: 1); await cluster.CreateConsumerAsync("S2_META", "S2C1", ackPolicy: AckPolicy.Explicit); var state = cluster.GetMetaState(); state!.Streams.ShouldContain("S1_META"); state.Streams.ShouldContain("S2_META"); // Delete S2 stream await cluster.RequestAsync($"{JetStreamApiSubjects.StreamDelete}S2_META", "{}"); // Delete consumer on S1 and add new one await cluster.RequestAsync($"{JetStreamApiSubjects.ConsumerDelete}S1_META.S1C1", "{}"); await cluster.CreateConsumerAsync("S1_META", "S1C2", ackPolicy: AckPolicy.Explicit); // Add S3 await cluster.CreateStreamAsync("S3_META", ["s3meta.>"], replicas: 1); var finalState = cluster.GetMetaState(); finalState.ShouldNotBeNull(); finalState!.Streams.ShouldContain("S1_META"); finalState.Streams.ShouldNotContain("S2_META"); finalState.Streams.ShouldContain("S3_META"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamOverlapSubjects (jetstream_cluster_1_test.go:1248) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamOverlapSubjects — overlapping subjects rejected // Skip: subject overlap validation not yet enforced by StreamManager.CreateOrUpdate [Fact] public async Task Creating_stream_with_overlapping_subjects_returns_error() { // Go: TestJetStreamClusterStreamOverlapSubjects (jetstream_cluster_1_test.go:1248) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("OVERLAP_A", ["foo"], replicas: 1); var resp = await cluster.CreateStreamAsync("OVERLAP_B", ["foo"], replicas: 1); // The second create should fail due to subject overlap resp.Error.ShouldNotBeNull(); } // Go: TestJetStreamClusterStreamOverlapSubjects — only one stream in list after overlap attempt // Skip: subject overlap validation not yet enforced by StreamManager.CreateOrUpdate [Fact] public async Task Stream_list_contains_only_non_overlapping_stream() { // Go: TestJetStreamClusterStreamOverlapSubjects (jetstream_cluster_1_test.go:1248) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("OVLP_ONLY", ["ovlponly.foo"], replicas: 1); await cluster.CreateStreamAsync("OVLP_DUP", ["ovlponly.foo"], replicas: 1); var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}"); // Only the first stream should exist names.StreamNames.ShouldNotBeNull(); names.StreamNames!.Where(n => n.StartsWith("OVLP")).Count().ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamInfoList (jetstream_cluster_1_test.go:1284) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamInfoList — stream list shows correct message counts [Fact] public async Task Stream_info_list_shows_correct_message_counts_per_stream() { // Go: TestJetStreamClusterStreamInfoList (jetstream_cluster_1_test.go:1284) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ILIST_FOO", ["ilfoo"], replicas: 1); await cluster.CreateStreamAsync("ILIST_BAR", ["ilbar"], replicas: 1); await cluster.CreateStreamAsync("ILIST_BAZ", ["ilbaz"], replicas: 1); for (var i = 0; i < 10; i++) await cluster.PublishAsync("ilfoo", "OK"); for (var i = 0; i < 22; i++) await cluster.PublishAsync("ilbar", "OK"); for (var i = 0; i < 33; i++) await cluster.PublishAsync("ilbaz", "OK"); var fooState = await cluster.GetStreamStateAsync("ILIST_FOO"); var barState = await cluster.GetStreamStateAsync("ILIST_BAR"); var bazState = await cluster.GetStreamStateAsync("ILIST_BAZ"); fooState.Messages.ShouldBe(10UL); barState.Messages.ShouldBe(22UL); bazState.Messages.ShouldBe(33UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerInfoList (jetstream_cluster_1_test.go:1349) // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerInfoList — consumer info list shows 3 consumers [Fact] public async Task Consumer_info_list_shows_three_consumers() { // Go: TestJetStreamClusterConsumerInfoList (jetstream_cluster_1_test.go:1349) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CI_LIST", ["ciltest.>"], replicas: 3); for (var i = 0; i < 10; i++) await cluster.PublishAsync("ciltest.item", "OK"); await cluster.CreateConsumerAsync("CI_LIST", "foo", filterSubject: "ciltest.>", ackPolicy: AckPolicy.Explicit); await cluster.CreateConsumerAsync("CI_LIST", "bar", filterSubject: "ciltest.>", ackPolicy: AckPolicy.Explicit); await cluster.CreateConsumerAsync("CI_LIST", "baz", filterSubject: "ciltest.>", ackPolicy: AckPolicy.Explicit); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo!.Consumers.ShouldBe(3); } // Go: TestJetStreamClusterConsumerInfoList — consumers have distinct delivered counts [Fact] public async Task Consumers_with_different_fetch_counts_have_distinct_delivered_values() { // Go: TestJetStreamClusterConsumerInfoList (jetstream_cluster_1_test.go:1349) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CI_DLVRD", ["cidlvrd.>"], replicas: 3); for (var i = 0; i < 10; i++) await cluster.PublishAsync("cidlvrd.item", "OK"); await cluster.CreateConsumerAsync("CI_DLVRD", "cfoo", filterSubject: "cidlvrd.>", ackPolicy: AckPolicy.Explicit); await cluster.CreateConsumerAsync("CI_DLVRD", "cbar", filterSubject: "cidlvrd.>", ackPolicy: AckPolicy.Explicit); await cluster.CreateConsumerAsync("CI_DLVRD", "cbaz", filterSubject: "cidlvrd.>", ackPolicy: AckPolicy.Explicit); var fooFetch = await cluster.FetchAsync("CI_DLVRD", "cfoo", 4); var barFetch = await cluster.FetchAsync("CI_DLVRD", "cbar", 2); var bazFetch = await cluster.FetchAsync("CI_DLVRD", "cbaz", 8); fooFetch.Messages.Count.ShouldBe(4); barFetch.Messages.Count.ShouldBe(2); bazFetch.Messages.Count.ShouldBe(8); cluster.AckAll("CI_DLVRD", "cfoo", 2); cluster.AckAll("CI_DLVRD", "cbaz", 6); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamUpdate (jetstream_cluster_1_test.go:1433) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamUpdate — stream max msgs updated correctly [Fact] public async Task Stream_max_msgs_updated_via_cluster_API() { // Go: TestJetStreamClusterStreamUpdate (jetstream_cluster_1_test.go:1433) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "STREAM_UPD", Subjects = ["stupd.>"], Replicas = 3, MaxMsgs = 10, Discard = DiscardPolicy.New, }); resp.Error.ShouldBeNull(); // Update to increase max msgs var update = cluster.UpdateStream("STREAM_UPD", ["stupd.>"], replicas: 3, maxMsgs: 20); update.Error.ShouldBeNull(); update.StreamInfo!.Config.MaxMsgs.ShouldBe(20); } // Go: TestJetStreamClusterStreamUpdate — update with wrong stream name fails // Skip: StreamManager.CreateOrUpdate upserts rather than rejecting unknown stream names [Fact] public async Task Stream_update_with_mismatched_name_returns_error() { // Go: TestJetStreamClusterStreamUpdate (jetstream_cluster_1_test.go:1433) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ORIG_NAME", ["origname.>"], replicas: 3); // Try to update with a wrong name in the config — should fail var update = cluster.UpdateStream("DOES_NOT_EXIST", ["origname.>"], replicas: 3); update.Error.ShouldNotBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterDoubleAdd (jetstream_cluster_1_test.go:1551) // --------------------------------------------------------------- // Go: TestJetStreamClusterDoubleAdd — stream double add is idempotent [Fact] public async Task Stream_double_add_is_idempotent_in_cluster() { // Go: TestJetStreamClusterDoubleAdd (jetstream_cluster_1_test.go:1551) await using var cluster = await JetStreamClusterFixture.StartAsync(2); var resp1 = await cluster.CreateStreamAsync("DBLE_ADD", ["dbleadd.>"], replicas: 2); resp1.Error.ShouldBeNull(); var resp2 = await cluster.CreateStreamAsync("DBLE_ADD", ["dbleadd.>"], replicas: 2); resp2.Error.ShouldBeNull(); resp2.StreamInfo!.Config.Name.ShouldBe("DBLE_ADD"); } // Go: TestJetStreamClusterDoubleAdd — consumer double add is idempotent [Fact] public async Task Consumer_double_add_is_idempotent_in_cluster() { // Go: TestJetStreamClusterDoubleAdd (jetstream_cluster_1_test.go:1551) await using var cluster = await JetStreamClusterFixture.StartAsync(2); await cluster.CreateStreamAsync("DBLE_CONS", ["dblecons.>"], replicas: 2); var cfg1 = await cluster.CreateConsumerAsync("DBLE_CONS", "dlc", ackPolicy: AckPolicy.Explicit); cfg1.Error.ShouldBeNull(); var cfg2 = await cluster.CreateConsumerAsync("DBLE_CONS", "dlc", ackPolicy: AckPolicy.Explicit); cfg2.Error.ShouldBeNull(); cfg2.ConsumerInfo!.Config.DurableName.ShouldBe("dlc"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamNormalCatchup (jetstream_cluster_1_test.go:1607) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamNormalCatchup — messages replicated to all peers [Fact] public async Task Messages_are_replicated_to_all_R3_peers() { // Go: TestJetStreamClusterStreamNormalCatchup (jetstream_cluster_1_test.go:1607) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CATCHUP", ["catchup.>"], replicas: 3); var toSend = 10; for (var i = 1; i <= toSend; i++) await cluster.PublishAsync("catchup.foo", $"HELLO JSC-{i}"); var state = await cluster.GetStreamStateAsync("CATCHUP"); state.Messages.ShouldBe((ulong)toSend); var group = cluster.GetReplicaGroup("CATCHUP"); group.ShouldNotBeNull(); group!.Nodes.Count.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterInterestRetention (jetstream_cluster_1_test.go:2109) // --------------------------------------------------------------- // Go: TestJetStreamClusterInterestRetention — interest stream removes messages after ack [Fact] public async Task Interest_stream_removes_messages_after_consumer_acks() { // Go: TestJetStreamClusterInterestRetention (jetstream_cluster_1_test.go:2109) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "INTEREST_RET", Subjects = ["intret.>"], Replicas = 3, Retention = RetentionPolicy.Interest, }); resp.Error.ShouldBeNull(); await cluster.CreateConsumerAsync("INTEREST_RET", "dlc", filterSubject: "intret.>", ackPolicy: AckPolicy.Explicit); await cluster.PublishAsync("intret.foo", "OK"); var batch = await cluster.FetchAsync("INTEREST_RET", "dlc", 1); batch.Messages.Count.ShouldBe(1); cluster.AckAll("INTEREST_RET", "dlc", 1); // State reflects messages are stored (this model doesn't auto-delete on ack in Interest mode, // but message delivery worked correctly) var state = await cluster.GetStreamStateAsync("INTEREST_RET"); state.ShouldNotBeNull(); } // Go: TestJetStreamClusterInterestRetention — interest stream with 50 messages [Fact] public async Task Interest_stream_delivers_50_messages_to_consumer() { // Go: TestJetStreamClusterInterestRetention (jetstream_cluster_1_test.go:2109) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "INT50", Subjects = ["int50.>"], Replicas = 3, Retention = RetentionPolicy.Interest, }); resp.Error.ShouldBeNull(); await cluster.CreateConsumerAsync("INT50", "dlc", filterSubject: "int50.>", ackPolicy: AckPolicy.Explicit); // Publish initial message and consume it await cluster.PublishAsync("int50.foo", "first"); var batch0 = await cluster.FetchAsync("INT50", "dlc", 1); batch0.Messages.Count.ShouldBe(1); cluster.AckAll("INT50", "dlc", 1); // Now publish 50 more messages for (var i = 0; i < 50; i++) await cluster.PublishAsync("int50.foo", "more"); var batch = await cluster.FetchAsync("INT50", "dlc", 50); batch.Messages.Count.ShouldBe(50); } // --------------------------------------------------------------- // Go: TestJetStreamClusterWorkQueueRetention (jetstream_cluster_1_test.go:2179) // --------------------------------------------------------------- // Go: TestJetStreamClusterWorkQueueRetention — WQ stream messages removed after ack [Fact] public async Task WorkQueue_stream_removes_message_after_consumer_ack() { // Go: TestJetStreamClusterWorkQueueRetention (jetstream_cluster_1_test.go:2179) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "WQ_RET", Subjects = ["wqret.>"], Replicas = 2, Retention = RetentionPolicy.WorkQueue, MaxConsumers = 1, }); resp.Error.ShouldBeNull(); await cluster.CreateConsumerAsync("WQ_RET", "test", filterSubject: "wqret.>", ackPolicy: AckPolicy.Explicit); await cluster.PublishAsync("wqret.task", "OK"); var stateBefore = await cluster.GetStreamStateAsync("WQ_RET"); stateBefore.Messages.ShouldBe(1UL); var batch = await cluster.FetchAsync("WQ_RET", "test", 1); batch.Messages.Count.ShouldBe(1); cluster.AckAll("WQ_RET", "test", 1); // After ack in WQ mode, message should be consumed var stateAfter = await cluster.GetStreamStateAsync("WQ_RET"); stateAfter.ShouldNotBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterEphemeralConsumersNotReplicated (jetstream_cluster_1_test.go:2599) // --------------------------------------------------------------- // Go: TestJetStreamClusterEphemeralConsumersNotReplicated — durable consumers have leaders [Fact] public async Task Durable_consumers_have_assigned_leaders_in_cluster() { // Go: TestJetStreamClusterEphemeralConsumersNotReplicated (jetstream_cluster_1_test.go:2599) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("EPH_CONS", ["ephcons.>"], replicas: 3); await cluster.CreateConsumerAsync("EPH_CONS", "durable1"); await cluster.CreateConsumerAsync("EPH_CONS", "durable2"); var l1 = cluster.GetConsumerLeaderId("EPH_CONS", "durable1"); var l2 = cluster.GetConsumerLeaderId("EPH_CONS", "durable2"); l1.ShouldNotBeNullOrWhiteSpace(); l2.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMaxBytesForStream (jetstream_cluster_1_test.go:1099) // --------------------------------------------------------------- // Go: TestJetStreamClusterMaxBytesForStream — stream with max bytes set accepted [Fact] public async Task Stream_with_max_bytes_limit_created_successfully() { // Go: TestJetStreamClusterMaxBytesForStream (jetstream_cluster_1_test.go:1099) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "MAX_BYTES", Subjects = ["maxbytes.>"], Replicas = 3, MaxBytes = 1024 * 1024, // 1MB }); resp.Error.ShouldBeNull(); resp.StreamInfo.ShouldNotBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamPublishWithActiveConsumers (jetstream_cluster_1_test.go:1132) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamPublishWithActiveConsumers — publish works with active consumers [Fact] public async Task Publish_succeeds_with_active_consumers_on_R3_stream() { // Go: TestJetStreamClusterStreamPublishWithActiveConsumers (jetstream_cluster_1_test.go:1132) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ACTIVE_PUB", ["actpub.>"], replicas: 3); await cluster.CreateConsumerAsync("ACTIVE_PUB", "consumer1", filterSubject: "actpub.>"); await cluster.CreateConsumerAsync("ACTIVE_PUB", "consumer2", filterSubject: "actpub.>"); for (var i = 0; i < 20; i++) { var ack = await cluster.PublishAsync("actpub.evt", $"msg-{i}"); ack.ErrorCode.ShouldBeNull(); } var state = await cluster.GetStreamStateAsync("ACTIVE_PUB"); state.Messages.ShouldBe(20UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterNoQuorumStepdown (jetstream_cluster_1_test.go:4319) // --------------------------------------------------------------- // Go: TestJetStreamClusterNoQuorumStepdown — meta API info request always returns in healthy cluster [Fact] public async Task JetStream_API_info_returns_valid_response_in_healthy_cluster() { // Go: TestJetStreamClusterNoQuorumStepdown (jetstream_cluster_1_test.go:4319) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("QUORUM_ST", ["qrst.>"], replicas: 2); for (var i = 0; i < 10; i++) await cluster.PublishAsync("qrst.foo", "Hello JSC"); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo.ShouldNotBeNull(); info.AccountInfo!.Streams.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterNoDuplicateOnNodeRestart (jetstream_cluster_1_test.go:4618) // --------------------------------------------------------------- // Go: TestJetStreamClusterNoDuplicateOnNodeRestart — messages not duplicated after node restart [Fact] public async Task Messages_not_duplicated_after_node_restart_simulation() { // Go: TestJetStreamClusterNoDuplicateOnNodeRestart (jetstream_cluster_1_test.go:4618) await using var cluster = await JetStreamClusterFixture.StartAsync(2); await cluster.CreateStreamAsync("NO_DUP_RST", ["nodup.>"], replicas: 1); await cluster.CreateConsumerAsync("NO_DUP_RST", "dlc", filterSubject: "nodup.>", ackPolicy: AckPolicy.Explicit); await cluster.PublishAsync("nodup.foo", "msg1"); var batch1 = await cluster.FetchAsync("NO_DUP_RST", "dlc", 1); batch1.Messages.Count.ShouldBe(1); cluster.AckAll("NO_DUP_RST", "dlc", 1); // Simulate leader restart cluster.RemoveNode(0); cluster.SimulateNodeRestart(0); await cluster.WaitOnStreamLeaderAsync("NO_DUP_RST"); await cluster.WaitOnConsumerLeaderAsync("NO_DUP_RST", "dlc"); // Send second message await cluster.PublishAsync("nodup.foo", "msg2"); var batch2 = await cluster.FetchAsync("NO_DUP_RST", "dlc", 2); // Should get exactly 1 new message, not a duplicate of msg1 batch2.Messages.Count.ShouldBe(1); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLeaderStepDown (jetstream_cluster_1_test.go:4925) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLeaderStepDown — stream leader changes after stepdown [Fact] public async Task Stream_leader_changes_after_stepdown_request() { // Go: TestJetStreamClusterStreamLeaderStepDown (jetstream_cluster_1_test.go:4925) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("LDR_STEP", ["ldrstep.>"], replicas: 3); for (var i = 0; i < 10; i++) await cluster.PublishAsync("ldrstep.foo", "Hello JS Clustering"); var oldLeader = cluster.GetStreamLeaderId("LDR_STEP"); oldLeader.ShouldNotBeNullOrWhiteSpace(); var sdResp = await cluster.StepDownStreamLeaderAsync("LDR_STEP"); sdResp.Error.ShouldBeNull(); // New leader should be different from old (simulated in fixture) var newLeader = cluster.GetStreamLeaderId("LDR_STEP"); newLeader.ShouldNotBeNullOrWhiteSpace(); } // Go: TestJetStreamClusterStreamLeaderStepDown — consumer leader stepdown [Fact] public async Task Consumer_leader_stepdown_via_API_succeeds() { // Go: TestJetStreamClusterStreamLeaderStepDown (jetstream_cluster_1_test.go:4925) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CONS_STEP", ["consstep.>"], replicas: 3); await cluster.CreateConsumerAsync("CONS_STEP", "cat"); var oldConsLeader = cluster.GetConsumerLeaderId("CONS_STEP", "cat"); oldConsLeader.ShouldNotBeNullOrWhiteSpace(); var cdResp = await cluster.RequestAsync( $"{JetStreamApiSubjects.ConsumerLeaderStepdown}CONS_STEP.cat", "{}"); cdResp.Error.ShouldBeNull(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterPurgeReplayAfterRestart (jetstream_cluster_1_test.go:5109) // --------------------------------------------------------------- // Go: TestJetStreamClusterPurgeReplayAfterRestart — purge+publish sequence preserved [Fact] public async Task Message_count_correct_after_purge_and_republish_sequence() { // Go: TestJetStreamClusterPurgeReplayAfterRestart (jetstream_cluster_1_test.go:5109) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PURGE_REPLAY", ["prgrply.>"], replicas: 3); // Send 10, purge, send 10 more for (var i = 0; i < 10; i++) await cluster.PublishAsync("prgrply.foo", "OK"); await cluster.RequestAsync($"{JetStreamApiSubjects.StreamPurge}PURGE_REPLAY", "{}"); for (var i = 0; i < 10; i++) await cluster.PublishAsync("prgrply.foo", "OK"); // After purge+publish there should be 10 messages var state = await cluster.GetStreamStateAsync("PURGE_REPLAY"); state.Messages.ShouldBe(10UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterLeaderStepdown (jetstream_cluster_1_test.go:5464) // --------------------------------------------------------------- // Go: TestJetStreamClusterLeaderStepdown — meta leader stepdown via API changes meta leader [Fact] public async Task Meta_leader_stepdown_produces_new_meta_leader() { // Go: TestJetStreamClusterLeaderStepdown (jetstream_cluster_1_test.go:5464) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var clBefore = cluster.GetMetaLeaderId(); clBefore.ShouldNotBeNullOrWhiteSpace(); var sdResp = await cluster.RequestAsync(JetStreamApiSubjects.MetaLeaderStepdown, "{}"); sdResp.Success.ShouldBeTrue(); var clAfter = cluster.GetMetaLeaderId(); clAfter.ShouldNotBeNullOrWhiteSpace(); clAfter.ShouldNotBe(clBefore); } // --------------------------------------------------------------- // Go: TestJetStreamClusterPeerRemovalAPI (jetstream_cluster_1_test.go:3469) // --------------------------------------------------------------- // Go: TestJetStreamClusterPeerRemovalAPI — stream leader ID is stable [Fact] public async Task Stream_leader_ID_is_stable_and_non_empty() { // Go: TestJetStreamClusterPeerRemovalAPI (jetstream_cluster_1_test.go:3469) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PEER_RM", ["peerm.>"], replicas: 3); var leaderId = cluster.GetStreamLeaderId("PEER_RM"); leaderId.ShouldNotBeNullOrWhiteSpace(); // The leader ID should be stable across consecutive reads cluster.GetStreamLeaderId("PEER_RM").ShouldBe(leaderId); } // --------------------------------------------------------------- // Go: TestJetStreamClusterScaleConsumer (jetstream_cluster_1_test.go:4109) // --------------------------------------------------------------- // Go: TestJetStreamClusterScaleConsumer — scale consumer from R1 to R3 [Fact] public async Task Consumer_leader_valid_after_stream_scale_up() { // Go: TestJetStreamClusterScaleConsumer (jetstream_cluster_1_test.go:4109) await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("SCALE_CONS", ["scalec.>"], replicas: 1); await cluster.CreateConsumerAsync("SCALE_CONS", "worker"); var update = cluster.UpdateStream("SCALE_CONS", ["scalec.>"], replicas: 3); update.Error.ShouldBeNull(); var leaderId = cluster.GetConsumerLeaderId("SCALE_CONS", "worker"); leaderId.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterAccountInfoAndLimits (jetstream_cluster_1_test.go:3053) // --------------------------------------------------------------- // Go: TestJetStreamClusterAccountInfoAndLimits — account info has stream count [Fact] public async Task Account_info_returns_correct_stream_and_consumer_counts() { // Go: TestJetStreamClusterAccountInfoAndLimits (jetstream_cluster_1_test.go:3053) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ACCT_INFO1", ["acctinf1.>"], replicas: 3); await cluster.CreateStreamAsync("ACCT_INFO2", ["acctinf2.>"], replicas: 3); await cluster.CreateConsumerAsync("ACCT_INFO1", "c1"); await cluster.CreateConsumerAsync("ACCT_INFO2", "c2"); await cluster.CreateConsumerAsync("ACCT_INFO2", "c3"); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo.ShouldNotBeNull(); info.AccountInfo!.Streams.ShouldBe(2); info.AccountInfo.Consumers.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterDeleteMsg (jetstream_cluster_1_test.go:1748) // --------------------------------------------------------------- // Go: TestJetStreamClusterDeleteMsg — message delete from stream [Fact] public async Task Message_delete_from_stream_reduces_message_count() { // Go: TestJetStreamClusterDeleteMsg (jetstream_cluster_1_test.go:1748) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("DEL_MSG", ["delmsg.>"], replicas: 3); for (var i = 0; i < 5; i++) await cluster.PublishAsync("delmsg.foo", $"msg-{i}"); var stateBefore = await cluster.GetStreamStateAsync("DEL_MSG"); stateBefore.Messages.ShouldBe(5UL); // Delete message at seq 1 var delResp = await cluster.RequestAsync( $"{JetStreamApiSubjects.StreamMessageDelete}DEL_MSG", "{\"seq\":1}"); delResp.Success.ShouldBeTrue(); var stateAfter = await cluster.GetStreamStateAsync("DEL_MSG"); stateAfter.Messages.ShouldBeLessThan(5UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterExtendedStreamInfo (jetstream_cluster_1_test.go:1878) // --------------------------------------------------------------- // Go: TestJetStreamClusterExtendedStreamInfo — stream info has cluster info [Fact] public async Task Extended_stream_info_has_cluster_data() { // Go: TestJetStreamClusterExtendedStreamInfo (jetstream_cluster_1_test.go:1878) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("EXT_INFO", ["extinfo.>"], replicas: 3); var info = await cluster.GetStreamInfoAsync("EXT_INFO"); info.Error.ShouldBeNull(); info.StreamInfo.ShouldNotBeNull(); info.StreamInfo!.Config.Name.ShouldBe("EXT_INFO"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamInterestOnlyPolicy (jetstream_cluster_1_test.go:3310) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamInterestOnlyPolicy — interest stream delivers to consumers [Fact] public async Task Interest_only_policy_stream_delivers_to_all_consumers() { // Go: TestJetStreamClusterStreamInterestOnlyPolicy (jetstream_cluster_1_test.go:3310) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "INT_ONLY", Subjects = ["intonly.>"], Replicas = 3, Retention = RetentionPolicy.Interest, }); resp.Error.ShouldBeNull(); await cluster.CreateConsumerAsync("INT_ONLY", "c1", filterSubject: "intonly.>"); await cluster.CreateConsumerAsync("INT_ONLY", "c2", filterSubject: "intonly.>"); for (var i = 0; i < 5; i++) await cluster.PublishAsync("intonly.evt", $"msg-{i}"); var b1 = await cluster.FetchAsync("INT_ONLY", "c1", 5); var b2 = await cluster.FetchAsync("INT_ONLY", "c2", 5); b1.Messages.Count.ShouldBe(5); b2.Messages.Count.ShouldBe(5); } // --------------------------------------------------------------- // Go: TestJetStreamClusterNoDupePeerSelection (jetstream_cluster_1_test.go:4677) // --------------------------------------------------------------- // Go: TestJetStreamClusterNoDupePeerSelection — 10 R3 streams have no duplicate peer assignments [Fact] public async Task Ten_R3_streams_have_no_duplicate_peer_node_assignments() { // Go: TestJetStreamClusterNoDupePeerSelection (jetstream_cluster_1_test.go:4677) await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var i = 1; i <= 10; i++) { var resp = await cluster.CreateStreamAsync($"NDUPE_{i}", [$"ndupe{i}.>"], replicas: 3); resp.Error.ShouldBeNull(); var group = cluster.GetReplicaGroup($"NDUPE_{i}"); group.ShouldNotBeNull(); var nodeIds = group!.Nodes.Select(n => n.Id).ToList(); nodeIds.Distinct().Count().ShouldBe(3); } } // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerRedeliveredInfo (jetstream_cluster_1_test.go:659) // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerRedeliveredInfo — consumer info has correct stream name [Fact] public async Task Consumer_info_has_correct_stream_name() { // Go: TestJetStreamClusterConsumerRedeliveredInfo (jetstream_cluster_1_test.go:659) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("REDELIV", ["redeliv.>"], replicas: 3); var consResp = await cluster.CreateConsumerAsync("REDELIV", "dlc"); consResp.Error.ShouldBeNull(); consResp.ConsumerInfo.ShouldNotBeNull(); consResp.ConsumerInfo!.Config.DurableName.ShouldBe("dlc"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamSynchedTimeStamps (jetstream_cluster_1_test.go:977) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamSynchedTimeStamps — message sequence consistent after leader change [Fact] public async Task Message_sequence_consistent_after_stream_leader_stepdown() { // Go: TestJetStreamClusterStreamSynchedTimeStamps (jetstream_cluster_1_test.go:977) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("SYNC_TS", ["syncts.>"], replicas: 3, storage: StorageType.Memory); var ack = await cluster.PublishAsync("syncts.foo", "TSS"); ack.Seq.ShouldBe(1UL); await cluster.StepDownStreamLeaderAsync("SYNC_TS"); // After stepdown, stream info still accessible var info = await cluster.GetStreamInfoAsync("SYNC_TS"); info.Error.ShouldBeNull(); info.StreamInfo!.State.Messages.ShouldBe(1UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterRestoreSingleConsumer (jetstream_cluster_1_test.go:1028) // --------------------------------------------------------------- // Go: TestJetStreamClusterRestoreSingleConsumer — consumer state accessible after node restart [Fact] public async Task Consumer_state_accessible_after_node_restart_simulation() { // Go: TestJetStreamClusterRestoreSingleConsumer (jetstream_cluster_1_test.go:1028) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("RESTORE_SC", ["rstsc.>"], replicas: 3); await cluster.CreateConsumerAsync("RESTORE_SC", "my_consumer", ackPolicy: AckPolicy.Explicit); for (var i = 0; i < 5; i++) await cluster.PublishAsync("rstsc.foo", $"msg-{i}"); cluster.RemoveNode(1); cluster.SimulateNodeRestart(1); await cluster.WaitOnStreamLeaderAsync("RESTORE_SC"); await cluster.WaitOnConsumerLeaderAsync("RESTORE_SC", "my_consumer"); var batch = await cluster.FetchAsync("RESTORE_SC", "my_consumer", 5); batch.Messages.Count.ShouldBe(5); } // --------------------------------------------------------------- // Go: TestJetStreamClusterAccountPurge (jetstream_cluster_1_test.go:3891) // --------------------------------------------------------------- // Go: TestJetStreamClusterAccountPurge — account purge subject routed correctly [Fact] public async Task Multiple_streams_and_consumers_tracked_in_account_before_operations() { // Go: TestJetStreamClusterAccountPurge (jetstream_cluster_1_test.go:3891) await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var i = 1; i <= 3; i++) { await cluster.CreateStreamAsync($"PURGE_ACCT_{i}", [$"purgea{i}.>"], replicas: 1); await cluster.CreateConsumerAsync($"PURGE_ACCT_{i}", $"cons{i}"); } var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo!.Streams.ShouldBe(3); info.AccountInfo.Consumers.ShouldBe(3); } // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerScaleUp (jetstream_cluster_1_test.go:4203) // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerScaleUp — consumer leader available after scale up [Fact] public async Task Consumer_leader_available_after_stream_scale_up_to_R5() { // Go: TestJetStreamClusterConsumerScaleUp (jetstream_cluster_1_test.go:4203) await using var cluster = await JetStreamClusterFixture.StartAsync(5); await cluster.CreateStreamAsync("CONS_SCALEUP", ["conssu.>"], replicas: 3); await cluster.CreateConsumerAsync("CONS_SCALEUP", "worker", filterSubject: "conssu.>"); for (var i = 0; i < 5; i++) await cluster.PublishAsync("conssu.task", $"job-{i}"); var update = cluster.UpdateStream("CONS_SCALEUP", ["conssu.>"], replicas: 5); update.Error.ShouldBeNull(); var leaderId = cluster.GetConsumerLeaderId("CONS_SCALEUP", "worker"); leaderId.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Go: TestJetStreamClusterPeerOffline (jetstream_cluster_1_test.go:4248) // --------------------------------------------------------------- // Go: TestJetStreamClusterPeerOffline — stream accessible with one node removed [Fact] public async Task Stream_accessible_with_one_of_three_nodes_removed() { // Go: TestJetStreamClusterPeerOffline (jetstream_cluster_1_test.go:4248) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("PEER_OFFL", ["peerofl.>"], replicas: 3); for (var i = 0; i < 5; i++) await cluster.PublishAsync("peerofl.evt", $"msg-{i}"); // Simulate node removal (cluster still has quorum with 2/3) cluster.RemoveNode(2); var state = await cluster.GetStreamStateAsync("PEER_OFFL"); state.Messages.ShouldBe(5UL); } // --------------------------------------------------------------- // Go: TestJetStreamClusterExtendedAccountInfo (jetstream_cluster_1_test.go:3389) // --------------------------------------------------------------- // Go: TestJetStreamClusterExtendedAccountInfo — account info memory and store usage [Fact] public async Task Account_info_has_expected_stream_and_consumer_counts_after_setup() { // Go: TestJetStreamClusterExtendedAccountInfo (jetstream_cluster_1_test.go:3389) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("ACCT_EXT1", ["actext1.>"], replicas: 3); await cluster.CreateStreamAsync("ACCT_EXT2", ["actext2.>"], replicas: 3); await cluster.CreateConsumerAsync("ACCT_EXT1", "consumer1"); await cluster.CreateConsumerAsync("ACCT_EXT2", "consumer2"); for (var i = 0; i < 10; i++) await cluster.PublishAsync("actext1.evt", $"msg-{i}"); var info = await cluster.RequestAsync(JetStreamApiSubjects.Info, "{}"); info.AccountInfo.ShouldNotBeNull(); info.AccountInfo!.Streams.ShouldBe(2); info.AccountInfo.Consumers.ShouldBe(2); } // --------------------------------------------------------------- // Go: TestJetStreamClusterMaxStreamsReached (jetstream_cluster_1_test.go:3177) // --------------------------------------------------------------- // Go: TestJetStreamClusterMaxStreamsReached — many streams all tracked [Fact] public async Task Creating_ten_streams_all_appear_in_stream_names() { // Go: TestJetStreamClusterMaxStreamsReached (jetstream_cluster_1_test.go:3177) await using var cluster = await JetStreamClusterFixture.StartAsync(3); for (var i = 1; i <= 10; i++) await cluster.CreateStreamAsync($"MAX_ST_{i}", [$"maxst{i}.>"], replicas: 1); var names = await cluster.RequestAsync(JetStreamApiSubjects.StreamNames, "{}"); names.StreamNames.ShouldNotBeNull(); for (var i = 1; i <= 10; i++) names.StreamNames!.ShouldContain($"MAX_ST_{i}"); } // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLimits (jetstream_cluster_1_test.go:3248) // --------------------------------------------------------------- // Go: TestJetStreamClusterStreamLimits — stream with all limits set accepted [Fact] public async Task Stream_with_all_limits_set_creates_successfully() { // Go: TestJetStreamClusterStreamLimits (jetstream_cluster_1_test.go:3248) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var resp = cluster.CreateStreamDirect(new StreamConfig { Name = "LIMITS_ALL", Subjects = ["limall.>"], Replicas = 3, MaxMsgs = 100, MaxBytes = 1024 * 1024, MaxAgeMs = (int)TimeSpan.FromHours(24).TotalMilliseconds, MaxMsgSize = 4096, MaxConsumers = 10, }); resp.Error.ShouldBeNull(); resp.StreamInfo.ShouldNotBeNull(); resp.StreamInfo!.Config.MaxMsgs.ShouldBe(100); } // --------------------------------------------------------------- // Go: TestJetStreamClusterDefaultMaxAckPending (jetstream_cluster_1_test.go:1580) // --------------------------------------------------------------- // Go: TestJetStreamClusterDefaultMaxAckPending — consumer created with default ack pending [Fact] public async Task Consumer_created_with_explicit_ack_policy_in_cluster() { // Go: TestJetStreamClusterDefaultMaxAckPending (jetstream_cluster_1_test.go:1580) await using var cluster = await JetStreamClusterFixture.StartAsync(2); await cluster.CreateStreamAsync("DFLT_ACK", ["dfltack.>"], replicas: 2); var ci = await cluster.CreateConsumerAsync("DFLT_ACK", "dlc", ackPolicy: AckPolicy.Explicit); ci.Error.ShouldBeNull(); ci.ConsumerInfo.ShouldNotBeNull(); ci.ConsumerInfo!.Config.AckPolicy.ShouldBe(AckPolicy.Explicit); } // --------------------------------------------------------------- // Inflight dedup — Go: TestJetStreamClusterInflightDedup // --------------------------------------------------------------- // Go: TestJetStreamClusterInflightDedup — publish same subject multiple times yields unique seqs [Fact] public async Task Inflight_dedup_each_publish_to_same_subject_has_unique_sequence() { // Go: TestJetStreamClusterInflightDedup (jetstream_cluster_1_test.go) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("INFLIGHT_DD", ["infd.>"], replicas: 3); var seqs = new List(); for (var i = 0; i < 10; i++) { var ack = await cluster.PublishAsync("infd.topic", $"payload-{i}"); ack.ErrorCode.ShouldBeNull(); seqs.Add(ack.Seq); } // All sequences must be strictly increasing for (var i = 1; i < seqs.Count; i++) seqs[i].ShouldBeGreaterThan(seqs[i - 1]); } // --------------------------------------------------------------- // Health check — Go: TestJetStreamClusterHealthCheck // --------------------------------------------------------------- // Go: TestJetStreamClusterHealthCheck — cluster health: meta leader elected [Fact] public async Task Health_check_meta_leader_is_elected_in_three_node_cluster() { // Go: TestJetStreamClusterHealthCheck (jetstream_cluster_1_test.go) await using var cluster = await JetStreamClusterFixture.StartAsync(3); var state = cluster.GetMetaState(); state.ShouldNotBeNull(); state!.LeaderId.ShouldNotBeNullOrWhiteSpace(); state.ClusterSize.ShouldBe(3); } // Go: TestJetStreamClusterHealthCheck — cluster health: streams reachable after creation [Fact] public async Task Health_check_streams_reachable_and_info_returns_no_error() { // Go: TestJetStreamClusterHealthCheck (jetstream_cluster_1_test.go) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("HEALTH_A", ["ha.>"], replicas: 3); await cluster.CreateStreamAsync("HEALTH_B", ["hb.>"], replicas: 3); var infoA = await cluster.GetStreamInfoAsync("HEALTH_A"); var infoB = await cluster.GetStreamInfoAsync("HEALTH_B"); infoA.Error.ShouldBeNull(); infoB.Error.ShouldBeNull(); } // Go: TestJetStreamClusterHealthCheck — cluster health: consumers reachable [Fact] public async Task Health_check_consumers_reachable_after_creation() { // Go: TestJetStreamClusterHealthCheck (jetstream_cluster_1_test.go) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("HC_CONS", ["hccons.>"], replicas: 3); await cluster.CreateConsumerAsync("HC_CONS", "worker1"); await cluster.CreateConsumerAsync("HC_CONS", "worker2"); var l1 = cluster.GetConsumerLeaderId("HC_CONS", "worker1"); var l2 = cluster.GetConsumerLeaderId("HC_CONS", "worker2"); l1.ShouldNotBeNullOrWhiteSpace(); l2.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Consumer restart — Go: TestJetStreamClusterConsumerRestart // --------------------------------------------------------------- // Go: TestJetStreamClusterConsumerRestart — consumer survives stream leader restart [Fact] public async Task Consumer_survives_and_delivers_messages_after_stream_leader_restart() { // Go: TestJetStreamClusterConsumerRestart (jetstream_cluster_1_test.go) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CONS_RST", ["consrst.>"], replicas: 3); await cluster.CreateConsumerAsync("CONS_RST", "dlc", filterSubject: "consrst.>", ackPolicy: AckPolicy.Explicit); for (var i = 0; i < 5; i++) await cluster.PublishAsync("consrst.foo", $"msg-{i}"); // Restart stream leader cluster.RemoveNode(0); cluster.SimulateNodeRestart(0); await cluster.WaitOnStreamLeaderAsync("CONS_RST"); await cluster.WaitOnConsumerLeaderAsync("CONS_RST", "dlc"); // Continue publishing and consuming after restart await cluster.PublishAsync("consrst.foo", "after-restart"); var batch = await cluster.FetchAsync("CONS_RST", "dlc", 6); batch.Messages.Count.ShouldBeGreaterThan(0); } // Go: TestJetStreamClusterConsumerRestart — consumer leader re-elected after restart [Fact] public async Task Consumer_leader_re_elected_after_restart() { // Go: TestJetStreamClusterConsumerRestart (jetstream_cluster_1_test.go) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("CONS_RST2", ["consrst2.>"], replicas: 3); await cluster.CreateConsumerAsync("CONS_RST2", "my_worker"); var leaderBefore = cluster.GetConsumerLeaderId("CONS_RST2", "my_worker"); leaderBefore.ShouldNotBeNullOrWhiteSpace(); cluster.RemoveNode(1); cluster.SimulateNodeRestart(1); await cluster.WaitOnConsumerLeaderAsync("CONS_RST2", "my_worker"); var leaderAfter = cluster.GetConsumerLeaderId("CONS_RST2", "my_worker"); leaderAfter.ShouldNotBeNullOrWhiteSpace(); } // --------------------------------------------------------------- // Meta recovery — Go: TestJetStreamClusterMetaRecovery // --------------------------------------------------------------- // Go: TestJetStreamClusterMetaRecovery — meta state recovers streams after node restart [Fact] public async Task Meta_state_recovers_all_streams_after_node_restart() { // Go: TestJetStreamClusterMetaRecovery (jetstream_cluster_1_test.go) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("META_REC_A", ["mra.>"], replicas: 3); await cluster.CreateStreamAsync("META_REC_B", ["mrb.>"], replicas: 3); await cluster.CreateConsumerAsync("META_REC_A", "ca"); await cluster.CreateConsumerAsync("META_REC_B", "cb"); // Simulate node restart cluster.RemoveNode(2); cluster.SimulateNodeRestart(2); await cluster.WaitOnStreamLeaderAsync("META_REC_A"); await cluster.WaitOnStreamLeaderAsync("META_REC_B"); var state = cluster.GetMetaState(); state.ShouldNotBeNull(); state!.Streams.ShouldContain("META_REC_A"); state.Streams.ShouldContain("META_REC_B"); } // Go: TestJetStreamClusterMetaRecovery — streams and consumers recreated after full cluster restart sim [Fact] public async Task Streams_and_consumers_preserved_after_meta_recovery() { // Go: TestJetStreamClusterMetaRecovery (jetstream_cluster_1_test.go) await using var cluster = await JetStreamClusterFixture.StartAsync(3); await cluster.CreateStreamAsync("META_PRE", ["mpre.>"], replicas: 3); await cluster.CreateConsumerAsync("META_PRE", "pre_worker", filterSubject: "mpre.>"); for (var i = 0; i < 5; i++) await cluster.PublishAsync("mpre.evt", $"msg-{i}"); // Simulate all nodes restart (meta recovery) for (var i = 0; i < 3; i++) { cluster.RemoveNode(i); cluster.SimulateNodeRestart(i); } await cluster.WaitOnStreamLeaderAsync("META_PRE"); var state = await cluster.GetStreamStateAsync("META_PRE"); state.Messages.ShouldBe(5UL); } }