using Admin.NET.Plugin.AiDOP.DataPlatform; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.DataPlatform; public class NeutralRequiredColumnsGuardTests { [Fact] public void EveryRequiredColumn_IsProjectedOrDeclaredMissing() { var root = RepoRoot(); var plugin = Path.Combine(root, "server", "Plugins", "Admin.NET.Plugin.AiDOP"); foreach (var channel in NeutralRequiredColumns.Channels) { var path = Path.Combine(plugin, channel.RelativePath); Assert.True(File.Exists(path), channel.Channel + " 缺少 " + path); var text = File.ReadAllText(path); var start = text.IndexOf(channel.Marker, StringComparison.Ordinal); Assert.True(start >= 0, channel.Channel + " 找不到 " + channel.Marker); var body = text[start..]; foreach (var column in channel.Present) Assert.Contains(column, body, StringComparison.Ordinal); var covered = channel.Present.Concat(channel.Missing).ToHashSet(StringComparer.Ordinal); if (channel.Channel != "NATIVE") { var missing = NeutralRequiredColumns.InvTrans.Where(c => !covered.Contains(c)).ToList(); Assert.True(missing.Count == 0, channel.Channel + " 未覆盖: " + string.Join(",", missing)); } } } /// /// approved_time 是 S5_L1_001、S7_L1_001 唯一的时间基准。2026-09 指标 SQL 改用它时没登记成必填列, /// 于是整列为空也没人报错,S5/S7 全租户静默掉数。此断言锁住这条契约。 /// [Fact] public void ApprovedTime_IsRequiredColumn() { Assert.Contains("approved_time", NeutralRequiredColumns.InvTrans); Assert.Contains(NeutralRequiredColumns.For("INV_TRANS"), c => c.Name == "approved_time"); } /// /// 每个写 mdp_std_inv_trans 的地方都必须在 Channels 里登记。 /// 漏登记的写入口不受必填列守卫约束:ASN 发运投影就是这么带着空 approved_time 上线的。 /// [Fact] public void EveryInvTransWriter_IsRegisteredAsChannel() { var plugin = Path.Combine(RepoRoot(), "server", "Plugins", "Admin.NET.Plugin.AiDOP"); var registered = NeutralRequiredColumns.Channels .Select(c => Path.Combine(plugin, c.RelativePath)) .ToHashSet(StringComparer.OrdinalIgnoreCase); var unregistered = Directory .EnumerateFiles(plugin, "*.cs", SearchOption.AllDirectories) .Where(f => File.ReadAllText(f).Contains("INSERT INTO mdp_std_inv_trans", StringComparison.Ordinal)) .Where(f => !registered.Contains(f)) .ToList(); Assert.True(unregistered.Count == 0, "未登记为必填列通道: " + string.Join(", ", unregistered)); } private static string RepoRoot() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null) { if (Directory.Exists(Path.Combine(dir.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP"))) return dir.FullName; dir = dir.Parent; } throw new DirectoryNotFoundException("repo root"); } }