UseShellExecute=false only appends .exe when searching PATH, so an npm-installed claude.cmd was never found even though it works from a shell. Adds a shared ExecutableResolver in ClaudeDo.Data (PATH/PATHEXT aware, with known npm/claude install-dir fallbacks) and wires it into ClaudeCliPreflight and ClaudeProcess; shims are launched via cmd.exe /c.
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);
|
|
}
|
|
}
|