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 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; } }