using Omika.Core.Observability.Forwarding; namespace Omika.Tests.Observability.Forwarding; /// Tests du spool disque du forwarder (checkpoint, bornes, suppression, quota, rétention). public class DiagnosticsForwarderSpoolTests : IDisposable { private const string File001 = "omika-mobile-diagnostic-20261002-001.jsonl"; private const string File002 = "omika-mobile-diagnostic-20261002-002.jsonl"; private const string File003 = "omika-mobile-diagnostic-20261002-003.jsonl"; private readonly string _dir = ForwardingTestHarness.CreateTempDirectory(); private static DiagnosticsForwardingSpoolOptions DefaultSpool => new(); public void Dispose() => ForwardingTestHarness.DeleteDirectory(_dir); private DiagnosticsForwarderSpool NewSpool(DiagnosticsForwardingSpoolOptions? options = null) => new(_dir, "forwarder-checkpoint.json", options ?? DefaultSpool); [Fact] public void ReadNext_EmptyDirectory_ReturnsReachedEnd() { var spool = NewSpool(); DiagnosticsForwarderBatch batch = spool.ReadNext(null, 10, 1024); Assert.Empty(batch.Records); Assert.True(batch.ReachedEnd); } [Fact] public void ReadNext_ReadsRecordsInOrder_AndResumesAcrossFiles() { ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord(component: "a"), ForwardingTestHarness.NewRecord(component: "b")]); ForwardingTestHarness.WriteSpoolFile(_dir, File002, [ForwardingTestHarness.NewRecord(component: "c")]); var spool = NewSpool(); DiagnosticsForwarderBatch first = spool.ReadNext(null, 2, 1_000_000); Assert.Equal(2, first.Records.Count); Assert.Equal("a", first.Records[0].Event.Component); Assert.Equal("b", first.Records[1].Event.Component); Assert.NotNull(first.NextPosition); Assert.Equal(File001, first.NextPosition!.FileName); DiagnosticsForwarderBatch second = spool.ReadNext(first.NextPosition, 2, 1_000_000); Assert.Single(second.Records); Assert.Equal("c", second.Records[0].Event.Component); Assert.Equal(File002, second.NextPosition!.FileName); } [Fact] public void ReadNext_RespectsMaxBytes_StillReturnsAtLeastOneRecord() { ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord(), ForwardingTestHarness.NewRecord()]); var spool = NewSpool(); DiagnosticsForwarderBatch batch = spool.ReadNext(null, 100, 10); Assert.Single(batch.Records); // premier seuil (octets) : 1 seul, jamais zéro. } [Fact] public void Commit_ThenLoadCheckpoint_ResumesFromNextLine() { ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord(component: "a"), ForwardingTestHarness.NewRecord(component: "b"), ForwardingTestHarness.NewRecord(component: "c")]); var spool = NewSpool(); DiagnosticsForwarderBatch first = spool.ReadNext(null, 1, 1_000_000); Assert.Single(first.Records); Assert.Equal("a", first.Records[0].Event.Component); spool.Commit(first.NextPosition!); DiagnosticsForwarderPosition? checkpoint = spool.LoadCheckpoint(); Assert.NotNull(checkpoint); Assert.Equal(File001, checkpoint!.FileName); Assert.Equal(0, checkpoint.LineIndex); DiagnosticsForwarderBatch second = spool.ReadNext(checkpoint, 10, 1_000_000); Assert.Equal(2, second.Records.Count); Assert.Equal("b", second.Records[0].Event.Component); Assert.Equal("c", second.Records[1].Event.Component); } [Fact] public void PruneConsumed_DeletesOlderFiles_NeverActiveFile() { ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]); ForwardingTestHarness.WriteSpoolFile(_dir, File002, [ForwardingTestHarness.NewRecord()]); ForwardingTestHarness.WriteSpoolFile(_dir, File003, [ForwardingTestHarness.NewRecord()]); var spool = NewSpool(); int deleted = spool.PruneConsumed(new DiagnosticsForwarderPosition { FileName = File002, LineIndex = 0 }); Assert.Equal(1, deleted); // seul 001 (antérieur) est supprimé. Assert.False(File.Exists(Path.Combine(_dir, File001))); Assert.True(File.Exists(Path.Combine(_dir, File002))); Assert.True(File.Exists(Path.Combine(_dir, File003))); // actif préservé. } [Fact] public void EnforceBounds_Quota_DeletesOldestFiles() { int chunk = 400 * 1024; // 3 × 400 Ko = 1.2 Mo > 1 Mo de quota. string padding = new string('a', chunk); ForwardingTestHarness.WriteRawSpoolFile(_dir, File001, padding); ForwardingTestHarness.WriteRawSpoolFile(_dir, File002, padding); ForwardingTestHarness.WriteRawSpoolFile(_dir, File003, padding); var spool = NewSpool(new DiagnosticsForwardingSpoolOptions { MaxTotalSizeMb = 1 }); int deleted = spool.EnforceBounds(DateTimeOffset.UtcNow); Assert.Equal(1, deleted); // le plus ancien est évincé, total ≤ 1 Mo. Assert.False(File.Exists(Path.Combine(_dir, File001))); Assert.True(File.Exists(Path.Combine(_dir, File002))); Assert.True(File.Exists(Path.Combine(_dir, File003))); } [Fact] public void EnforceBounds_Retention_DeletesExpiredFiles_NotActive() { ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]); ForwardingTestHarness.WriteSpoolFile(_dir, File002, [ForwardingTestHarness.NewRecord()]); // 001 est ancien, 002 est l'actif (et récent). File.SetLastWriteTimeUtc(Path.Combine(_dir, File001), DateTime.UtcNow.AddDays(-2)); File.SetLastWriteTimeUtc(Path.Combine(_dir, File002), DateTime.UtcNow); var spool = NewSpool(new DiagnosticsForwardingSpoolOptions { RetentionDays = 1, MaxTotalSizeMb = 1024 }); int deleted = spool.EnforceBounds(DateTimeOffset.UtcNow); Assert.Equal(1, deleted); Assert.False(File.Exists(Path.Combine(_dir, File001))); Assert.True(File.Exists(Path.Combine(_dir, File002))); } }