NeutralRequiredColumnsGuardTests.cs 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. using Admin.NET.Plugin.AiDOP.DataPlatform;
  2. using Xunit;
  3. namespace Admin.NET.Plugin.AiDOP.Tests.DataPlatform;
  4. public class NeutralRequiredColumnsGuardTests
  5. {
  6. [Fact]
  7. public void EveryRequiredColumn_IsProjectedOrDeclaredMissing()
  8. {
  9. var root = RepoRoot();
  10. var plugin = Path.Combine(root, "server", "Plugins", "Admin.NET.Plugin.AiDOP");
  11. foreach (var channel in NeutralRequiredColumns.Channels)
  12. {
  13. var path = Path.Combine(plugin, channel.RelativePath);
  14. Assert.True(File.Exists(path), channel.Channel + " 缺少 " + path);
  15. var text = File.ReadAllText(path);
  16. var start = text.IndexOf(channel.Marker, StringComparison.Ordinal);
  17. Assert.True(start >= 0, channel.Channel + " 找不到 " + channel.Marker);
  18. var body = text[start..];
  19. foreach (var column in channel.Present)
  20. Assert.Contains(column, body, StringComparison.Ordinal);
  21. var covered = channel.Present.Concat(channel.Missing).ToHashSet(StringComparer.Ordinal);
  22. if (channel.Channel != "NATIVE")
  23. {
  24. var missing = NeutralRequiredColumns.InvTrans.Where(c => !covered.Contains(c)).ToList();
  25. Assert.True(missing.Count == 0, channel.Channel + " 未覆盖: " + string.Join(",", missing));
  26. }
  27. }
  28. }
  29. /// <summary>
  30. /// approved_time 是 S5_L1_001、S7_L1_001 唯一的时间基准。2026-09 指标 SQL 改用它时没登记成必填列,
  31. /// 于是整列为空也没人报错,S5/S7 全租户静默掉数。此断言锁住这条契约。
  32. /// </summary>
  33. [Fact]
  34. public void ApprovedTime_IsRequiredColumn()
  35. {
  36. Assert.Contains("approved_time", NeutralRequiredColumns.InvTrans);
  37. Assert.Contains(NeutralRequiredColumns.For("INV_TRANS"), c => c.Name == "approved_time");
  38. }
  39. /// <summary>
  40. /// 每个写 mdp_std_inv_trans 的地方都必须在 Channels 里登记。
  41. /// 漏登记的写入口不受必填列守卫约束:ASN 发运投影就是这么带着空 approved_time 上线的。
  42. /// </summary>
  43. [Fact]
  44. public void EveryInvTransWriter_IsRegisteredAsChannel()
  45. {
  46. var plugin = Path.Combine(RepoRoot(), "server", "Plugins", "Admin.NET.Plugin.AiDOP");
  47. var registered = NeutralRequiredColumns.Channels
  48. .Select(c => Path.Combine(plugin, c.RelativePath))
  49. .ToHashSet(StringComparer.OrdinalIgnoreCase);
  50. var unregistered = Directory
  51. .EnumerateFiles(plugin, "*.cs", SearchOption.AllDirectories)
  52. .Where(f => File.ReadAllText(f).Contains("INSERT INTO mdp_std_inv_trans", StringComparison.Ordinal))
  53. .Where(f => !registered.Contains(f))
  54. .ToList();
  55. Assert.True(unregistered.Count == 0, "未登记为必填列通道: " + string.Join(", ", unregistered));
  56. }
  57. private static string RepoRoot()
  58. {
  59. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  60. while (dir != null)
  61. {
  62. if (Directory.Exists(Path.Combine(dir.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP")))
  63. return dir.FullName;
  64. dir = dir.Parent;
  65. }
  66. throw new DirectoryNotFoundException("repo root");
  67. }
  68. }