- introduce `sdt` subcommands for run, debug, setup, env, favorite, and explain - add project/workspace discovery plus config bootstrap and migration helpers - expand tests for CLI parsing, project role detection, and headless flows
273 lines
11 KiB
C#
273 lines
11 KiB
C#
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 root = CreateTempDir();
|
|
var scan = new BootstrapScanResult(
|
|
ProjectRoot: root,
|
|
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);
|
|
Directory.CreateDirectory(Path.Combine(root, "ui", "src-tauri"));
|
|
File.WriteAllText(Path.Combine(scripts, "publish-app.py"), "print('ok')");
|
|
File.WriteAllText(Path.Combine(scripts, "publish-sidecar.py"), "print('ok')");
|
|
File.WriteAllText(Path.Combine(root, "SidecarService.csproj"), "<Project Sdk=\"Microsoft.NET.Sdk\" />");
|
|
File.WriteAllText(Path.Combine(root, "ui", "package.json"), """{ "name": "demo-ui", "scripts": { "build": "echo build", "tauri": "echo tauri" } }""");
|
|
File.WriteAllText(Path.Combine(root, "ui", "src-tauri", "tauri.conf.json"), "{}");
|
|
|
|
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");
|
|
Assert.Contains(cfg.Workflows, w => w.Id == "tauri");
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildDefaultConfig_DoesNotReferenceMissingSidecarDependency()
|
|
{
|
|
var root = CreateTempDir();
|
|
var scripts = Path.Combine(root, "scripts");
|
|
Directory.CreateDirectory(scripts);
|
|
Directory.CreateDirectory(Path.Combine(root, "ui", "src-tauri"));
|
|
File.WriteAllText(Path.Combine(scripts, "publish-app.py"), "print('ok')");
|
|
File.WriteAllText(Path.Combine(scripts, "publish-sidecar.py"), "print('ok')");
|
|
File.WriteAllText(Path.Combine(root, "ui", "package.json"), """{ "name": "demo-ui", "scripts": { "build": "echo build", "tauri": "echo tauri" } }""");
|
|
File.WriteAllText(Path.Combine(root, "ui", "src-tauri", "tauri.conf.json"), "{}");
|
|
|
|
var scan = ConfigBootstrapper.Scan(root);
|
|
var cfg = ConfigBootstrapper.BuildDefaultConfig(scan);
|
|
|
|
Assert.DoesNotContain(cfg.Workflows, w => w.Id == "sidecar");
|
|
var tauri = Assert.Single(cfg.Workflows, w => w.Id == "tauri");
|
|
Assert.Empty(tauri.DependsOn);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildDefaultConfig_DoesNotGenerateTauriWorkflow_WithoutTauriConfig()
|
|
{
|
|
var root = CreateTempDir();
|
|
var scripts = Path.Combine(root, "scripts");
|
|
Directory.CreateDirectory(scripts);
|
|
Directory.CreateDirectory(Path.Combine(root, "ui"));
|
|
File.WriteAllText(Path.Combine(scripts, "publish-app.py"), "print('ok')");
|
|
File.WriteAllText(Path.Combine(root, "ui", "package.json"), """{ "name": "demo-ui", "scripts": { "build": "echo build" } }""");
|
|
|
|
var scan = ConfigBootstrapper.Scan(root);
|
|
var cfg = ConfigBootstrapper.BuildDefaultConfig(scan);
|
|
|
|
Assert.Contains(cfg.Workflows, w => w.Id == "web");
|
|
Assert.DoesNotContain(cfg.Workflows, w => w.Id == "tauri");
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildDefaultConfig_AddsPrimaryDotnetPublishWorkflow_ForRootExecutableProject()
|
|
{
|
|
var root = CreateTempDir();
|
|
File.WriteAllText(Path.Combine(root, "demo.csproj"), """
|
|
<Project Sdk="Microsoft.NET.Sdk">
|
|
<PropertyGroup>
|
|
<OutputType>Exe</OutputType>
|
|
<TargetFramework>net10.0</TargetFramework>
|
|
<AssemblyName>sdt</AssemblyName>
|
|
</PropertyGroup>
|
|
</Project>
|
|
""");
|
|
|
|
var scan = ConfigBootstrapper.Scan(root);
|
|
var cfg = ConfigBootstrapper.BuildDefaultConfig(scan);
|
|
|
|
var publish = Assert.Single(cfg.Workflows, w => w.Id == "publish-primary-dotnet");
|
|
Assert.Equal("Publish sdt", publish.Label, ignoreCase: true);
|
|
Assert.Contains("output/", publish.Description, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Equal("publish cli", publish.GuidedName, ignoreCase: true);
|
|
Assert.Contains("publish tool", publish.Aliases, StringComparer.OrdinalIgnoreCase);
|
|
Assert.Equal("dotnet", Assert.Single(publish.Steps[0].Requires).Tool, ignoreCase: true);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildDefaultConfig_ClarifiesStageOutputScope()
|
|
{
|
|
var root = CreateTempDir();
|
|
var scripts = Path.Combine(root, "scripts");
|
|
Directory.CreateDirectory(scripts);
|
|
File.WriteAllText(Path.Combine(scripts, "publish-output.py"), "print('ok')");
|
|
|
|
var scan = ConfigBootstrapper.Scan(root);
|
|
var cfg = ConfigBootstrapper.BuildDefaultConfig(scan);
|
|
|
|
var stage = Assert.Single(cfg.Workflows, w => w.Id == "stage-output");
|
|
Assert.Equal("Stage All Detected Outputs", stage.Label, ignoreCase: true);
|
|
Assert.Contains("every detected ship target", stage.Description, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Equal("stage all outputs", stage.GuidedName, ignoreCase: true);
|
|
}
|
|
|
|
[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"), "<project/>");
|
|
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"), "<Project Sdk=\"Microsoft.NET.Sdk\" />");
|
|
File.WriteAllText(Path.Combine(appB, "AppB.csproj"), "<Project Sdk=\"Microsoft.NET.Sdk\" />");
|
|
|
|
var scan = ConfigBootstrapper.Scan(appB);
|
|
Assert.Equal("AppB", scan.DotnetWorkingDir, ignoreCase: true);
|
|
}
|
|
|
|
[Fact]
|
|
public void Scan_PrefersTauriPackageRoot_ForNodeWorkingDir()
|
|
{
|
|
var root = CreateTempDir();
|
|
var workspacePkg = Path.Combine(root, "journal");
|
|
var appPkg = Path.Combine(workspacePkg, "Journal.App");
|
|
Directory.CreateDirectory(appPkg);
|
|
Directory.CreateDirectory(Path.Combine(appPkg, "src-tauri"));
|
|
|
|
File.WriteAllText(Path.Combine(workspacePkg, "package.json"), """{ "name": "workspace", "scripts": { "build": "echo build" } }""");
|
|
File.WriteAllText(Path.Combine(appPkg, "package.json"), """{ "name": "journal-app", "scripts": { "build": "echo build", "tauri": "echo tauri" } }""");
|
|
File.WriteAllText(Path.Combine(appPkg, "src-tauri", "tauri.conf.json"), "{}");
|
|
|
|
var scan = ConfigBootstrapper.Scan(root);
|
|
|
|
Assert.Equal(Path.Combine("journal", "Journal.App"), scan.NodeWorkingDir, ignoreCase: true);
|
|
}
|
|
|
|
private static string CreateTempDir()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), "sdt-bootstrap-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(path);
|
|
return path;
|
|
}
|
|
}
|