using Admin.NET.Plugin.AiDOP.Service.S8; using System.Text.RegularExpressions; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S8; public class S8HomeAlertQueryServiceTests { private static readonly DateTime Now = new(2026, 9, 30, 12, 0, 0); [Fact] public void SeriousOutranksFollow() { var decision = S8ModuleLampPolicy.Decide(1, 4, new[] { Healthy() }, Now); Assert.Equal("CRITICAL", decision.BusinessState); Assert.Equal("HEALTHY", decision.MonitorState); } [Fact] public void SeriousOpenException_StaysRedWhenRuleIsDisabled() { var decision = S8ModuleLampPolicy.Decide(1, 0, new[] { Rule(enabled: false) }, Now); Assert.Equal("CRITICAL", decision.BusinessState); Assert.Equal("PAUSED", decision.MonitorState); Assert.Equal("RULE_DISABLED", decision.MonitorReasonCode); } [Fact] public void FollowOnly_IsWarningWhileMonitorStaysHealthy() { var decision = S8ModuleLampPolicy.Decide(0, 2, new[] { Healthy() }, Now); Assert.Equal("WARNING", decision.BusinessState); Assert.Equal("HEALTHY", decision.MonitorState); Assert.Equal("OK", decision.MonitorReasonCode); } [Fact] public void GreenRequiresHealthyMonitorAndZeroOpenExceptions() { var decision = S8ModuleLampPolicy.Decide(0, 0, new[] { Healthy() }, Now); Assert.Equal("NORMAL", decision.BusinessState); Assert.Equal("HEALTHY", decision.MonitorState); } [Fact] public void UnmonitoredModule_IsUnknown_NotGreen() { var decision = S8ModuleLampPolicy.Decide(0, 0, Array.Empty(), Now); Assert.Equal("UNKNOWN", decision.BusinessState); Assert.Equal("UNMONITORED", decision.MonitorState); Assert.Equal("NO_RULE", decision.MonitorReasonCode); } [Fact] public void StaleRunnableRule_IsDegraded_AndDoesNotTurnGreen() { var decision = S8ModuleLampPolicy.Decide(0, 0, new[] { Healthy(Now.AddMinutes(-11)) }, Now); Assert.Equal("UNKNOWN", decision.BusinessState); Assert.Equal("DEGRADED", decision.MonitorState); Assert.Equal("RUN_STALE", decision.MonitorReasonCode); } [Fact] public void FailedRun_OutranksStaleAndNeverRun() { var rules = new[] { Healthy(Now.AddHours(-2)), Rule(enabled: true, lastStatus: "FAILED", lastRunAt: Now.AddMinutes(-1)) }; var decision = S8ModuleLampPolicy.Decide(0, 0, rules, Now); Assert.Equal("DEGRADED", decision.MonitorState); Assert.Equal("RUN_FAILED", decision.MonitorReasonCode); } [Fact] public void NeverRun_IsDegraded() { var decision = S8ModuleLampPolicy.Decide(0, 0, new[] { Rule(enabled: true) }, Now); Assert.Equal("DEGRADED", decision.MonitorState); Assert.Equal("NEVER_RUN", decision.MonitorReasonCode); } [Fact] public void EnabledRulePausedUntilFuture_IsPausedNotUnmonitored() { var decision = S8ModuleLampPolicy.Decide( 3, 0, new[] { Rule(enabled: true, pausedUntil: Now.AddHours(1), lastStatus: "SUCCESS", lastRunAt: Now) }, Now); Assert.Equal("CRITICAL", decision.BusinessState); Assert.Equal("PAUSED", decision.MonitorState); Assert.Equal("RULE_PAUSED", decision.MonitorReasonCode); } [Fact] public void OneHealthyRunnableRule_IgnoresASiblingPausedRule() { var rules = new[] { Healthy(), Rule(enabled: true, pausedUntil: Now.AddHours(1)) }; var decision = S8ModuleLampPolicy.Decide(0, 0, rules, Now); Assert.Equal("NORMAL", decision.BusinessState); Assert.Equal("HEALTHY", decision.MonitorState); } [Fact] public void FreshWindow_IsAtLeastTenMinutes() { Assert.Equal(600, S8ModuleLampPolicy.FreshWindowSeconds(30)); Assert.Equal(800, S8ModuleLampPolicy.FreshWindowSeconds(400)); } [Fact] public void TrendCountsCreatedAt_AndDoesNotUseLastDetectedAt() { var code = ReadService(); var trend = MethodBody(code, "public async Task> GetCreatedTrendAsync"); var summary = MethodBody(code, "public async Task GetCreatedSummaryAsync"); Assert.Contains("DATE(created_at)", trend); Assert.DoesNotContain("last_detected_at", trend); Assert.DoesNotContain("status NOT IN", summary); Assert.DoesNotContain("factory_id", trend); Assert.Contains("ado_s8_watch_rule", code); var lamps = MethodBody(code, "public async Task GetModuleLampsAsync"); Assert.Contains("status NOT IN ('CLOSED','REJECTED','RECOVERED')", lamps); Assert.Contains("S8ModuleCode.All", lamps); Assert.DoesNotContain("DateTime.Now", lamps); var endpoint = MethodBody(ReadController(), "public async Task GetS8ModuleLamps"); Assert.Contains("[HttpGet(\"s8-module-lamps\")]", ReadController()); Assert.Contains("AidopTenantHelper.GetTenantId(HttpContext)", endpoint); Assert.DoesNotContain("[FromQuery]", endpoint); Assert.DoesNotContain("factoryId", endpoint); foreach (var signature in new[] { "public async Task GetS8Alerts", "public async Task GetS8AlertSummary", "public async Task GetS8HomeTrend" }) { Assert.DoesNotContain("ado_s8_alert_record", MethodBody(ReadController(), signature)); } } private static S8WatchRuleSignal Healthy(DateTime? lastRunAt = null) => Rule(enabled: true, lastStatus: "SUCCESS", lastRunAt: lastRunAt ?? Now.AddMinutes(-1)); private static S8WatchRuleSignal Rule( bool enabled, string? lastStatus = null, DateTime? lastRunAt = null, DateTime? pausedUntil = null) => new("S1", enabled, 300, lastStatus, lastRunAt, pausedUntil); private static string ReadService() => Strip(Path.Combine(PluginRoot, "Service/S8/S8HomeAlertQueryService.cs")); private static string ReadController() => Strip(Path.Combine(PluginRoot, "Controllers/AidopKanbanController.cs")); private static string PluginRoot => Path.GetFullPath( Path.Combine(AppContext.BaseDirectory, "../../../../Admin.NET.Plugin.AiDOP")); private static string Strip(string path) { var raw = File.ReadAllText(path); var noBlock = Regex.Replace(raw, @"/\*.*?\*/", string.Empty, RegexOptions.Singleline); return Regex.Replace(noBlock, @"^\s*//.*$", string.Empty, RegexOptions.Multiline); } private static string MethodBody(string code, string signature) { var start = code.IndexOf(signature, StringComparison.Ordinal); Assert.True(start > 0, signature); var from = start + signature.Length; var end = code.Length; foreach (var kw in new[] { "\n public ", "\n private ", "\n internal ", "\n protected " }) { var i = code.IndexOf(kw, from, StringComparison.Ordinal); if (i > 0 && i < end) end = i; } return code[start..end]; } }