using Admin.NET.Plugin.AiDOP.Service.S8; using System.Reflection; using System.Text.RegularExpressions; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.S8; /// /// S8-REAL-DATA-PRESENTATION-1:结果 KPI 的来源标注守卫。 /// /// 修复前的事实(2026-09-21 取证,代码 + 实库双证): /// ORDER_DELIVERY_RATE / PO_DELIVERY_RATE / WO_COMPLETION_RATE / /// ORDER_FLOW_CYCLE_RATIO 四项分别读 ado_s8_order_flow_order / /// _procurement_pivot / _manufacturing_process / _stage, /// 并以 Source="ORDER_FLOW_CALC"(前端「订单链路计算」)对外宣称是当前实算。 /// 而这四张表唯一的写入方是 SeedData,实库全部行 data_source='SEED',查询也不过滤 SEED。 /// 另有 envelope 无条件写死 MIXED_BASELINE(前端「当前基线 · 部分订单链路计算」), /// 即便 items 为空也照样宣称「部分算了」。 /// /// 为什么是源码契约测试: 依赖 SqlSugar 实库, /// 进程内无法构造;沿用本仓既有手法(反射元数据 + 去注释源码契约), /// 与 S8DebugTenantGuardTests / S8LegacySymbolsRemovedTests 同范式。 /// 断言一律在**剥离注释后**的源码上进行,避免文档里提到某个词就把守卫变成噪音。 /// public class S8KpiProvenanceTests { private static string PluginRoot => Path.GetFullPath( Path.Combine(AppContext.BaseDirectory, "../../../../Admin.NET.Plugin.AiDOP")); private static string ReadCode(string relativePath) { var raw = File.ReadAllText(Path.Combine(PluginRoot, relativePath)); var noBlock = Regex.Replace(raw, @"/\*.*?\*/", string.Empty, RegexOptions.Singleline); return Regex.Replace(noBlock, @"^\s*//.*$", string.Empty, RegexOptions.Multiline); } private static string MonitoringCode => ReadCode("Service/S8/S8MonitoringService.cs"); /// SEED 订单链路表对应的实体名——它们不得出现在监控/KPI 服务的运行期取数里。 private static readonly string[] SeedOrderFlowEntities = { "AdoS8OrderFlowOrder", "AdoS8OrderFlowProcurementPivot", "AdoS8OrderFlowManufacturingProcess", "AdoS8OrderFlowStage", }; // ───────────── ⑧ HeadlineKpi_DoesNotReadSeedAsCurrentFact ───────────── /// KPI 服务不得再依赖任何 ado_s8_order_flow_* SEED 仓储。 [Fact] public void HeadlineKpi_DoesNotReadSeedAsCurrentFact() { var code = MonitoringCode; foreach (var entity in SeedOrderFlowEntities) Assert.DoesNotContain(entity, code); Assert.DoesNotContain("ado_s8_order_flow", code); } /// /// 反射元数据层的同一条守卫:构造函数不得再注入 SEED 订单链路仓储。 /// 改文案绕不过这一条。 /// [Fact] public void MonitoringService_TakesNoSeedOrderFlowRepository() { var ctor = Assert.Single(typeof(S8MonitoringService).GetConstructors()); var injected = ctor.GetParameters() .Select(p => p.ParameterType.IsGenericType ? p.ParameterType.GetGenericArguments()[0].Name : p.ParameterType.Name) .ToList(); foreach (var entity in SeedOrderFlowEntities) Assert.DoesNotContain(entity, injected); } /// /// 不得再发射 ORDER_FLOW_CALC / ORDER_FLOW_CALC_EMPTY 这两个「实算」provenance。 /// 判据精确到赋值形态,避免误伤文档中对历史行为的说明。 /// [Fact] public void NoOrderFlowCalcProvenanceIsEmitted() { var code = MonitoringCode; Assert.DoesNotContain("\"ORDER_FLOW_CALC_EMPTY\"", code); // 唯一允许保留的是 envelope 的**判定**(读 items 的 Source),不是发射点。 var occurrences = Regex.Matches(code, "\"ORDER_FLOW_CALC\"").Count; Assert.Equal(1, occurrences); Assert.Contains("i.Source == \"ORDER_FLOW_CALC\"", code); } // ───────────── ⑩ MissingRealAuthority_ReturnsNoData ───────────── /// /// 四项无真实 Authority 的 KPI 必须走「不出当前值」的路径: /// 分发器不再按 metricCode 分支,一律返回 null → 调用方落 PENDING_REAL。 /// [Fact] public void MissingRealAuthority_ReturnsNoData() { var code = MonitoringCode; // 四个指标码不得再出现在任何 switch 分发分支里 foreach (var metric in new[] { "\"ORDER_DELIVERY_RATE\" =>", "\"PO_DELIVERY_RATE\" =>", "\"WO_COMPLETION_RATE\" =>", "\"ORDER_FLOW_CYCLE_RATIO\" =>", }) Assert.DoesNotContain(metric, code); // 旧的四个计算方法必须整体消失 foreach (var m in new[] { "ComputeOrderDeliveryRateAsync", "ComputePoDeliveryRateAsync", "ComputeWoCompletionRateAsync", "ComputeOrderFlowCycleRatioAsync", }) Assert.DoesNotContain(m, code); // PENDING_REAL 兜底路径仍在 Assert.Contains("\"PENDING_REAL\"", code); } /// /// IQC_PASS_RATE 作为如实标注的正向样板必须保持不变: /// 常量值 + DEMO_BASELINE 标注(前端「当前基线」),不得被顺手改成实算。 /// [Fact] public void ReferenceKpi_IsExplicitlyLabeledReference() { var code = MonitoringCode; Assert.Contains("IQC_PASS_RATE", code); Assert.Contains("\"DEMO_BASELINE\"", code); } // ───────────── envelope provenance ───────────── /// /// envelope 的 Source 必须由 items 真实构成推出,不得写死。 /// 旧实现无条件 MIXED_BASELINE —— items 为空时照样宣称「部分订单链路计算」。 /// [Fact] public void EnvelopeProvenance_IsDerivedFromItems_NotHardcoded() { var code = MonitoringCode; Assert.DoesNotContain("Source = \"MIXED_BASELINE\",", code); Assert.Contains("hasRealCalc", code); Assert.Contains("hasRealCalc ? \"MIXED_BASELINE\" : \"DICTIONARY_MOCK\"", code); } // ───────────── ⑬ NoDirectBusinessSourceRead ───────────── /// /// 去 seed 不得演变成「直读业务源表」。监控/KPI 服务里不允许出现源系统表名。 /// [Fact] public void NoDirectBusinessSourceRead() { var code = MonitoringCode; foreach (var src in new[] { "PurOrdMaster", "PurOrdDetail", "WorkOrdMaster", "NbrMaster", "NbrDetail", "qms_", "crm_", "mdp_stg_", }) Assert.DoesNotContain(src, code); } // ───────────── ⑫ TenantA_KpiCannotAggregateTenantB ───────────── /// /// 结果 KPI 的字典查询仍须按租户收口(平台默认行 tenant_id=0 与本租户行), /// 且作用域一律服务端解析,不接受调用方传入。 /// [Fact] public void ResultKpi_StaysTenantScoped() { var code = MonitoringCode; Assert.Contains("x.TenantId == S8ConfigScope.GlobalTenantId", code); Assert.Contains("x.TenantId == tenantId", code); var controller = ReadCode("Controllers/S8/AdoS8MonitoringController.cs"); Assert.Contains("_scope.ResolveAsync()", controller); Assert.DoesNotContain("ClearFilter", controller); } }