| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- 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<S8WatchRuleSignal>(), 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<List<S8HomeTrendPoint>> GetCreatedTrendAsync");
- var summary = MethodBody(code, "public async Task<S8HomeAlertSummary> 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<S8ModuleLampPayload> 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<IActionResult> 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<IActionResult> GetS8Alerts",
- "public async Task<IActionResult> GetS8AlertSummary",
- "public async Task<IActionResult> 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];
- }
- }
|