Merge branch 'claudedo/0c5a46e7b2d84538a54b2e13b44af01e'
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests.Checks;
|
||||
|
||||
public sealed class ClaudeAuthCheckTests : IDisposable
|
||||
{
|
||||
private readonly string _dir;
|
||||
|
||||
public ClaudeAuthCheckTests()
|
||||
{
|
||||
_dir = Path.Combine(Path.GetTempPath(), $"cdclaudeauth_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
File.WriteAllText(Path.Combine(_dir, "claude.exe"), "");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeCliLookup MakeLookup(FakeProcessRunner runner) =>
|
||||
new(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
[Fact]
|
||||
public async Task Logged_in_reports_ok_without_sending_a_prompt()
|
||||
{
|
||||
var runner = new FakeProcessRunner(
|
||||
(0, "2.1.230 (Claude Code)"),
|
||||
(0, "{\"loggedIn\":true,\"authMethod\":\"claude.ai\"}"));
|
||||
var check = new ClaudeAuthCheck(MakeLookup(runner));
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
Assert.All(runner.Calls, call => Assert.DoesNotContain("-p ", call.Arguments));
|
||||
Assert.Contains(runner.Calls, call => call.Arguments.Contains("auth") && call.Arguments.Contains("status"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Logged_out_reports_failed()
|
||||
{
|
||||
var runner = new FakeProcessRunner(
|
||||
(0, "2.1.230 (Claude Code)"),
|
||||
(0, "{\"loggedIn\":false}"));
|
||||
var check = new ClaudeAuthCheck(MakeLookup(runner));
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.NotNull(result.HintKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unparseable_output_reports_unknown_not_failed()
|
||||
{
|
||||
var runner = new FakeProcessRunner(
|
||||
(0, "2.1.230 (Claude Code)"),
|
||||
(0, "not json"));
|
||||
var check = new ClaudeAuthCheck(MakeLookup(runner));
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Unknown, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cli_missing_reports_unknown()
|
||||
{
|
||||
var emptyDir = Path.Combine(Path.GetTempPath(), $"cdclaudeauth_empty_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(emptyDir);
|
||||
try
|
||||
{
|
||||
var runner = new FakeProcessRunner();
|
||||
var check = new ClaudeAuthCheck(new ClaudeCliLookup(runner, pathOverride: emptyDir, pathExtOverride: ".exe"));
|
||||
var ctx = new InstallContext { ClaudeBin = "claudedo_totally_missing_cmd_9f3a1" };
|
||||
|
||||
var result = await check.RunAsync(ctx, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Unknown, result.Status);
|
||||
Assert.Empty(runner.Calls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(emptyDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests.Checks;
|
||||
|
||||
public sealed class ClaudeCliCheckTests : IDisposable
|
||||
{
|
||||
private readonly string _dir;
|
||||
|
||||
public ClaudeCliCheckTests()
|
||||
{
|
||||
_dir = Path.Combine(Path.GetTempPath(), $"cdclaudecli_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Found_as_exe_reports_ok_with_path_and_version_and_no_shim_detail()
|
||||
{
|
||||
File.WriteAllText(Path.Combine(_dir, "claude.exe"), "");
|
||||
var runner = new FakeProcessRunner((0, "2.1.230 (Claude Code)"));
|
||||
var check = new ClaudeCliCheck(new ClaudeCliLookup(runner, pathOverride: _dir, pathExtOverride: ".exe"));
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
Assert.Contains(_dir, result.Message);
|
||||
Assert.Contains("2.1.230", result.Message);
|
||||
Assert.Null(result.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Found_as_shim_flags_it_in_detail()
|
||||
{
|
||||
File.WriteAllText(Path.Combine(_dir, "claude.cmd"), "");
|
||||
var runner = new FakeProcessRunner((0, "2.1.230 (Claude Code)"));
|
||||
var check = new ClaudeCliCheck(new ClaudeCliLookup(runner, pathOverride: _dir, pathExtOverride: ".cmd"));
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
Assert.NotNull(result.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Not_found_reports_failed_naming_searched_locations()
|
||||
{
|
||||
var runner = new FakeProcessRunner();
|
||||
var check = new ClaudeCliCheck(new ClaudeCliLookup(runner, pathOverride: _dir, pathExtOverride: ".exe"));
|
||||
// A distinctive, definitely-missing name — a plain "claude" would still resolve via
|
||||
// ExecutableResolver's fixed fallback dirs (e.g. %USERPROFILE%\.local\bin) on a machine
|
||||
// that has the real CLI installed, regardless of pathOverride.
|
||||
var ctx = new InstallContext { ClaudeBin = "claudedo_totally_missing_cmd_9f3a1" };
|
||||
|
||||
var result = await check.RunAsync(ctx, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Equal(CheckSeverity.Error, result.Severity);
|
||||
Assert.Contains(_dir, result.Message);
|
||||
Assert.NotNull(result.HintKey);
|
||||
Assert.Empty(runner.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Nonzero_exit_reports_failed()
|
||||
{
|
||||
File.WriteAllText(Path.Combine(_dir, "claude.exe"), "");
|
||||
var runner = new FakeProcessRunner((1, "some error"));
|
||||
var check = new ClaudeCliCheck(new ClaudeCliLookup(runner, pathOverride: _dir, pathExtOverride: ".exe"));
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests.Checks;
|
||||
|
||||
public sealed class ClaudeCliLookupTests : IDisposable
|
||||
{
|
||||
private readonly string _dir;
|
||||
|
||||
public ClaudeCliLookupTests()
|
||||
{
|
||||
_dir = Path.Combine(Path.GetTempPath(), $"cdclaudelookup_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
File.WriteAllText(Path.Combine(_dir, "claude.exe"), "");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Version_is_executed_only_once_across_repeated_calls()
|
||||
{
|
||||
var runner = new FakeProcessRunner((0, "2.1.230 (Claude Code)"));
|
||||
var lookup = new ClaudeCliLookup(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
await lookup.ResolveAsync(new InstallContext(), CancellationToken.None);
|
||||
await lookup.ResolveAsync(new InstallContext(), CancellationToken.None);
|
||||
await lookup.ResolveAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Single(runner.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Concurrent_calls_still_run_version_only_once()
|
||||
{
|
||||
var runner = new FakeProcessRunner((0, "2.1.230 (Claude Code)"));
|
||||
var lookup = new ClaudeCliLookup(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
await Task.WhenAll(Enumerable.Range(0, 8)
|
||||
.Select(_ => lookup.ResolveAsync(new InstallContext(), CancellationToken.None)));
|
||||
|
||||
Assert.Single(runner.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Version_is_executed_only_once_when_shared_across_all_four_checks()
|
||||
{
|
||||
var runner = new FakeProcessRunner((0, "2.1.230 (Claude Code)"));
|
||||
var lookup = new ClaudeCliLookup(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
var ctx = new InstallContext();
|
||||
|
||||
await new ClaudeCliCheck(lookup).RunAsync(ctx, CancellationToken.None);
|
||||
await new ClaudeVersionCheck(lookup).RunAsync(ctx, CancellationToken.None);
|
||||
await new ClaudeAuthCheck(lookup).RunAsync(ctx, CancellationToken.None);
|
||||
await new PermissionModeAutoCheck(lookup).RunAsync(ctx, CancellationToken.None);
|
||||
|
||||
var versionCalls = runner.Calls.Count(c => c.Arguments.Contains("--version"));
|
||||
Assert.Equal(1, versionCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cli_not_found_returns_null_resolved_without_running_a_process()
|
||||
{
|
||||
var emptyDir = Path.Combine(Path.GetTempPath(), $"cdclaudelookup_empty_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(emptyDir);
|
||||
try
|
||||
{
|
||||
var runner = new FakeProcessRunner();
|
||||
var lookup = new ClaudeCliLookup(runner, pathOverride: emptyDir, pathExtOverride: ".exe");
|
||||
var ctx = new InstallContext { ClaudeBin = "claudedo_totally_missing_cmd_9f3a1" };
|
||||
|
||||
var result = await lookup.ResolveAsync(ctx, CancellationToken.None);
|
||||
|
||||
Assert.Null(result.Resolved);
|
||||
Assert.Empty(runner.Calls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(emptyDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests.Checks;
|
||||
|
||||
public sealed class ClaudeVersionCheckTests : IDisposable
|
||||
{
|
||||
private readonly string _dir;
|
||||
|
||||
public ClaudeVersionCheckTests()
|
||||
{
|
||||
_dir = Path.Combine(Path.GetTempPath(), $"cdclaudeversion_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
File.WriteAllText(Path.Combine(_dir, "claude.exe"), "");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeCliLookup MakeLookup(FakeProcessRunner runner) =>
|
||||
new(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
[Theory]
|
||||
[InlineData("2.1.220 (Claude Code)", CheckStatus.Ok)] // exactly at the floor
|
||||
[InlineData("2.1.219 (Claude Code)", CheckStatus.Failed)] // one patch below the floor
|
||||
[InlineData("2.1.191", CheckStatus.Failed)]
|
||||
[InlineData("v2.1.191", CheckStatus.Failed)]
|
||||
[InlineData("2.10.0", CheckStatus.Ok)] // minor 10 > minor 1, not a lexical trap
|
||||
[InlineData("garbage output, no version here", CheckStatus.Unknown)]
|
||||
public async Task Version_parsing_and_gate(string versionOutput, CheckStatus expected)
|
||||
{
|
||||
var runner = new FakeProcessRunner((0, versionOutput));
|
||||
var check = new ClaudeVersionCheck(MakeLookup(runner));
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(expected, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Minor_version_compares_numerically_not_lexically()
|
||||
{
|
||||
Assert.True(new Version(2, 10, 0) > new Version(2, 9, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cli_not_found_reports_unknown_not_failed()
|
||||
{
|
||||
var emptyDir = Path.Combine(Path.GetTempPath(), $"cdclaudeversion_empty_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(emptyDir);
|
||||
try
|
||||
{
|
||||
var runner = new FakeProcessRunner();
|
||||
var check = new ClaudeVersionCheck(new ClaudeCliLookup(runner, pathOverride: emptyDir, pathExtOverride: ".exe"));
|
||||
var ctx = new InstallContext { ClaudeBin = "claudedo_totally_missing_cmd_9f3a1" };
|
||||
|
||||
var result = await check.RunAsync(ctx, CancellationToken.None);
|
||||
Assert.Empty(runner.Calls);
|
||||
|
||||
Assert.Equal(CheckStatus.Unknown, result.Status);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(emptyDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests.Checks;
|
||||
|
||||
public sealed class PermissionModeAutoCheckTests : IDisposable
|
||||
{
|
||||
private readonly string _dir;
|
||||
|
||||
private const string HelpWithAuto =
|
||||
"Options:\n --permission-mode <mode> Permission mode (choices: \"acceptEdits\", \"auto\", \"bypassPermissions\", \"manual\", \"dontAsk\", \"plan\")\n";
|
||||
|
||||
private const string HelpWithoutAuto =
|
||||
"Options:\n --permission-mode <mode> Permission mode (choices: \"acceptEdits\", \"bypassPermissions\", \"manual\")\n";
|
||||
|
||||
public PermissionModeAutoCheckTests()
|
||||
{
|
||||
_dir = Path.Combine(Path.GetTempPath(), $"cdpermmode_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
File.WriteAllText(Path.Combine(_dir, "claude.exe"), "");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeCliLookup MakeLookup(FakeProcessRunner runner) =>
|
||||
new(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
[Fact]
|
||||
public async Task Auto_listed_in_help_reports_ok()
|
||||
{
|
||||
var runner = new FakeProcessRunner((0, "2.1.230"), (0, HelpWithAuto));
|
||||
var check = new PermissionModeAutoCheck(MakeLookup(runner));
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
Assert.Equal(CheckSeverity.Warning, result.Severity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Auto_missing_from_help_reports_failed_with_hint()
|
||||
{
|
||||
var runner = new FakeProcessRunner((0, "2.1.230"), (0, HelpWithoutAuto));
|
||||
var check = new PermissionModeAutoCheck(MakeLookup(runner));
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Equal(CheckSeverity.Warning, result.Severity);
|
||||
Assert.NotNull(result.HintKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Help_not_determinable_reports_unknown()
|
||||
{
|
||||
var runner = new FakeProcessRunner((0, "2.1.230"), (1, ""));
|
||||
var check = new PermissionModeAutoCheck(MakeLookup(runner));
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Unknown, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cli_missing_reports_unknown()
|
||||
{
|
||||
var emptyDir = Path.Combine(Path.GetTempPath(), $"cdpermmode_empty_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(emptyDir);
|
||||
try
|
||||
{
|
||||
var runner = new FakeProcessRunner();
|
||||
var check = new PermissionModeAutoCheck(new ClaudeCliLookup(runner, pathOverride: emptyDir, pathExtOverride: ".exe"));
|
||||
var ctx = new InstallContext { ClaudeBin = "claudedo_totally_missing_cmd_9f3a1" };
|
||||
|
||||
var result = await check.RunAsync(ctx, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Unknown, result.Status);
|
||||
Assert.Empty(runner.Calls);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(emptyDir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user