S5IqcSpecResolverDecideTests.cs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  2. using Xunit;
  3. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  4. /// <summary>
  5. /// B-2 · S5IqcSpecResolver.Decide 纯裁决契约测试。
  6. ///
  7. /// 覆盖 B-2 五条核心验收:
  8. /// ① 有唯一可信检规 → 固化;② 无检规 → NO_SPEC 不阻断;
  9. /// ③ 跨 wjbh 歧义 → AMBIGUOUS 不瞎选(**防止把错误检规永久固化的核心安全门**);
  10. /// ④ MANUAL 优先于 AUTO;⑤ 同 wjbh 多版本按生效日期而非 id/字符串序裁决。
  11. ///
  12. /// 全部为纯函数测试,不触碰任何真实 UAT 业务数据。
  13. /// </summary>
  14. public class S5IqcSpecResolverDecideTests
  15. {
  16. private static readonly DateTime Ref = new(2026, 9, 3);
  17. private static S5IqcSpecCandidate C(long id, string wjbh, string bb, string? sxrq, bool manual = false)
  18. => new() { SpecId = id, SpecNo = wjbh, SpecVersion = bb, EffectiveDateRaw = sxrq, IsManual = manual };
  19. // ── ② 无候选 → NO_SPEC,不阻断 ──
  20. [Fact]
  21. public void NoCandidate_ReturnsNoSpec()
  22. {
  23. var r = S5IqcSpecResolver.Decide(Array.Empty<S5IqcSpecCandidate>(), Ref);
  24. Assert.Equal(S5IqcSpecResolveStatus.NoSpec, r.Status);
  25. Assert.False(r.IsMatched);
  26. Assert.Null(r.SpecId);
  27. Assert.Equal(0, r.CandidateCount);
  28. }
  29. [Fact]
  30. public void NullCandidateList_ReturnsNoSpec()
  31. => Assert.Equal(S5IqcSpecResolveStatus.NoSpec, S5IqcSpecResolver.Decide(null, Ref).Status);
  32. // ── ① 单候选已生效 → MATCHED(A04 / 1AB9275 场景)──
  33. [Fact]
  34. public void SingleDatedCandidate_Matched()
  35. {
  36. var r = S5IqcSpecResolver.Decide([C(3000000000000010004, "U01-1-63", "A.1", "2021.07.13")], Ref);
  37. Assert.Equal(S5IqcSpecResolveStatus.Matched, r.Status);
  38. Assert.True(r.IsMatched);
  39. Assert.Equal(3000000000000010004, r.SpecId);
  40. Assert.Equal("U01-1-63", r.SpecNo);
  41. Assert.Equal("A.1", r.SpecVersion);
  42. // 固化的是**原始文本**,不是归一化值
  43. Assert.Equal("2021.07.13", r.EffectiveDateRaw);
  44. }
  45. // ── 单候选无生效日期 → MATCHED_UNDATED(Golden / 314DD002 场景,sxrq = NULL)──
  46. [Theory]
  47. [InlineData(null)]
  48. [InlineData("")]
  49. [InlineData("AUTODED")] // 实测 qms_jygf 中 383 行为该字面量
  50. public void SingleUndatedOrUnparsableCandidate_MatchedUndated(string? sxrq)
  51. {
  52. var r = S5IqcSpecResolver.Decide([C(844226424377413, "UATA-IQC-01", "A.1", sxrq)], Ref);
  53. Assert.Equal(S5IqcSpecResolveStatus.MatchedUndated, r.Status);
  54. Assert.True(r.IsMatched);
  55. Assert.Equal(844226424377413, r.SpecId);
  56. Assert.Equal(sxrq, r.EffectiveDateRaw);
  57. }
  58. // ── 单候选但生效日期在未来 → NO_EFFECTIVE_SPEC,不固化 ──
  59. [Fact]
  60. public void SingleFutureCandidate_NoEffectiveSpec()
  61. {
  62. var r = S5IqcSpecResolver.Decide([C(1, "F-1", "A.1", "2099.01.01")], Ref);
  63. Assert.Equal(S5IqcSpecResolveStatus.NoEffectiveSpec, r.Status);
  64. Assert.False(r.IsMatched);
  65. Assert.Null(r.SpecId);
  66. }
  67. // ── ③ 跨 wjbh 歧义 → AMBIGUOUS(核心安全门;即使日期一新一旧也不得择一)──
  68. [Fact]
  69. public void MultipleDistinctWjbh_Ambiguous_EvenWhenDatesDiffer()
  70. {
  71. var r = S5IqcSpecResolver.Decide(
  72. [
  73. C(10, "BI00-1-92", "A.7", "2024.01.01"),
  74. C(11, "S-BI00-1-92", "A.0", "2020.01.01"),
  75. ], Ref);
  76. Assert.Equal(S5IqcSpecResolveStatus.Ambiguous, r.Status);
  77. Assert.False(r.IsMatched);
  78. Assert.Null(r.SpecId);
  79. Assert.True(r.CandidateCount >= 2);
  80. Assert.Contains("不同检规编号", r.Warning);
  81. }
  82. // ── ⑤ 同 wjbh 多版本 → 取生效日期最大者(91A0DC9 / Y101-1-11 场景)──
  83. [Fact]
  84. public void SameWjbhMultiVersion_PicksLatestEffectiveDate()
  85. {
  86. // 故意让"日期较新"的那条 specId 更小、版本字符串字典序更小(A.1 < X.1),
  87. // 以证明既不是按 specId 排序、也不是按版本字符串排序。
  88. var r = S5IqcSpecResolver.Decide(
  89. [
  90. C(3000000000000010006, "Y101-1-11", "X.1", "2023.01.13"),
  91. C(3000000000000010005, "Y101-1-11", "A.1", "2024.07.11"),
  92. ], Ref);
  93. Assert.Equal(S5IqcSpecResolveStatus.Matched, r.Status);
  94. Assert.Equal(3000000000000010005, r.SpecId);
  95. Assert.Equal("A.1", r.SpecVersion);
  96. Assert.Equal("2024.07.11", r.EffectiveDateRaw);
  97. }
  98. /// <summary>输入顺序不得影响结果(排除对 DB 默认返回顺序的依赖)。</summary>
  99. [Fact]
  100. public void SameWjbhMultiVersion_OrderIndependent()
  101. {
  102. var a = S5IqcSpecResolver.Decide([C(5, "W", "A.1", "2024.07.11"), C(6, "W", "X.1", "2023.01.13")], Ref);
  103. var b = S5IqcSpecResolver.Decide([C(6, "W", "X.1", "2023.01.13"), C(5, "W", "A.1", "2024.07.11")], Ref);
  104. Assert.Equal(a.SpecId, b.SpecId);
  105. Assert.Equal(5, a.SpecId);
  106. }
  107. // ── 同 wjbh、同最大生效日期、不同 specId → 不再靠 id 兜底 ──
  108. [Fact]
  109. public void SameWjbhSameMaxDate_DifferentSpecIds_Ambiguous()
  110. {
  111. var r = S5IqcSpecResolver.Decide(
  112. [
  113. C(20, "W-1", "A.1", "2024.05.01"),
  114. C(21, "W-1", "A.2", "2024.05.01"),
  115. ], Ref);
  116. Assert.Equal(S5IqcSpecResolveStatus.Ambiguous, r.Status);
  117. Assert.Null(r.SpecId);
  118. }
  119. // ── 同 wjbh、无已生效版本、恰好 1 条未标注日期 → MATCHED_UNDATED ──
  120. [Fact]
  121. public void SameWjbh_OnlyOneUndated_MatchedUndated()
  122. {
  123. var r = S5IqcSpecResolver.Decide(
  124. [
  125. C(30, "W-2", "A.1", "2099.01.01"), // 未来
  126. C(31, "W-2", "A.2", null), // 未标注
  127. ], Ref);
  128. Assert.Equal(S5IqcSpecResolveStatus.MatchedUndated, r.Status);
  129. Assert.Equal(31, r.SpecId);
  130. }
  131. // ── 同 wjbh、多条未标注日期 → AMBIGUOUS(无业务依据)──
  132. [Fact]
  133. public void SameWjbh_MultipleUndated_Ambiguous()
  134. {
  135. var r = S5IqcSpecResolver.Decide([C(40, "W-3", "A.1", null), C(41, "W-3", "A.2", "")], Ref);
  136. Assert.Equal(S5IqcSpecResolveStatus.Ambiguous, r.Status);
  137. Assert.Null(r.SpecId);
  138. }
  139. // ── 同 wjbh、全部为未来版本 → NO_EFFECTIVE_SPEC ──
  140. [Fact]
  141. public void SameWjbh_AllFuture_NoEffectiveSpec()
  142. {
  143. var r = S5IqcSpecResolver.Decide([C(50, "W-4", "A.1", "2099.01.01"), C(51, "W-4", "A.2", "2098.01.01")], Ref);
  144. Assert.Equal(S5IqcSpecResolveStatus.NoEffectiveSpec, r.Status);
  145. Assert.Null(r.SpecId);
  146. }
  147. // ── ④ MANUAL 优先于 AUTO EXACT(即使 AUTO 日期更新)──
  148. [Fact]
  149. public void ManualBinding_WinsOverAutoExact()
  150. {
  151. var r = S5IqcSpecResolver.Decide(
  152. [
  153. C(60, "AUTO-SPEC", "A.9", "2026.01.01"),
  154. C(61, "MANUAL-SPEC", "A.1", "2019.01.01", manual: true),
  155. ], Ref);
  156. Assert.Equal(S5IqcSpecResolveStatus.MatchedManual, r.Status);
  157. Assert.Equal(61, r.SpecId);
  158. }
  159. /// <summary>人工绑定即使无生效日期也优先,且不会被降级为 UNDATED 以外的状态。</summary>
  160. [Fact]
  161. public void ManualBinding_UndatedStillWins()
  162. {
  163. var r = S5IqcSpecResolver.Decide(
  164. [
  165. C(70, "AUTO", "A.1", "2020.01.01"),
  166. C(71, "MAN", "A.1", null, manual: true),
  167. ], Ref);
  168. Assert.Equal(S5IqcSpecResolveStatus.MatchedManual, r.Status);
  169. Assert.Equal(71, r.SpecId);
  170. }
  171. // ── 多条互斥 MANUAL → AMBIGUOUS,禁止静默选一条 ──
  172. [Fact]
  173. public void MultipleConflictingManualBindings_Ambiguous()
  174. {
  175. var r = S5IqcSpecResolver.Decide(
  176. [
  177. C(80, "MAN-A", "A.1", "2024.01.01", manual: true),
  178. C(81, "MAN-B", "A.1", "2023.01.01", manual: true),
  179. ], Ref);
  180. Assert.Equal(S5IqcSpecResolveStatus.Ambiguous, r.Status);
  181. Assert.Null(r.SpecId);
  182. Assert.Contains("人工绑定", r.Warning);
  183. }
  184. /// <summary>同一 spec 因多个 token 命中而重复出现,不应被误判为歧义。</summary>
  185. [Fact]
  186. public void SameSpecViaDuplicateRows_NotAmbiguous()
  187. {
  188. var r = S5IqcSpecResolver.Decide([C(90, "W-5", "A.1", "2024.01.01"), C(90, "W-5", "A.1", "2024.01.01")], Ref);
  189. Assert.Equal(S5IqcSpecResolveStatus.Matched, r.Status);
  190. Assert.Equal(90, r.SpecId);
  191. }
  192. // ── 生效日期归一化:多格式 + 不可解析 ──
  193. [Theory]
  194. [InlineData("2021.07.13", true)]
  195. [InlineData("2021-07-13", true)]
  196. [InlineData("2021/07/13", true)]
  197. [InlineData("2025-05-28 00:00:00", true)]
  198. [InlineData("AUTODED", false)]
  199. [InlineData("", false)]
  200. [InlineData(null, false)]
  201. public void NormalizeDate_ParsesKnownFormats(string? raw, bool expectParsed)
  202. => Assert.Equal(expectParsed, S5IqcSpecResolver.NormalizeDate(raw) != null);
  203. /// <summary>生效日期恰好等于参考日 → 视为已生效(边界包含)。</summary>
  204. [Fact]
  205. public void EffectiveDateEqualToReference_IsEffective()
  206. {
  207. var r = S5IqcSpecResolver.Decide([C(100, "W-6", "A.1", "2026-09-03")], Ref);
  208. Assert.Equal(S5IqcSpecResolveStatus.Matched, r.Status);
  209. }
  210. }