using Sdt.Config; using Sdt.Core; using Sdt.Runner; using Xunit; namespace DevTool.Tests; public sealed class ToolchainManagerServiceTests { [Fact] public async Task ProbeConfiguredTools_IncludesToolchainAndWorkflowRequirements() { var config = new DevToolConfig { Toolchains = new ToolchainConfig { Python = new PythonToolchain(), Node = new NodeToolchain { PackageManager = "npm" } }, Workflows = [ new WorkflowDefinition { Id = "build", Label = "Build", Steps = [ new WorkflowStep { Id = "s1", Label = "S1", Requires = [new ToolRequirement { Tool = "dotnet" }] } ] } ] }; var service = new ToolchainManagerService(new AvailableProbe(), new NoOpInstaller()); var probes = await service.ProbeConfiguredToolsAsync(config, Directory.GetCurrentDirectory()); var tools = probes.Select(p => p.Tool).ToHashSet(StringComparer.OrdinalIgnoreCase); Assert.Contains("python", tools); Assert.Contains("node", tools); Assert.Contains("npm", tools); Assert.Contains("dotnet", tools); } [Fact] public async Task AutoFixMissingTools_AttemptsInstallAndVerifies() { var config = new DevToolConfig { Toolchains = new ToolchainConfig { Node = new NodeToolchain { PackageManager = "npm" } } }; var probe = new SequenceProbe(); var installer = new SuccessInstaller(); var service = new ToolchainManagerService(probe, installer); var results = await service.AutoFixMissingToolsAsync( config, Directory.GetCurrentDirectory(), (_, _) => Task.FromResult(true), (_, _) => { }); Assert.Contains(results, r => r.Tool.Equals("npm", StringComparison.OrdinalIgnoreCase) && r.Success); } private sealed class AvailableProbe : IToolProbe { public Task ProbeAsync(string tool, string projectRoot, DevToolConfig? config = null, IReadOnlyDictionary? envOverrides = null, CancellationToken cancellationToken = default) => Task.FromResult(new ProbeResult(tool, true, Version: "1.0.0")); } private sealed class SequenceProbe : IToolProbe { private readonly Dictionary _count = new(StringComparer.OrdinalIgnoreCase); public Task ProbeAsync(string tool, string projectRoot, DevToolConfig? config = null, IReadOnlyDictionary? envOverrides = null, CancellationToken cancellationToken = default) { _count.TryGetValue(tool, out var c); _count[tool] = c + 1; if (tool.Equals("node", StringComparison.OrdinalIgnoreCase)) return Task.FromResult(new ProbeResult(tool, true, Version: "1.0.0")); // npm: first probe missing, later available (after install). var available = c > 0; return Task.FromResult(new ProbeResult(tool, available, Version: available ? "1.0.0" : null)); } } private sealed class NoOpInstaller : IPrereqInstaller { public Task GetInstallPlanAsync(string tool, string projectRoot, DevToolConfig? config = null, CancellationToken cancellationToken = default) => Task.FromResult(new InstallPlan(tool, true, "noop", [new InstallCommand("echo", ["ok"])])); public Task RunInstallAsync(InstallCommand command, string projectRoot, Action onOutput, IReadOnlyDictionary? envOverrides = null, CancellationToken cancellationToken = default) => Task.FromResult(new RunResult(0, TimeSpan.Zero)); } private sealed class SuccessInstaller : IPrereqInstaller { public Task GetInstallPlanAsync(string tool, string projectRoot, DevToolConfig? config = null, CancellationToken cancellationToken = default) => Task.FromResult(new InstallPlan(tool, true, "install", [new InstallCommand("echo", ["install"])])); public Task RunInstallAsync(InstallCommand command, string projectRoot, Action onOutput, IReadOnlyDictionary? envOverrides = null, CancellationToken cancellationToken = default) => Task.FromResult(new RunResult(0, TimeSpan.FromMilliseconds(5))); } }