using Admin.NET.Plugin.AiDOP.MaterialWarehouse; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse; /// /// B-2 · S5IqcSpecResolver.Decide 纯裁决契约测试。 /// /// 覆盖 B-2 五条核心验收: /// ① 有唯一可信检规 → 固化;② 无检规 → NO_SPEC 不阻断; /// ③ 跨 wjbh 歧义 → AMBIGUOUS 不瞎选(**防止把错误检规永久固化的核心安全门**); /// ④ MANUAL 优先于 AUTO;⑤ 同 wjbh 多版本按生效日期而非 id/字符串序裁决。 /// /// 全部为纯函数测试,不触碰任何真实 UAT 业务数据。 /// public class S5IqcSpecResolverDecideTests { private static readonly DateTime Ref = new(2026, 9, 3); private static S5IqcSpecCandidate C(long id, string wjbh, string bb, string? sxrq, bool manual = false) => new() { SpecId = id, SpecNo = wjbh, SpecVersion = bb, EffectiveDateRaw = sxrq, IsManual = manual }; // ── ② 无候选 → NO_SPEC,不阻断 ── [Fact] public void NoCandidate_ReturnsNoSpec() { var r = S5IqcSpecResolver.Decide(Array.Empty(), Ref); Assert.Equal(S5IqcSpecResolveStatus.NoSpec, r.Status); Assert.False(r.IsMatched); Assert.Null(r.SpecId); Assert.Equal(0, r.CandidateCount); } [Fact] public void NullCandidateList_ReturnsNoSpec() => Assert.Equal(S5IqcSpecResolveStatus.NoSpec, S5IqcSpecResolver.Decide(null, Ref).Status); // ── ① 单候选已生效 → MATCHED(A04 / 1AB9275 场景)── [Fact] public void SingleDatedCandidate_Matched() { var r = S5IqcSpecResolver.Decide([C(3000000000000010004, "U01-1-63", "A.1", "2021.07.13")], Ref); Assert.Equal(S5IqcSpecResolveStatus.Matched, r.Status); Assert.True(r.IsMatched); Assert.Equal(3000000000000010004, r.SpecId); Assert.Equal("U01-1-63", r.SpecNo); Assert.Equal("A.1", r.SpecVersion); // 固化的是**原始文本**,不是归一化值 Assert.Equal("2021.07.13", r.EffectiveDateRaw); } // ── 单候选无生效日期 → MATCHED_UNDATED(Golden / 314DD002 场景,sxrq = NULL)── [Theory] [InlineData(null)] [InlineData("")] [InlineData("AUTODED")] // 实测 qms_jygf 中 383 行为该字面量 public void SingleUndatedOrUnparsableCandidate_MatchedUndated(string? sxrq) { var r = S5IqcSpecResolver.Decide([C(844226424377413, "UATA-IQC-01", "A.1", sxrq)], Ref); Assert.Equal(S5IqcSpecResolveStatus.MatchedUndated, r.Status); Assert.True(r.IsMatched); Assert.Equal(844226424377413, r.SpecId); Assert.Equal(sxrq, r.EffectiveDateRaw); } // ── 单候选但生效日期在未来 → NO_EFFECTIVE_SPEC,不固化 ── [Fact] public void SingleFutureCandidate_NoEffectiveSpec() { var r = S5IqcSpecResolver.Decide([C(1, "F-1", "A.1", "2099.01.01")], Ref); Assert.Equal(S5IqcSpecResolveStatus.NoEffectiveSpec, r.Status); Assert.False(r.IsMatched); Assert.Null(r.SpecId); } // ── ③ 跨 wjbh 歧义 → AMBIGUOUS(核心安全门;即使日期一新一旧也不得择一)── [Fact] public void MultipleDistinctWjbh_Ambiguous_EvenWhenDatesDiffer() { var r = S5IqcSpecResolver.Decide( [ C(10, "BI00-1-92", "A.7", "2024.01.01"), C(11, "S-BI00-1-92", "A.0", "2020.01.01"), ], Ref); Assert.Equal(S5IqcSpecResolveStatus.Ambiguous, r.Status); Assert.False(r.IsMatched); Assert.Null(r.SpecId); Assert.True(r.CandidateCount >= 2); Assert.Contains("不同检规编号", r.Warning); } // ── ⑤ 同 wjbh 多版本 → 取生效日期最大者(91A0DC9 / Y101-1-11 场景)── [Fact] public void SameWjbhMultiVersion_PicksLatestEffectiveDate() { // 故意让"日期较新"的那条 specId 更小、版本字符串字典序更小(A.1 < X.1), // 以证明既不是按 specId 排序、也不是按版本字符串排序。 var r = S5IqcSpecResolver.Decide( [ C(3000000000000010006, "Y101-1-11", "X.1", "2023.01.13"), C(3000000000000010005, "Y101-1-11", "A.1", "2024.07.11"), ], Ref); Assert.Equal(S5IqcSpecResolveStatus.Matched, r.Status); Assert.Equal(3000000000000010005, r.SpecId); Assert.Equal("A.1", r.SpecVersion); Assert.Equal("2024.07.11", r.EffectiveDateRaw); } /// 输入顺序不得影响结果(排除对 DB 默认返回顺序的依赖)。 [Fact] public void SameWjbhMultiVersion_OrderIndependent() { var a = S5IqcSpecResolver.Decide([C(5, "W", "A.1", "2024.07.11"), C(6, "W", "X.1", "2023.01.13")], Ref); var b = S5IqcSpecResolver.Decide([C(6, "W", "X.1", "2023.01.13"), C(5, "W", "A.1", "2024.07.11")], Ref); Assert.Equal(a.SpecId, b.SpecId); Assert.Equal(5, a.SpecId); } // ── 同 wjbh、同最大生效日期、不同 specId → 不再靠 id 兜底 ── [Fact] public void SameWjbhSameMaxDate_DifferentSpecIds_Ambiguous() { var r = S5IqcSpecResolver.Decide( [ C(20, "W-1", "A.1", "2024.05.01"), C(21, "W-1", "A.2", "2024.05.01"), ], Ref); Assert.Equal(S5IqcSpecResolveStatus.Ambiguous, r.Status); Assert.Null(r.SpecId); } // ── 同 wjbh、无已生效版本、恰好 1 条未标注日期 → MATCHED_UNDATED ── [Fact] public void SameWjbh_OnlyOneUndated_MatchedUndated() { var r = S5IqcSpecResolver.Decide( [ C(30, "W-2", "A.1", "2099.01.01"), // 未来 C(31, "W-2", "A.2", null), // 未标注 ], Ref); Assert.Equal(S5IqcSpecResolveStatus.MatchedUndated, r.Status); Assert.Equal(31, r.SpecId); } // ── 同 wjbh、多条未标注日期 → AMBIGUOUS(无业务依据)── [Fact] public void SameWjbh_MultipleUndated_Ambiguous() { var r = S5IqcSpecResolver.Decide([C(40, "W-3", "A.1", null), C(41, "W-3", "A.2", "")], Ref); Assert.Equal(S5IqcSpecResolveStatus.Ambiguous, r.Status); Assert.Null(r.SpecId); } // ── 同 wjbh、全部为未来版本 → NO_EFFECTIVE_SPEC ── [Fact] public void SameWjbh_AllFuture_NoEffectiveSpec() { var r = S5IqcSpecResolver.Decide([C(50, "W-4", "A.1", "2099.01.01"), C(51, "W-4", "A.2", "2098.01.01")], Ref); Assert.Equal(S5IqcSpecResolveStatus.NoEffectiveSpec, r.Status); Assert.Null(r.SpecId); } // ── ④ MANUAL 优先于 AUTO EXACT(即使 AUTO 日期更新)── [Fact] public void ManualBinding_WinsOverAutoExact() { var r = S5IqcSpecResolver.Decide( [ C(60, "AUTO-SPEC", "A.9", "2026.01.01"), C(61, "MANUAL-SPEC", "A.1", "2019.01.01", manual: true), ], Ref); Assert.Equal(S5IqcSpecResolveStatus.MatchedManual, r.Status); Assert.Equal(61, r.SpecId); } /// 人工绑定即使无生效日期也优先,且不会被降级为 UNDATED 以外的状态。 [Fact] public void ManualBinding_UndatedStillWins() { var r = S5IqcSpecResolver.Decide( [ C(70, "AUTO", "A.1", "2020.01.01"), C(71, "MAN", "A.1", null, manual: true), ], Ref); Assert.Equal(S5IqcSpecResolveStatus.MatchedManual, r.Status); Assert.Equal(71, r.SpecId); } // ── 多条互斥 MANUAL → AMBIGUOUS,禁止静默选一条 ── [Fact] public void MultipleConflictingManualBindings_Ambiguous() { var r = S5IqcSpecResolver.Decide( [ C(80, "MAN-A", "A.1", "2024.01.01", manual: true), C(81, "MAN-B", "A.1", "2023.01.01", manual: true), ], Ref); Assert.Equal(S5IqcSpecResolveStatus.Ambiguous, r.Status); Assert.Null(r.SpecId); Assert.Contains("人工绑定", r.Warning); } /// 同一 spec 因多个 token 命中而重复出现,不应被误判为歧义。 [Fact] public void SameSpecViaDuplicateRows_NotAmbiguous() { var r = S5IqcSpecResolver.Decide([C(90, "W-5", "A.1", "2024.01.01"), C(90, "W-5", "A.1", "2024.01.01")], Ref); Assert.Equal(S5IqcSpecResolveStatus.Matched, r.Status); Assert.Equal(90, r.SpecId); } // ── 生效日期归一化:多格式 + 不可解析 ── [Theory] [InlineData("2021.07.13", true)] [InlineData("2021-07-13", true)] [InlineData("2021/07/13", true)] [InlineData("2025-05-28 00:00:00", true)] [InlineData("AUTODED", false)] [InlineData("", false)] [InlineData(null, false)] public void NormalizeDate_ParsesKnownFormats(string? raw, bool expectParsed) => Assert.Equal(expectParsed, S5IqcSpecResolver.NormalizeDate(raw) != null); /// 生效日期恰好等于参考日 → 视为已生效(边界包含)。 [Fact] public void EffectiveDateEqualToReference_IsEffective() { var r = S5IqcSpecResolver.Decide([C(100, "W-6", "A.1", "2026-09-03")], Ref); Assert.Equal(S5IqcSpecResolveStatus.Matched, r.Status); } }