using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Stability; namespace ZB.MOM.WW.OtOpcUa.Core.Tests.Stability; [Trait("Category", "Unit")] public sealed class MemoryTrackingTests { private static readonly DateTime T0 = new(2026, 4, 19, 12, 0, 0, DateTimeKind.Utc); [Fact] public void WarmingUp_Returns_Warming_UntilWindowElapses() { var tracker = new MemoryTracking(DriverTier.A, TimeSpan.FromMinutes(5)); tracker.Sample(100_000_000, T0).ShouldBe(MemoryTrackingAction.Warming); tracker.Sample(105_000_000, T0.AddMinutes(1)).ShouldBe(MemoryTrackingAction.Warming); tracker.Sample(102_000_000, T0.AddMinutes(4.9)).ShouldBe(MemoryTrackingAction.Warming); tracker.Phase.ShouldBe(TrackingPhase.WarmingUp); tracker.BaselineBytes.ShouldBe(0); } [Fact] public void WindowElapsed_CapturesBaselineAsMedian_AndTransitionsToSteady() { var tracker = new MemoryTracking(DriverTier.A, TimeSpan.FromMinutes(5)); tracker.Sample(100_000_000, T0); tracker.Sample(200_000_000, T0.AddMinutes(1)); tracker.Sample(150_000_000, T0.AddMinutes(2)); var first = tracker.Sample(150_000_000, T0.AddMinutes(5)); tracker.Phase.ShouldBe(TrackingPhase.Steady); tracker.BaselineBytes.ShouldBe(150_000_000L, "median of 4 samples [100, 200, 150, 150] = (150+150)/2 = 150"); first.ShouldBe(MemoryTrackingAction.None, "150 MB is the baseline itself, well under soft threshold"); } [Theory] [InlineData(DriverTier.A, 3, 50)] [InlineData(DriverTier.B, 3, 100)] [InlineData(DriverTier.C, 2, 500)] public void GetTierConstants_MatchesDecision146(DriverTier tier, int expectedMultiplier, long expectedFloorMB) { var (multiplier, floor) = MemoryTracking.GetTierConstants(tier); multiplier.ShouldBe(expectedMultiplier); floor.ShouldBe(expectedFloorMB * 1024 * 1024); } [Fact] public void SoftThreshold_UsesMax_OfMultiplierAndFloor_SmallBaseline() { // Tier A: mult=3, floor=50 MB. Baseline 10 MB → 3×10=30 MB < 10+50=60 MB → floor wins. var tracker = WarmupWithBaseline(DriverTier.A, 10L * 1024 * 1024); tracker.SoftThresholdBytes.ShouldBe(60L * 1024 * 1024); } [Fact] public void SoftThreshold_UsesMax_OfMultiplierAndFloor_LargeBaseline() { // Tier A: mult=3, floor=50 MB. Baseline 200 MB → 3×200=600 MB > 200+50=250 MB → multiplier wins. var tracker = WarmupWithBaseline(DriverTier.A, 200L * 1024 * 1024); tracker.SoftThresholdBytes.ShouldBe(600L * 1024 * 1024); } [Fact] public void HardThreshold_IsTwiceSoft() { var tracker = WarmupWithBaseline(DriverTier.B, 200L * 1024 * 1024); tracker.HardThresholdBytes.ShouldBe(tracker.SoftThresholdBytes * 2); } [Fact] public void Sample_Below_Soft_Returns_None() { var tracker = WarmupWithBaseline(DriverTier.A, 100L * 1024 * 1024); tracker.Sample(200L * 1024 * 1024, T0.AddMinutes(10)).ShouldBe(MemoryTrackingAction.None); } [Fact] public void Sample_AtSoft_Returns_SoftBreach() { // Tier A, baseline 200 MB → soft = 600 MB. Sample exactly at soft. var tracker = WarmupWithBaseline(DriverTier.A, 200L * 1024 * 1024); tracker.Sample(tracker.SoftThresholdBytes, T0.AddMinutes(10)) .ShouldBe(MemoryTrackingAction.SoftBreach); } [Fact] public void Sample_AtHard_Returns_HardBreach() { var tracker = WarmupWithBaseline(DriverTier.A, 200L * 1024 * 1024); tracker.Sample(tracker.HardThresholdBytes, T0.AddMinutes(10)) .ShouldBe(MemoryTrackingAction.HardBreach); } [Fact] public void Sample_AboveHard_Returns_HardBreach() { var tracker = WarmupWithBaseline(DriverTier.A, 200L * 1024 * 1024); tracker.Sample(tracker.HardThresholdBytes + 100_000_000, T0.AddMinutes(10)) .ShouldBe(MemoryTrackingAction.HardBreach); } private static MemoryTracking WarmupWithBaseline(DriverTier tier, long baseline) { var tracker = new MemoryTracking(tier, TimeSpan.FromMinutes(5)); tracker.Sample(baseline, T0); tracker.Sample(baseline, T0.AddMinutes(5)); tracker.BaselineBytes.ShouldBe(baseline); return tracker; } }