diff --git a/src/core/Akka.TestKit/TestKitBase.cs b/src/core/Akka.TestKit/TestKitBase.cs index 6c6f188cde9..28df99051a9 100644 --- a/src/core/Akka.TestKit/TestKitBase.cs +++ b/src/core/Akka.TestKit/TestKitBase.cs @@ -28,6 +28,10 @@ namespace Akka.TestKit /// public abstract partial class TestKitBase : IActorRefFactory { + // AsyncLocal for proper timeout propagation across async boundaries. + // This ensures WithinAsync timeout flows correctly to EventFilter and other async operations. + private readonly AsyncLocal _asyncLocalEnd = new(); + private class TestState { public TestState() @@ -445,9 +449,21 @@ public TimeSpan Remaining { get { + // Check AsyncLocal first (async context takes precedence) + var asyncEnd = _asyncLocalEnd.Value; + if (asyncEnd.HasValue) + { + if (asyncEnd < TimeSpan.Zero) + throw new InvalidOperationException($"End can not be negative, was: {asyncEnd}"); + + var asyncRemaining = asyncEnd.Value - Now; + return asyncRemaining < TimeSpan.Zero ? TimeSpan.Zero : asyncRemaining; + } + + // Fallback to instance field if(_testState.End is null) throw new InvalidOperationException(@"Remaining may not be called outside of ""within"""); - + if (_testState.End < TimeSpan.Zero) throw new InvalidOperationException($"End can not be negative, was: {_testState.End}"); @@ -466,6 +482,18 @@ public TimeSpan Remaining /// TBD protected TimeSpan RemainingOr(TimeSpan duration) { + // Check AsyncLocal first (async context takes precedence for proper timeout propagation) + var asyncEnd = _asyncLocalEnd.Value; + if (asyncEnd.HasValue) + { + if (asyncEnd < TimeSpan.Zero) + throw new InvalidOperationException($"End can not be negative, was: {asyncEnd}"); + + var asyncRemaining = asyncEnd.Value - Now; + return asyncRemaining < TimeSpan.Zero ? TimeSpan.Zero : asyncRemaining; + } + + // Fallback to instance field for backward compatibility with sync code paths if (!_testState.End.HasValue) return duration; if (_testState.End < TimeSpan.Zero) throw new InvalidOperationException($"End can not be negative, was: {_testState.End}"); diff --git a/src/core/Akka.TestKit/TestKitBase_Within.cs b/src/core/Akka.TestKit/TestKitBase_Within.cs index 3d78d480c17..b08b2b6f50b 100644 --- a/src/core/Akka.TestKit/TestKitBase_Within.cs +++ b/src/core/Akka.TestKit/TestKitBase_Within.cs @@ -293,7 +293,10 @@ public async Task WithinAsync( var maxDiff = max.Min(rem); var prevEnd = _testState.End; + var prevAsyncEnd = _asyncLocalEnd.Value; // Save previous AsyncLocal value for nesting support + _testState.End = start + maxDiff; + _asyncLocalEnd.Value = start + maxDiff; // Set AsyncLocal for proper async propagation T ret = default; using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) @@ -320,6 +323,7 @@ public async Task WithinAsync( // Make sure we stop the delay task cts.Cancel(); _testState.End = prevEnd; + _asyncLocalEnd.Value = prevAsyncEnd; // Restore previous AsyncLocal value } }