using Admin.NET.Plugin.AiDOP.Entity.S8;
using Admin.NET.Plugin.AiDOP.Service.S8;
using Xunit;
namespace Admin.NET.Test.S8;
///
/// S8-CONFIG-AUTHORITY-FIX-1:结果 KPI 的「停用即不显示」门禁。
///
/// 修复前的缺陷:GetResultKpiSummaryAsync 只按 mechanism / is_result_kpi / 租户作用域取行,
/// 完全不读 enabled。运行库里 5 条结果 KPI 全部 enabled=0(remark "[RESERVED] S9 result KPI"),
/// 却照常出现在监控总览——配置页面的停用开关对业务零效力。
///
/// 本文件锁定的契约与失败后果:
/// A. enabled=true → 纳入 → 否则正常指标消失,属功能倒退
/// B. enabled=false → 不纳入 → 否则"停用"仍然显示,用户认为配置坏了
/// C. is_result_kpi=false → 不纳入 → 否则机制字典项(Rule 向导用)会污染结果 KPI 卡片
/// D. 门禁必须在**覆盖解析之后** → 这是最容易写错的一条:
/// 若把 Enabled 下推成 SQL WHERE,租户 override enabled=false 的行会先被滤掉,
/// 分组里只剩 enabled=true 的全局行 → 结果回落成"显示",与停用意图完全相反。
/// E. 不触碰 Rule 机制字典 → 门禁只作用于结果 KPI 路径
///
/// 不接 DB、不接 DI、不触发任何真实规则。
///
public class S8ResultKpiEnabledGateTests
{
private const long TenantId = 838257186181189L;
private const long FactoryId = 838257186320453L;
private static AdoS8MonitorMetric Metric(
string code, bool enabled, bool isResultKpi = true,
long tenantId = 0, long factoryId = 0, string mechanism = "RATIO", int sortNo = 0) => new()
{
MetricCode = code,
MetricName = code,
Mechanism = mechanism,
IsResultKpi = isResultKpi,
Enabled = enabled,
TenantId = tenantId,
FactoryId = factoryId,
SortNo = sortNo
};
private static List CodesOf(IEnumerable rows) =>
rows.Select(x => x.MetricCode).ToList();
// ============================================================
// A/B/C. 基础门禁
// ============================================================
[Fact]
public void T1_EnabledResultKpi_IsReturned()
{
var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
{
Metric("ORDER_DELIVERY_RATE", enabled: true)
});
Assert.Equal(new[] { "ORDER_DELIVERY_RATE" }, CodesOf(result));
}
[Fact]
public void T2_DisabledResultKpi_IsNotReturned()
{
// 运行库现状:5 条结果 KPI 全部 enabled=0,修复前它们照常显示。
var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
{
Metric("IQC_PASS_RATE", enabled: false)
});
Assert.Empty(result);
}
[Fact]
public void T3_NonResultKpi_IsNotReturned()
{
// is_result_kpi=false 的是 Rule 向导用的机制字典项(DATE / VALUE_RANGE),
// 即使 enabled=true 也不属于结果 KPI 卡片。
// 注:调用方 SQL 已按 IsResultKpi 过滤,这里再断言一次,防止将来有人把过滤挪进本方法时写反。
var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
{
Metric("ORDER_DUE_AT", enabled: true, isResultKpi: false, mechanism: "DATE")
}.Where(m => m.IsResultKpi));
Assert.Empty(result);
}
// ============================================================
// D. 覆盖解析与门禁的先后顺序 —— 本文件最关键的两条
// ============================================================
[Fact]
public void T4_TenantOverrideDisable_HidesMetric_EvenWhenGlobalEnabled()
{
// 全局启用 + 租户停用 → 租户看不到。
// 若实现把 Enabled 下推成 SQL WHERE,租户行会先被滤掉、只剩全局行,
// 结果反而变成"可见"——本用例正是为了钉死这个反向缺陷。
var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
{
Metric("ORDER_DELIVERY_RATE", enabled: true), // 平台全局
Metric("ORDER_DELIVERY_RATE", enabled: false, tenantId: TenantId, factoryId: FactoryId) // 租户覆盖:停用
});
Assert.Empty(result);
}
[Fact]
public void T5_TenantOverrideEnable_ShowsMetric_EvenWhenGlobalDisabled()
{
// 反向:全局停用 + 租户启用 → 租户可见。
// 覆盖语义由 OrderByDescending(FactoryId).ThenByDescending(TenantId) 承载,本用例确认它对两个方向都成立。
var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
{
Metric("PO_DELIVERY_RATE", enabled: false),
Metric("PO_DELIVERY_RATE", enabled: true, tenantId: TenantId, factoryId: FactoryId)
});
Assert.Equal(new[] { "PO_DELIVERY_RATE" }, CodesOf(result));
}
[Fact]
public void T5b_EachMetricCode_ResolvesToExactlyOneEffectiveRow()
{
// 覆盖解析必须"每个 code 只留一条",否则同一 KPI 会在卡片上重复出现。
var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
{
Metric("WO_COMPLETION_RATE", enabled: true),
Metric("WO_COMPLETION_RATE", enabled: true, tenantId: TenantId),
Metric("WO_COMPLETION_RATE", enabled: true, tenantId: TenantId, factoryId: FactoryId)
});
Assert.Single(result);
// 工厂行优先级最高。
Assert.Equal(FactoryId, result[0].FactoryId);
}
// ============================================================
// E. Rule 机制字典不受影响
// ============================================================
[Fact]
public void T6_RuleMechanismDictionary_IsUntouchedByTheGate()
{
// 运行库中 ado_s8_monitor_metric 有 8 条 enabled=1 的机制字典项(DATE / VALUE_RANGE),
// 由 S8ConfigDraftService(Rule 向导)消费。本门禁只在结果 KPI 路径调用,
// 这里断言:即使把它们混进候选集,也只会因 IsResultKpi=false 被调用方排除,
// 而不会被本方法当作结果 KPI 返回。
var mixed = new[]
{
Metric("ORDER_DUE_AT", enabled: true, isResultKpi: false, mechanism: "DATE"),
Metric("INV_QTY", enabled: true, isResultKpi: false, mechanism: "VALUE_RANGE"),
Metric("ORDER_FLOW_STAGE_VARIANCE", enabled: true, isResultKpi: false, mechanism: "VALUE_RANGE"),
Metric("ORDER_DELIVERY_RATE", enabled: true)
};
// 模拟调用方 SQL 的 mechanism + is_result_kpi 过滤。
var candidates = mixed.Where(m => m.Mechanism == "RATIO" && m.IsResultKpi);
var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(candidates);
Assert.Equal(new[] { "ORDER_DELIVERY_RATE" }, CodesOf(result));
// 机制字典项全部保持 enabled=true,未被本批改动。
Assert.All(mixed.Where(m => !m.IsResultKpi), m => Assert.True(m.Enabled));
}
// ============================================================
// 排序稳定性(卡片顺序不应因门禁而乱)
// ============================================================
[Fact]
public void EnabledMetrics_KeepSortNoThenCodeOrdering()
{
var result = S8MonitoringService.ResolveEnabledEffectiveResultKpis(new[]
{
Metric("B_RATE", enabled: true, sortNo: 2),
Metric("A_RATE", enabled: true, sortNo: 2),
Metric("Z_RATE", enabled: true, sortNo: 1),
Metric("X_RATE", enabled: false, sortNo: 0) // 停用项不参与排序结果
});
Assert.Equal(new[] { "Z_RATE", "A_RATE", "B_RATE" }, CodesOf(result));
}
}