using Admin.NET.Plugin.AiDOP.Controllers; using Admin.NET.Plugin.AiDOP.Entity; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.SmartOps; public class SmartDiagnosisTreeTests { [Fact] public void SpecifiedRoot_WithL2_ReturnsOnlyOwnChildren() { var kpis = SampleTree(); var values = new Dictionary(StringComparer.OrdinalIgnoreCase); var resolved = SmartDiagnosisTree.ResolveRoot(L1(kpis), values, "S1_L1_001"); Assert.Null(resolved.Error); Assert.Equal(SmartDiagnosisTree.SelectionSpecified, resolved.SelectionMode); Assert.Equal("S1_L1_001", resolved.Root!.MetricCode); var children = SmartDiagnosisTree.DirectChildren(kpis, resolved.Root.Id, 2); Assert.Equal(new[] { "S1_L2_001", "S1_L2_004" }, children.Select(x => x.MetricCode).ToArray()); Assert.DoesNotContain(children, x => x.MetricCode == "S1_L2_002"); } [Fact] public void SpecifiedRoot_WithoutL2_ReturnsEmptyChildren() { var kpis = SampleTree(); var resolved = SmartDiagnosisTree.ResolveRoot(L1(kpis), EmptyValues(), "S1_L1_004"); Assert.Null(resolved.Error); Assert.Equal("S1_L1_004", resolved.Root!.MetricCode); Assert.Empty(SmartDiagnosisTree.DirectChildren(kpis, resolved.Root.Id, 2)); } [Fact] public void GlobalWorst_WithoutL2_StillSelectsThatRoot() { var kpis = SampleTree(); var values = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["S1_L1_001"] = new(20m, 20m), ["S1_L1_002"] = new(90m, 95m), ["S1_L1_004"] = new(101.94m, 15m), }; var resolved = SmartDiagnosisTree.ResolveRoot(L1(kpis), values, null); Assert.Null(resolved.Error); Assert.Equal(SmartDiagnosisTree.SelectionGlobalWorst, resolved.SelectionMode); Assert.Equal("S1_L1_004", resolved.Root!.MetricCode); Assert.Empty(SmartDiagnosisTree.DirectChildren(kpis, resolved.Root.Id, 2)); } [Fact] public void MixedL3_OnlyConfiguredChildrenReturned() { var kpis = SampleTree(); var l2WithL3 = kpis.First(x => x.MetricCode == "S1_L2_001"); var l2WithoutL3 = kpis.First(x => x.MetricCode == "S1_L2_004"); var withL3 = SmartDiagnosisTree.DirectChildren(kpis, l2WithL3.Id, 3); var withoutL3 = SmartDiagnosisTree.DirectChildren(kpis, l2WithoutL3.Id, 3); Assert.Single(withL3); Assert.Equal("S1_L3_001", withL3[0].MetricCode); Assert.Empty(withoutL3); } [Fact] public void L3WithoutL4_ReturnsEmptyLeaf() { var kpis = SampleTree(); var l3 = kpis.First(x => x.MetricCode == "S1_L3_001"); Assert.Empty(SmartDiagnosisTree.DirectChildren(kpis, l3.Id, 4)); } [Fact] public void InvalidSpecifiedMetric_DoesNotFallBackToGlobal() { var kpis = SampleTree(); var values = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["S1_L1_004"] = new(101.94m, 15m), }; var resolved = SmartDiagnosisTree.ResolveRoot(L1(kpis), values, "NOT_AN_L1"); Assert.Null(resolved.Root); Assert.Equal(SmartDiagnosisTree.SelectionSpecified, resolved.SelectionMode); Assert.Equal("metricCode 不是当前模块已启用的 L1 指标。", resolved.Error); } [Fact] public void BuildDiagnosisRate_UsesFormula_NotHardcoded58() { Assert.Equal("84%", SmartDiagnosisTree.BuildDiagnosisRate(1.79m, 1.5m, "lower_is_better")); Assert.Equal("50%", SmartDiagnosisTree.BuildDiagnosisRate(50m, 100m, "higher_is_better")); Assert.Equal("-", SmartDiagnosisTree.BuildDiagnosisRate(null, 1.5m, "lower_is_better")); Assert.Equal("-", SmartDiagnosisTree.BuildDiagnosisRate(1.79m, 0m, "lower_is_better")); } private static List L1(List kpis) => kpis.Where(x => x.MetricLevel == 1).OrderBy(x => x.SortNo).ToList(); private static Dictionary EmptyValues() => new(StringComparer.OrdinalIgnoreCase); private static List SampleTree() { var l1Cycle = Kpi(337, "S1_L1_001", "订单评审周期(天)", 1, null, 1, "lower_is_better"); var l1Rate = Kpi(338, "S1_L1_002", "订单评审满足率(%)", 1, null, 2, "higher_is_better"); var l1Turn = Kpi(340, "S1_L1_004", "成品库存周转(天)", 1, null, 4, "lower_is_better"); var l2Contract = Kpi(369, "S1_L2_001", "合同评审周期(天)", 2, l1Cycle.Id, 1, "lower_is_better"); var l2Rate = Kpi(370, "S1_L2_002", "合同评审满足率(%)", 2, l1Rate.Id, 2, "higher_is_better"); var l2Bom = Kpi(372, "S1_L2_004", "BOM评审周期(天)", 2, l1Cycle.Id, 4, "lower_is_better"); var l3 = Kpi(401, "S1_L3_001", "意见评审", 3, l2Contract.Id, 1, "lower_is_better"); return [l1Cycle, l1Rate, l1Turn, l2Contract, l2Rate, l2Bom, l3]; } private static AdoSmartOpsKpiMaster Kpi( long id, string code, string name, int level, long? parentId, int sortNo, string direction) => new() { Id = id, MetricCode = code, MetricName = name, MetricLevel = level, ParentId = parentId, SortNo = sortNo, Direction = direction, ModuleCode = "S1", IsEnabled = true, }; }