using ClaudeDo.Installer.Checks; using ClaudeDo.Installer.Core; using ClaudeDo.Installer.Tests.Checks; namespace ClaudeDo.Installer.Tests.Core; public sealed class ClaudeHelpLauncherTests : IDisposable { private readonly string _dir; public ClaudeHelpLauncherTests() { _dir = Path.Combine(Path.GetTempPath(), $"cdclaudehelp_{Guid.NewGuid():N}"); Directory.CreateDirectory(_dir); } public void Dispose() { try { Directory.Delete(_dir, recursive: true); } catch { } } private ClaudeHelpLauncher MakeLauncher(FakeProcessRunner? runner = null, FakeProcessLauncher? launcher = null, string? pathExtOverride = ".exe") => new(runner ?? new FakeProcessRunner(), launcher ?? new FakeProcessLauncher(), pathOverride: _dir, pathExtOverride: pathExtOverride); private static EnvironmentCheckReport MakeReport() => new(new[] { CheckResult.Ok(GitCheck.CheckId, CheckSeverity.Error, "checks.git.title", "C:\\Program Files\\Git\\git.exe — 2.43.0"), CheckResult.Ok(ClaudeCliCheck.CheckId, CheckSeverity.Error, "checks.claudeCli.title", "C:\\npm\\claude.cmd — 2.1.230"), CheckResult.Fail(ClaudeAuthCheck.CheckId, CheckSeverity.Error, "checks.claudeAuth.title", "Not logged in.", "checks.claudeAuth.hint", detail: "exit code 1: some raw CLI output"), }); // ----- BuildReportAsync ----- [Fact] public async Task Report_includes_all_check_ids_severities_and_failed_details() { var launcher = MakeLauncher(); var report = MakeReport(); var text = await launcher.BuildReportAsync(report, new InstallContext(), CancellationToken.None); Assert.Contains(GitCheck.CheckId, text); Assert.Contains(ClaudeCliCheck.CheckId, text); Assert.Contains(ClaudeAuthCheck.CheckId, text); Assert.Contains(CheckSeverity.Error.ToString(), text); Assert.Contains(CheckStatus.Failed.ToString(), text); Assert.Contains("exit code 1: some raw CLI output", text); } [Fact] public async Task Report_does_not_include_environment_variable_values() { const string marker = "sk-super-secret-token-12345"; Environment.SetEnvironmentVariable("CLAUDEDO_TEST_SECRET", marker); try { var launcher = MakeLauncher(); var text = await launcher.BuildReportAsync(MakeReport(), new InstallContext(), CancellationToken.None); Assert.DoesNotContain(marker, text); Assert.DoesNotContain(".credentials.json", text); } finally { Environment.SetEnvironmentVariable("CLAUDEDO_TEST_SECRET", null); } } [Fact] public async Task Report_includes_planned_install_directory_and_ports() { var launcher = MakeLauncher(); var ctx = new InstallContext { InstallDirectory = @"C:\Somewhere\ClaudeDo", SignalRPort = 11111, ExternalMcpPort = 22222 }; var text = await launcher.BuildReportAsync(MakeReport(), ctx, CancellationToken.None); Assert.Contains(@"C:\Somewhere\ClaudeDo", text); Assert.Contains("11111", text); Assert.Contains("22222", text); } // ----- LaunchTerminal ----- private string MakeClaudeExe() { var path = Path.Combine(_dir, "claude.exe"); File.WriteAllText(path, ""); return path; } [Fact] public void Uses_wt_when_available() { MakeClaudeExe(); File.WriteAllText(Path.Combine(_dir, "wt.exe"), ""); var fakeLauncher = new FakeProcessLauncher(); var launcher = MakeLauncher(launcher: fakeLauncher); var result = launcher.LaunchTerminal(Path.Combine(_dir, "report.md"), new InstallContext()); Assert.True(result.Success); Assert.Contains("wt.exe", fakeLauncher.LastStartInfo!.FileName); } [Fact] public void Falls_back_to_cmd_when_wt_is_not_available() { MakeClaudeExe(); var fakeLauncher = new FakeProcessLauncher(); var launcher = MakeLauncher(launcher: fakeLauncher); var result = launcher.LaunchTerminal(Path.Combine(_dir, "report.md"), new InstallContext()); Assert.True(result.Success); Assert.Equal("cmd.exe", fakeLauncher.LastStartInfo!.FileName); } [Fact] public void Working_directory_is_temp_not_install_directory() { MakeClaudeExe(); var fakeLauncher = new FakeProcessLauncher(); var launcher = MakeLauncher(launcher: fakeLauncher); var ctx = new InstallContext { InstallDirectory = @"C:\Program Files\ClaudeDo" }; launcher.LaunchTerminal(Path.Combine(_dir, "report.md"), ctx); Assert.Equal(Path.GetTempPath(), fakeLauncher.LastStartInfo!.WorkingDirectory); Assert.DoesNotContain(ctx.InstallDirectory, fakeLauncher.LastStartInfo!.WorkingDirectory); } [Fact] public void Quotes_the_claude_path_when_it_contains_spaces() { var spacedDir = Path.Combine(_dir, "path with spaces"); Directory.CreateDirectory(spacedDir); var claudePath = Path.Combine(spacedDir, "claude.exe"); File.WriteAllText(claudePath, ""); var fakeLauncher = new FakeProcessLauncher(); var runner = new FakeProcessRunner(); var launcher = new ClaudeHelpLauncher(runner, fakeLauncher, pathOverride: spacedDir, pathExtOverride: ".exe"); var result = launcher.LaunchTerminal(Path.Combine(_dir, "report.md"), new InstallContext()); Assert.True(result.Success); Assert.Contains($"\"{claudePath}\"", fakeLauncher.LastStartInfo!.Arguments); } [Fact] public void Cmd_shim_as_claude_works() { var claudePath = Path.Combine(_dir, "claude.cmd"); File.WriteAllText(claudePath, ""); var fakeLauncher = new FakeProcessLauncher(); var launcher = MakeLauncher(launcher: fakeLauncher, pathExtOverride: ".cmd"); var result = launcher.LaunchTerminal(Path.Combine(_dir, "report.md"), new InstallContext()); Assert.True(result.Success); Assert.Contains(claudePath, fakeLauncher.LastStartInfo!.Arguments); } [Fact] public void Claude_not_found_returns_error_result_not_exception() { var fakeLauncher = new FakeProcessLauncher(); var launcher = MakeLauncher(launcher: fakeLauncher); var ctx = new InstallContext { ClaudeBin = "claudedo_totally_missing_cmd_9f3a1" }; var result = launcher.LaunchTerminal(Path.Combine(_dir, "report.md"), ctx); Assert.False(result.Success); Assert.NotNull(result.ErrorMessage); Assert.Null(fakeLauncher.LastStartInfo); } [Fact] public void Failed_process_start_returns_error_result_not_exception() { MakeClaudeExe(); var fakeLauncher = new FakeProcessLauncher(throwOnStart: new InvalidOperationException("boom")); var launcher = MakeLauncher(launcher: fakeLauncher); var result = launcher.LaunchTerminal(Path.Combine(_dir, "report.md"), new InstallContext()); Assert.False(result.Success); Assert.Equal("boom", result.ErrorMessage); } // ----- LaunchAsync (report + terminal wiring) ----- [Fact] public async Task LaunchAsync_writes_report_and_points_the_prompt_at_it() { MakeClaudeExe(); var fakeLauncher = new FakeProcessLauncher(); var launcher = MakeLauncher(launcher: fakeLauncher); var result = await launcher.LaunchAsync(MakeReport(), new InstallContext(), CancellationToken.None); var reportPath = Path.Combine(Path.GetTempPath(), ClaudeHelpLauncher.ReportFileName); try { Assert.True(result.Success); Assert.True(File.Exists(reportPath)); Assert.Contains(GitCheck.CheckId, await File.ReadAllTextAsync(reportPath)); Assert.Contains(reportPath, fakeLauncher.LastStartInfo!.Arguments); } finally { try { File.Delete(reportPath); } catch { } } } }