Mutex was Local\ (per Windows session), so a second RDP/fast-user-switching session bypassed the single-instance guard. A Kestrel bind failure on top of that had no try/catch, so a second worker (or any process on 47821/47822) died as an unhandled crash with nothing in the log to explain why the UI never connects. Mutex is now Global\, bind failures and any last-resort unhandled exception log a Fatal line and exit(1) instead of crashing raw.
39 lines
1.1 KiB
C#
39 lines
1.1 KiB
C#
using ClaudeDo.Worker.Lifecycle;
|
|
using Xunit;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Lifecycle;
|
|
|
|
public sealed class WorkerStartupFailureTests
|
|
{
|
|
[Fact]
|
|
public void FormatBindFailureMessage_IncludesBothPorts_WhenExternalMcpEnabled()
|
|
{
|
|
var message = WorkerStartupFailure.FormatBindFailureMessage(
|
|
new IOException("Failed to bind to address http://127.0.0.1:47821: address already in use."),
|
|
signalRPort: 47821,
|
|
externalMcpPort: 47822);
|
|
|
|
Assert.Contains("47821", message);
|
|
Assert.Contains("47822", message);
|
|
Assert.Contains("address already in use", message);
|
|
}
|
|
|
|
[Fact]
|
|
public void FormatBindFailureMessage_OmitsExternalPort_WhenDisabled()
|
|
{
|
|
var message = WorkerStartupFailure.FormatBindFailureMessage(
|
|
new IOException("boom"),
|
|
signalRPort: 47821,
|
|
externalMcpPort: 0);
|
|
|
|
Assert.Contains("47821", message);
|
|
Assert.DoesNotContain("0", message);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExitCode_IsNonZero()
|
|
{
|
|
Assert.NotEqual(0, WorkerStartupFailure.ExitCode);
|
|
}
|
|
}
|