Four IEnvironmentCheck implementations in src/ClaudeDo.Installer/Checks/: - ClaudeCliCheck (Error) — resolves ctx.ClaudeBin via ExecutableResolver, runs --version; failure message lists searched PATH entries + fallback dirs, flags shim resolution (.cmd/.ps1) in Detail. - ClaudeVersionCheck (Error) — tolerant version parsing (ignores surrounding text), numeric System.Version comparison against a named floor constant (2.1.220, see docs/explore-notes/installer-preflight.md §3). Unparseable or missing CLI -> Unknown, never Failed. - ClaudeAuthCheck (Error) — `claude auth status --json`, never sends a prompt. - PermissionModeAutoCheck (Warning) — per the note's §2 conclusion, real auto-mode eligibility (org/model/plan) has no cheap static signal, so this only confirms `claude --help` still lists "auto" as a --permission-mode choice. Kept as its own check rather than folded into ClaudeVersionCheck, since the note explicitly separates "flag recognized" from "mode eligible" and only the former is checkable at all. All four share a new ClaudeCliLookup that resolves the CLI and runs --version exactly once per install run (memoized, semaphore-guarded), so none of the three version-dependent checks repeats that call. Foundation prerequisites this task depends on (docs/explore-notes/ installer-preflight.md, ExecutableResolver, the IEnvironmentCheck/CheckResult/ EnvironmentCheckService abstraction, ProcessCommand, IProcessRunner) existed only on an unmerged sibling branch, not on main. Rather than merging that whole unreviewed branch, the specific prerequisite files were copied in as-is (git history shows their origin). GitCheck/GitIdentityCheck/PortCheck/ WriteAccessCheck from that branch were intentionally NOT brought in — out of scope for this task. Deviations/decisions worth flagging: - Added ExecutableResolver.FallbackDirectories() (public) so ClaudeCliCheck can name the checked fallback locations in its failure message; the prior branch kept that list private. - Paths.cs now qualifies System.Environment explicitly. Adding the ClaudeDo.Data.Environment namespace makes bare `Environment.X` inside any ClaudeDo.Data.* namespace resolve to the sibling namespace instead of System.Environment (C# prefers nested/enclosing namespace members over usings) — this broke the build until qualified. Not done (explicitly out of scope): no DI wiring into the wizard UI, no XAML — matches how the prerequisite Git/Port/WriteAccess checks were also left unwired.
128 lines
3.9 KiB
C#
128 lines
3.9 KiB
C#
using ClaudeDo.Data.Environment;
|
|
|
|
namespace ClaudeDo.Data.Tests;
|
|
|
|
public sealed class ExecutableResolverTests : IDisposable
|
|
{
|
|
private readonly string _root;
|
|
|
|
public ExecutableResolverTests()
|
|
{
|
|
_root = Path.Combine(Path.GetTempPath(), $"claudedo_exeresolve_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_root);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_root, recursive: true); } catch { }
|
|
}
|
|
|
|
private string MakeDir(string name)
|
|
{
|
|
var dir = Path.Combine(_root, name);
|
|
Directory.CreateDirectory(dir);
|
|
return dir;
|
|
}
|
|
|
|
private static void Touch(string path) => File.WriteAllText(path, "");
|
|
|
|
[Fact]
|
|
public void Command_with_directory_separator_is_treated_as_path()
|
|
{
|
|
var dir = MakeDir("bin1");
|
|
var exePath = Path.Combine(dir, "myclaude.exe");
|
|
Touch(exePath);
|
|
|
|
var result = ExecutableResolver.Resolve(exePath, pathOverride: "C:\\does\\not\\matter");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal(exePath, result!.Path);
|
|
Assert.False(result.IsShim);
|
|
}
|
|
|
|
[Fact]
|
|
public void Path_like_command_without_extension_resolves_via_pathext()
|
|
{
|
|
var dir = MakeDir("bin2");
|
|
var cmdPath = Path.Combine(dir, "myclaude.cmd");
|
|
Touch(cmdPath);
|
|
var commandWithoutExt = Path.Combine(dir, "myclaude");
|
|
|
|
var result = ExecutableResolver.Resolve(commandWithoutExt, pathExtOverride: ".com;.exe;.bat;.cmd");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal(cmdPath, result!.Path);
|
|
Assert.True(result.IsShim);
|
|
}
|
|
|
|
[Fact]
|
|
public void Cmd_file_in_path_is_found_when_no_exe_exists()
|
|
{
|
|
var dir = MakeDir("bin3");
|
|
Touch(Path.Combine(dir, "claude.cmd"));
|
|
|
|
var result = ExecutableResolver.Resolve("claude", pathOverride: dir, pathExtOverride: ".com;.exe;.bat;.cmd");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal(Path.Combine(dir, "claude.cmd"), result!.Path);
|
|
Assert.True(result.IsShim);
|
|
}
|
|
|
|
[Fact]
|
|
public void Exe_wins_against_cmd_in_same_directory()
|
|
{
|
|
var dir = MakeDir("bin4");
|
|
Touch(Path.Combine(dir, "claude.cmd"));
|
|
Touch(Path.Combine(dir, "claude.exe"));
|
|
|
|
var result = ExecutableResolver.Resolve("claude", pathOverride: dir, pathExtOverride: ".com;.exe;.bat;.cmd");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal(Path.Combine(dir, "claude.exe"), result!.Path);
|
|
Assert.False(result.IsShim);
|
|
}
|
|
|
|
[Fact]
|
|
public void Directory_order_in_path_is_respected()
|
|
{
|
|
var dir1 = MakeDir("bin5a");
|
|
var dir2 = MakeDir("bin5b");
|
|
Touch(Path.Combine(dir1, "claude.cmd"));
|
|
Touch(Path.Combine(dir2, "claude.exe"));
|
|
|
|
var pathOverride = $"{dir1}{Path.PathSeparator}{dir2}";
|
|
var result = ExecutableResolver.Resolve("claude", pathOverride: pathOverride, pathExtOverride: ".com;.exe;.bat;.cmd");
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal(Path.Combine(dir1, "claude.cmd"), result!.Path);
|
|
Assert.True(result.IsShim);
|
|
}
|
|
|
|
[Fact]
|
|
public void Unresolvable_command_returns_null()
|
|
{
|
|
var dir = MakeDir("bin6");
|
|
|
|
var result = ExecutableResolver.Resolve(
|
|
"claudedo_totally_missing_cmd_9f3a1",
|
|
pathOverride: dir,
|
|
pathExtOverride: ".com;.exe;.bat;.cmd");
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildShimStartInfo_uses_cmd_exe_and_quotes_paths_with_spaces()
|
|
{
|
|
const string shimPath = @"C:\Program Files\claude\claude.cmd";
|
|
|
|
var startInfo = ExecutableResolver.BuildShimStartInfo(shimPath, new[] { "--version", "arg2" });
|
|
|
|
Assert.Equal("cmd.exe", startInfo.FileName);
|
|
Assert.Contains("/c", startInfo.Arguments);
|
|
Assert.Contains($"\"{shimPath}\"", startInfo.Arguments);
|
|
Assert.Contains("--version", startInfo.Arguments);
|
|
Assert.Contains("arg2", startInfo.Arguments);
|
|
}
|
|
}
|