S8NotificationLayerMergeTests.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. using Admin.NET.Plugin.AiDOP.Entity.S8;
  2. using Admin.NET.Plugin.AiDOP.Service.S8;
  3. using Xunit;
  4. namespace Admin.NET.Plugin.AiDOP.Tests.S8;
  5. /// <summary>
  6. /// S8-STEP6C-CFG-NOTIFY-FIX-AND-CERT-1 · N-1 回归。
  7. ///
  8. /// 覆盖 <see cref="S8NotificationLayerMerge"/> 的逐层覆盖语义。这是纯函数测试,不触 DB、不触 pusher。
  9. /// runtime resolver 与配置页 ListAsync 都调用同一个 Effective(),因此本文件同时守住两处。
  10. ///
  11. /// 被守住的缺陷(修正前真实存在):工厂只为某一层建行,平台默认在该 scene+severity 下的
  12. /// **其余层级会整体消失** —— 工厂本意「补一层」,实际「换掉整组」,且页面上不可见。
  13. /// </summary>
  14. public class S8NotificationLayerMergeTests
  15. {
  16. private const long GlobalTenant = 0L;
  17. private const long GlobalFactory = 0L;
  18. private const long FactoryTenant = 838257186181189L;
  19. private const long FactoryFactory = 838257186320453L;
  20. private static AdoS8NotificationLayer Row(
  21. long id, long tenantId, long factoryId, string scene, string severity, string level) =>
  22. new()
  23. {
  24. Id = id,
  25. TenantId = tenantId,
  26. FactoryId = factoryId,
  27. SceneCode = scene,
  28. Severity = severity,
  29. LevelCode = level,
  30. TargetRoleIds = "ROLE_X",
  31. NotifyChannel = "log,SignalR"
  32. };
  33. private static AdoS8NotificationLayer Global(long id, string scene, string sev, string level) =>
  34. Row(id, GlobalTenant, GlobalFactory, scene, sev, level);
  35. private static AdoS8NotificationLayer Factory(long id, string scene, string sev, string level) =>
  36. Row(id, FactoryTenant, FactoryFactory, scene, sev, level);
  37. /// <summary>
  38. /// N-1 的核心回归:工厂只覆盖 L2,平台默认的 L1 / L3 必须**继续生效**。
  39. /// 修正前此处会只剩 1 行(工厂的 L2),L1/L3 静默消失。
  40. /// </summary>
  41. [Fact]
  42. public void FactoryOverrideOnOneLevel_KeepsPlatformDefaultsOnOtherLevels()
  43. {
  44. var candidates = new[]
  45. {
  46. Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
  47. Global(2, "S1", "FOLLOW", "L2_MANAGER"),
  48. Global(3, "S1", "FOLLOW", "L3_DIRECTOR"),
  49. Factory(100, "S1", "FOLLOW", "L2_MANAGER"),
  50. };
  51. var effective = S8NotificationLayerMerge.Effective(candidates);
  52. Assert.Equal(3, effective.Count);
  53. Assert.Equal(new[] { "L1_OPERATOR", "L2_MANAGER", "L3_DIRECTOR" }, effective.Select(x => x.LevelCode));
  54. // L1 / L3 仍来自平台默认,L2 被工厂接管
  55. Assert.Equal(GlobalTenant, effective[0].TenantId);
  56. Assert.Equal(FactoryTenant, effective[1].TenantId);
  57. Assert.Equal(100, effective[1].Id);
  58. Assert.Equal(GlobalTenant, effective[2].TenantId);
  59. }
  60. /// <summary>同层级有工厂行时,该层级的平台默认必须被替换掉(不能两行都发)。</summary>
  61. [Fact]
  62. public void FactoryRow_ReplacesPlatformDefault_OnSameLevel()
  63. {
  64. var candidates = new[]
  65. {
  66. Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
  67. Factory(100, "S1", "FOLLOW", "L1_OPERATOR"),
  68. };
  69. var effective = S8NotificationLayerMerge.Effective(candidates);
  70. Assert.Single(effective);
  71. Assert.Equal(100, effective[0].Id);
  72. }
  73. /// <summary>覆盖不跨 scene / severity:S1 的工厂行不得影响 S2,FOLLOW 的不得影响 SERIOUS。</summary>
  74. [Fact]
  75. public void Override_DoesNotLeakAcrossSceneOrSeverity()
  76. {
  77. var candidates = new[]
  78. {
  79. Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
  80. Global(2, "S1", "SERIOUS", "L1_OPERATOR"),
  81. Global(3, "S2", "FOLLOW", "L1_OPERATOR"),
  82. Factory(100, "S1", "FOLLOW", "L1_OPERATOR"),
  83. };
  84. var effective = S8NotificationLayerMerge.Effective(candidates);
  85. Assert.Equal(3, effective.Count);
  86. Assert.Equal(100, effective.Single(x => x.SceneCode == "S1" && x.Severity == "FOLLOW").Id);
  87. Assert.Equal(2, effective.Single(x => x.SceneCode == "S1" && x.Severity == "SERIOUS").Id);
  88. Assert.Equal(3, effective.Single(x => x.SceneCode == "S2").Id);
  89. }
  90. /// <summary>
  91. /// severity 参与分组前必须归一:平台默认写 FOLLOW、工厂行写 legacy 的 LOW,
  92. /// 二者是同一档,必须归到同一组由工厂接管,而不是各发一条。
  93. /// </summary>
  94. [Fact]
  95. public void SeverityIsNormalizedBeforeGrouping()
  96. {
  97. var candidates = new[]
  98. {
  99. Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
  100. Factory(100, "S1", "LOW", "L1_OPERATOR"),
  101. };
  102. var effective = S8NotificationLayerMerge.Effective(candidates);
  103. Assert.Single(effective);
  104. Assert.Equal(100, effective[0].Id);
  105. }
  106. /// <summary>level_code 归一:大小写 / 空白差异不得让工厂行与平台默认分到两组。</summary>
  107. [Fact]
  108. public void LevelIsNormalizedBeforeGrouping()
  109. {
  110. var candidates = new[]
  111. {
  112. Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
  113. Factory(100, "S1", "FOLLOW", " l1_operator "),
  114. };
  115. var effective = S8NotificationLayerMerge.Effective(candidates);
  116. Assert.Single(effective);
  117. Assert.Equal(100, effective[0].Id);
  118. }
  119. /// <summary>
  120. /// 历史脏数据:level_code = '' 的行自成一组,既不吞掉合法层级、也不被合法层级吞掉。
  121. /// (库中实测存在这样的行。)
  122. /// </summary>
  123. [Fact]
  124. public void EmptyLevelCode_FormsItsOwnGroup()
  125. {
  126. var candidates = new[]
  127. {
  128. Global(1, "S1", "SERIOUS", "L1_OPERATOR"),
  129. Factory(100, "S1", "SERIOUS", ""),
  130. };
  131. var effective = S8NotificationLayerMerge.Effective(candidates);
  132. Assert.Equal(2, effective.Count);
  133. Assert.Contains(effective, x => x.Id == 1);
  134. Assert.Contains(effective, x => x.Id == 100);
  135. }
  136. /// <summary>
  137. /// 无唯一索引,同键可能重复行:必须全部保留(丢弃会让 runtime 与页面不一致),
  138. /// 且顺序按 Id 稳定,不依赖 DB 返回序。
  139. /// </summary>
  140. [Fact]
  141. public void DuplicateRowsOnSameKey_AreAllKept_InStableIdOrder()
  142. {
  143. var candidates = new[]
  144. {
  145. Factory(300, "S1", "FOLLOW", "L1_OPERATOR"),
  146. Factory(100, "S1", "FOLLOW", "L1_OPERATOR"),
  147. Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
  148. };
  149. var effective = S8NotificationLayerMerge.Effective(candidates);
  150. Assert.Equal(new[] { 100L, 300L }, effective.Select(x => x.Id));
  151. }
  152. /// <summary>输入顺序不影响输出:稳定确定序是 scene → severity → level → id。</summary>
  153. [Fact]
  154. public void OrderingIsDeterministic_RegardlessOfInputOrder()
  155. {
  156. var rows = new[]
  157. {
  158. Global(3, "S2", "FOLLOW", "L1_OPERATOR"),
  159. Global(2, "S1", "SERIOUS", "L1_OPERATOR"),
  160. Global(1, "S1", "FOLLOW", "L2_MANAGER"),
  161. Global(4, "S1", "FOLLOW", "L1_OPERATOR"),
  162. };
  163. var forward = S8NotificationLayerMerge.Effective(rows).Select(x => x.Id).ToList();
  164. var reversed = S8NotificationLayerMerge.Effective(rows.Reverse().ToList()).Select(x => x.Id).ToList();
  165. Assert.Equal(new[] { 4L, 1L, 2L, 3L }, forward);
  166. Assert.Equal(forward, reversed);
  167. }
  168. /// <summary>只有平台默认时全部生效;只有工厂行时同理。</summary>
  169. [Fact]
  170. public void SingleSidedCandidates_PassThrough()
  171. {
  172. var globalsOnly = S8NotificationLayerMerge.Effective(new[]
  173. {
  174. Global(1, "S1", "FOLLOW", "L1_OPERATOR"),
  175. Global(2, "S1", "FOLLOW", "L2_MANAGER"),
  176. });
  177. Assert.Equal(2, globalsOnly.Count);
  178. var factoryOnly = S8NotificationLayerMerge.Effective(new[]
  179. {
  180. Factory(100, "S1", "FOLLOW", "L1_OPERATOR"),
  181. });
  182. Assert.Single(factoryOnly);
  183. Assert.Empty(S8NotificationLayerMerge.Effective(Array.Empty<AdoS8NotificationLayer>()));
  184. }
  185. }