using NSubstitute; using NATS.Server.JetStream.MirrorSource; using NATS.Server.JetStream.Models; using NATS.Server.JetStream.Storage; namespace NATS.Server.JetStream.Tests; // Go reference: server/stream.go:3478-3505 (calculateRetryBackoff), // server/stream.go:3125-3400 (setupMirrorConsumer retry logic) public class MirrorSourceRetryTests { [Fact] public void Mirror_retry_uses_exponential_backoff() { // Go reference: server/stream.go:3478-3505 calculateRetryBackoff var mirror = MirrorCoordinatorTestHelper.Create(); mirror.RecordFailure(); var delay1 = mirror.GetRetryDelay(); delay1.ShouldBeGreaterThanOrEqualTo(TimeSpan.FromMilliseconds(250)); mirror.RecordFailure(); var delay2 = mirror.GetRetryDelay(); delay2.ShouldBeGreaterThan(delay1); // Cap at max for (int i = 0; i < 20; i++) mirror.RecordFailure(); var delayMax = mirror.GetRetryDelay(); delayMax.ShouldBeLessThanOrEqualTo(TimeSpan.FromSeconds(30)); } [Fact] public void Mirror_success_resets_backoff() { // Go reference: server/stream.go setupMirrorConsumer — success resets retry var mirror = MirrorCoordinatorTestHelper.Create(); for (int i = 0; i < 5; i++) mirror.RecordFailure(); mirror.RecordSuccess(); var delay = mirror.GetRetryDelay(); delay.ShouldBe(TimeSpan.FromMilliseconds(250)); } [Fact] public void Mirror_tracks_sequence_gap() { // Go reference: server/stream.go:2863-3014 processInboundMirrorMsg — gap detection var mirror = MirrorCoordinatorTestHelper.Create(); mirror.RecordSourceSeq(1); mirror.RecordSourceSeq(2); mirror.RecordSourceSeq(5); // gap: 3, 4 missing mirror.HasGap.ShouldBeTrue(); mirror.GapStart.ShouldBe(3UL); mirror.GapEnd.ShouldBe(4UL); } [Fact] public void Mirror_tracks_error_state() { // Go reference: server/stream.go mirror error state tracking var mirror = MirrorCoordinatorTestHelper.Create(); mirror.SetError("connection refused"); mirror.HasError.ShouldBeTrue(); mirror.ErrorMessage.ShouldBe("connection refused"); mirror.ClearError(); mirror.HasError.ShouldBeFalse(); } [Fact] public void Source_dedup_window_prunes_expired() { // Go reference: server/stream.go duplicate window pruning var source = SourceCoordinatorTestHelper.Create(); source.RecordMsgId("msg-1"); source.RecordMsgId("msg-2"); source.IsDuplicate("msg-1").ShouldBeTrue(); source.IsDuplicate("msg-3").ShouldBeFalse(); // Simulate time passing beyond dedup window source.PruneDedupWindow(DateTimeOffset.UtcNow.AddMinutes(5)); source.IsDuplicate("msg-1").ShouldBeFalse(); } } public static class MirrorCoordinatorTestHelper { public static MirrorCoordinator Create() { var store = Substitute.For(); return new MirrorCoordinator(store); } } public static class SourceCoordinatorTestHelper { public static SourceCoordinator Create() { var store = Substitute.For(); var config = new StreamSourceConfig { Name = "test-source", DuplicateWindowMs = 60_000 }; return new SourceCoordinator(store, config); } }