using Akka.Actor; using Akka.TestKit.Xunit2; using ScadaLink.AuditLog.Central; namespace ScadaLink.AuditLog.Tests.Central; /// /// Bundle E (M6-T7) tests for . /// The tracker subscribes to the actor system's EventStream for /// publications and maintains a /// per-site latch the central health surface can read. Since reconciliation is /// central-driven, the "stalled" state semantically belongs to central — not /// to the per-site /// payload (which the site itself emits). The tracker therefore lives as a /// central singleton, not on the site health collector. /// public class SiteAuditTelemetryStalledTrackerTests : TestKit { /// /// Helper: publishes a stalled-changed event on the actor system's /// EventStream and waits a moment for the tracker's subscribe callback to /// run. AwaitAssert avoids racing on the stream's async fan-out. /// private void PublishAndWait(SiteAuditTelemetryStalledTracker tracker, SiteAuditTelemetryStalledChanged evt) { Sys.EventStream.Publish(evt); AwaitAssert( () => { var snapshot = tracker.Snapshot(); Assert.True(snapshot.TryGetValue(evt.SiteId, out var stalled), $"tracker did not record event for {evt.SiteId}"); Assert.Equal(evt.Stalled, stalled); }, duration: TimeSpan.FromSeconds(2), interval: TimeSpan.FromMilliseconds(20)); } [Fact] public void Initial_Snapshot_IsEmpty() { using var tracker = new SiteAuditTelemetryStalledTracker(Sys); var snapshot = tracker.Snapshot(); Assert.Empty(snapshot); } [Fact] public void StalledTrue_Event_TrackerReports_Stalled() { using var tracker = new SiteAuditTelemetryStalledTracker(Sys); PublishAndWait(tracker, new SiteAuditTelemetryStalledChanged("siteA", Stalled: true)); var snapshot = tracker.Snapshot(); Assert.True(snapshot["siteA"]); } [Fact] public void StalledFalse_Event_TrackerReports_NotStalled() { using var tracker = new SiteAuditTelemetryStalledTracker(Sys); // First flip the site into stalled so the false transition has a // prior value to overwrite — mirrors how the reconciliation actor // only publishes false after a true. PublishAndWait(tracker, new SiteAuditTelemetryStalledChanged("siteA", Stalled: true)); PublishAndWait(tracker, new SiteAuditTelemetryStalledChanged("siteA", Stalled: false)); var snapshot = tracker.Snapshot(); Assert.False(snapshot["siteA"]); } [Fact] public void Multiple_Sites_Tracked_Independently() { using var tracker = new SiteAuditTelemetryStalledTracker(Sys); PublishAndWait(tracker, new SiteAuditTelemetryStalledChanged("siteA", Stalled: true)); PublishAndWait(tracker, new SiteAuditTelemetryStalledChanged("siteB", Stalled: false)); PublishAndWait(tracker, new SiteAuditTelemetryStalledChanged("siteC", Stalled: true)); var snapshot = tracker.Snapshot(); Assert.Equal(3, snapshot.Count); Assert.True(snapshot["siteA"]); Assert.False(snapshot["siteB"]); Assert.True(snapshot["siteC"]); } [Fact] public void Constructor_With_Null_ActorSystem_Throws() { Assert.Throws( () => new SiteAuditTelemetryStalledTracker((ActorSystem)null!)); } [Fact] public void Dispose_Unsubscribes_From_EventStream() { var tracker = new SiteAuditTelemetryStalledTracker(Sys); PublishAndWait(tracker, new SiteAuditTelemetryStalledChanged("siteA", Stalled: true)); tracker.Dispose(); // After dispose any further events are ignored — the snapshot // reflects the last known state at dispose time. Sys.EventStream.Publish(new SiteAuditTelemetryStalledChanged("siteA", Stalled: false)); // Give the stream a moment in case the unsubscribe is racey; the // assertion is that siteA stays at true. Thread.Sleep(50); Assert.True(tracker.Snapshot()["siteA"]); } }