using Omika.Core.Observability.External; namespace Omika.Tests.Observability.External; /// Disjoncteur du pont externe (§16) : déterministe (horloge injectée). public class ExternalCircuitBreakerTests { [Fact] public void StartsHealthy() { var circuit = NewCircuit(); Assert.Equal(ExternalObservabilityState.Healthy, circuit.State); Assert.True(circuit.AllowExport()); } [Fact] public void OpensAfterThresholdFailures() { var circuit = NewCircuit(); // seuil = 3 circuit.RecordFailure(); circuit.RecordFailure(); Assert.Equal(ExternalObservabilityState.Degraded, circuit.State); Assert.True(circuit.AllowExport()); // dégradé mais autorisé circuit.RecordFailure(); Assert.Equal(ExternalObservabilityState.Open, circuit.State); } [Fact] public void DropsWhileOpen() { var clock = new MutableClock(); var circuit = new ExternalCircuitBreaker(2, TimeSpan.FromSeconds(30), clock.Now); circuit.RecordFailure(); circuit.RecordFailure(); Assert.False(circuit.AllowExport()); // open : drop Assert.False(circuit.AllowExport()); } [Fact] public void ProbesAfterCooldown() { var clock = new MutableClock(); var circuit = new ExternalCircuitBreaker(1, TimeSpan.FromSeconds(30), clock.Now); circuit.RecordFailure(); Assert.False(circuit.AllowExport()); clock.Advance(TimeSpan.FromSeconds(31)); Assert.True(circuit.AllowExport()); // half-open : autorise UNE sonde Assert.Equal(ExternalObservabilityState.Degraded, circuit.State); } [Fact] public void SuccessClosesCircuit() { var clock = new MutableClock(); var circuit = new ExternalCircuitBreaker(1, TimeSpan.FromSeconds(30), clock.Now); circuit.RecordFailure(); clock.Advance(TimeSpan.FromSeconds(31)); Assert.True(circuit.AllowExport()); circuit.RecordSuccess(); Assert.Equal(ExternalObservabilityState.Healthy, circuit.State); Assert.True(circuit.AllowExport()); } [Fact] public void ProbeFailureReopensCircuit() { var clock = new MutableClock(); var circuit = new ExternalCircuitBreaker(1, TimeSpan.FromSeconds(30), clock.Now); circuit.RecordFailure(); clock.Advance(TimeSpan.FromSeconds(31)); Assert.True(circuit.AllowExport()); // half-open sonde circuit.RecordFailure(); // sonde échoue → ré-ouverture Assert.Equal(ExternalObservabilityState.Open, circuit.State); Assert.False(circuit.AllowExport()); } [Fact] public void NeverAllowsNegativeOrZeroCooldown() { var clock = new MutableClock(); var circuit = new ExternalCircuitBreaker(1, TimeSpan.Zero, clock.Now); circuit.RecordFailure(); Assert.False(circuit.AllowExport()); // cooldown ramené à >= 1 s : encore ouvert clock.Advance(TimeSpan.FromSeconds(2)); Assert.True(circuit.AllowExport()); // sonde possible après cooldown positif } private static ExternalCircuitBreaker NewCircuit() => new(3, TimeSpan.FromSeconds(30)); private sealed class MutableClock { private DateTimeOffset _now = new(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); public DateTimeOffset Now() => _now; public void Advance(TimeSpan delta) => _now += delta; } }