feat(installer): add Git, GitIdentity, Port, and WriteAccess preflight checks
Implements IEnvironmentCheck for the four checks derivable without a Claude CLI probe: - GitCheck (Error) - resolves git via ExecutableResolver (handles .cmd shims), parses `git --version`. - GitIdentityCheck (Warning) - user.name/user.email presence; Unknown (not Failed) if git itself is missing, so it doesn't duplicate GitCheck's failure. - PortCheck (Warning) - loopback bind probe for SignalRPort/ExternalMcpPort; resolves the owning process via a new NetstatPortOwnerResolver and treats a port held by the running ClaudeDo.Worker (update/repair case) as Ok. Both ports are configurable, hence a warning. - WriteAccessCheck (Error) - create+delete a probe file in InstallDirectory and ~/.todo-app (walking up to the first existing parent), not an ACL read (ACLs lie on virtualized paths). Process calls go through a new IProcessRunner wrapping the existing static ProcessRunner, so checks are fakeable in tests instead of spawning real processes. DotnetRuntimeCheck was intentionally not added: per docs/explore-notes/installer-preflight.md, App/Worker publish self-contained (no preinstalled runtime needed), and the Installer's own .NET 8 Desktop Runtime requirement is self-proving - a framework-dependent apphost can't reach managed code at all if that runtime is missing, so a check running from inside the process can never observe a failure. Brings in two prerequisite commits this task builds on that hadn't reached this branch yet: the IEnvironmentCheck/EnvironmentCheckService scaffolding and the installer-preflight.md research note.
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 GitIdentityCheckTests : IDisposable
|
||||
{
|
||||
private readonly string _dir;
|
||||
|
||||
public GitIdentityCheckTests()
|
||||
{
|
||||
_dir = Path.Combine(Path.GetTempPath(), $"cdgitid_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
private void MakeGitExe() => File.WriteAllText(Path.Combine(_dir, "git.exe"), "");
|
||||
|
||||
[Fact]
|
||||
public async Task Both_set_reports_ok()
|
||||
{
|
||||
MakeGitExe();
|
||||
var runner = new FakeProcessRunner((0, "Mika Kuns\n"), (0, "mika@example.com\n"));
|
||||
var check = new GitIdentityCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Only_name_set_reports_failed_naming_email()
|
||||
{
|
||||
MakeGitExe();
|
||||
var runner = new FakeProcessRunner((0, "Mika Kuns\n"), (1, ""));
|
||||
var check = new GitIdentityCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Contains("user.email", result.Message);
|
||||
Assert.DoesNotContain("user.name", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Only_email_set_reports_failed_naming_name()
|
||||
{
|
||||
MakeGitExe();
|
||||
var runner = new FakeProcessRunner((1, ""), (0, "mika@example.com\n"));
|
||||
var check = new GitIdentityCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Contains("user.name", result.Message);
|
||||
Assert.DoesNotContain("user.email", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Neither_set_reports_failed_naming_both()
|
||||
{
|
||||
MakeGitExe();
|
||||
var runner = new FakeProcessRunner((1, ""), (1, ""));
|
||||
var check = new GitIdentityCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Contains("user.name", result.Message);
|
||||
Assert.Contains("user.email", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Git_missing_reports_unknown_not_failed()
|
||||
{
|
||||
var runner = new FakeProcessRunner();
|
||||
var check = new GitIdentityCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Unknown, result.Status);
|
||||
Assert.Empty(runner.Calls);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user