using System.Net; using System.Net.Sockets; using ClaudeDo.Installer.Checks; using ClaudeDo.Installer.Checks.Interfaces; using ClaudeDo.Installer.Core; namespace ClaudeDo.Installer.Tests.Checks; public sealed class PortCheckTests { private sealed class FakeOwnerResolver : IPortOwnerResolver { private readonly string? _owner; public FakeOwnerResolver(string? owner) => _owner = owner; public Task FindOwningProcessNameAsync(int port, CancellationToken ct) => Task.FromResult(_owner); } private static int GetFreePort() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); return port; } [Fact] public async Task Both_ports_free_reports_ok() { var ctx = new InstallContext { SignalRPort = GetFreePort(), ExternalMcpPort = GetFreePort() }; var check = new PortCheck(new FakeOwnerResolver(null)); var result = await check.RunAsync(ctx, CancellationToken.None); Assert.Equal(CheckStatus.Ok, result.Status); } [Fact] public async Task Port_in_use_by_other_process_reports_failed() { var port = GetFreePort(); using var occupying = new TcpListener(IPAddress.Loopback, port); occupying.Start(); var ctx = new InstallContext { SignalRPort = port, ExternalMcpPort = GetFreePort() }; var check = new PortCheck(new FakeOwnerResolver("SomeOtherApp")); var result = await check.RunAsync(ctx, CancellationToken.None); Assert.Equal(CheckStatus.Failed, result.Status); Assert.Equal(CheckSeverity.Warning, result.Severity); Assert.Contains("SomeOtherApp", result.Message); } [Fact] public async Task Port_in_use_by_own_worker_reports_ok() { var port = GetFreePort(); using var occupying = new TcpListener(IPAddress.Loopback, port); occupying.Start(); var ctx = new InstallContext { SignalRPort = port, ExternalMcpPort = GetFreePort() }; var check = new PortCheck(new FakeOwnerResolver("ClaudeDo.Worker")); var result = await check.RunAsync(ctx, CancellationToken.None); Assert.Equal(CheckStatus.Ok, result.Status); Assert.Contains("ClaudeDo Worker", result.Message); } }