|
@@ -0,0 +1,211 @@
|
|
|
|
|
+using Admin.NET.Plugin.AiDOP.Infrastructure;
|
|
|
|
|
+using Admin.NET.Plugin.AiDOP.SmartOps;
|
|
|
|
|
+using Xunit;
|
|
|
|
|
+
|
|
|
|
|
+namespace Admin.NET.Plugin.AiDOP.Tests.DataPlatform;
|
|
|
|
|
+
|
|
|
|
|
+public class HomeGridLastKnownValueContractTests
|
|
|
|
|
+{
|
|
|
|
|
+ private static readonly DateTime Today = new(2026, 9, 29);
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void LatestNoData_ShowsPreviousOk_AsLastKnown()
|
|
|
|
|
+ {
|
|
|
|
|
+ var card = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ Row(1, 3m, 10m, "OK", new DateTime(2026, 9, 23)),
|
|
|
|
|
+ Row(2, null, null, "NO_DATA", new DateTime(2026, 9, 28)),
|
|
|
|
|
+ 7, Today);
|
|
|
|
|
+ Assert.Equal(3m, card.CurrentValue);
|
|
|
|
|
+ Assert.Equal(10m, card.TargetValue);
|
|
|
|
|
+ Assert.Equal("LAST_KNOWN_VALUE", card.DisplayState);
|
|
|
|
|
+ Assert.Equal(new DateTime(2026, 9, 23), card.BizDate);
|
|
|
|
|
+ Assert.Equal("NO_DATA", card.LatestResultStatus);
|
|
|
|
|
+ Assert.Equal(new DateTime(2026, 9, 28), card.LatestBizDate);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void LatestOk_IsCurrentValue()
|
|
|
|
|
+ {
|
|
|
|
|
+ var card = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ Row(5, 1m, 2m, "OK", Today),
|
|
|
|
|
+ Row(5, 1m, 2m, "OK", Today),
|
|
|
|
|
+ 7, Today);
|
|
|
|
|
+ Assert.Equal("CURRENT_VALUE", card.DisplayState);
|
|
|
|
|
+ Assert.Equal(0, card.ValueAgeDays);
|
|
|
|
|
+ Assert.Equal("FRESH", card.FreshnessState);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void NeverHadValue_IsNoData()
|
|
|
|
|
+ {
|
|
|
|
|
+ var card = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ null, Row(3, null, null, "NO_DATA", Today), 7, Today);
|
|
|
|
|
+ Assert.Null(card.CurrentValue);
|
|
|
|
|
+ Assert.Equal("NO_DATA", card.DisplayState);
|
|
|
|
|
+ Assert.Equal("NO_VALUE", card.FreshnessState);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void SourceUnsupported_WithoutHistory_StaysUnsupported()
|
|
|
|
|
+ {
|
|
|
|
|
+ var card = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ null, Row(4, null, null, "SOURCE_UNSUPPORTED", Today), 7, Today);
|
|
|
|
|
+ Assert.Equal("SOURCE_UNSUPPORTED", card.DisplayState);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void ValueAndTarget_ComeFromTheSameUsableRow()
|
|
|
|
|
+ {
|
|
|
|
|
+ var card = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ Row(8, 100m, 80m, "OK", new DateTime(2026, 9, 23)),
|
|
|
|
|
+ Row(9, null, 1m, "NO_DATA", Today),
|
|
|
|
|
+ 7, Today);
|
|
|
|
|
+ Assert.Equal(100m, card.CurrentValue);
|
|
|
|
|
+ Assert.Equal(80m, card.TargetValue);
|
|
|
|
|
+ Assert.NotEqual(1m, card.TargetValue);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void BlankHistoricalStatus_IsNormalizedToOk()
|
|
|
|
|
+ {
|
|
|
|
|
+ var card = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ Row(6, 4m, null, "", new DateTime(2026, 9, 1)),
|
|
|
|
|
+ Row(7, null, null, "NO_DATA", Today),
|
|
|
|
|
+ 7, Today);
|
|
|
|
|
+ Assert.Equal("OK", card.ResultStatus);
|
|
|
|
|
+ Assert.Equal("NO_DATA", card.LatestResultStatus);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void DailySla_EightDaysIsStale_SevenDaysIsFresh()
|
|
|
|
|
+ {
|
|
|
|
|
+ var fresh = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today.AddDays(-7)),
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today.AddDays(-7)),
|
|
|
|
|
+ 7, Today);
|
|
|
|
|
+ var stale = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today.AddDays(-8)),
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today.AddDays(-8)),
|
|
|
|
|
+ 7, Today);
|
|
|
|
|
+ Assert.Equal("FRESH", fresh.FreshnessState);
|
|
|
|
|
+ Assert.Equal("STALE", stale.FreshnessState);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void MonthlySla_ThirtyDaysFresh_FortySixDaysStale()
|
|
|
|
|
+ {
|
|
|
|
|
+ var fresh = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today.AddDays(-30)),
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today.AddDays(-30)),
|
|
|
|
|
+ 45, Today);
|
|
|
|
|
+ var stale = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today.AddDays(-46)),
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today.AddDays(-46)),
|
|
|
|
|
+ 45, Today);
|
|
|
|
|
+ Assert.Equal("FRESH", fresh.FreshnessState);
|
|
|
|
|
+ Assert.Equal("STALE", stale.FreshnessState);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void MissingSla_IsPolicyMissing_NotFresh()
|
|
|
|
|
+ {
|
|
|
|
|
+ var card = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today),
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today),
|
|
|
|
|
+ null, Today);
|
|
|
|
|
+ Assert.Equal("POLICY_MISSING", card.FreshnessState);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void FutureBizDate_ClampsAgeToZero()
|
|
|
|
|
+ {
|
|
|
|
|
+ var card = HomeGridDisplayPolicy.Decide(
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today.AddDays(2)),
|
|
|
|
|
+ Row(1, 1m, null, "OK", Today.AddDays(2)),
|
|
|
|
|
+ 7, Today);
|
|
|
|
|
+ Assert.True(card.FutureBizDate);
|
|
|
|
|
+ Assert.Equal(0, card.ValueAgeDays);
|
|
|
|
|
+ Assert.Equal("FRESH", card.FreshnessState);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void Sql_UsesSameScope_AndDoesNotFallBackOutsideTheRange()
|
|
|
|
|
+ {
|
|
|
|
|
+ var source = File.ReadAllText(Path.Combine(RepoRoot(),
|
|
|
|
|
+ "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Controllers", "AidopKanbanController.Generic.cs"));
|
|
|
|
|
+ Assert.Contains("ORDER BY v.biz_date DESC, v.calc_time DESC, v.id DESC", source);
|
|
|
|
|
+ Assert.Equal(2, Count(source, "AND (@ds IS NULL OR v.biz_date>=@ds)"));
|
|
|
|
|
+ Assert.Equal(2, Count(source, "AND (@de IS NULL OR v.biz_date<=@de)"));
|
|
|
|
|
+ Assert.Contains("v.metric_value IS NOT NULL", source);
|
|
|
|
|
+ Assert.Contains("v.result_status='OK' OR v.result_status IS NULL OR v.result_status=''", source);
|
|
|
|
|
+ Assert.Contains("v.tenant_id=@t AND v.factory_id=@f AND v.module_code=@m", source);
|
|
|
|
|
+ Assert.Contains("禁止拿未筛选的最后真实值补上", source);
|
|
|
|
|
+ Assert.DoesNotContain("ToDictionary", Slice(source, "latestAttemptSql", "public async Task<IActionResult> GetDetailKpisGeneric"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void FutureBizDateWarning_NamesOnlyTenantFactoryMetricAndDate()
|
|
|
|
|
+ {
|
|
|
|
|
+ var source = File.ReadAllText(Path.Combine(RepoRoot(),
|
|
|
|
|
+ "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Controllers", "AidopKanbanController.Generic.cs"));
|
|
|
|
|
+ var i = source.IndexOf("home grid future biz date", StringComparison.Ordinal);
|
|
|
|
|
+ Assert.True(i > 0);
|
|
|
|
|
+ var window = source[i..(i + 220)];
|
|
|
|
|
+ Assert.Contains("tenant={TenantId}", window);
|
|
|
|
|
+ Assert.Contains("factory={FactoryId}", window);
|
|
|
|
|
+ Assert.Contains("metric={MetricCode}", window);
|
|
|
|
|
+ Assert.Contains("bizDate={BizDate}", window);
|
|
|
|
|
+ Assert.DoesNotContain("resultReason", window, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Theory]
|
|
|
|
|
+ [InlineData("S5_L1_003", "天", 45)]
|
|
|
|
|
+ [InlineData("S5_L1_004", "天", 45)]
|
|
|
|
|
+ [InlineData("S6_L1_003", "天", 45)]
|
|
|
|
|
+ [InlineData("S7_L1_003", "天", 45)]
|
|
|
|
|
+ [InlineData("S9_L1_001", "天", 45)]
|
|
|
|
|
+ [InlineData("S8_L1_001", "实时", 1)]
|
|
|
|
|
+ [InlineData("S1_L1_001", "天", 7)]
|
|
|
|
|
+ public void SeedHelper_UsesMonthlyCodeThenRealtimeThenSeven(string code, string frequency, int expected)
|
|
|
|
|
+ {
|
|
|
|
|
+ Assert.Equal(expected, AidopKpiMasterSeed.ResolveFreshnessSlaDays(code, frequency));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static HomeGridDisplayPolicy.Row Row(long id, decimal? value, decimal? target, string? status, DateTime bizDate) =>
|
|
|
|
|
+ new()
|
|
|
|
|
+ {
|
|
|
|
|
+ Id = id,
|
|
|
|
|
+ MetricValue = value,
|
|
|
|
|
+ TargetValue = target,
|
|
|
|
|
+ ResultStatus = status,
|
|
|
|
|
+ BizDate = bizDate,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ private static string RepoRoot()
|
|
|
|
|
+ {
|
|
|
|
|
+ var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
|
|
|
|
+ while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
|
|
|
|
|
+ dir = dir.Parent;
|
|
|
|
|
+ return dir?.FullName ?? throw new InvalidOperationException("repo root not found");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static string Slice(string src, string start, string end)
|
|
|
|
|
+ {
|
|
|
|
|
+ var i = src.IndexOf(start, StringComparison.Ordinal);
|
|
|
|
|
+ var j = src.IndexOf(end, i, StringComparison.Ordinal);
|
|
|
|
|
+ Assert.True(i >= 0 && j > i);
|
|
|
|
|
+ return src[i..j];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static int Count(string src, string token)
|
|
|
|
|
+ {
|
|
|
|
|
+ var n = 0;
|
|
|
|
|
+ var i = 0;
|
|
|
|
|
+ while ((i = src.IndexOf(token, i, StringComparison.Ordinal)) >= 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ n++;
|
|
|
|
|
+ i += token.Length;
|
|
|
|
|
+ }
|
|
|
|
|
+ return n;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|