S8HomeAlertQueryServiceTests.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. using Admin.NET.Plugin.AiDOP.Service.S8;
  2. using System.Text.RegularExpressions;
  3. using Xunit;
  4. namespace Admin.NET.Plugin.AiDOP.Tests.S8;
  5. public class S8HomeAlertQueryServiceTests
  6. {
  7. private static readonly DateTime Now = new(2026, 9, 30, 12, 0, 0);
  8. [Fact]
  9. public void SeriousOutranksFollow()
  10. {
  11. var decision = S8ModuleLampPolicy.Decide(1, 4, new[] { Healthy() }, Now);
  12. Assert.Equal("CRITICAL", decision.BusinessState);
  13. Assert.Equal("HEALTHY", decision.MonitorState);
  14. }
  15. [Fact]
  16. public void SeriousOpenException_StaysRedWhenRuleIsDisabled()
  17. {
  18. var decision = S8ModuleLampPolicy.Decide(1, 0, new[] { Rule(enabled: false) }, Now);
  19. Assert.Equal("CRITICAL", decision.BusinessState);
  20. Assert.Equal("PAUSED", decision.MonitorState);
  21. Assert.Equal("RULE_DISABLED", decision.MonitorReasonCode);
  22. }
  23. [Fact]
  24. public void FollowOnly_IsWarningWhileMonitorStaysHealthy()
  25. {
  26. var decision = S8ModuleLampPolicy.Decide(0, 2, new[] { Healthy() }, Now);
  27. Assert.Equal("WARNING", decision.BusinessState);
  28. Assert.Equal("HEALTHY", decision.MonitorState);
  29. Assert.Equal("OK", decision.MonitorReasonCode);
  30. }
  31. [Fact]
  32. public void GreenRequiresHealthyMonitorAndZeroOpenExceptions()
  33. {
  34. var decision = S8ModuleLampPolicy.Decide(0, 0, new[] { Healthy() }, Now);
  35. Assert.Equal("NORMAL", decision.BusinessState);
  36. Assert.Equal("HEALTHY", decision.MonitorState);
  37. }
  38. [Fact]
  39. public void UnmonitoredModule_IsUnknown_NotGreen()
  40. {
  41. var decision = S8ModuleLampPolicy.Decide(0, 0, Array.Empty<S8WatchRuleSignal>(), Now);
  42. Assert.Equal("UNKNOWN", decision.BusinessState);
  43. Assert.Equal("UNMONITORED", decision.MonitorState);
  44. Assert.Equal("NO_RULE", decision.MonitorReasonCode);
  45. }
  46. [Fact]
  47. public void StaleRunnableRule_IsDegraded_AndDoesNotTurnGreen()
  48. {
  49. var decision = S8ModuleLampPolicy.Decide(0, 0, new[] { Healthy(Now.AddMinutes(-11)) }, Now);
  50. Assert.Equal("UNKNOWN", decision.BusinessState);
  51. Assert.Equal("DEGRADED", decision.MonitorState);
  52. Assert.Equal("RUN_STALE", decision.MonitorReasonCode);
  53. }
  54. [Fact]
  55. public void FailedRun_OutranksStaleAndNeverRun()
  56. {
  57. var rules = new[]
  58. {
  59. Healthy(Now.AddHours(-2)),
  60. Rule(enabled: true, lastStatus: "FAILED", lastRunAt: Now.AddMinutes(-1))
  61. };
  62. var decision = S8ModuleLampPolicy.Decide(0, 0, rules, Now);
  63. Assert.Equal("DEGRADED", decision.MonitorState);
  64. Assert.Equal("RUN_FAILED", decision.MonitorReasonCode);
  65. }
  66. [Fact]
  67. public void NeverRun_IsDegraded()
  68. {
  69. var decision = S8ModuleLampPolicy.Decide(0, 0, new[] { Rule(enabled: true) }, Now);
  70. Assert.Equal("DEGRADED", decision.MonitorState);
  71. Assert.Equal("NEVER_RUN", decision.MonitorReasonCode);
  72. }
  73. [Fact]
  74. public void EnabledRulePausedUntilFuture_IsPausedNotUnmonitored()
  75. {
  76. var decision = S8ModuleLampPolicy.Decide(
  77. 3, 0, new[] { Rule(enabled: true, pausedUntil: Now.AddHours(1), lastStatus: "SUCCESS", lastRunAt: Now) }, Now);
  78. Assert.Equal("CRITICAL", decision.BusinessState);
  79. Assert.Equal("PAUSED", decision.MonitorState);
  80. Assert.Equal("RULE_PAUSED", decision.MonitorReasonCode);
  81. }
  82. [Fact]
  83. public void OneHealthyRunnableRule_IgnoresASiblingPausedRule()
  84. {
  85. var rules = new[]
  86. {
  87. Healthy(),
  88. Rule(enabled: true, pausedUntil: Now.AddHours(1))
  89. };
  90. var decision = S8ModuleLampPolicy.Decide(0, 0, rules, Now);
  91. Assert.Equal("NORMAL", decision.BusinessState);
  92. Assert.Equal("HEALTHY", decision.MonitorState);
  93. }
  94. [Fact]
  95. public void FreshWindow_IsAtLeastTenMinutes()
  96. {
  97. Assert.Equal(600, S8ModuleLampPolicy.FreshWindowSeconds(30));
  98. Assert.Equal(800, S8ModuleLampPolicy.FreshWindowSeconds(400));
  99. }
  100. [Fact]
  101. public void TrendCountsCreatedAt_AndDoesNotUseLastDetectedAt()
  102. {
  103. var code = ReadService();
  104. var trend = MethodBody(code, "public async Task<List<S8HomeTrendPoint>> GetCreatedTrendAsync");
  105. var summary = MethodBody(code, "public async Task<S8HomeAlertSummary> GetCreatedSummaryAsync");
  106. Assert.Contains("DATE(created_at)", trend);
  107. Assert.DoesNotContain("last_detected_at", trend);
  108. Assert.DoesNotContain("status NOT IN", summary);
  109. Assert.DoesNotContain("factory_id", trend);
  110. Assert.Contains("ado_s8_watch_rule", code);
  111. var lamps = MethodBody(code, "public async Task<S8ModuleLampPayload> GetModuleLampsAsync");
  112. Assert.Contains("status NOT IN ('CLOSED','REJECTED','RECOVERED')", lamps);
  113. Assert.Contains("S8ModuleCode.All", lamps);
  114. Assert.DoesNotContain("DateTime.Now", lamps);
  115. var endpoint = MethodBody(ReadController(), "public async Task<IActionResult> GetS8ModuleLamps");
  116. Assert.Contains("[HttpGet(\"s8-module-lamps\")]", ReadController());
  117. Assert.Contains("AidopTenantHelper.GetTenantId(HttpContext)", endpoint);
  118. Assert.DoesNotContain("[FromQuery]", endpoint);
  119. Assert.DoesNotContain("factoryId", endpoint);
  120. foreach (var signature in new[]
  121. {
  122. "public async Task<IActionResult> GetS8Alerts",
  123. "public async Task<IActionResult> GetS8AlertSummary",
  124. "public async Task<IActionResult> GetS8HomeTrend"
  125. })
  126. {
  127. Assert.DoesNotContain("ado_s8_alert_record", MethodBody(ReadController(), signature));
  128. }
  129. }
  130. private static S8WatchRuleSignal Healthy(DateTime? lastRunAt = null) =>
  131. Rule(enabled: true, lastStatus: "SUCCESS", lastRunAt: lastRunAt ?? Now.AddMinutes(-1));
  132. private static S8WatchRuleSignal Rule(
  133. bool enabled,
  134. string? lastStatus = null,
  135. DateTime? lastRunAt = null,
  136. DateTime? pausedUntil = null) =>
  137. new("S1", enabled, 300, lastStatus, lastRunAt, pausedUntil);
  138. private static string ReadService() => Strip(Path.Combine(PluginRoot, "Service/S8/S8HomeAlertQueryService.cs"));
  139. private static string ReadController() => Strip(Path.Combine(PluginRoot, "Controllers/AidopKanbanController.cs"));
  140. private static string PluginRoot => Path.GetFullPath(
  141. Path.Combine(AppContext.BaseDirectory, "../../../../Admin.NET.Plugin.AiDOP"));
  142. private static string Strip(string path)
  143. {
  144. var raw = File.ReadAllText(path);
  145. var noBlock = Regex.Replace(raw, @"/\*.*?\*/", string.Empty, RegexOptions.Singleline);
  146. return Regex.Replace(noBlock, @"^\s*//.*$", string.Empty, RegexOptions.Multiline);
  147. }
  148. private static string MethodBody(string code, string signature)
  149. {
  150. var start = code.IndexOf(signature, StringComparison.Ordinal);
  151. Assert.True(start > 0, signature);
  152. var from = start + signature.Length;
  153. var end = code.Length;
  154. foreach (var kw in new[] { "\n public ", "\n private ", "\n internal ", "\n protected " })
  155. {
  156. var i = code.IndexOf(kw, from, StringComparison.Ordinal);
  157. if (i > 0 && i < end) end = i;
  158. }
  159. return code[start..end];
  160. }
  161. }