using Microsoft.Extensions.Options; using Omika.Diagnostics.Import; using Omika.Diagnostics.Tests.Fakes; namespace Omika.Diagnostics.Tests; /// §52 — Fail-open PostgreSQL : l'importeur survit, backoff, JSONL intact, checkpoint non avancé. public class DiagnosticImporterFailOpenTests { [Fact] public async Task RepositoryDown_ReturnsRetryable_AndDoesNotAdvanceCheckpoint() { var source = new FakeDiagnosticJsonlSource(new[] { DiagnosticTestData.Record(Guid.NewGuid(), "A"), DiagnosticTestData.Record(Guid.NewGuid(), "B") }); var importer = new DiagnosticImporter( source, new ThrowingDiagnosticRepository(), Options.Create(new DiagnosticImportOptions { BatchSize = 100 })); DiagnosticImportResult result = await importer.ImportOnceAsync(); Assert.False(result.Succeeded); Assert.True(result.Retryable); Assert.Equal("InvalidOperationException", result.Error); // Le checkpoint n'a PAS avancé : le JSONL reste intact et sera relu. Assert.Equal(0, source.CommitCount); Assert.Null(source.LoadCheckpoint()); } [Fact] public async Task RepositoryDown_NextPassReadsSameRecords() { var source = new FakeDiagnosticJsonlSource(new[] { DiagnosticTestData.Record(Guid.NewGuid(), "A") }); var importer = new DiagnosticImporter( source, new ThrowingDiagnosticRepository(), Options.Create(new DiagnosticImportOptions { BatchSize = 100 })); await importer.ImportOnceAsync(); await importer.ImportOnceAsync(); // Toujours 0 commit : reprise idempotente, jamais de perte de données. Assert.Equal(0, source.CommitCount); } [Fact] public async Task Backoff_IsBoundedExponential_NoCpuSpin() { var options = new DiagnosticImportOptions { RetryDelaySeconds = 5, MaxRetryDelaySeconds = 300 }; TimeSpan first = DiagnosticBackoff.Compute(options, 1); TimeSpan second = DiagnosticBackoff.Compute(options, 2); TimeSpan capped = DiagnosticBackoff.Compute(options, 1000); Assert.True(second > first); Assert.True(capped <= TimeSpan.FromSeconds(options.MaxRetryDelaySeconds)); } }