// // Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later. // using System.Collections.Immutable; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Testing.Temporal; namespace StellaOps.Testing.Chaos.Tests; /// /// Unit tests for . /// public sealed class ConvergenceTrackerTests { private readonly SimulatedTimeProvider _timeProvider; private readonly DefaultConvergenceTracker _tracker; public ConvergenceTrackerTests() { _timeProvider = new SimulatedTimeProvider(); _tracker = new DefaultConvergenceTracker( _timeProvider, NullLogger.Instance, pollInterval: TimeSpan.FromMilliseconds(1)); // Use 1ms to avoid real delays } [Fact] public async Task CaptureSnapshotAsync_NoProbes_ReturnsEmptySnapshot() { // Act var snapshot = await _tracker.CaptureSnapshotAsync(TestContext.Current.CancellationToken); // Assert Assert.Empty(snapshot.ProbeResults); Assert.Equal(_timeProvider.GetUtcNow(), snapshot.CapturedAt); } [Fact] public async Task CaptureSnapshotAsync_WithProbes_CapturesAllResults() { // Arrange var probe1 = new DelegateProbe("probe-1", _ => Task.FromResult( new ProbeResult(true, ImmutableDictionary.Empty, []))); var probe2 = new DelegateProbe("probe-2", _ => Task.FromResult( new ProbeResult(false, ImmutableDictionary.Empty, ["error"]))); _tracker.RegisterProbe(probe1); _tracker.RegisterProbe(probe2); // Act var snapshot = await _tracker.CaptureSnapshotAsync(TestContext.Current.CancellationToken); // Assert Assert.Equal(2, snapshot.ProbeResults.Count); Assert.True(snapshot.ProbeResults["probe-1"].IsHealthy); Assert.False(snapshot.ProbeResults["probe-2"].IsHealthy); } [Fact] public async Task CaptureSnapshotAsync_ProbeThrows_RecordsFailure() { // Arrange var failingProbe = new DelegateProbe("failing", _ => throw new InvalidOperationException("Probe failed")); _tracker.RegisterProbe(failingProbe); // Act var snapshot = await _tracker.CaptureSnapshotAsync(TestContext.Current.CancellationToken); // Assert Assert.Single(snapshot.ProbeResults); Assert.False(snapshot.ProbeResults["failing"].IsHealthy); Assert.Contains("Probe failed", snapshot.ProbeResults["failing"].Anomalies[0]); } [Fact] public async Task RegisterProbe_AddsProbe() { // Arrange var probe = new DelegateProbe("test", _ => Task.FromResult( new ProbeResult(true, ImmutableDictionary.Empty, []))); // Act _tracker.RegisterProbe(probe); // Assert - should be included in snapshot var snapshot = await _tracker.CaptureSnapshotAsync(TestContext.Current.CancellationToken); Assert.Contains("test", snapshot.ProbeResults.Keys); } [Fact] public async Task UnregisterProbe_RemovesProbe() { // Arrange var probe = new DelegateProbe("test", _ => Task.FromResult( new ProbeResult(true, ImmutableDictionary.Empty, []))); _tracker.RegisterProbe(probe); // Act _tracker.UnregisterProbe("test"); // Assert - should not be in snapshot var snapshot = await _tracker.CaptureSnapshotAsync(TestContext.Current.CancellationToken); Assert.DoesNotContain("test", snapshot.ProbeResults.Keys); } [Fact] public async Task WaitForConvergenceAsync_AllHealthy_ReturnsConverged() { // Arrange var probe = new DelegateProbe("healthy", _ => Task.FromResult( new ProbeResult(true, ImmutableDictionary.Empty, []))); _tracker.RegisterProbe(probe); var expectations = new ConvergenceExpectations(RequireAllHealthy: true); // Act var result = await _tracker.WaitForConvergenceAsync(expectations, TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); // Assert Assert.True(result.HasConverged); Assert.Empty(result.Violations); Assert.Equal(1, result.ConvergenceAttempts); Assert.NotNull(result.TimeToConverge); } [Fact] public async Task WaitForConvergenceAsync_UnhealthyComponent_ReturnsNotConverged() { // Arrange var probe = new DelegateProbe("unhealthy", _ => Task.FromResult( new ProbeResult(false, ImmutableDictionary.Empty, []))); _tracker.RegisterProbe(probe); var expectations = new ConvergenceExpectations(RequireAllHealthy: true); // Act var result = await _tracker.WaitForConvergenceAsync(expectations, TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken); // Assert Assert.False(result.HasConverged); Assert.Contains("Unhealthy components: unhealthy", result.Violations); Assert.Null(result.TimeToConverge); } [Fact] public async Task WaitForConvergenceAsync_EventuallyConverges_ReturnsSuccess() { // Arrange var callCount = 0; var probe = new DelegateProbe("eventual", _ => { callCount++; var isHealthy = callCount >= 3; // Becomes healthy after 2 failures return Task.FromResult( new ProbeResult(isHealthy, ImmutableDictionary.Empty, [])); }); _tracker.RegisterProbe(probe); var expectations = new ConvergenceExpectations(RequireAllHealthy: true); // Act var result = await _tracker.WaitForConvergenceAsync(expectations, TimeSpan.FromMilliseconds(100), TestContext.Current.CancellationToken); // Assert Assert.True(result.HasConverged); Assert.True(result.ConvergenceAttempts >= 3); // At least 3 attempts to converge } [Fact] public async Task WaitForConvergenceAsync_RequiredComponent_NotFound_ReportsViolation() { // Arrange var expectations = new ConvergenceExpectations( RequireAllHealthy: false, RequiredHealthyComponents: ["missing-component"]); // Act var result = await _tracker.WaitForConvergenceAsync(expectations, TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken); // Assert Assert.False(result.HasConverged); Assert.Contains("Required component 'missing-component' not found", result.Violations); } [Fact] public async Task WaitForConvergenceAsync_RequiredComponent_Unhealthy_ReportsViolation() { // Arrange var probe = new DelegateProbe("critical-service", _ => Task.FromResult( new ProbeResult(false, ImmutableDictionary.Empty, []))); _tracker.RegisterProbe(probe); var expectations = new ConvergenceExpectations( RequireAllHealthy: false, RequiredHealthyComponents: ["critical-service"]); // Act var result = await _tracker.WaitForConvergenceAsync(expectations, TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken); // Assert Assert.False(result.HasConverged); Assert.Contains("Required component 'critical-service' is unhealthy", result.Violations); } [Fact] public async Task WaitForConvergenceAsync_Cancellation_Throws() { // Arrange var probe = new DelegateProbe("slow", async ct => { await Task.Delay(TimeSpan.FromSeconds(10), ct); return new ProbeResult(true, ImmutableDictionary.Empty, []); }); _tracker.RegisterProbe(probe); using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50)); // Act & Assert await Assert.ThrowsAsync( () => _tracker.WaitForConvergenceAsync( new ConvergenceExpectations(), TimeSpan.FromSeconds(10), cts.Token)); } [Fact] public async Task WaitForConvergenceAsync_OrphanedResources_ReportsViolation() { // Arrange var probe = new DelegateProbe("resource-tracker", _ => Task.FromResult( new ProbeResult(true, ImmutableDictionary.Empty, ["orphan file detected"]))); _tracker.RegisterProbe(probe); var expectations = new ConvergenceExpectations(RequireNoOrphanedResources: true); // Act var result = await _tracker.WaitForConvergenceAsync(expectations, TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken); // Assert Assert.False(result.HasConverged); Assert.Contains(result.Violations, v => v.Contains("Orphaned resources")); } [Fact] public async Task WaitForConvergenceAsync_MetricValidation_ReportsViolation() { // Arrange var metrics = new Dictionary { ["cpu_usage"] = 95.0 }; var probe = new DelegateProbe("metrics", _ => Task.FromResult( new ProbeResult(true, metrics.ToImmutableDictionary(), []))); _tracker.RegisterProbe(probe); var validators = new Dictionary> { ["cpu_usage"] = value => (double)value < 80.0 // Should fail - CPU is 95% }.ToImmutableDictionary(); var expectations = new ConvergenceExpectations( RequireAllHealthy: false, RequireMetricsAccurate: true, MetricValidators: validators); // Act var result = await _tracker.WaitForConvergenceAsync(expectations, TimeSpan.FromMilliseconds(50), TestContext.Current.CancellationToken); // Assert Assert.False(result.HasConverged); Assert.Contains("Metric 'cpu_usage' failed validation", result.Violations); } } /// /// Unit tests for probe implementations. /// public sealed class ProbeTests { [Fact] public async Task ComponentHealthProbe_ReturnsInjectorHealth() { // Arrange var registry = new FailureInjectorRegistry(); var injector = registry.GetOrCreateInjector("postgres-main"); await injector.InjectAsync("postgres-main", FailureType.Degraded, TestContext.Current.CancellationToken); var probe = new ComponentHealthProbe(registry, "postgres-main"); // Act var result = await probe.ProbeAsync(TestContext.Current.CancellationToken); // Assert Assert.False(result.IsHealthy); Assert.Equal("component:postgres-main", probe.Name); } [Fact] public async Task DelegateProbe_ExecutesDelegate() { // Arrange var executed = false; var probe = new DelegateProbe("custom", _ => { executed = true; return Task.FromResult(new ProbeResult( true, ImmutableDictionary.Empty, [])); }); // Act var result = await probe.ProbeAsync(TestContext.Current.CancellationToken); // Assert Assert.True(executed); Assert.True(result.IsHealthy); Assert.Equal("custom", probe.Name); } [Fact] public async Task AggregateProbe_CombinesResults() { // Arrange var probe1 = new DelegateProbe("p1", _ => Task.FromResult( new ProbeResult(true, new Dictionary { ["m1"] = 1 }.ToImmutableDictionary(), []))); var probe2 = new DelegateProbe("p2", _ => Task.FromResult( new ProbeResult(false, new Dictionary { ["m2"] = 2 }.ToImmutableDictionary(), ["error"]))); var aggregate = new AggregateProbe("combined", [probe1, probe2]); // Act var result = await aggregate.ProbeAsync(TestContext.Current.CancellationToken); // Assert Assert.False(result.IsHealthy); // One unhealthy means aggregate is unhealthy Assert.Equal(2, result.Metrics.Count); Assert.Contains("p1:m1", result.Metrics.Keys); Assert.Contains("p2:m2", result.Metrics.Keys); Assert.Single(result.Anomalies); Assert.Contains("p2: error", result.Anomalies); Assert.Equal("combined", aggregate.Name); } [Fact] public async Task AggregateProbe_AllHealthy_IsHealthy() { // Arrange var probe1 = new DelegateProbe("p1", _ => Task.FromResult( new ProbeResult(true, ImmutableDictionary.Empty, []))); var probe2 = new DelegateProbe("p2", _ => Task.FromResult( new ProbeResult(true, ImmutableDictionary.Empty, []))); var aggregate = new AggregateProbe("all-healthy", [probe1, probe2]); // Act var result = await aggregate.ProbeAsync(TestContext.Current.CancellationToken); // Assert Assert.True(result.IsHealthy); Assert.Empty(result.Anomalies); } }