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); } } }