using ClaudeDo.Installer.Checks; using ClaudeDo.Installer.Core; namespace ClaudeDo.Installer.Tests; public sealed class EnvironmentCheckServiceTests { private sealed class FakeCheck : IEnvironmentCheck { private readonly Func>? _run; public FakeCheck(string id, CheckSeverity severity, Func>? run = null) { Id = id; Severity = severity; _run = run; } public string Id { get; } public CheckSeverity Severity { get; } public Task RunAsync(InstallContext ctx, CancellationToken ct) => _run is null ? Task.FromResult(CheckResult.Ok(Id, Severity, "title", "ok")) : _run(ctx, ct); } private sealed class ThrowingCheck : IEnvironmentCheck { public string Id => "throwing"; public CheckSeverity Severity => CheckSeverity.Error; public Task RunAsync(InstallContext ctx, CancellationToken ct) => throw new InvalidOperationException("boom"); } [Fact] public async Task RunAllAsync_PreservesInputOrder_RegardlessOfCompletionOrder() { var slow = new FakeCheck("slow", CheckSeverity.Warning, async (_, ct) => { await Task.Delay(50, ct); return CheckResult.Ok("slow", CheckSeverity.Warning, "title", "ok"); }); var fast = new FakeCheck("fast", CheckSeverity.Warning); var service = new EnvironmentCheckService(new IEnvironmentCheck[] { slow, fast }); var report = await service.RunAllAsync(new InstallContext(), CancellationToken.None); Assert.Equal(new[] { "slow", "fast" }, report.Results.Select(r => r.Id)); } [Fact] public async Task HasBlockingError_True_WhenErrorAndFailed() { var check = new FakeCheck("id", CheckSeverity.Error, (_, _) => Task.FromResult(CheckResult.Fail("id", CheckSeverity.Error, "title", "bad"))); var service = new EnvironmentCheckService(new IEnvironmentCheck[] { check }); var report = await service.RunAllAsync(new InstallContext(), CancellationToken.None); Assert.True(report.HasBlockingError); } [Fact] public async Task HasBlockingError_False_WhenWarningAndFailed() { var check = new FakeCheck("id", CheckSeverity.Warning, (_, _) => Task.FromResult(CheckResult.Fail("id", CheckSeverity.Warning, "title", "bad"))); var service = new EnvironmentCheckService(new IEnvironmentCheck[] { check }); var report = await service.RunAllAsync(new InstallContext(), CancellationToken.None); Assert.False(report.HasBlockingError); } [Fact] public async Task HasBlockingError_False_WhenErrorAndUnknown() { var check = new FakeCheck("id", CheckSeverity.Error, (_, _) => Task.FromResult(CheckResult.Unknown("id", CheckSeverity.Error, "title", "dunno"))); var service = new EnvironmentCheckService(new IEnvironmentCheck[] { check }); var report = await service.RunAllAsync(new InstallContext(), CancellationToken.None); Assert.False(report.HasBlockingError); } [Fact] public async Task RunAllAsync_ThrowingCheck_BecomesUnknown_OthersStillRun_ServiceDoesNotThrow() { var throwing = new ThrowingCheck(); var ok = new FakeCheck("ok", CheckSeverity.Warning); var service = new EnvironmentCheckService(new IEnvironmentCheck[] { throwing, ok }); var report = await service.RunAllAsync(new InstallContext(), CancellationToken.None); var throwingResult = report.Results[0]; Assert.Equal(CheckStatus.Unknown, throwingResult.Status); Assert.Contains("boom", throwingResult.Detail); var okResult = report.Results[1]; Assert.Equal(CheckStatus.Ok, okResult.Status); } [Fact] public async Task RunAllAsync_PassesCancellationToken_ToChecks() { CancellationToken? seen = null; var check = new FakeCheck("id", CheckSeverity.Warning, (_, ct) => { seen = ct; return Task.FromResult(CheckResult.Ok("id", CheckSeverity.Warning, "title", "ok")); }); var service = new EnvironmentCheckService(new IEnvironmentCheck[] { check }); using var cts = new CancellationTokenSource(); await service.RunAllAsync(new InstallContext(), cts.Token); Assert.Equal(cts.Token, seen); } }