using Admin.NET.Plugin.AiDOP.Entity.S8;
using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
using Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
using Xunit;
namespace Admin.NET.Test.S8;
///
/// TIMEOUT evaluator 纯逻辑回归:运行参数解析 + dedup_key 生成稳定性。
/// 不接 DB / 不接 DI。
///
/// S8-RULE-GOVERNANCE-BATCH1 的变化:本文件原先覆盖的是
/// S8TimeoutParams.Parse —— 那个类同时解析判定语义(dueAtField / statusField /
/// completedStates / objectIdField / exceptionTypeCode)与运行参数(graceMinutes)。
/// 判定语义已迁入代码定义,对它的断言移至 S8RuleDefinitionContractTests;
/// 本文件改为覆盖剩下的那一半:。
///
/// dedup_key 相关断言**逐字保留** —— 那部分语义本批未改。
///
public class S8TimeoutEvaluatorTests
{
private static S8RuleDefinition Rule01Definition() =>
new S8RuleCatalog(new IS8RuleDefinitionSource[] { new S8PurchaseDeliveryRuleDefinitions() })
.GetRequired(S8PurchaseDeliveryRuleDefinitions.PurchaseDeliveryDateDelayCode);
private static AdoS8WatchRule Rule(string? paramsJson, string severity = "SERIOUS") => new()
{
RuleCode = S8PurchaseDeliveryRuleDefinitions.PurchaseDeliveryDateDelayCode,
Severity = severity,
PollIntervalSeconds = 300,
TriggerCountRequired = 1,
RecoverCountRequired = 2,
ParamsJson = paramsJson
};
// ───────────────── 运行参数解析(原 params 解析用例的继承者) ─────────────────
[Fact]
public void RuntimeParameters_Parse_FullRuntimeSpec()
{
var json = """
{
"graceMinutes": 5,
"defaultOccurrenceDeptId": 245,
"defaultResponsibleDeptId": 246
}
""";
var p = S8RuleRuntimeParameters.Resolve(Rule(json), Rule01Definition());
Assert.Equal(5, p.GraceMinutes);
Assert.Equal(245L, p.DefaultOccurrenceDeptId);
Assert.Equal(246L, p.DefaultResponsibleDeptId);
Assert.Equal(300, p.PollIntervalSeconds);
Assert.Equal(1, p.TriggerCountRequired);
Assert.Equal(2, p.RecoverCountRequired);
Assert.Equal(S8SeverityCode.Serious, p.Severity);
}
[Fact]
public void RuntimeParameters_Tolerates_MissingOptionalFields()
{
var p = S8RuleRuntimeParameters.Resolve(Rule("""{ }"""), Rule01Definition());
Assert.Equal(0, p.GraceMinutes); // 定义声明的默认值
Assert.Null(p.DefaultOccurrenceDeptId);
Assert.Null(p.DefaultResponsibleDeptId);
}
///
/// 关键回归:旧 A 类字段读到也必须忽略。
/// 这是「用户不能通过 params_json 改判定口径」在参数解析这一层的守卫;
/// 判定结果层面的守卫在 S8RuleDefinitionContractTests.T5_*。
///
[Fact]
public void RuntimeParameters_Ignores_LegacyDefinitionFields()
{
var json = """
{
"dueAtField": "evil_field",
"statusField": "evil_status",
"completedStates": ["OPEN"],
"objectIdField": "evil_id",
"exceptionTypeCode": "EVIL",
"graceMinutes": 7
}
""";
var p = S8RuleRuntimeParameters.Resolve(Rule(json), Rule01Definition());
// B 类照常生效
Assert.Equal(7, p.GraceMinutes);
// A 类不会以任何形式出现在回写的 params_json 里
var written = p.ToParamsJson();
Assert.DoesNotContain("evil", written, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("completedStates", written, StringComparison.Ordinal);
}
[Fact]
public void RuntimeParameters_DoesNotThrow_OnInvalidJson()
{
// 旧实现在这里抛 JsonException,由 evaluator 捕获后把整条规则标 params_parse_failed。
// 现在语义在代码里,一份读不懂的运行参数不该让规则停摆 —— 回落默认值即可。
var p = S8RuleRuntimeParameters.Resolve(Rule("not-a-json"), Rule01Definition());
Assert.Equal(0, p.GraceMinutes);
}
///
/// severity 非法值必须回落**定义声明的默认值**,而不是被 S8SeverityCode.Normalize
/// 静默降级成 FOLLOW —— 库里 severity='HIGH' 的那一行当年正是被那条兜底吞进来的。
///
[Theory]
[InlineData("HIGH")]
[InlineData("LOW")]
[InlineData("")]
[InlineData(null)]
public void RuntimeParameters_InvalidSeverity_FallsBackToDefinitionDefault(string? severity)
{
var p = S8RuleRuntimeParameters.Resolve(Rule("{}", severity!), Rule01Definition());
Assert.Equal(S8SeverityCode.Serious, p.Severity); // Rule 01 的定义默认值
}
[Theory]
[InlineData(S8SeverityCode.Follow)]
[InlineData(S8SeverityCode.Serious)]
public void RuntimeParameters_ValidSeverity_IsHonoured(string severity)
{
var p = S8RuleRuntimeParameters.Resolve(Rule("{}", severity), Rule01Definition());
Assert.Equal(severity, p.Severity);
}
[Fact]
public void RuntimeParameters_OutOfRangeColumnValues_FallBackToDefaults()
{
var rule = Rule("{}");
rule.PollIntervalSeconds = 5; // < 60,越界
rule.TriggerCountRequired = 99; // > 10,越界
var p = S8RuleRuntimeParameters.Resolve(rule, Rule01Definition());
Assert.Equal(300, p.PollIntervalSeconds);
Assert.Equal(1, p.TriggerCountRequired);
}
// ───────────────── dedup_key(本批未改,断言逐字保留) ─────────────────
[Fact]
public void BuildDedupKey_IsStable_SameInputsSameOutput()
{
var k1 = S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "G01_TEST_TIMEOUT", "ORDER", "ORDER-TIMEOUT-01");
var k2 = S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "G01_TEST_TIMEOUT", "ORDER", "ORDER-TIMEOUT-01");
Assert.Equal(k1, k2);
Assert.Equal("T1:F1:RG01_TEST_TIMEOUT:ORDER:ORDER-TIMEOUT-01", k1);
}
[Fact]
public void BuildDedupKey_Differentiates_OnTenantFactoryRuleObject()
{
var baseKey = S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "R", "ORDER", "ID");
Assert.NotEqual(baseKey, S8TimeoutRuleEvaluator.BuildDedupKey(2, 1, "R", "ORDER", "ID"));
Assert.NotEqual(baseKey, S8TimeoutRuleEvaluator.BuildDedupKey(1, 2, "R", "ORDER", "ID"));
Assert.NotEqual(baseKey, S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "R2", "ORDER", "ID"));
Assert.NotEqual(baseKey, S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "R", "TASK", "ID"));
Assert.NotEqual(baseKey, S8TimeoutRuleEvaluator.BuildDedupKey(1, 1, "R", "ORDER", "ID2"));
}
[Fact]
public void BuildDedupKey_Length_FitsVarchar128()
{
// 实体定义 dedup_key VARCHAR(128)。本断言锁定常规标识符不溢出列长度。
var k = S8TimeoutRuleEvaluator.BuildDedupKey(9999999, 9999999, "VERY_LONG_RULE_CODE_FOR_PADDING_64____", "EXTREMELY_LONG_OBJECT_TYPE______", "EXTREMELY_LONG_OBJECT_ID________");
Assert.True(k.Length <= 128, $"dedup_key length {k.Length} exceeds 128: {k}");
}
}