test(flaky): decouple fake-worker CreateAsync anti-hang net from the semantic timeout
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m11s
ci / java (push) Successful in 2m13s
ci / portable (push) Successful in 7m26s

The nightly (run #47) portable job failed CreateAsync_WhenFakeWorkerNeverSendsReady
_TimesOutAndKillsWorker: it asserts the factory's TimeoutException carries 'did not
complete startup' (thrown at the session's 1s startup timeout), but the test's
.WaitAsync(TestTimeout=5s) anti-hang net tripped first under CI load and surfaced
.NET's generic 'The operation has timed out.' — same commit passed on the push run
(#46), so it's a load-sensitive timeout race, not a regression. Raising runner
capacity to 4 makes concurrent-load flakes like this more likely.

Fix: give the two CreateAsync failure/timeout tests a dedicated 30s HangGuardTimeout,
far above the 1s semantic timeout under test, so the factory's own exception is
always the one observed. The net still catches a genuine hang.

Note: this class cannot run on macOS (named pipes -> Unix domain sockets fail on
the pipe path); verification is via CI Linux.
This commit is contained in:
Joseph Doherty
2026-07-14 05:35:05 -04:00
parent 4b8ba7a5bd
commit 5f4ecabc8a
@@ -15,6 +15,13 @@ public sealed class SessionWorkerClientFactoryFakeWorkerTests : IAsyncDisposable
{
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5);
// Anti-hang guard for the CreateAsync failure/timeout tests. It only exists to stop a genuine
// hang from wedging the run, so it must be far larger than any in-test semantic timeout (the
// factory startup timeout being exercised) — otherwise, under CI load, this WaitAsync net can
// trip before the factory's own timeout propagates and the test observes .NET's generic
// "The operation has timed out." instead of the asserted "did not complete startup".
private static readonly TimeSpan HangGuardTimeout = TimeSpan.FromSeconds(30);
private readonly List<IWorkerTaskLauncher> _launchers = [];
/// <summary>
@@ -79,7 +86,7 @@ public sealed class SessionWorkerClientFactoryFakeWorkerTests : IAsyncDisposable
GatewaySession session = CreateSession();
WorkerClientException exception = await Assert.ThrowsAsync<WorkerClientException>(
async () => await factory.CreateAsync(session, CancellationToken.None).WaitAsync(TestTimeout));
async () => await factory.CreateAsync(session, CancellationToken.None).WaitAsync(HangGuardTimeout));
Assert.Equal(WorkerClientErrorCode.ProtocolViolation, exception.ErrorCode);
Assert.True(launcher.Process.IsDisposed);
@@ -100,7 +107,7 @@ public sealed class SessionWorkerClientFactoryFakeWorkerTests : IAsyncDisposable
GatewaySession session = CreateSession(startupTimeout: TimeSpan.FromSeconds(1));
TimeoutException exception = await Assert.ThrowsAsync<TimeoutException>(
async () => await factory.CreateAsync(session, CancellationToken.None).WaitAsync(TestTimeout));
async () => await factory.CreateAsync(session, CancellationToken.None).WaitAsync(HangGuardTimeout));
Assert.Contains("did not complete startup", exception.Message);
Assert.Equal(1, launcher.Process.KillCount);