// Go: consumer.go:2550 (processAckMsg, processNak, processTerm, processAckProgress) using NATS.Server.JetStream.Consumers; namespace NATS.Server.Tests.JetStream.Consumers; public class AckProcessorNakTests { // Test 1: ProcessAck with empty payload acks the sequence [Fact] public void ProcessAck_empty_payload_acks_sequence() { // Go: consumer.go — empty ack payload treated as "+ACK" var ack = new AckProcessor(); ack.Register(1, ackWaitMs: 5000); ack.ProcessAck(1, ReadOnlySpan.Empty); ack.PendingCount.ShouldBe(0); ack.AckFloor.ShouldBe((ulong)1); } // Test 2: ProcessAck with -NAK schedules redelivery [Fact] public async Task ProcessAck_nak_payload_schedules_redelivery() { // Go: consumer.go — "-NAK" triggers rescheduled redelivery var ack = new AckProcessor(); ack.Register(1, ackWaitMs: 5000); ack.ProcessAck(1, "-NAK"u8); // Should still be pending (redelivery scheduled) ack.PendingCount.ShouldBe(1); // Should expire quickly (using ackWait fallback of 5000ms — verify it is still pending now) ack.TryGetExpired(out _, out _).ShouldBeFalse(); await Task.CompletedTask; } // Test 3: ProcessAck with -NAK {delay} uses custom delay [Fact] public async Task ProcessAck_nak_with_delay_uses_custom_delay() { // Go: consumer.go — "-NAK {delay}" parses optional explicit delay in milliseconds var ack = new AckProcessor(); ack.Register(1, ackWaitMs: 5000); ack.ProcessAck(1, "-NAK 1"u8); // Sequence still pending ack.PendingCount.ShouldBe(1); // With a 1ms delay, should expire quickly await Task.Delay(10); ack.TryGetExpired(out var seq, out _).ShouldBeTrue(); seq.ShouldBe((ulong)1); } // Test 4: ProcessAck with +TERM removes from pending [Fact] public void ProcessAck_term_removes_from_pending() { // Go: consumer.go — "+TERM" permanently terminates delivery; sequence never redelivered var ack = new AckProcessor(); ack.Register(1, ackWaitMs: 5000); ack.ProcessAck(1, "+TERM"u8); ack.PendingCount.ShouldBe(0); ack.HasPending.ShouldBeFalse(); } // Test 5: ProcessAck with +WPI resets deadline without incrementing delivery count [Fact] public async Task ProcessAck_wpi_resets_deadline_without_incrementing_deliveries() { // Go: consumer.go — "+WPI" resets ack deadline; delivery count must not change var ack = new AckProcessor(); ack.Register(1, ackWaitMs: 10); // Wait for the deadline to approach, then reset it via progress await Task.Delay(5); ack.ProcessAck(1, "+WPI"u8); // Deadline was just reset — should not be expired yet ack.TryGetExpired(out _, out var deliveries).ShouldBeFalse(); // Deliveries count must remain at 1 (not incremented by WPI) deliveries.ShouldBe(0); // Sequence still pending ack.PendingCount.ShouldBe(1); } // Test 6: Backoff array applies correct delay per redelivery attempt [Fact] public async Task ProcessNak_backoff_array_applies_delay_by_delivery_count() { // Go: consumer.go — backoff array indexes by (deliveries - 1) var ack = new AckProcessor(backoffMs: [1, 50, 5000]); ack.Register(1, ackWaitMs: 5000); // First NAK — delivery count is 1 → backoff[0] = 1ms ack.ProcessNak(1); await Task.Delay(10); ack.TryGetExpired(out _, out _).ShouldBeTrue(); // Now delivery count is 2 → backoff[1] = 50ms ack.ProcessNak(1); ack.TryGetExpired(out _, out _).ShouldBeFalse(); } // Test 7: Backoff array clamps at last entry for high delivery counts [Fact] public async Task ProcessNak_backoff_clamps_at_last_entry_for_high_delivery_count() { // Go: consumer.go — backoff index clamped to backoff.Length-1 when deliveries exceed array size var ack = new AckProcessor(backoffMs: [1, 2]); ack.Register(1, ackWaitMs: 5000); // Drive deliveries up: NAK twice to advance delivery count past array length ack.ProcessNak(1); // deliveries becomes 2 (index 1 = 2ms) await Task.Delay(10); ack.TryGetExpired(out _, out _).ShouldBeTrue(); ack.ProcessNak(1); // deliveries becomes 3 (index clamps to 1 = 2ms) await Task.Delay(10); ack.TryGetExpired(out var seq, out _).ShouldBeTrue(); seq.ShouldBe((ulong)1); } // Test 8: AckSequence advances AckFloor when contiguous [Fact] public void AckSequence_advances_ackfloor_for_contiguous_sequences() { // Go: consumer.go — acking contiguous sequences from floor advances AckFloor monotonically var ack = new AckProcessor(); ack.Register(1, ackWaitMs: 5000); ack.Register(2, ackWaitMs: 5000); ack.Register(3, ackWaitMs: 5000); ack.AckSequence(1); ack.AckFloor.ShouldBe((ulong)1); ack.AckSequence(2); ack.AckFloor.ShouldBe((ulong)2); } // Test 9: ProcessTerm increments TerminatedCount [Fact] public void ProcessTerm_increments_terminated_count() { // Go: consumer.go — terminated sequences tracked separately from acked sequences var ack = new AckProcessor(); ack.Register(1, ackWaitMs: 5000); ack.Register(2, ackWaitMs: 5000); ack.TerminatedCount.ShouldBe(0); ack.ProcessTerm(1); ack.TerminatedCount.ShouldBe(1); ack.ProcessTerm(2); ack.TerminatedCount.ShouldBe(2); } // Test 10: NAK after TERM is ignored (sequence already terminated) [Fact] public void ProcessNak_after_term_is_ignored() { // Go: consumer.go — once terminated, a sequence cannot be rescheduled via NAK var ack = new AckProcessor(backoffMs: [1]); ack.Register(1, ackWaitMs: 5000); ack.ProcessTerm(1); ack.PendingCount.ShouldBe(0); // Attempting to NAK a terminated sequence has no effect ack.ProcessNak(1); ack.PendingCount.ShouldBe(0); ack.TerminatedCount.ShouldBe(1); } }