using System.Text.RegularExpressions; using Admin.NET.Plugin.AiDOP.DataPlatform; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.DataPlatform; /// /// 目录必须盖住代码里声明的中立表,以及当前库已有、但 DDL 不在 MdpSchemaDefinition 的表。 /// 投影文件里的 INSERT INTO mdp_std_ 必须经过来源闸门。 /// public class MdpStdObjectCatalogGuardTests { private static readonly string[] PhysicalTablesOutsideSchema = [ "mdp_std_action_event", "mdp_std_delivery_exception", "mdp_std_delivery_result", "mdp_std_delivery_schedule", "mdp_std_employee", "mdp_std_inv_trans", "mdp_std_inventory", "mdp_std_inventory_balance_monthly", "mdp_std_iqc_inspection", "mdp_std_item", "mdp_std_material_readiness", "mdp_std_process_outsource_order", "mdp_std_purchase_order", "mdp_std_purchase_order_completion", "mdp_std_purchase_request", "mdp_std_ship_trans", "mdp_std_so", "mdp_std_supplier", "mdp_std_supplier_item", "mdp_std_supply_demand", "mdp_std_work_order_bom", "mdp_std_work_order_line", ]; [Fact] public void Catalog_CoversEveryNeutralTable() { var schema = ReadRepoFile(Path.Combine( "server", "Plugins", "Admin.NET.Plugin.AiDOP", "DataPlatform", "Schema", "MdpSchemaDefinition.cs")); var names = Regex.Matches(schema, "\\[\"(mdp_std_[a-z0-9_]+)\"\\]") .Select(m => m.Groups[1].Value) .Concat(PhysicalTablesOutsideSchema) .Where(n => !n.StartsWith("mdp_std_t8_", StringComparison.OrdinalIgnoreCase) && !n.Contains("_unmapped", StringComparison.OrdinalIgnoreCase) && !n.Contains("_bak_", StringComparison.OrdinalIgnoreCase)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); var covered = MdpStdObjectCatalog.All.SelectMany(d => d.Tables).ToHashSet(StringComparer.OrdinalIgnoreCase); var missing = names.Where(n => !covered.Contains(n)).OrderBy(n => n).ToList(); Assert.True(missing.Count == 0, "目录未覆盖: " + string.Join(", ", missing)); } [Fact] public void NeutralInserts_CallSourceGate() { var root = RepoRoot(); var plugin = Path.Combine(root, "server", "Plugins", "Admin.NET.Plugin.AiDOP"); var hits = new List(); foreach (var file in Directory.EnumerateFiles(plugin, "*.cs", SearchOption.AllDirectories)) { if (file.Contains($"{Path.DirectorySeparatorChar}Tests{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase)) continue; var text = File.ReadAllText(file); var targets = Regex.Matches(text, @"INSERT\s+INTO\s+(mdp_std_[A-Za-z0-9_]+)", RegexOptions.IgnoreCase) .Select(m => m.Groups[1].Value) .Where(n => !n.Contains("unmapped", StringComparison.OrdinalIgnoreCase) && !n.Contains("_bak_", StringComparison.OrdinalIgnoreCase)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (targets.Count == 0) continue; if (!text.Contains("MdpNeutralSourceGate", StringComparison.Ordinal) && !text.Contains("MdpTenantStdSourceResolver", StringComparison.Ordinal)) hits.Add(Path.GetRelativePath(plugin, file)); } Assert.True(hits.Count == 0, "中立层写入未调用来源闸门: " + string.Join(", ", hits)); } private static string ReadRepoFile(string relative) { var path = Path.Combine(RepoRoot(), relative); if (!File.Exists(path)) throw new FileNotFoundException(path); return File.ReadAllText(path); } 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"); } }