| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- using Admin.NET.Plugin.AiDOP.Service.S8;
- using System.Reflection;
- using System.Text.RegularExpressions;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S8;
- /// <summary>
- /// S8-REAL-DATA-PRESENTATION-1:结果 KPI 的来源标注守卫。
- ///
- /// <para><b>修复前的事实</b>(2026-09-21 取证,代码 + 实库双证):
- /// <c>ORDER_DELIVERY_RATE</c> / <c>PO_DELIVERY_RATE</c> / <c>WO_COMPLETION_RATE</c> /
- /// <c>ORDER_FLOW_CYCLE_RATIO</c> 四项分别读 <c>ado_s8_order_flow_order</c> /
- /// <c>_procurement_pivot</c> / <c>_manufacturing_process</c> / <c>_stage</c>,
- /// 并以 <c>Source="ORDER_FLOW_CALC"</c>(前端「订单链路计算」)对外宣称是当前实算。
- /// 而这四张表唯一的写入方是 SeedData,实库全部行 <c>data_source='SEED'</c>,查询也不过滤 SEED。
- /// 另有 envelope 无条件写死 <c>MIXED_BASELINE</c>(前端「当前基线 · 部分订单链路计算」),
- /// 即便 items 为空也照样宣称「部分算了」。</para>
- ///
- /// <para><b>为什么是源码契约测试</b>:<see cref="S8MonitoringService"/> 依赖 SqlSugar 实库,
- /// 进程内无法构造;沿用本仓既有手法(反射元数据 + 去注释源码契约),
- /// 与 <c>S8DebugTenantGuardTests</c> / <c>S8LegacySymbolsRemovedTests</c> 同范式。
- /// 断言一律在**剥离注释后**的源码上进行,避免文档里提到某个词就把守卫变成噪音。</para>
- /// </summary>
- 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");
- /// <summary>SEED 订单链路表对应的实体名——它们不得出现在监控/KPI 服务的运行期取数里。</summary>
- private static readonly string[] SeedOrderFlowEntities =
- {
- "AdoS8OrderFlowOrder",
- "AdoS8OrderFlowProcurementPivot",
- "AdoS8OrderFlowManufacturingProcess",
- "AdoS8OrderFlowStage",
- };
- // ───────────── ⑧ HeadlineKpi_DoesNotReadSeedAsCurrentFact ─────────────
- /// <summary>KPI 服务不得再依赖任何 ado_s8_order_flow_* SEED 仓储。</summary>
- [Fact]
- public void HeadlineKpi_DoesNotReadSeedAsCurrentFact()
- {
- var code = MonitoringCode;
- foreach (var entity in SeedOrderFlowEntities)
- Assert.DoesNotContain(entity, code);
- Assert.DoesNotContain("ado_s8_order_flow", code);
- }
- /// <summary>
- /// 反射元数据层的同一条守卫:构造函数不得再注入 SEED 订单链路仓储。
- /// 改文案绕不过这一条。
- /// </summary>
- [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);
- }
- /// <summary>
- /// 不得再发射 ORDER_FLOW_CALC / ORDER_FLOW_CALC_EMPTY 这两个「实算」provenance。
- /// 判据精确到赋值形态,避免误伤文档中对历史行为的说明。
- /// </summary>
- [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 ─────────────
- /// <summary>
- /// 四项无真实 Authority 的 KPI 必须走「不出当前值」的路径:
- /// 分发器不再按 metricCode 分支,一律返回 null → 调用方落 PENDING_REAL。
- /// </summary>
- [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);
- }
- /// <summary>
- /// IQC_PASS_RATE 作为如实标注的正向样板必须保持不变:
- /// 常量值 + DEMO_BASELINE 标注(前端「当前基线」),不得被顺手改成实算。
- /// </summary>
- [Fact]
- public void ReferenceKpi_IsExplicitlyLabeledReference()
- {
- var code = MonitoringCode;
- Assert.Contains("IQC_PASS_RATE", code);
- Assert.Contains("\"DEMO_BASELINE\"", code);
- }
- // ───────────── envelope provenance ─────────────
- /// <summary>
- /// envelope 的 Source 必须由 items 真实构成推出,不得写死。
- /// 旧实现无条件 MIXED_BASELINE —— items 为空时照样宣称「部分订单链路计算」。
- /// </summary>
- [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 ─────────────
- /// <summary>
- /// 去 seed 不得演变成「直读业务源表」。监控/KPI 服务里不允许出现源系统表名。
- /// </summary>
- [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 ─────────────
- /// <summary>
- /// 结果 KPI 的字典查询仍须按租户收口(平台默认行 tenant_id=0 与本租户行),
- /// 且作用域一律服务端解析,不接受调用方传入。
- /// </summary>
- [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);
- }
- }
|