S8ResultKpiEnabledGateTests.cs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. using Admin.NET.Plugin.AiDOP.Entity.S8;
  2. using Admin.NET.Plugin.AiDOP.Service.S8;
  3. using Xunit;
  4. namespace Admin.NET.Test.S8;
  5. /// <summary>
  6. /// S8-CONFIG-AUTHORITY-FIX-1:结果 KPI 的「停用即不显示」门禁。
  7. ///
  8. /// 修复前的缺陷:<c>GetResultKpiSummaryAsync</c> 只按 mechanism / is_result_kpi / 租户作用域取行,
  9. /// <b>完全不读 enabled</b>。运行库里 5 条结果 KPI 全部 enabled=0(remark "[RESERVED] S9 result KPI"),
  10. /// 却照常出现在监控总览——配置页面的停用开关对业务零效力。
  11. ///
  12. /// 本文件锁定的契约与失败后果:
  13. /// A. enabled=true → 纳入 → 否则正常指标消失,属功能倒退
  14. /// B. enabled=false → 不纳入 → 否则"停用"仍然显示,用户认为配置坏了
  15. /// C. is_result_kpi=false → 不纳入 → 否则机制字典项(Rule 向导用)会污染结果 KPI 卡片
  16. /// D. 门禁必须在**覆盖解析之后** → 这是最容易写错的一条:
  17. /// 若把 Enabled 下推成 SQL WHERE,租户 override enabled=false 的行会先被滤掉,
  18. /// 分组里只剩 enabled=true 的全局行 → 结果回落成"显示",与停用意图完全相反。
  19. /// E. 不触碰 Rule 机制字典 → 门禁只作用于结果 KPI 路径
  20. ///
  21. /// 不接 DB、不接 DI、不触发任何真实规则。
  22. /// </summary>
  23. public class S8ResultKpiEnabledGateTests
  24. {
  25. private const long TenantId = 838257186181189L;
  26. private const long FactoryId = 838257186320453L;
  27. private static AdoS8MonitorMetric Metric(
  28. string code, bool enabled, bool isResultKpi = true,
  29. long tenantId = 0, long factoryId = 0, string mechanism = "RATIO", int sortNo = 0) => new()
  30. {
  31. MetricCode = code,
  32. MetricName = code,
  33. Mechanism = mechanism,
  34. IsResultKpi = isResultKpi,
  35. Enabled = enabled,
  36. TenantId = tenantId,
  37. FactoryId = factoryId,
  38. SortNo = sortNo
  39. };
  40. private static List<string> CodesOf(IEnumerable<AdoS8MonitorMetric> rows) =>
  41. rows.Select(x => x.MetricCode).ToList();
  42. // ============================================================
  43. // A/B/C. 基础门禁
  44. // ============================================================
  45. [Fact]
  46. public void T1_EnabledResultKpi_IsReturned()
  47. {
  48. var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
  49. {
  50. Metric("ORDER_DELIVERY_RATE", enabled: true)
  51. });
  52. Assert.Equal(new[] { "ORDER_DELIVERY_RATE" }, CodesOf(result));
  53. }
  54. [Fact]
  55. public void T2_DisabledResultKpi_IsNotReturned()
  56. {
  57. // 运行库现状:5 条结果 KPI 全部 enabled=0,修复前它们照常显示。
  58. var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
  59. {
  60. Metric("IQC_PASS_RATE", enabled: false)
  61. });
  62. Assert.Empty(result);
  63. }
  64. [Fact]
  65. public void T3_NonResultKpi_IsNotReturned()
  66. {
  67. // is_result_kpi=false 的是 Rule 向导用的机制字典项(DATE / VALUE_RANGE),
  68. // 即使 enabled=true 也不属于结果 KPI 卡片。
  69. // 注:调用方 SQL 已按 IsResultKpi 过滤,这里再断言一次,防止将来有人把过滤挪进本方法时写反。
  70. var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
  71. {
  72. Metric("ORDER_DUE_AT", enabled: true, isResultKpi: false, mechanism: "DATE")
  73. }.Where(m => m.IsResultKpi));
  74. Assert.Empty(result);
  75. }
  76. // ============================================================
  77. // D. 覆盖解析与门禁的先后顺序 —— 本文件最关键的两条
  78. // ============================================================
  79. [Fact]
  80. public void T4_TenantOverrideDisable_HidesMetric_EvenWhenGlobalEnabled()
  81. {
  82. // 全局启用 + 租户停用 → 租户看不到。
  83. // 若实现把 Enabled 下推成 SQL WHERE,租户行会先被滤掉、只剩全局行,
  84. // 结果反而变成"可见"——本用例正是为了钉死这个反向缺陷。
  85. var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
  86. {
  87. Metric("ORDER_DELIVERY_RATE", enabled: true), // 平台全局
  88. Metric("ORDER_DELIVERY_RATE", enabled: false, tenantId: TenantId, factoryId: FactoryId) // 租户覆盖:停用
  89. });
  90. Assert.Empty(result);
  91. }
  92. [Fact]
  93. public void T5_TenantOverrideEnable_ShowsMetric_EvenWhenGlobalDisabled()
  94. {
  95. // 反向:全局停用 + 租户启用 → 租户可见。
  96. // 覆盖语义由 OrderByDescending(FactoryId).ThenByDescending(TenantId) 承载,本用例确认它对两个方向都成立。
  97. var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
  98. {
  99. Metric("PO_DELIVERY_RATE", enabled: false),
  100. Metric("PO_DELIVERY_RATE", enabled: true, tenantId: TenantId, factoryId: FactoryId)
  101. });
  102. Assert.Equal(new[] { "PO_DELIVERY_RATE" }, CodesOf(result));
  103. }
  104. [Fact]
  105. public void T5b_EachMetricCode_ResolvesToExactlyOneEffectiveRow()
  106. {
  107. // 覆盖解析必须"每个 code 只留一条",否则同一 KPI 会在卡片上重复出现。
  108. var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
  109. {
  110. Metric("WO_COMPLETION_RATE", enabled: true),
  111. Metric("WO_COMPLETION_RATE", enabled: true, tenantId: TenantId),
  112. Metric("WO_COMPLETION_RATE", enabled: true, tenantId: TenantId, factoryId: FactoryId)
  113. });
  114. Assert.Single(result);
  115. // 工厂行优先级最高。
  116. Assert.Equal(FactoryId, result[0].FactoryId);
  117. }
  118. // ============================================================
  119. // E. Rule 机制字典不受影响
  120. // ============================================================
  121. [Fact]
  122. public void T6_RuleMechanismDictionary_IsUntouchedByTheGate()
  123. {
  124. // 运行库中 ado_s8_monitor_metric 有 8 条 enabled=1 的机制字典项(DATE / VALUE_RANGE),
  125. // 由 S8ConfigDraftService(Rule 向导)消费。本门禁只在结果 KPI 路径调用,
  126. // 这里断言:即使把它们混进候选集,也只会因 IsResultKpi=false 被调用方排除,
  127. // 而不会被本方法当作结果 KPI 返回。
  128. var mixed = new[]
  129. {
  130. Metric("ORDER_DUE_AT", enabled: true, isResultKpi: false, mechanism: "DATE"),
  131. Metric("INV_QTY", enabled: true, isResultKpi: false, mechanism: "VALUE_RANGE"),
  132. Metric("ORDER_FLOW_STAGE_VARIANCE", enabled: true, isResultKpi: false, mechanism: "VALUE_RANGE"),
  133. Metric("ORDER_DELIVERY_RATE", enabled: true)
  134. };
  135. // 模拟调用方 SQL 的 mechanism + is_result_kpi 过滤。
  136. var candidates = mixed.Where(m => m.Mechanism == "RATIO" && m.IsResultKpi);
  137. var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(candidates);
  138. Assert.Equal(new[] { "ORDER_DELIVERY_RATE" }, CodesOf(result));
  139. // 机制字典项全部保持 enabled=true,未被本批改动。
  140. Assert.All(mixed.Where(m => !m.IsResultKpi), m => Assert.True(m.Enabled));
  141. }
  142. // ============================================================
  143. // 排序稳定性(卡片顺序不应因门禁而乱)
  144. // ============================================================
  145. [Fact]
  146. public void EnabledMetrics_KeepSortNoThenCodeOrdering()
  147. {
  148. var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
  149. {
  150. Metric("B_RATE", enabled: true, sortNo: 2),
  151. Metric("A_RATE", enabled: true, sortNo: 2),
  152. Metric("Z_RATE", enabled: true, sortNo: 1),
  153. Metric("X_RATE", enabled: false, sortNo: 0) // 停用项不参与排序结果
  154. });
  155. Assert.Equal(new[] { "Z_RATE", "A_RATE", "B_RATE" }, CodesOf(result));
  156. }
  157. }