using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.SmartOps; public class MissingKpiPipelineContractTests { [Fact] public void S2AndS4Pipelines_BackfillFourteenDayTrends() { var s2 = ReadSource("Production", "S2MdpSyncTransformService.cs"); var s4 = ReadSource("ProcurementExecution", "S4MdpSyncTransformService.cs"); Assert.Contains("for (var dayOffset = 13; dayOffset >= 0; dayOffset--)", s2); Assert.Contains("for (var dayOffset = 13; dayOffset >= 0; dayOffset--)", s4); Assert.Contains("YitIdHelper.NextId()", s2); Assert.Contains("YitIdHelper.NextId()", s4); Assert.Contains("factoryId: _runScope.FactoryId", s4); Assert.DoesNotContain("SELECT COALESCE(MAX(id), 0) + 1", s2); Assert.DoesNotContain("SELECT COALESCE(MAX(id), 0) + 1", s4); } [Fact] public void S3MaterialTurnover_UsesTenantScopedInboundAndIssueFacts() { var source = ReadSource("Supply", "S3MdpSyncTransformService.cs"); Assert.Contains("MetricCode = \"S3_L1_004\"", source); Assert.Contains("h.tenant_id=@TenantId", source); Assert.Contains("h.lbs IN ('采购入库','生产领料')", source); Assert.Contains("NULLIF(SUM(issue_qty), 0)", source); Assert.Contains("'S3_L2_001' AS MetricCode", source); Assert.Contains("'S3_L2_006' AS MetricCode", source); Assert.Contains("p.gw='计划'", source); Assert.Contains("for (var dayOffset = 13; dayOffset >= 0; dayOffset--)", source); Assert.Contains("GlobalRunGate.WaitAsync", source); Assert.Contains("GlobalRunGate.Release", source); } [Fact] public void S9QualityReturnRate_UsesS7BusinessTransactionsAndPpmScale() { var source = ReadSource("FinishedWarehouse", "S7MdpSyncTransformService.cs"); Assert.Contains("result.MergeSub(\"S9_L1_001\"", source); Assert.Contains("h.lbs IN ('销售退货','销售出库')", source); Assert.Contains("* 1_000_000m", source); Assert.Contains("option, \"S9\")", source); } [Fact] public void GenericDashboard_UsesLatestValuePerMetric() { var source = ReadSource("Controllers", "AidopKanbanController.Generic.cs"); Assert.Contains("v2.metric_code=v.metric_code", source); Assert.DoesNotContain("is_deleted=0 AND biz_date=@d", source); } [Fact] public void MonthlyKpis_CarryForwardLatestWhenDateFilterHasNoValue() { var source = ReadSource("Controllers", "AidopKanbanController.SmartOpsFilter.cs"); Assert.Contains("MonthlyCarryForwardMetricCodes", source); Assert.Contains("\"S5_L1_003\"", source); Assert.Contains("\"S6_L1_003\"", source); Assert.Contains("\"S6_L2_003\"", source); Assert.Contains("\"S7_L1_003\"", source); Assert.Contains("\"S9_L1_001\"", source); Assert.Contains("monthly_latest", source); Assert.Contains("HasBusinessDimension", source); Assert.Contains("SuppressUnfilteredValuesWhenBusinessFilter", source); Assert.Contains("IsAuthoritativeFilteredMode", source); } [Fact] public void S5StageKpis_UseReceiptAndIqcFacts() { var source = ReadSource("MaterialWarehouse", "S5MdpSyncTransformService.cs"); Assert.Contains("result.MergeSub(\"S5_L2_001\"", source); Assert.Contains("result.MergeSub(\"S5_L2_003\"", source); Assert.Contains("source_table='PurOrdRctMaster'", source); Assert.Contains("STR_TO_DATE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.PerformDate'))", source); Assert.Contains("FINSPESTARTDATE", source); Assert.Contains("ado_s9_kpi_value_l2_day", source); Assert.Contains("\"S5_L2_015\"", source); Assert.Contains("\"S5_L3_005\"", source); Assert.Contains("mdp_std_inv_trans", source); Assert.Contains("_purchaseReceiptMdpSync.RunInboundAsync", source); Assert.Contains("const int backfillDays = 14", source); Assert.Contains("warehouseFallback", source); Assert.DoesNotContain("COPY_PARENT_VALUE", source); } [Fact] public void S6WorkOrderCycle_GroupsByWorkOrderNotOrder() { var source = ReadSource("Manufacturing", "S6MdpSyncTransformService.cs"); Assert.Contains("result.MergeSub(\"S6_L2_001\"", source); Assert.Contains("group by rwnoid", source); Assert.Contains("NO_COMPLETED_WORK_ORDER", source); Assert.Contains("L2ValueTable", source); Assert.Contains("\"S6_L2_004\"", source); Assert.Contains("\"S6_L3_007\"", source); Assert.Contains("WorkOrdRouting", source); Assert.Contains("OpTransEmployee", source); Assert.Contains("const int backfillDays = 14", source); } [Fact] public void S7StageKpis_UseFqcShipmentAndInventoryFacts() { var source = ReadSource("FinishedWarehouse", "S7MdpSyncTransformService.cs"); Assert.Contains("Enumerable.Range(1, 16)", source); Assert.Contains("FROM qms_fqcbj q", source); Assert.Contains("mdp_std_inv_trans", source); Assert.Contains("\"S7_L2_016\"", source); Assert.Contains("L2ValueTable", source); Assert.Contains("const int backfillDays = 14", source); Assert.Contains("result.MergeSub(\"S9_L1_001\"", source); } [Fact] public void S1ProcessAndDeliveryReviewBranches_UseDistinctBusinessFlows() { var source = ReadSource("Order", "S1MdpSyncTransformService.cs"); Assert.Contains("'S1_L2_007' AS MetricCode", source); Assert.Contains("LIKE 'UAT-PROC-%'", source); Assert.Contains("LIKE 'UAT-DEL-%'", source); Assert.Contains("CONCAT('S1_L3_',branch_code", source); Assert.Contains("CONCAT('S1_L4_',branch_code", source); Assert.Contains("for (var dayOffset = 13; dayOffset >= 0; dayOffset--)", source); } [Fact] public void S8Snapshot_RefreshesFourteenDayTrendThroughFormalAggregator() { var source = ReadSource("Service/S8", "S8KpiSnapshotService.cs"); Assert.Contains("for (var dayOffset = 13; dayOffset >= 0; dayOffset--)", source); Assert.Contains("RefreshAsync(", source); Assert.Contains("_kpiTargetResolver.ResolveAsync", source); } [Fact] public void ModuleRebuildWorker_RechecksStaleJobsWhileRunning() { var source = ReadSource("DataPlatform/MdpRebuild", "ModuleRebuildWorker.cs"); Assert.Contains("StaleSweepInterval", source); Assert.Contains("DateTimeOffset.UtcNow >= nextStaleSweepAt", source); Assert.Contains("await FailStaleAsync(stoppingToken)", source); } [Fact] public void S8Snapshot_UsesExceptionTableAndKeepsNullTimeliness() { var source = ReadSource("Service/S8", "S8KpiSnapshotService.cs"); Assert.Contains("FROM ado_s8_exception", source); Assert.Contains("S8_L1_001", source); Assert.Contains("S8_L1_002", source); Assert.Contains("avgHours > 0 ? avgHours : null", source); Assert.DoesNotContain("metric_value=0", source); } private static string ReadSource(string folder, string fileName) { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null) { var candidate = Path.Combine( dir.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", folder, fileName); if (File.Exists(candidate)) return File.ReadAllText(candidate); dir = dir.Parent; } throw new FileNotFoundException(fileName); } }