S8TimeoutEvaluatorTests.cs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. using Admin.NET.Plugin.AiDOP.Entity.S8;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  3. using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
  4. using Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
  5. using Xunit;
  6. namespace Admin.NET.Test.S8;
  7. /// <summary>
  8. /// TIMEOUT evaluator 纯逻辑回归:运行参数解析 + dedup_key 生成稳定性。
  9. /// 不接 DB / 不接 DI。
  10. ///
  11. /// <para><b>S8-RULE-GOVERNANCE-BATCH1 的变化</b>:本文件原先覆盖的是
  12. /// <c>S8TimeoutParams.Parse</c> —— 那个类同时解析判定语义(dueAtField / statusField /
  13. /// completedStates / objectIdField / exceptionTypeCode)与运行参数(graceMinutes)。
  14. /// 判定语义已迁入代码定义,对它的断言移至 <c>S8RuleDefinitionContractTests</c>;
  15. /// 本文件改为覆盖剩下的那一半:<see cref="S8RuleRuntimeParameters"/>。</para>
  16. ///
  17. /// <para>dedup_key 相关断言**逐字保留** —— 那部分语义本批未改。</para>
  18. /// </summary>
  19. public class S8TimeoutEvaluatorTests
  20. {
  21. private static S8RuleDefinition Rule01Definition() =>
  22. new S8RuleCatalog(new IS8RuleDefinitionSource[] { new S8PurchaseDeliveryRuleDefinitions() })
  23. .GetRequired(S8PurchaseDeliveryRuleDefinitions.PurchaseDeliveryDateDelayCode);
  24. private static AdoS8WatchRule Rule(string? paramsJson, string severity = "SERIOUS") => new()
  25. {
  26. RuleCode = S8PurchaseDeliveryRuleDefinitions.PurchaseDeliveryDateDelayCode,
  27. Severity = severity,
  28. PollIntervalSeconds = 300,
  29. TriggerCountRequired = 1,
  30. RecoverCountRequired = 2,
  31. ParamsJson = paramsJson
  32. };
  33. // ───────────────── 运行参数解析(原 params 解析用例的继承者) ─────────────────
  34. [Fact]
  35. public void RuntimeParameters_Parse_FullRuntimeSpec()
  36. {
  37. var json = """
  38. {
  39. "graceMinutes": 5,
  40. "defaultOccurrenceDeptId": 245,
  41. "defaultResponsibleDeptId": 246
  42. }
  43. """;
  44. var p = S8RuleRuntimeParameters.Resolve(Rule(json), Rule01Definition());
  45. Assert.Equal(5, p.GraceMinutes);
  46. Assert.Equal(245L, p.DefaultOccurrenceDeptId);
  47. Assert.Equal(246L, p.DefaultResponsibleDeptId);
  48. Assert.Equal(300, p.PollIntervalSeconds);
  49. Assert.Equal(1, p.TriggerCountRequired);
  50. Assert.Equal(2, p.RecoverCountRequired);
  51. Assert.Equal(S8SeverityCode.Serious, p.Severity);
  52. }
  53. [Fact]
  54. public void RuntimeParameters_Tolerates_MissingOptionalFields()
  55. {
  56. var p = S8RuleRuntimeParameters.Resolve(Rule("""{ }"""), Rule01Definition());
  57. Assert.Equal(0, p.GraceMinutes); // 定义声明的默认值
  58. Assert.Null(p.DefaultOccurrenceDeptId);
  59. Assert.Null(p.DefaultResponsibleDeptId);
  60. }
  61. /// <summary>
  62. /// 关键回归:旧 A 类字段读到也必须忽略。
  63. /// 这是「用户不能通过 params_json 改判定口径」在参数解析这一层的守卫;
  64. /// 判定结果层面的守卫在 <c>S8RuleDefinitionContractTests.T5_*</c>。
  65. /// </summary>
  66. [Fact]
  67. public void RuntimeParameters_Ignores_LegacyDefinitionFields()
  68. {
  69. var json = """
  70. {
  71. "dueAtField": "evil_field",
  72. "statusField": "evil_status",
  73. "completedStates": ["OPEN"],
  74. "objectIdField": "evil_id",
  75. "exceptionTypeCode": "EVIL",
  76. "graceMinutes": 7
  77. }
  78. """;
  79. var p = S8RuleRuntimeParameters.Resolve(Rule(json), Rule01Definition());
  80. // B 类照常生效
  81. Assert.Equal(7, p.GraceMinutes);
  82. // A 类不会以任何形式出现在回写的 params_json 里
  83. var written = p.ToParamsJson();
  84. Assert.DoesNotContain("evil", written, StringComparison.OrdinalIgnoreCase);
  85. Assert.DoesNotContain("completedStates", written, StringComparison.Ordinal);
  86. }
  87. [Fact]
  88. public void RuntimeParameters_DoesNotThrow_OnInvalidJson()
  89. {
  90. // 旧实现在这里抛 JsonException,由 evaluator 捕获后把整条规则标 params_parse_failed。
  91. // 现在语义在代码里,一份读不懂的运行参数不该让规则停摆 —— 回落默认值即可。
  92. var p = S8RuleRuntimeParameters.Resolve(Rule("not-a-json"), Rule01Definition());
  93. Assert.Equal(0, p.GraceMinutes);
  94. }
  95. /// <summary>
  96. /// severity 非法值必须回落**定义声明的默认值**,而不是被 <c>S8SeverityCode.Normalize</c>
  97. /// 静默降级成 FOLLOW —— 库里 severity='HIGH' 的那一行当年正是被那条兜底吞进来的。
  98. /// </summary>
  99. [Theory]
  100. [InlineData("HIGH")]
  101. [InlineData("LOW")]
  102. [InlineData("")]
  103. [InlineData(null)]
  104. public void RuntimeParameters_InvalidSeverity_FallsBackToDefinitionDefault(string? severity)
  105. {
  106. var p = S8RuleRuntimeParameters.Resolve(Rule("{}", severity!), Rule01Definition());
  107. Assert.Equal(S8SeverityCode.Serious, p.Severity); // Rule 01 的定义默认值
  108. }
  109. [Theory]
  110. [InlineData(S8SeverityCode.Follow)]
  111. [InlineData(S8SeverityCode.Serious)]
  112. public void RuntimeParameters_ValidSeverity_IsHonoured(string severity)
  113. {
  114. var p = S8RuleRuntimeParameters.Resolve(Rule("{}", severity), Rule01Definition());
  115. Assert.Equal(severity, p.Severity);
  116. }
  117. [Fact]
  118. public void RuntimeParameters_OutOfRangeColumnValues_FallBackToDefaults()
  119. {
  120. var rule = Rule("{}");
  121. rule.PollIntervalSeconds = 5; // < 60,越界
  122. rule.TriggerCountRequired = 99; // > 10,越界
  123. var p = S8RuleRuntimeParameters.Resolve(rule, Rule01Definition());
  124. Assert.Equal(300, p.PollIntervalSeconds);
  125. Assert.Equal(1, p.TriggerCountRequired);
  126. }
  127. // ───────────────── dedup_key(本批未改,断言逐字保留) ─────────────────
  128. [Fact]
  129. public void BuildDedupKey_IsStable_SameInputsSameOutput()
  130. {
  131. var k1 = S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "G01_TEST_TIMEOUT", "ORDER", "ORDER-TIMEOUT-01");
  132. var k2 = S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "G01_TEST_TIMEOUT", "ORDER", "ORDER-TIMEOUT-01");
  133. Assert.Equal(k1, k2);
  134. Assert.Equal("T1:F1:RG01_TEST_TIMEOUT:ORDER:ORDER-TIMEOUT-01", k1);
  135. }
  136. [Fact]
  137. public void BuildDedupKey_Differentiates_OnTenantFactoryRuleObject()
  138. {
  139. var baseKey = S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "R", "ORDER", "ID");
  140. Assert.NotEqual(baseKey, S8TimeoutRuleEvaluator.BuildDedupKey(2, 1, "R", "ORDER", "ID"));
  141. Assert.NotEqual(baseKey, S8TimeoutRuleEvaluator.BuildDedupKey(1, 2, "R", "ORDER", "ID"));
  142. Assert.NotEqual(baseKey, S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "R2", "ORDER", "ID"));
  143. Assert.NotEqual(baseKey, S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "R", "TASK", "ID"));
  144. Assert.NotEqual(baseKey, S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "R", "ORDER", "ID2"));
  145. }
  146. [Fact]
  147. public void BuildDedupKey_Length_FitsVarchar128()
  148. {
  149. // 实体定义 dedup_key VARCHAR(128)。本断言锁定常规标识符不溢出列长度。
  150. var k = S8TimeoutRuleEvaluator.BuildDedupKey(9999999, 9999999, "VERY_LONG_RULE_CODE_FOR_PADDING_64____", "EXTREMELY_LONG_OBJECT_TYPE______", "EXTREMELY_LONG_OBJECT_ID________");
  151. Assert.True(k.Length <= 128, $"dedup_key length {k.Length} exceeds 128: {k}");
  152. }
  153. }