Fix flaky GalaxyHierarchyRefreshServiceTests timing race

ExecuteAsync_WhenFirstRefreshThrowsNonCancellationException_DoesNotFault
BackgroundService cancelled the service immediately after StartAsync, so
under parallel load the first RefreshAsync could be skipped (RefreshCallCount
0) and `await executeTask` rethrew TaskCanceledException before the IsFaulted
assertion. The test now waits for a TaskCompletionSource signal that the
first refresh was attempted before cancelling, and uses Task.WhenAny so a
Canceled ExecuteTask does not rethrow. Confirmed stable across full-suite
runs (408/408).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-18 23:14:47 -04:00
parent 24de7e21d9
commit bb5603b7ec
@@ -22,13 +22,23 @@ public sealed class GalaxyHierarchyRefreshServiceTests
using CancellationTokenSource cts = new();
await service.StartAsync(cts.Token);
// Wait until the first RefreshAsync has actually been attempted (and
// thrown) before cancelling, so cancellation cannot race ahead of the
// first-load path under test — this is what made the test flaky under
// parallel load.
await cache.FirstRefreshAttempted.WaitAsync(TimeSpan.FromSeconds(10));
await cts.CancelAsync();
// The background loop must have stopped cleanly: ExecuteTask completes
// (RanToCompletion or Canceled) rather than faulting on the first refresh.
// The background loop must have stopped cleanly: ExecuteTask reaches a
// terminal state that is not Faulted (RanToCompletion or Canceled)
// rather than faulting on the first refresh. WhenAny is used so a
// Canceled task does not rethrow before the IsFaulted assertion.
Task? executeTask = service.ExecuteTask;
Assert.NotNull(executeTask);
await executeTask;
Task completed = await Task.WhenAny(executeTask, Task.Delay(TimeSpan.FromSeconds(10)));
Assert.Same(executeTask, completed);
Assert.False(executeTask.IsFaulted);
Assert.Equal(1, cache.RefreshCallCount);
@@ -49,13 +59,20 @@ public sealed class GalaxyHierarchyRefreshServiceTests
private sealed class ThrowingCache(Exception toThrow) : IGalaxyHierarchyCache
{
private readonly TaskCompletionSource firstRefreshAttempted =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public int RefreshCallCount { get; private set; }
/// <summary>Completes once <see cref="RefreshAsync"/> has been invoked at least once.</summary>
public Task FirstRefreshAttempted => firstRefreshAttempted.Task;
public GalaxyHierarchyCacheEntry Current => GalaxyHierarchyCacheEntry.Empty;
public Task RefreshAsync(CancellationToken cancellationToken)
{
RefreshCallCount++;
firstRefreshAttempted.TrySetResult();
throw toThrow;
}