| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- 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;
- /// <summary>
- /// 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
- /// </summary>
- 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<S8DatasetDefinition> 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<S8MonitoringRowSet> 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<S8MonitoringRowSet> 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<IS8MonitoringDataProvider>());
- private static AdoS8WatchRule StandardRule(string datasetCode, string ruleType = "TIMEOUT") => new()
- {
- RuleCode = "UT_RULE",
- RuleType = ruleType,
- 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<S8BizException>(() =>
- 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<IS8MonitoringDataProvider>());
- 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. 无数据集的规则不得通过 Gate
- // ============================================================
- //
- // 原用例 LegacyRule_NotSubjectToDatasetGate 断言「mode=NULL + expression 的规则
- // 直接 Pass Gate」——那时 Legacy 是一条合法的平行路径,Gate 只管 Dataset 侧。
- // S8-STANDARD-DATASET-HARD-CUTOVER-1 之后不再有平行路径:
- // 一条没有 dataset_code 的规则不是"走另一条路",而是**根本无法取数**,必须被拦下。
- [Fact]
- public void RuleWithoutDatasetCode_FailsGate()
- {
- var orphan = new AdoS8WatchRule
- {
- RuleCode = "NO_DATASET",
- RuleType = "TIMEOUT",
- DatasetCode = null
- };
- var gate = new S8DatasetEnableGate(Catalog(), EmptyRegistry());
- var result = gate.Check(orphan, 1, 1);
- Assert.False(result.Ok);
- Assert.Equal(S8DatasetReasonCode.CodeMissing, result.ReasonCode);
- // 保存级同样拦下:没有数据集的规则连草稿都不该存成"看起来可运行"的样子。
- Assert.Throws<S8BizException>(() =>
- S8WatchRuleDataAccessValidator.ValidateForSave(orphan, 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));
- }
- }
|