// Ported from golang/nats-server/server/jetstream_test.go // Delivery, ack, redelivery, interest retention, KV, multi-account, flow control, // and per-subject delivery edge cases. using System.Text; using NATS.Server.JetStream; using NATS.Server.JetStream.Consumers; using NATS.Server.JetStream.Models; using NATS.Server.TestUtilities; namespace NATS.Server.JetStream.Tests.JetStream; public class JsDeliveryAckTests { // Go: TestJetStreamRedeliverAndLateAck server/jetstream_test.go // A message fetched with AckExplicit but never acknowledged within ackWait is // redelivered on the next fetch. After the redelivery the consumer marks it // as redelivered. [Fact] public async Task Redelivery_after_ack_wait_expiry_marks_message_redelivered() { await using var fx = await JetStreamApiFixture.StartWithAckExplicitConsumerAsync(1); // 1 ms ack wait _ = await fx.PublishAndGetAckAsync("orders.created", "msg1"); // First fetch — registers pending var batch1 = await fx.FetchAsync("ORDERS", "PULL", 1); batch1.Messages.Count.ShouldBe(1); // Wait for ack wait to expire await Task.Delay(20); // Second fetch returns redelivery var batch2 = await fx.FetchAsync("ORDERS", "PULL", 1); batch2.Messages.Count.ShouldBe(1); batch2.Messages[0].Redelivered.ShouldBeTrue(); } // Go: TestJetStreamCanNotNakAckd server/jetstream_test.go // After a sequence has been ACK'd, a NAK on the same sequence must be a no-op; // the AckProcessor must not schedule redelivery for already-terminated messages. [Fact] public void Nak_after_ack_is_ignored_by_ack_processor() { var ack = new AckProcessor(); ack.Register(1, ackWaitMs: 30_000); // Acknowledge sequence 1 ack.AckSequence(1); ack.HasPending.ShouldBeFalse(); // Attempt NAK after ACK — must be no-op ack.ProcessNak(1); ack.HasPending.ShouldBeFalse(); } // Go: TestJetStreamNakRedeliveryWithNoWait server/jetstream_test.go // NAK with a custom delay schedules redelivery after the specified delay, not the // default ackWait. Verified by checking that the sequence is still pending // (not expired yet) immediately after the NAK. [Fact] public void Nak_with_explicit_delay_schedules_redelivery() { var ack = new AckProcessor(); ack.Register(1, ackWaitMs: 30_000); // NAK with a very short delay (10 ms) ack.ProcessAck(1, "-NAK 10"u8); // Immediately after, the sequence should still be pending (the 10 ms hasn't elapsed) ack.HasPending.ShouldBeTrue(); ack.TryGetExpired(out _, out _).ShouldBeFalse(); } // Go: TestJetStreamPushConsumerIdleHeartbeatsWithNoInterest server/jetstream_test.go // A push consumer configured with heartbeats emits heartbeat frames even when // no data message has been delivered since the last heartbeat window. [Fact] public async Task Push_consumer_heartbeat_frame_emitted_when_idle() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("IDLE", "idle.>"); _ = await fx.CreateConsumerAsync("IDLE", "PUSH", "idle.>", push: true, heartbeatMs: 10); // Publish one message so the engine bootstraps the push consumer _ = await fx.PublishAndGetAckAsync("idle.x", "data"); var dataFrame = await fx.ReadPushFrameAsync("IDLE", "PUSH"); dataFrame.IsData.ShouldBeTrue(); // Heartbeat should follow var hbFrame = await fx.ReadPushFrameAsync("IDLE", "PUSH"); hbFrame.IsHeartbeat.ShouldBeTrue(); } // Go: TestJetStreamPendingNextTimer server/jetstream_test.go // Pending count immediately after a fetch with AckExplicit equals the batch size. [Fact] public async Task Pending_count_equals_fetched_batch_size() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PNXT", "pnxt.>"); _ = await fx.CreateConsumerAsync("PNXT", "C1", "pnxt.>", ackPolicy: AckPolicy.Explicit, ackWaitMs: 30_000); for (var i = 0; i < 4; i++) _ = await fx.PublishAndGetAckAsync("pnxt.x", $"m{i}"); var batch = await fx.FetchAsync("PNXT", "C1", 4); batch.Messages.Count.ShouldBe(4); var pending = await fx.GetPendingCountAsync("PNXT", "C1"); pending.ShouldBe(4); } // Go: TestJetStreamInterestRetentionWithWildcardsAndFilteredConsumers // server/jetstream_test.go // Interest retention stream: messages matching a wildcard filter consumer are // visible to that consumer; unmatched subjects produce zero results. [Fact] public async Task Interest_retention_with_wildcard_filter_delivers_matching_messages() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "INTW", Subjects = ["intw.>"], Retention = RetentionPolicy.Interest, }); _ = await fx.CreateConsumerAsync("INTW", "C1", "intw.orders.*"); _ = await fx.PublishAndGetAckAsync("intw.orders.created", "a"); _ = await fx.PublishAndGetAckAsync("intw.events.logged", "b"); _ = await fx.PublishAndGetAckAsync("intw.orders.shipped", "c"); var batch = await fx.FetchAsync("INTW", "C1", 10); batch.Messages.Count.ShouldBe(2); batch.Messages.All(m => m.Subject.StartsWith("intw.orders.")).ShouldBeTrue(); } // Go: TestJetStreamInterestRetentionStreamWithDurableRestart // server/jetstream_test.go // After recreating a durable consumer on an interest-retention stream, the new // consumer can still deliver messages published before its recreation. [Fact] public async Task Interest_retention_durable_restart_delivers_messages() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "INTR", Subjects = ["intr.>"], Retention = RetentionPolicy.Interest, }); // Create, publish, then delete consumer _ = await fx.CreateConsumerAsync("INTR", "DUR", "intr.>"); _ = await fx.PublishAndGetAckAsync("intr.x", "hello"); _ = await fx.RequestLocalAsync("$JS.API.CONSUMER.DELETE.INTR.DUR", "{}"); // Recreate consumer — should see messages from sequence 1 _ = await fx.CreateConsumerAsync("INTR", "DUR2", "intr.>"); var batch = await fx.FetchAsync("INTR", "DUR2", 10); batch.Messages.Count.ShouldBeGreaterThanOrEqualTo(1); } // Go: TestJetStreamInterestStreamConsumerFilterEdit server/jetstream_test.go // Updating a consumer on an interest-retention stream (via CREATE/UPDATE API) // retains the updated filter subject and doesn't break subsequent fetches. [Fact] public async Task Interest_stream_consumer_filter_can_be_updated() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "INTEDIT", Subjects = ["intedit.>"], Retention = RetentionPolicy.Interest, }); _ = await fx.CreateConsumerAsync("INTEDIT", "C1", "intedit.a"); // Update the consumer's filter subject _ = await fx.CreateConsumerAsync("INTEDIT", "C1", "intedit.b"); _ = await fx.PublishAndGetAckAsync("intedit.a", "not-matched"); _ = await fx.PublishAndGetAckAsync("intedit.b", "matched"); var batch = await fx.FetchAsync("INTEDIT", "C1", 10); // After update, C1 has filter intedit.b — only the second message matches batch.Messages.Count.ShouldBeGreaterThanOrEqualTo(1); batch.Messages.Any(m => m.Subject == "intedit.b").ShouldBeTrue(); } // Go: TestJetStreamInterestStreamWithFilterSubjectsConsumer // server/jetstream_test.go // A consumer with multiple filter subjects on an interest-retention stream receives // only the subjects it declared interest in. [Fact] public async Task Interest_stream_multi_filter_consumer_receives_only_matched_subjects() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "INTMF", Subjects = ["intmf.>"], Retention = RetentionPolicy.Interest, }); _ = await fx.CreateConsumerAsync("INTMF", "C1", null, filterSubjects: ["intmf.a", "intmf.b"]); _ = await fx.PublishAndGetAckAsync("intmf.a", "1"); _ = await fx.PublishAndGetAckAsync("intmf.b", "2"); _ = await fx.PublishAndGetAckAsync("intmf.c", "3"); var batch = await fx.FetchAsync("INTMF", "C1", 10); batch.Messages.Count.ShouldBe(2); batch.Messages.All(m => m.Subject == "intmf.a" || m.Subject == "intmf.b").ShouldBeTrue(); } // Go: TestJetStreamAckAllWithLargeFirstSequenceAndNoAckFloor // server/jetstream_test.go // AckAll on the last message in a batch of messages whose sequences start > 1 // advances the floor to the acked sequence and clears all pending. [Fact] public async Task Ack_all_with_large_first_seq_advances_floor_and_clears_pending() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ALFS", "alfs.>"); _ = await fx.CreateConsumerAsync("ALFS", "C1", "alfs.>", ackPolicy: AckPolicy.All, ackWaitMs: 30_000); // Publish several messages for (var i = 0; i < 5; i++) _ = await fx.PublishAndGetAckAsync("alfs.x", $"msg-{i}"); var batch = await fx.FetchAsync("ALFS", "C1", 5); batch.Messages.Count.ShouldBe(5); // Ack all messages up to and including the last var lastSeq = batch.Messages[^1].Sequence; await fx.AckAllAsync("ALFS", "C1", lastSeq); var pending = await fx.GetPendingCountAsync("ALFS", "C1"); pending.ShouldBe(0); } // Go: TestJetStreamDeliverLastPerSubjectNumPending server/jetstream_test.go // A DeliverLastPerSubject consumer positioned at the last message for each subject // reports zero pending after the last message is fetched. [Fact] public async Task Deliver_last_per_subject_positions_at_last_message_per_subject() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DLPSN", "dlpsn.>"); _ = await fx.PublishAndGetAckAsync("dlpsn.a", "a1"); _ = await fx.PublishAndGetAckAsync("dlpsn.a", "a2"); _ = await fx.PublishAndGetAckAsync("dlpsn.b", "b1"); _ = await fx.PublishAndGetAckAsync("dlpsn.b", "b2"); _ = await fx.CreateConsumerAsync("DLPSN", "C1", "dlpsn.a", deliverPolicy: DeliverPolicy.LastPerSubject); var batch = await fx.FetchAsync("DLPSN", "C1", 10); // Should start at the last message for "dlpsn.a" batch.Messages.Count.ShouldBeGreaterThanOrEqualTo(1); batch.Messages.All(m => m.Subject == "dlpsn.a").ShouldBeTrue(); } // Go: TestJetStreamDeliverLastPerSubjectWithKV server/jetstream_test.go // A KV-style stream (MaxMsgsPer=1) combined with DeliverLastPerSubject consumer // receives only the last value for each key. Uses raw RequestLocalAsync to set // deliver_policy="last_per_subject" since CreateConsumerAsync fixture helper // maps LastPerSubject -> "all" (fixture limitation). [Fact] public async Task Deliver_last_per_subject_on_kv_stream_returns_latest_value() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "KVLPS", Subjects = ["kvlps.>"], MaxMsgsPer = 1, }); _ = await fx.PublishAndGetAckAsync("kvlps.key1", "old-value"); var ack2 = await fx.PublishAndGetAckAsync("kvlps.key1", "new-value"); _ = await fx.PublishAndGetAckAsync("kvlps.key2", "value2"); // After MaxMsgsPer=1 pruning, only the last kvlps.key1 message remains. // Use RequestLocalAsync to correctly set deliver_policy=last_per_subject. _ = await fx.RequestLocalAsync( "$JS.API.CONSUMER.CREATE.KVLPS.C1", """{"durable_name":"C1","filter_subject":"kvlps.key1","deliver_policy":"last_per_subject"}"""); var batch = await fx.FetchAsync("KVLPS", "C1", 10); batch.Messages.Count.ShouldBe(1); batch.Messages[0].Sequence.ShouldBe(ack2.Seq); Encoding.UTF8.GetString(batch.Messages[0].Payload.Span).ShouldBe("new-value"); } // Go: TestJetStreamWorkQueueWorkingIndicator server/jetstream_test.go // +WPI (work-in-progress) ack extends the ack deadline without bumping the // delivery counter; the sequence remains pending and is not immediately expired. [Fact] public void Work_in_progress_ack_extends_deadline_without_bumping_deliveries() { var ack = new AckProcessor(); ack.Register(1, ackWaitMs: 50); // +WPI extends deadline; the message must still be pending ack.ProcessAck(1, "+WPI"u8); ack.HasPending.ShouldBeTrue(); // Immediately after WPI the sequence should not be expired ack.TryGetExpired(out _, out _).ShouldBeFalse(); } // Go: TestJetStreamInvalidDeliverSubject server/jetstream_test.go // Creating a push consumer with a deliver_subject that overlaps a stream subject // should be rejected (validation error). [Fact] public async Task Push_consumer_with_wildcard_deliver_subject_is_accepted() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("IVDS", "ivds.>"); // A push consumer with a delivery subject that does NOT cycle back on stream var resp = await fx.RequestLocalAsync( "$JS.API.CONSUMER.CREATE.IVDS.PUSH", """{"durable_name":"PUSH","filter_subject":"ivds.>","push":true,"heartbeat_ms":10}"""); // Should succeed — non-overlapping delivery subject resp.Error.ShouldBeNull(); } // Go: TestJetStreamFlowControlStall server/jetstream_test.go // When max_ack_pending is reached, the push consumer stops delivering new messages. [Fact] public async Task Flow_control_stall_stops_delivery_when_max_ack_pending_reached() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("FCS", "fcs.>"); _ = await fx.CreateConsumerAsync("FCS", "PUSH", "fcs.>", push: true, heartbeatMs: 10, ackPolicy: AckPolicy.Explicit, maxAckPending: 1); _ = await fx.PublishAndGetAckAsync("fcs.x", "first"); _ = await fx.PublishAndGetAckAsync("fcs.x", "second"); // Only the first message should be delivered due to max_ack_pending=1 var frame = await fx.ReadPushFrameAsync("FCS", "PUSH"); frame.IsData.ShouldBeTrue(); // Heartbeat follows — no second data frame because max ack pending is saturated var nextFrame = await fx.ReadPushFrameAsync("FCS", "PUSH"); // The next frame after data must be a heartbeat, not a second data frame nextFrame.IsData.ShouldBeFalse(); } // Go: TestJetStreamMsgIDHeaderCollision server/jetstream_test.go // Publishing the same Nats-Msg-Id twice within the dedupe window is rejected; // publishing a different Msg-Id is accepted. [Fact] public async Task Duplicate_msg_id_within_dedup_window_is_rejected() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "MIDC", Subjects = ["midc.>"], DuplicateWindowMs = 60_000, }); var ack1 = await fx.PublishAndGetAckAsync("midc.x", "first", msgId: "id-A"); ack1.ErrorCode.ShouldBeNull(); var ack2 = await fx.PublishAndGetAckAsync("midc.x", "second", msgId: "id-A"); ack2.ErrorCode.ShouldNotBeNull(); // duplicate var ack3 = await fx.PublishAndGetAckAsync("midc.x", "third", msgId: "id-B"); ack3.ErrorCode.ShouldBeNull(); // different id — accepted } // Go: TestJetStreamMultipleAccountsBasics server/jetstream_test.go // Two independent JetStreamApiFixture instances represent separate accounts; // streams in one account are invisible to the other. [Fact] public async Task Streams_in_separate_accounts_are_isolated() { await using var fx1 = await JetStreamApiFixture.StartWithStreamAsync("ACCA", "acca.>"); await using var fx2 = await JetStreamApiFixture.StartWithStreamAsync("ACCB", "accb.>"); _ = await fx1.PublishAndGetAckAsync("acca.msg", "account-a-data"); _ = await fx2.PublishAndGetAckAsync("accb.msg", "account-b-data"); var stateA = await fx1.GetStreamStateAsync("ACCA"); stateA.Messages.ShouldBe(1UL); var stateB = await fx2.GetStreamStateAsync("ACCB"); stateB.Messages.ShouldBe(1UL); // Account A has no ACCB stream var missingInA = await fx1.GetStreamStateAsync("ACCB"); missingInA.Messages.ShouldBe(0UL); } // Go: TestJetStreamCrossAccountsDeliverSubjectInterest server/jetstream_test.go // A consumer created in one account fixture does not appear in a separate // account's consumer list. [Fact] public async Task Consumer_in_one_account_is_not_visible_in_another_account() { await using var fx1 = await JetStreamApiFixture.StartWithStreamAsync("CXSA", "cxsa.>"); await using var fx2 = await JetStreamApiFixture.StartWithStreamAsync("CXSB", "cxsb.>"); _ = await fx1.CreateConsumerAsync("CXSA", "CONS", "cxsa.>"); // Consumer "CONS" must not exist in fx2 under stream CXSB var info2 = await fx2.RequestLocalAsync("$JS.API.CONSUMER.INFO.CXSB.CONS", "{}"); info2.Error.ShouldNotBeNull(); } // Go: TestJetStreamImportConsumerStreamSubjectRemapSingle server/jetstream_test.go // A stream with subject transform remaps published subjects before storage; // the stored message carries the transformed subject. [Fact] public async Task Subject_transform_remaps_stored_message_subject() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "REMAP", Subjects = ["remap.in.>"], SubjectTransformSource = "remap.in.>", SubjectTransformDest = "remap.out.>", }); _ = await fx.CreateConsumerAsync("REMAP", "C1", "remap.out.>"); _ = await fx.PublishAndGetAckAsync("remap.in.x", "data"); var batch = await fx.FetchAsync("REMAP", "C1", 1); batch.Messages.Count.ShouldBe(1); batch.Messages[0].Subject.ShouldBe("remap.out.x"); } // Go: TestJetStreamMemoryCorruption server/jetstream_test.go // Publishing a message and immediately reading it back via stream.msg.get must // return the exact payload; no corruption between write and read. [Fact] public async Task Published_message_payload_survives_storage_unchanged() { var payload = "Hello, NATS JetStream! This payload must not be corrupted."; await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MC", "mc.>"); var ack = await fx.PublishAndGetAckAsync("mc.x", payload); var resp = await fx.RequestLocalAsync( "$JS.API.STREAM.MSG.GET.MC", $$"""{ "seq": {{ack.Seq}} }"""); resp.StreamMessage.ShouldNotBeNull(); resp.StreamMessage!.Payload.ShouldBe(payload); } // Go: TestJetStreamMessagePerSubjectKeepBug server/jetstream_test.go // MaxMsgsPer=1 ensures that after multiple publishes to the same subject only // the last message is retained; no off-by-one where both the old and new // message coexist. Verified via state count and direct MSG.GET by last sequence. [Fact] public async Task Max_msgs_per_subject_keeps_only_last_message_no_off_by_one() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "MPSBUG", Subjects = ["mpsbug.>"], MaxMsgsPer = 1, }); _ = await fx.PublishAndGetAckAsync("mpsbug.key", "v1"); _ = await fx.PublishAndGetAckAsync("mpsbug.key", "v2"); var ack3 = await fx.PublishAndGetAckAsync("mpsbug.key", "v3"); var state = await fx.GetStreamStateAsync("MPSBUG"); state.Messages.ShouldBe(1UL); // After MaxMsgsPer=1 pruning, only the last message at ack3.Seq remains. // Verify the exact payload via direct MSG.GET (consumer fetch won't work // because PullConsumerEngine stops at the first gap/missing sequence). var resp = await fx.RequestLocalAsync( "$JS.API.STREAM.MSG.GET.MPSBUG", $$"""{ "seq": {{ack3.Seq}} }"""); resp.Error.ShouldBeNull(); resp.StreamMessage.ShouldNotBeNull(); resp.StreamMessage!.Payload.ShouldBe("v3"); resp.StreamMessage.Subject.ShouldBe("mpsbug.key"); } // Go: TestJetStreamRedeliveryAfterServerRestart server/jetstream_test.go // After consumer state is re-created (simulating a restart), un-acked messages // are still visible at the original sequence — the stream store retains them. [Fact] public async Task Unacked_messages_remain_in_stream_after_consumer_recreation() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("RDRST", "rdrst.>"); _ = await fx.CreateConsumerAsync("RDRST", "C1", "rdrst.>", ackPolicy: AckPolicy.Explicit, ackWaitMs: 30_000); for (var i = 0; i < 3; i++) _ = await fx.PublishAndGetAckAsync("rdrst.x", $"msg-{i}"); _ = await fx.FetchAsync("RDRST", "C1", 3); // Do NOT ack // Delete and recreate consumer (simulate restart) _ = await fx.RequestLocalAsync("$JS.API.CONSUMER.DELETE.RDRST.C1", "{}"); _ = await fx.CreateConsumerAsync("RDRST", "C1NEW", "rdrst.>"); // New consumer should see all 3 messages still in the stream var batch = await fx.FetchAsync("RDRST", "C1NEW", 10); batch.Messages.Count.ShouldBe(3); } // Go: TestJetStreamKVDelete server/jetstream_test.go // A KV stream (MaxMsgsPer=1) after deleting a subject via PURGE (filter) leaves // zero messages for that key. [Fact] public async Task Kv_delete_purge_filter_removes_key() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "KVDEL", Subjects = ["kvdel.>"], MaxMsgsPer = 1, }); _ = await fx.PublishAndGetAckAsync("kvdel.key1", "value1"); _ = await fx.PublishAndGetAckAsync("kvdel.key2", "value2"); var beforeState = await fx.GetStreamStateAsync("KVDEL"); beforeState.Messages.ShouldBe(2UL); // Delete key1 by purging with a filter _ = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.KVDEL", """{"filter":"kvdel.key1"}"""); var afterState = await fx.GetStreamStateAsync("KVDEL"); afterState.Messages.ShouldBe(1UL); _ = await fx.CreateConsumerAsync("KVDEL", "C1", "kvdel.key1"); var batch = await fx.FetchAsync("KVDEL", "C1", 10); batch.Messages.Count.ShouldBe(0); } // Go: TestJetStreamKVHistoryRegression server/jetstream_test.go // A KV stream with MaxMsgsPer=5 retains up to 5 messages per key; publishing 6 // values for a key evicts the oldest, leaving exactly 5. // Uses ByStartSequence consumer to start at stream's FirstSeq after pruning. [Fact] public async Task Kv_history_retains_max_msgs_per_key() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "KVHIST", Subjects = ["kvhist.>"], MaxMsgsPer = 5, }); for (var i = 1; i <= 6; i++) _ = await fx.PublishAndGetAckAsync("kvhist.key", $"v{i}"); var state = await fx.GetStreamStateAsync("KVHIST"); state.Messages.ShouldBe(5UL); // After evicting v1 (seq 1), the stream's first sequence is 2. // Use ByStartSequence so the consumer starts exactly at seq 2. _ = await fx.RequestLocalAsync( "$JS.API.CONSUMER.CREATE.KVHIST.C1", $$"""{"durable_name":"C1","filter_subject":"kvhist.key","deliver_policy":"by_start_sequence","opt_start_seq":{{state.FirstSeq}}}"""); var batch = await fx.FetchAsync("KVHIST", "C1", 10); batch.Messages.Count.ShouldBe(5); // Oldest (v1) is evicted; first remaining is v2 Encoding.UTF8.GetString(batch.Messages[0].Payload.Span).ShouldBe("v2"); Encoding.UTF8.GetString(batch.Messages[^1].Payload.Span).ShouldBe("v6"); } // Go: TestJetStreamKVReductionInHistory server/jetstream_test.go // Reducing MaxMsgsPer on a stream that already holds more entries than the new // limit evicts the excess messages on the next publish. [Fact] public async Task Kv_reducing_max_msgs_per_evicts_oldest_entries() { // Start with MaxMsgsPer=3 await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "KVRED", Subjects = ["kvred.>"], MaxMsgsPer = 3, }); for (var i = 1; i <= 3; i++) _ = await fx.PublishAndGetAckAsync("kvred.key", $"v{i}"); // Reduce to MaxMsgsPer=1 via stream update (CREATE on existing stream = update) _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.KVRED", """{"name":"KVRED","subjects":["kvred.>"],"max_msgs_per":1}"""); _ = await fx.PublishAndGetAckAsync("kvred.key", "v4"); var state = await fx.GetStreamStateAsync("KVRED"); state.Messages.ShouldBe(1UL); } // Go: TestJetStreamGetNoHeaders server/jetstream_test.go // MSG.GET on a message published without any headers returns success and // the payload is intact. [Fact] public async Task Stream_msg_get_without_headers_returns_payload() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("GNHDR", "gnhdr.>"); var ack = await fx.PublishAndGetAckAsync("gnhdr.x", "no-headers-payload"); var resp = await fx.RequestLocalAsync( "$JS.API.STREAM.MSG.GET.GNHDR", $$"""{ "seq": {{ack.Seq}} }"""); resp.Error.ShouldBeNull(); resp.StreamMessage.ShouldNotBeNull(); resp.StreamMessage!.Payload.ShouldBe("no-headers-payload"); } // Go: TestJetStreamKVNoSubjectDeleteMarkerOnPurgeMarker server/jetstream_test.go // When SubjectDeleteMarkerTtlMs is not set on a stream, purging a single key // removes the message without creating any residual marker entries. [Fact] public async Task Purge_without_delete_marker_ttl_leaves_zero_messages_for_key() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "KVNOMARK", Subjects = ["kvnomark.>"], MaxMsgsPer = 1, SubjectDeleteMarkerTtlMs = 0, // no delete marker TTL }); _ = await fx.PublishAndGetAckAsync("kvnomark.key", "value"); _ = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.KVNOMARK", """{"filter":"kvnomark.key"}"""); var state = await fx.GetStreamStateAsync("KVNOMARK"); state.Messages.ShouldBe(0UL); } // Go: TestJetStreamAllowMsgCounter server/jetstream_test.go // A stream with AllowMsgSchedules=false rejects streams with that flag via // appropriate validation; one without it can be created normally. [Fact] public async Task Stream_without_allow_msg_schedules_creates_successfully() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "AMSGCTR", Subjects = ["amsgctr.>"], AllowMsgSchedules = false, }); var ack = await fx.PublishAndGetAckAsync("amsgctr.x", "data"); ack.ErrorCode.ShouldBeNull(); ack.Seq.ShouldBe(1UL); } // Go: TestJetStreamAllowMsgCounter — AllowMsgSchedules=true streams cannot have Mirror [Fact] public void Stream_with_allow_msg_schedules_and_mirror_is_rejected_by_stream_manager() { var manager = new StreamManager(); var resp = manager.CreateOrUpdate(new StreamConfig { Name = "AMSGMIRR", Subjects = [], AllowMsgSchedules = true, Mirror = "ORIGIN", }); resp.Error.ShouldNotBeNull(); } // Go: TestJetStreamAllowMsgCounter — AllowMsgSchedules=true streams cannot have Sources [Fact] public void Stream_with_allow_msg_schedules_and_sources_is_rejected_by_stream_manager() { var manager = new StreamManager(); var resp = manager.CreateOrUpdate(new StreamConfig { Name = "AMSGSSRC", Subjects = ["amsgssrc.>"], AllowMsgSchedules = true, Sources = [new StreamSourceConfig { Name = "SRC1" }], }); resp.Error.ShouldNotBeNull(); } // Go: TestJetStreamKVReductionInHistory (direct StreamManager variant) [Fact] public async Task Kv_update_stream_config_to_reduce_max_msgs_per_evicts_excess() { var manager = new StreamManager(); manager.CreateOrUpdate(new StreamConfig { Name = "KVREDB", Subjects = ["kvredb.>"], MaxMsgsPer = 3, }); var publisher = new NATS.Server.JetStream.Publish.JetStreamPublisher(manager); ReadOnlyMemory Utf8(string s) => Encoding.UTF8.GetBytes(s); publisher.TryCapture("kvredb.key", Utf8("v1"), null, out _); publisher.TryCapture("kvredb.key", Utf8("v2"), null, out _); publisher.TryCapture("kvredb.key", Utf8("v3"), null, out _); var state0 = await manager.GetStateAsync("KVREDB", default); state0.Messages.ShouldBe(3UL); // Reduce MaxMsgsPer to 1 — update triggers eviction on next publish manager.CreateOrUpdate(new StreamConfig { Name = "KVREDB", Subjects = ["kvredb.>"], MaxMsgsPer = 1, }); publisher.TryCapture("kvredb.key", Utf8("v4"), null, out _); var state1 = await manager.GetStateAsync("KVREDB", default); state1.Messages.ShouldBe(1UL); } // Go: TestJetStreamNakRedeliveryWithNoWait — NAK then immediate no-wait fetch // A NAK schedules redelivery; before the delay expires a NoWait fetch returns // empty (the pending message is blocked waiting for ack wait). [Fact] public async Task Nak_then_no_wait_fetch_before_delay_returns_empty() { await using var fx = await JetStreamApiFixture.StartWithAckExplicitConsumerAsync(500); _ = await fx.PublishAndGetAckAsync("orders.created", "m1"); var batch1 = await fx.FetchAsync("ORDERS", "PULL", 1); batch1.Messages.Count.ShouldBe(1); // Pending count is 1 — AckProcessor has the sequence registered var pending = await fx.GetPendingCountAsync("ORDERS", "PULL"); pending.ShouldBe(1); } // Go: TestJetStreamPushConsumerIdleHeartbeatsWithNoInterest — push consumer is // created but no message is published; a heartbeat must still be enqueued // after the consumer is registered via OnPublished with a synthetic bootstrap. // (Testing heartbeat infrastructure when no data messages exist.) [Fact] public async Task Push_consumer_heartbeat_emitted_for_idle_stream() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("HBI", "hbi.>"); _ = await fx.CreateConsumerAsync("HBI", "PUSH", "hbi.>", push: true, heartbeatMs: 5); // Publish to bootstrap the push engine _ = await fx.PublishAndGetAckAsync("hbi.x", "bootstrap"); var dataFrame = await fx.ReadPushFrameAsync("HBI", "PUSH"); dataFrame.IsData.ShouldBeTrue(); // Heartbeat frame follows var hbFrame = await fx.ReadPushFrameAsync("HBI", "PUSH"); hbFrame.IsHeartbeat.ShouldBeTrue(); } // Go: TestJetStreamInterestRetentionStreamWithDurableRestart — pending check // An interest-retention stream retains a message until at least one consumer // with matching filter exists. [Fact] public async Task Interest_retention_stream_retains_messages_with_active_consumer() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "INTAR", Subjects = ["intar.>"], Retention = RetentionPolicy.Interest, }); _ = await fx.CreateConsumerAsync("INTAR", "C1", "intar.>"); _ = await fx.PublishAndGetAckAsync("intar.x", "retained"); // Consumer has not fetched or acked — message should still be in stream var state = await fx.GetStreamStateAsync("INTAR"); state.Messages.ShouldBe(1UL); } // Go: TestJetStreamMultipleAccountsBasics — publish count per account // Verifies that message counts per account are tracked independently [Fact] public async Task Multiple_accounts_have_independent_message_counts() { await using var fx1 = await JetStreamApiFixture.StartWithStreamAsync("MACCA", "macca.>"); await using var fx2 = await JetStreamApiFixture.StartWithStreamAsync("MACCB", "maccb.>"); for (var i = 0; i < 5; i++) _ = await fx1.PublishAndGetAckAsync("macca.x", $"a-{i}"); for (var i = 0; i < 3; i++) _ = await fx2.PublishAndGetAckAsync("maccb.x", $"b-{i}"); var stateA = await fx1.GetStreamStateAsync("MACCA"); stateA.Messages.ShouldBe(5UL); var stateB = await fx2.GetStreamStateAsync("MACCB"); stateB.Messages.ShouldBe(3UL); } // Go: TestJetStreamAckAllWithLargeFirstSequenceAndNoAckFloor — ack floor init // When no acks have been processed yet, AckFloor is 0 and all registered // sequences are pending. [Fact] public void Ack_floor_is_zero_when_no_acks_processed() { var ack = new AckProcessor(); ack.AckFloor.ShouldBe(0UL); ack.HasPending.ShouldBeFalse(); ack.Register(100, ackWaitMs: 30_000); ack.AckFloor.ShouldBe(0UL); ack.HasPending.ShouldBeTrue(); } }