using NATS.Server.JetStream.Consumers; namespace NATS.Server.JetStream.Tests.JetStream.Consumers; /// /// Tests for enhanced AckProcessor with RedeliveryTracker integration. /// Go reference: consumer.go:4854 (processInboundAcks). /// public class AckProcessorEnhancedTests { [Fact] public void ProcessAck_removes_from_pending() { var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 30000); var processor = new AckProcessor(tracker); processor.Register(1, "deliver.subj"); processor.PendingCount.ShouldBe(1); processor.ProcessAck(1); processor.PendingCount.ShouldBe(0); } [Fact] public void ProcessNak_schedules_redelivery() { var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 30000); var processor = new AckProcessor(tracker); processor.Register(1, "deliver.subj"); processor.ProcessNak(1, delayMs: 500); processor.PendingCount.ShouldBe(1); // still pending until redelivered } [Fact] public void ProcessTerm_removes_permanently() { var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 30000); var processor = new AckProcessor(tracker); processor.Register(1, "deliver.subj"); processor.ProcessTerm(1); processor.PendingCount.ShouldBe(0); processor.TerminatedCount.ShouldBe(1); } [Fact] public void ProcessProgress_resets_deadline_to_full_ack_wait() { // Go: consumer.go — processAckProgress (+WPI): resets deadline to UtcNow + ackWait // Verify the invariant: after ProcessProgress, the deadline is strictly in the future // by at least (ackWait - epsilon) milliseconds, without relying on wall-clock delays. var ackWaitMs = 1000; var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: ackWaitMs); var processor = new AckProcessor(tracker); processor.Register(1, "deliver.subj"); var before = DateTimeOffset.UtcNow; processor.ProcessProgress(1); var after = DateTimeOffset.UtcNow; var deadline = processor.GetDeadline(1); // Deadline must be at least (before + ackWait) and at most (after + ackWait + epsilon) deadline.ShouldBeGreaterThanOrEqualTo(before.AddMilliseconds(ackWaitMs)); deadline.ShouldBeLessThanOrEqualTo(after.AddMilliseconds(ackWaitMs + 50)); } [Fact] public void MaxAckPending_blocks_new_registrations() { var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 30000); var processor = new AckProcessor(tracker, maxAckPending: 2); processor.Register(1, "d.1"); processor.Register(2, "d.2"); processor.CanRegister().ShouldBeFalse(); } [Fact] public void CanRegister_true_when_unlimited() { var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 30000); var processor = new AckProcessor(tracker); // maxAckPending=0 means unlimited processor.Register(1, "d.1"); processor.CanRegister().ShouldBeTrue(); } [Fact] public void ParseAckType_identifies_all_types() { AckProcessor.ParseAckType("+ACK"u8).ShouldBe(AckType.Ack); AckProcessor.ParseAckType("-NAK"u8).ShouldBe(AckType.Nak); AckProcessor.ParseAckType("+TERM"u8).ShouldBe(AckType.Term); AckProcessor.ParseAckType("+WPI"u8).ShouldBe(AckType.Progress); } [Fact] public void ParseAckType_returns_unknown_for_invalid() { AckProcessor.ParseAckType("GARBAGE"u8).ShouldBe(AckType.Unknown); AckProcessor.ParseAckType(""u8).ShouldBe(AckType.Unknown); } [Fact] public void GetDeadline_returns_min_for_unknown_sequence() { var tracker = new RedeliveryTracker(maxDeliveries: 5, ackWaitMs: 1000); var processor = new AckProcessor(tracker); // Unknown sequence should return DateTimeOffset.MinValue processor.GetDeadline(999).ShouldBe(DateTimeOffset.MinValue); } }