using System; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Stability; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests; [Trait("Category", "Unit")] public sealed class MemoryWatchdogTests { private const long Mb = 1024 * 1024; [Fact] public void Baseline_sample_returns_None() { var w = new MemoryWatchdog(baselineBytes: 300 * Mb); w.Sample(320 * Mb, DateTime.UtcNow).ShouldBe(WatchdogAction.None); } [Fact] public void Warn_threshold_uses_larger_of_1_5x_or_plus_200MB() { // Baseline 300 → warn threshold = max(450, 500) = 500 MB var w = new MemoryWatchdog(baselineBytes: 300 * Mb); w.Sample(499 * Mb, DateTime.UtcNow).ShouldBe(WatchdogAction.None); w.Sample(500 * Mb, DateTime.UtcNow).ShouldBe(WatchdogAction.Warn); } [Fact] public void Soft_recycle_triggers_at_2x_or_plus_200MB_whichever_larger() { // Baseline 400 → soft = max(800, 600) = 800 MB var w = new MemoryWatchdog(baselineBytes: 400 * Mb); w.Sample(799 * Mb, DateTime.UtcNow).ShouldBe(WatchdogAction.Warn); w.Sample(800 * Mb, DateTime.UtcNow).ShouldBe(WatchdogAction.SoftRecycle); } [Fact] public void Hard_kill_triggers_at_absolute_ceiling() { var w = new MemoryWatchdog(baselineBytes: 1000 * Mb); w.Sample(1501 * Mb, DateTime.UtcNow).ShouldBe(WatchdogAction.HardKill); } [Fact] public void Sustained_slope_triggers_soft_recycle_before_absolute_threshold() { // Baseline 1000 MB → warn = 1200, soft = 2000 (absolute). Slope 6 MB/min over 30 min = 180 MB // delta — still well below the absolute soft threshold; slope detector must fire on its own. var w = new MemoryWatchdog(baselineBytes: 1000 * Mb) { SustainedSlopeBytesPerMinute = 5 * Mb }; var t0 = new DateTime(2026, 4, 17, 12, 0, 0, DateTimeKind.Utc); long rss = 1050 * Mb; var slopeFired = false; for (var i = 0; i <= 35; i++) { var action = w.Sample(rss, t0.AddMinutes(i)); if (action == WatchdogAction.SoftRecycle) { slopeFired = true; break; } rss += 6 * Mb; } slopeFired.ShouldBeTrue("slope detector should fire once the 30-min window fills"); } }