using Sdt.Config; using Xunit; namespace DevTool.Tests; public sealed class ConfigBootstrapperTests { [Fact] public void Scan_DetectsDotnetAndNode() { var root = CreateTempDir(); File.WriteAllText(Path.Combine(root, "sample.sln"), ""); File.WriteAllText(Path.Combine(root, "package.json"), """{ "name": "demo", "scripts": { "build": "echo build" } }"""); var scan = ConfigBootstrapper.Scan(root); Assert.Contains("dotnet", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); Assert.Contains("node", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); Assert.Contains("npm", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); } [Fact] public void Scan_DetectsPythonFromScriptsDirectory() { var root = CreateTempDir(); var scripts = Path.Combine(root, "scripts"); Directory.CreateDirectory(scripts); File.WriteAllText(Path.Combine(scripts, "publish-app.py"), "print('ok')"); var scan = ConfigBootstrapper.Scan(root); Assert.Contains("python", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); } [Fact] public void Scan_DoesNotDetectNode_ForDependencyOnlyPackageJson() { var root = CreateTempDir(); File.WriteAllText(Path.Combine(root, "package.json"), """{ "dependencies": { "leftpad": "1.0.0" } }"""); var scan = ConfigBootstrapper.Scan(root); Assert.DoesNotContain("node", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); Assert.DoesNotContain("npm", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); } [Fact] public void BuildDefaultConfig_ProducesWorkflowsAndDebugSection() { var scan = new BootstrapScanResult( ProjectRoot: Path.GetTempPath(), ProjectName: "demo", ProjectType: "dotnet", ToolFamilies: ["dotnet", "git"], DotnetWorkingDir: ".", NodeWorkingDir: null, PythonRequirementsFile: null, HasDockerCompose: false, RootHints: ["*.sln"]); var cfg = ConfigBootstrapper.BuildDefaultConfig(scan); Assert.NotNull(cfg.Debug); Assert.Contains(cfg.Workflows, w => w.Id == "build"); Assert.Contains(cfg.Workflows, w => w.Id == "repo-health"); Assert.False(cfg.Debug!.Diagnostics.IncludeAllEnv); Assert.Contains("SDT_LOG_LEVEL", cfg.Debug.Diagnostics.CaptureEnvKeys); Assert.NotNull(cfg.EnvProfiles); Assert.Contains(cfg.EnvProfiles!.Profiles, p => p.Id == "dev"); Assert.Contains(cfg.EnvProfiles.Profiles, p => p.Id == "ci"); Assert.Contains(cfg.EnvProfiles.Profiles, p => p.Id == "release"); } [Fact] public void BuildDefaultConfig_IncludesScriptDrivenWorkflow_WhenHelpersExist() { var root = CreateTempDir(); var scripts = Path.Combine(root, "scripts"); Directory.CreateDirectory(scripts); File.WriteAllText(Path.Combine(scripts, "publish-app.py"), "print('ok')"); File.WriteAllText(Path.Combine(scripts, "publish-sidecar.py"), "print('ok')"); var scan = ConfigBootstrapper.Scan(root); var cfg = ConfigBootstrapper.BuildDefaultConfig(scan); Assert.Contains(cfg.Workflows, w => w.Id == "web"); Assert.Contains(cfg.Workflows, w => w.Id == "sidecar"); } [Fact] public void WriteDefaultConfig_WritesDevtoolJson() { var root = CreateTempDir(); var scan = new BootstrapScanResult( ProjectRoot: root, ProjectName: "demo", ProjectType: "dotnet", ToolFamilies: ["dotnet"], DotnetWorkingDir: ".", NodeWorkingDir: null, PythonRequirementsFile: null, HasDockerCompose: false, RootHints: ["*.sln"]); var cfg = ConfigBootstrapper.BuildDefaultConfig(scan); var path = ConfigBootstrapper.WriteDefaultConfig(root, cfg); Assert.True(File.Exists(path)); } [Fact] public void Scan_IgnoresExcludedDirectories_ForToolDetection() { var root = CreateTempDir(); var nodeModules = Path.Combine(root, "node_modules", "nested"); Directory.CreateDirectory(nodeModules); File.WriteAllText(Path.Combine(nodeModules, "package.json"), "{}"); var scan = ConfigBootstrapper.Scan(root); Assert.DoesNotContain("node", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); } [Fact] public void Scan_DetectsGoAndSetsProjectType() { var root = CreateTempDir(); File.WriteAllText(Path.Combine(root, "go.mod"), "module example.com/demo"); var scan = ConfigBootstrapper.Scan(root); Assert.Contains("go", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); Assert.Equal("go", scan.ProjectType, ignoreCase: true); } [Fact] public void Scan_DetectsMavenAndGradleAsJavaProject() { var root = CreateTempDir(); File.WriteAllText(Path.Combine(root, "pom.xml"), ""); File.WriteAllText(Path.Combine(root, "build.gradle"), "plugins {}"); var scan = ConfigBootstrapper.Scan(root); Assert.Contains("maven", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); Assert.Contains("gradle", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); Assert.Contains("java", scan.ToolFamilies, StringComparer.OrdinalIgnoreCase); Assert.Equal("java", scan.ProjectType, ignoreCase: true); } [Fact] public void Scan_ResolvesDotnetWorkingDir_FromNearestLaunchPath() { var workspace = CreateTempDir(); File.WriteAllText(Path.Combine(workspace, "package.json"), """{ "name": "workspace-root" }"""); var appA = Path.Combine(workspace, "AppA"); var appB = Path.Combine(workspace, "AppB"); Directory.CreateDirectory(appA); Directory.CreateDirectory(appB); File.WriteAllText(Path.Combine(appA, "AppA.csproj"), ""); File.WriteAllText(Path.Combine(appB, "AppB.csproj"), ""); var scan = ConfigBootstrapper.Scan(appB); Assert.Equal("AppB", scan.DotnetWorkingDir, ignoreCase: true); } private static string CreateTempDir() { var path = Path.Combine(Path.GetTempPath(), "sdt-bootstrap-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path); return path; } }