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);
}
}