namespace BuildFlow.Architecture.Tests; using Xunit; /// /// BuildFlow Host 架构约束测试。确保后台只有 BuildFlow Host 一个启动入口, /// 业务模块(Domain/Application/Infrastructure 三层)不能退化为独立进程。 /// HTTP 层已统一并入 BuildFlow.Host,模块不再保留 Api 类库。 /// public class HostArchitectureTests { [Fact] public void BusinessModulesShouldNotContainStandaloneProgramEntryPoints() { var repoRoot = FindRepoRoot(); var moduleRoot = Path.Combine(repoRoot.FullName, "src", "modules"); var programFiles = Directory .EnumerateFiles(moduleRoot, "Program.cs", SearchOption.AllDirectories) .Where(path => !ContainsPathSegment(path, "bin") && !ContainsPathSegment(path, "obj")) .Select(path => Path.GetRelativePath(repoRoot.FullName, path).Replace('\\', '/')) .Order(StringComparer.Ordinal) .ToArray(); Assert.True( programFiles.Length == 0, "业务模块不允许包含独立 Program.cs,后台应只由 src/BuildFlow.Host/Program.cs 统一启动。当前残留:" + Environment.NewLine + string.Join(Environment.NewLine, programFiles)); } [Fact] public void BusinessModulesShouldNotContainLaunchProfiles() { var repoRoot = FindRepoRoot(); var moduleRoot = Path.Combine(repoRoot.FullName, "src", "modules"); var launchProfiles = Directory .EnumerateFiles(moduleRoot, "launchSettings.json", SearchOption.AllDirectories) .Where(path => !ContainsPathSegment(path, "bin") && !ContainsPathSegment(path, "obj")) .Select(path => Path.GetRelativePath(repoRoot.FullName, path).Replace('\\', '/')) .Order(StringComparer.Ordinal) .ToArray(); Assert.True( launchProfiles.Length == 0, "业务模块不允许保留独立启动配置 launchSettings.json。当前残留:" + Environment.NewLine + string.Join(Environment.NewLine, launchProfiles)); } [Fact] public void BusinessModulesShouldNotHaveStandaloneApiProjects() { var repoRoot = FindRepoRoot(); var moduleRoot = Path.Combine(repoRoot.FullName, "src", "modules"); var apiProjects = Directory .EnumerateFiles(moduleRoot, "*.Api.csproj", SearchOption.AllDirectories) .Concat(Directory.EnumerateFiles(moduleRoot, "*Api.csproj", SearchOption.AllDirectories)) .Where(path => !ContainsPathSegment(path, "bin") && !ContainsPathSegment(path, "obj")) .Distinct(StringComparer.Ordinal) .Select(path => Path.GetRelativePath(repoRoot.FullName, path).Replace('\\', '/')) .Order(StringComparer.Ordinal) .ToArray(); Assert.True( apiProjects.Length == 0, "业务模块不允许保留独立 Api 类库项目(HTTP 层已统一并入 BuildFlow.Host)。当前残留:" + Environment.NewLine + string.Join(Environment.NewLine, apiProjects)); } private static DirectoryInfo FindRepoRoot() { var current = new DirectoryInfo(AppContext.BaseDirectory); while (current is not null) { if (File.Exists(Path.Combine(current.FullName, "BuildFlow.slnx"))) { return current; } current = current.Parent; } throw new InvalidOperationException("无法定位 BuildFlow.slnx,架构测试无法确定仓库边界。"); } private static bool ContainsPathSegment(string path, string segment) { return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) .Any(part => string.Equals(part, segment, StringComparison.Ordinal)); } }