|
|
@@ -20,8 +20,7 @@ namespace Admin.NET.Test.S8;
|
|
|
/// 会过得了列校验、卡在 DedupKey 上,数据库静默截断会把两个 PO Line 折叠成一条异常。
|
|
|
/// E. due_at 双分支 → need_date 为 NULL 时回落 due_date;两者实测 18/36 行不同,不是等价列。
|
|
|
/// F. 判定归 evaluator → Provider 不滤 status,"逾期但已完成不命中"必须由 evaluator 判出来。
|
|
|
-/// G. STANDARD 绝不回落 → 取数失败偷偷执行 rule.Expression 会让迁移失败被静默隐藏:
|
|
|
-/// 规则看起来在正常跑,实际跑的还是旧 SQL。
|
|
|
+/// G. 取数路径唯一 → Provider 取数失败必须暴露为 dataset_query_failed,而不是换一条路。
|
|
|
///
|
|
|
/// 不接 DB、不接 DI、不触发任何真实规则。SQL 在真实库上的行为已由实施前的只读核验覆盖
|
|
|
/// (4 租户身份唯一、跨租户交集 0、外层走 idx_stat_date),本文件锁定的是结构契约。
|
|
|
@@ -50,7 +49,6 @@ public class S8PurchaseDeliveryProviderMappingTests
|
|
|
SourceObjectType = "PURCHASE_ORDER_LINE",
|
|
|
Severity = "SERIOUS",
|
|
|
RuleType = "TIMEOUT",
|
|
|
- DataAccessMode = S8DataAccessMode.StandardDataset,
|
|
|
DatasetCode = PurchaseDelivery
|
|
|
};
|
|
|
|
|
|
@@ -302,7 +300,7 @@ public class S8PurchaseDeliveryProviderMappingTests
|
|
|
new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "DELAYED"));
|
|
|
|
|
|
var hits = S8TimeoutRuleEvaluator.EvaluateRows(
|
|
|
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, 0, new DateTime(2026, 9, 5));
|
|
|
+ rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
|
|
|
|
|
|
var hit = Assert.Single(hits);
|
|
|
Assert.Equal("PO202608260002#3", hit.SourceObjectId);
|
|
|
@@ -318,7 +316,7 @@ public class S8PurchaseDeliveryProviderMappingTests
|
|
|
new DateTime(2026, 9, 17), new DateTime(2026, 8, 9), "OPEN", supplierName: "怡祥"));
|
|
|
|
|
|
var hits = S8TimeoutRuleEvaluator.EvaluateRows(
|
|
|
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, 0, new DateTime(2026, 9, 5));
|
|
|
+ rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
|
|
|
|
|
|
Assert.Empty(hits);
|
|
|
}
|
|
|
@@ -333,7 +331,7 @@ public class S8PurchaseDeliveryProviderMappingTests
|
|
|
new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "COMPLETED"));
|
|
|
|
|
|
var hits = S8TimeoutRuleEvaluator.EvaluateRows(
|
|
|
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, 0, new DateTime(2026, 9, 5));
|
|
|
+ rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
|
|
|
|
|
|
Assert.Empty(hits);
|
|
|
}
|
|
|
@@ -348,7 +346,7 @@ public class S8PurchaseDeliveryProviderMappingTests
|
|
|
Row("PO-CANCELLED", "1", new DateTime(2026, 8, 1), null, "CANCELLED"));
|
|
|
|
|
|
var hits = S8TimeoutRuleEvaluator.EvaluateRows(
|
|
|
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, 0, new DateTime(2026, 9, 5));
|
|
|
+ rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
|
|
|
|
|
|
Assert.Empty(hits);
|
|
|
}
|
|
|
@@ -360,7 +358,7 @@ public class S8PurchaseDeliveryProviderMappingTests
|
|
|
var rowSet = RowSet(Row("PO-PARTIAL", "1", new DateTime(2026, 8, 1), null, "PARTIAL"));
|
|
|
|
|
|
var hits = S8TimeoutRuleEvaluator.EvaluateRows(
|
|
|
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, 0, new DateTime(2026, 9, 5));
|
|
|
+ rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
|
|
|
|
|
|
Assert.Single(hits);
|
|
|
}
|
|
|
@@ -377,9 +375,9 @@ public class S8PurchaseDeliveryProviderMappingTests
|
|
|
new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "COMPLETED"));
|
|
|
|
|
|
var hitsBefore = S8TimeoutRuleEvaluator.EvaluateRows(
|
|
|
- before, Params(), NewRule(), 838257186181189, 838257186320453, 0, new DateTime(2026, 9, 5));
|
|
|
+ before, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
|
|
|
var hitsAfter = S8TimeoutRuleEvaluator.EvaluateRows(
|
|
|
- after, Params(), NewRule(), 838257186181189, 838257186320453, 0, new DateTime(2026, 9, 5));
|
|
|
+ after, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5));
|
|
|
|
|
|
var dedupKey = Assert.Single(hitsBefore).DedupKey;
|
|
|
Assert.Empty(hitsAfter);
|
|
|
@@ -393,7 +391,7 @@ public class S8PurchaseDeliveryProviderMappingTests
|
|
|
new DateTime(2026, 8, 17, 4, 33, 7), new DateTime(2026, 9, 10), "DELAYED"));
|
|
|
|
|
|
var hit = Assert.Single(S8TimeoutRuleEvaluator.EvaluateRows(
|
|
|
- rowSet, Params(), NewRule(), 838257186181189, 838257186320453, 0, new DateTime(2026, 9, 5)));
|
|
|
+ rowSet, Params(), NewRule(), 838257186181189, 838257186320453, new DateTime(2026, 9, 5)));
|
|
|
|
|
|
Assert.Equal("PURCHASE_DELIVERY_ABNORMAL", hit.ExceptionTypeCode);
|
|
|
Assert.Equal("PURCHASE_ORDER_LINE", hit.SourceObjectType);
|
|
|
@@ -409,64 +407,33 @@ public class S8PurchaseDeliveryProviderMappingTests
|
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
|
- // G. STANDARD_DATASET 绝不回落 Legacy
|
|
|
+ // G. 取数路径唯一
|
|
|
// ============================================================
|
|
|
+ //
|
|
|
+ // 原 T12 / T12b 断言「STANDARD_DATASET 绝不回落 Legacy」,做法是注入一个
|
|
|
+ // SpyLegacy 探针并断言 CallCount == 0,同时在 rule.Expression 里塞一条
|
|
|
+ // "DROP TABLE ado_s8_exception; SELECT SHOULD_NEVER_RUN" 当诱饵。
|
|
|
+ //
|
|
|
+ // S8-STANDARD-DATASET-HARD-CUTOVER-1 之后这两条已无法成立、也无需成立:
|
|
|
+ // · IS8LegacySqlDataProvider 不存在 → 探针类型写不出来;
|
|
|
+ // · AdoS8WatchRule.Expression / DataSourceId 不存在 → 诱饵放不进去。
|
|
|
+ // 「不回落」从一条需要反复验证的运行期行为,变成了类型系统层面的事实。
|
|
|
+ //
|
|
|
+ // 结构性替代断言见 S8MonitoringDataGatewayTests.Gateway_HasNoLegacyDependency
|
|
|
+ // 与 WatchRule_HasNoSqlOrDataSourceFields;此处只保留失败分类这一条仍有意义的行为。
|
|
|
|
|
|
[Fact]
|
|
|
- public async Task T12_StandardDataset_NeverFallsBackToLegacySql()
|
|
|
+ public async Task T12_ProviderFailure_SurfacesAsQueryFailed()
|
|
|
{
|
|
|
- var legacy = new SpyLegacy();
|
|
|
var gateway = new S8MonitoringDataGateway(
|
|
|
- legacy,
|
|
|
- new S8MonitoringDataProviderRegistry(new IS8MonitoringDataProvider[] { new EmptyPurchaseDeliveryProvider() }),
|
|
|
- new S8DatasetCatalog(new IS8DatasetDefinitionSource[] { new S8BusinessDatasetDefinitions() }),
|
|
|
- NullLogger<S8MonitoringDataGateway>.Instance);
|
|
|
-
|
|
|
- var rule = NewRule();
|
|
|
- // 历史遗留字段里即使躺着可执行 SQL,STANDARD_DATASET 路径也绝不能碰它。
|
|
|
- rule.Expression = "DROP TABLE ado_s8_exception; SELECT SHOULD_NEVER_RUN";
|
|
|
- rule.DataSourceId = 99;
|
|
|
-
|
|
|
- var result = await gateway.LoadAsync(838257186181189, 838257186320453, rule, "TIMEOUT", 60, 1000);
|
|
|
-
|
|
|
- Assert.Equal(S8DataAccessMode.StandardDataset, result.DataAccessMode);
|
|
|
- Assert.Equal(0, legacy.CallCount);
|
|
|
- Assert.Null(legacy.LastExpression);
|
|
|
- }
|
|
|
-
|
|
|
- [Fact]
|
|
|
- public async Task T12b_ProviderFailure_SurfacesAsQueryFailed_WithoutLegacyFallback()
|
|
|
- {
|
|
|
- var legacy = new SpyLegacy();
|
|
|
- var gateway = new S8MonitoringDataGateway(
|
|
|
- legacy,
|
|
|
new S8MonitoringDataProviderRegistry(new IS8MonitoringDataProvider[] { new ThrowingPurchaseDeliveryProvider() }),
|
|
|
new S8DatasetCatalog(new IS8DatasetDefinitionSource[] { new S8BusinessDatasetDefinitions() }),
|
|
|
NullLogger<S8MonitoringDataGateway>.Instance);
|
|
|
|
|
|
- var rule = NewRule();
|
|
|
- rule.Expression = "SELECT SHOULD_NEVER_RUN";
|
|
|
-
|
|
|
var ex = await Assert.ThrowsAsync<S8RuleEvaluatorException>(() =>
|
|
|
- gateway.LoadAsync(838257186181189, 838257186320453, rule, "TIMEOUT", 60, 1000));
|
|
|
+ gateway.LoadAsync(838257186181189, 838257186320453, NewRule(), "TIMEOUT", 1000));
|
|
|
|
|
|
Assert.Equal(S8DatasetReasonCode.QueryFailed, ex.Reason);
|
|
|
- Assert.Equal(0, legacy.CallCount); // ← 本测试的核心断言
|
|
|
- }
|
|
|
-
|
|
|
- private sealed class SpyLegacy : IS8LegacySqlDataProvider
|
|
|
- {
|
|
|
- public int CallCount { get; private set; }
|
|
|
- public string LastExpression { get; private set; }
|
|
|
-
|
|
|
- public Task<S8MonitoringDataResult> LoadAsync(
|
|
|
- long tenantId, long factoryId, AdoS8WatchRule rule, string ruleType,
|
|
|
- int timeoutSeconds, CancellationToken cancellationToken = default)
|
|
|
- {
|
|
|
- CallCount++;
|
|
|
- LastExpression = rule.Expression;
|
|
|
- return Task.FromResult(new S8MonitoringDataResult { RowSet = S8MonitoringRowSet.Empty });
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
private sealed class EmptyPurchaseDeliveryProvider : IS8MonitoringDataProvider
|