using Admin.NET.Plugin.AiDOP.Entity.S8;
using Admin.NET.Plugin.AiDOP.Service.S8;
using Admin.NET.Plugin.AiDOP.Service.S8.Rules;
using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess;
using Xunit;
namespace Admin.NET.Test.S8;
///
/// S8-DATASET-FOUNDATION-HARDENING-2:Dataset Catalog 与 Provider Registry 解耦 + 统一 Enable Gate。
///
/// 本批要锁定的核心区分:
/// · Dataset 根本不存在 → dataset_not_defined
/// · Dataset 已定义但无 Provider → dataset_provider_not_registered(草稿可存,不可启用)
/// · Dataset 声明支持某机制但缺必需字段 → dataset_missing_required_field(配置自相矛盾)
/// · STANDARD_DATASET 取数失败绝不回落 Legacy SQL
///
public class S8DatasetCatalogAndGateTests
{
private const string DefinedWithProvider = "FAKE_TEST_DATASET_READY";
private const string DefinedWithoutProvider = "FAKE_TEST_DATASET_NO_PROVIDER";
private const string DefinedButDisabled = "FAKE_TEST_DATASET_DISABLED";
private const string ContradictoryDataset = "FAKE_TEST_DATASET_CONTRADICTORY";
private const string OutOfRangeOnlyDataset = "FAKE_TEST_DATASET_OOR_ONLY";
private static S8DatasetCapabilities TimeoutCapable() => new()
{
SupportsTimeout = true,
HasSourceObjectId = true,
HasRelatedObjectCode = true,
HasDueAt = true,
HasStatus = true
};
private sealed class FakeDefinitionSource : IS8DatasetDefinitionSource
{
public IEnumerable GetDefinitions() => new[]
{
new S8DatasetDefinition
{
DatasetCode = DefinedWithProvider,
DisplayName = "测试用就绪数据集",
Kind = S8DatasetKind.Object,
Capabilities = TimeoutCapable()
},
new S8DatasetDefinition
{
DatasetCode = DefinedWithoutProvider,
DisplayName = "测试用未实现数据集",
Kind = S8DatasetKind.Object,
Capabilities = TimeoutCapable()
},
new S8DatasetDefinition
{
DatasetCode = DefinedButDisabled,
DisplayName = "测试用停用数据集",
Kind = S8DatasetKind.Metric,
Capabilities = TimeoutCapable(),
Status = S8DatasetStatus.Disabled
},
new S8DatasetDefinition
{
// 自相矛盾:声明支持 TIMEOUT,却没有 due_at 能力。
DatasetCode = ContradictoryDataset,
DisplayName = "测试用矛盾数据集",
Kind = S8DatasetKind.Object,
Capabilities = new S8DatasetCapabilities
{
SupportsTimeout = true,
HasSourceObjectId = true,
HasRelatedObjectCode = true,
HasDueAt = false,
HasStatus = true
}
},
new S8DatasetDefinition
{
DatasetCode = OutOfRangeOnlyDataset,
DisplayName = "测试用仅超差数据集",
Kind = S8DatasetKind.Metric,
Capabilities = new S8DatasetCapabilities
{
SupportsOutOfRange = true,
HasSourceObjectId = true,
HasRelatedObjectCode = true,
HasMeasuredValue = true
}
}
};
}
private sealed class ReadyProvider : IS8MonitoringDataProvider
{
public string DatasetCode => DefinedWithProvider;
public S8DatasetCapabilities Capabilities => TimeoutCapable();
public Task LoadAsync(S8MonitoringDataRequest request, CancellationToken cancellationToken = default)
{
request.EnsureValid();
return Task.FromResult(S8MonitoringRowSet.Empty);
}
}
private sealed class ThrowingProvider : IS8MonitoringDataProvider
{
public string DatasetCode => DefinedWithProvider;
public S8DatasetCapabilities Capabilities => TimeoutCapable();
public Task LoadAsync(S8MonitoringDataRequest request, CancellationToken cancellationToken = default)
=> throw new InvalidOperationException("provider blew up");
}
private static S8DatasetCatalog Catalog() => new(new[] { new FakeDefinitionSource() });
private static S8MonitoringDataProviderRegistry RegistryWithProvider() => new(new[] { new ReadyProvider() });
private static S8MonitoringDataProviderRegistry EmptyRegistry() =>
new(Array.Empty());
private static AdoS8WatchRule StandardRule(string datasetCode, string ruleType = "TIMEOUT") => new()
{
RuleCode = "UT_RULE",
RuleType = ruleType,
DataAccessMode = S8DataAccessMode.StandardDataset,
DatasetCode = datasetCode
};
// ============================================================
// A. Catalog ≠ Provider Registry
// ============================================================
[Fact]
public void Catalog_KnowsDataset_EvenWhenProviderNotRegistered()
{
var catalog = Catalog();
Assert.True(catalog.IsDefined(DefinedWithoutProvider));
Assert.Equal("测试用未实现数据集", catalog.Find(DefinedWithoutProvider)!.DisplayName);
Assert.False(EmptyRegistry().IsRegistered(DefinedWithoutProvider));
}
[Fact]
public void DraftSave_Allowed_WhenDatasetDefinedButProviderMissing()
{
// 允许业务先把规则配好,等 Provider 上线再启用。
var rule = StandardRule(DefinedWithoutProvider);
S8WatchRuleDataAccessValidator.ValidateForSave(rule, Catalog());
}
[Fact]
public void Enable_Rejected_WhenProviderMissing_WithProviderReasonCode()
{
var gate = new S8DatasetEnableGate(Catalog(), EmptyRegistry());
var result = gate.Check(StandardRule(DefinedWithoutProvider), tenantId: 1, factoryId: 1);
Assert.False(result.Ok);
Assert.Equal(S8DatasetReasonCode.ProviderNotRegistered, result.ReasonCode);
}
// ============================================================
// B. Dataset 不存在,必须与 provider missing 区分
// ============================================================
[Fact]
public void UndefinedDataset_Reports_NotDefined_NotProviderMissing()
{
var gate = new S8DatasetEnableGate(Catalog(), EmptyRegistry());
var result = gate.Check(StandardRule("TOTALLY_UNKNOWN_DATASET"), 1, 1);
Assert.False(result.Ok);
Assert.Equal(S8DatasetReasonCode.NotDefined, result.ReasonCode);
Assert.NotEqual(S8DatasetReasonCode.ProviderNotRegistered, result.ReasonCode);
}
[Fact]
public void DraftSave_Rejected_ForUndefinedDataset()
{
var ex = Assert.Throws(() =>
S8WatchRuleDataAccessValidator.ValidateForSave(StandardRule("TOTALLY_UNKNOWN_DATASET"), Catalog()));
Assert.Contains("TOTALLY_UNKNOWN_DATASET", ex.Message);
}
// ============================================================
// C. Capability mismatch
// ============================================================
[Fact]
public void Enable_Rejected_WhenDatasetDoesNotSupportRuleType()
{
var registry = new S8MonitoringDataProviderRegistry(Array.Empty());
var gate = new S8DatasetEnableGate(Catalog(), registry);
// OUT_OF_RANGE-only 数据集挂 TIMEOUT 规则。
var result = gate.Check(StandardRule(OutOfRangeOnlyDataset, "TIMEOUT"), 1, 1);
Assert.False(result.Ok);
Assert.Equal(S8DatasetReasonCode.CapabilityMismatch, result.ReasonCode);
}
// ============================================================
// D. 必需 canonical 字段缺失(单一布尔位不可信)
// ============================================================
[Fact]
public void Enable_Rejected_WhenMechanismClaimedButRequiredFieldMissing()
{
// SupportsTimeout=true 但 HasDueAt=false:配置自相矛盾,必须识别。
var gate = new S8DatasetEnableGate(Catalog(), EmptyRegistry());
var result = gate.Check(StandardRule(ContradictoryDataset), 1, 1);
Assert.False(result.Ok);
Assert.Equal(S8DatasetReasonCode.MissingRequiredField, result.ReasonCode);
Assert.Contains(S8CanonicalColumns.DueAt, result.Message);
}
[Fact]
public void RuleTypeRequirements_MatchRealEvaluatorNeeds()
{
Assert.Equal(
new[] { S8CanonicalColumns.SourceObjectId, S8CanonicalColumns.RelatedObjectCode, S8CanonicalColumns.DueAt, S8CanonicalColumns.Status },
S8RuleTypeFieldRequirements.For("TIMEOUT"));
Assert.Equal(
new[] { S8CanonicalColumns.SourceObjectId, S8CanonicalColumns.RelatedObjectCode, S8CanonicalColumns.MeasuredValue },
S8RuleTypeFieldRequirements.For("OUT_OF_RANGE"));
Assert.Equal(
new[] { S8CanonicalColumns.SourceObjectId, S8CanonicalColumns.RelatedObjectCode, S8CanonicalColumns.TargetQty, S8CanonicalColumns.ActualQty },
S8RuleTypeFieldRequirements.For("SHORTAGE"));
}
// ============================================================
// Disabled dataset
// ============================================================
[Fact]
public void Enable_Rejected_WhenDatasetLifecycleDisabled()
{
var gate = new S8DatasetEnableGate(Catalog(), RegistryWithProvider());
var result = gate.Check(StandardRule(DefinedButDisabled), 1, 1);
Assert.False(result.Ok);
Assert.Equal(S8DatasetReasonCode.NotReady, result.ReasonCode);
}
// ============================================================
// G. Tenant / Factory scope
// ============================================================
[Fact]
public void Enable_Rejected_OnInvalidTenantScope()
{
var gate = new S8DatasetEnableGate(Catalog(), RegistryWithProvider());
var result = gate.Check(StandardRule(DefinedWithProvider), tenantId: 0, factoryId: 1);
Assert.False(result.Ok);
Assert.Equal(S8DatasetReasonCode.InvalidTenantScope, result.ReasonCode);
}
[Fact]
public void Enable_Rejected_OnInvalidFactoryScope()
{
var gate = new S8DatasetEnableGate(Catalog(), RegistryWithProvider());
var result = gate.Check(StandardRule(DefinedWithProvider), tenantId: 1, factoryId: 0);
Assert.False(result.Ok);
Assert.Equal(S8DatasetReasonCode.InvalidFactoryScope, result.ReasonCode);
}
[Fact]
public void Enable_Passes_WhenEverythingSatisfied()
{
var gate = new S8DatasetEnableGate(Catalog(), RegistryWithProvider());
var result = gate.Check(StandardRule(DefinedWithProvider), 797403760988229, 1);
Assert.True(result.Ok);
Assert.Null(result.ReasonCode);
}
// ============================================================
// F. Legacy 规则不经过 Dataset Gate
// ============================================================
[Fact]
public void LegacyRule_NotSubjectToDatasetGate()
{
var legacy = new AdoS8WatchRule
{
RuleCode = "LEGACY",
RuleType = "TIMEOUT",
DataAccessMode = null,
DatasetCode = null,
Expression = "SELECT 1"
};
var gate = new S8DatasetEnableGate(Catalog(), EmptyRegistry());
Assert.True(gate.Check(legacy, 1, 1).Ok);
S8WatchRuleDataAccessValidator.ValidateForSave(legacy, Catalog());
}
// ============================================================
// 诊断能力
// ============================================================
[Fact]
public void Diagnostics_DistinguishesReadyFromProviderMissing()
{
var diagnostics = new S8DatasetDiagnosticsService(Catalog(), RegistryWithProvider());
Assert.Equal(S8DatasetStatus.Ready, diagnostics.Describe(DefinedWithProvider)!.EffectiveStatus);
Assert.Equal(S8DatasetStatus.ProviderMissing, diagnostics.Describe(DefinedWithoutProvider)!.EffectiveStatus);
Assert.Equal(S8DatasetStatus.Disabled, diagnostics.Describe(DefinedButDisabled)!.EffectiveStatus);
Assert.Null(diagnostics.Describe("TOTALLY_UNKNOWN_DATASET"));
}
[Fact]
public void Diagnostics_ReportsUsableRuleTypes_ExcludingContradictoryOnes()
{
var diagnostics = new S8DatasetDiagnosticsService(Catalog(), RegistryWithProvider());
Assert.Contains("TIMEOUT", diagnostics.Describe(DefinedWithProvider)!.UsableRuleTypes);
// 声明支持 TIMEOUT 但缺 due_at:不得出现在可用列表里。
Assert.DoesNotContain("TIMEOUT", diagnostics.Describe(ContradictoryDataset)!.UsableRuleTypes);
Assert.Contains("OUT_OF_RANGE", diagnostics.Describe(OutOfRangeOnlyDataset)!.UsableRuleTypes);
}
[Fact]
public void Diagnostics_ListsAllDefinedDatasets_RegardlessOfProvider()
{
var diagnostics = new S8DatasetDiagnosticsService(Catalog(), EmptyRegistry());
var all = diagnostics.List();
Assert.Equal(5, all.Count);
Assert.All(all, d => Assert.False(d.ProviderRegistered));
}
}