Summary
Microsoft.Agents.AI.Workflows.UnitTests.InputWaiterTests.InputWaiter_WaitForInputAsync_CompletesWhenTimeoutExpiresAsync fails intermittently in the merge_group, blocking the queue for PRs that do not touch the workflows code at all. The most recent occurrence blocked PR #7337, which only adds a sample.
Failure signature
failed Microsoft.Agents.AI.Workflows.UnitTests.InputWaiterTests.InputWaiter_WaitForInputAsync_CompletesWhenTimeoutExpiresAsync (5s 103ms)
Expected object to refer to System.Threading.Tasks.Task`1[System.Threading.Tasks.VoidTaskResult] {Status=WaitingForActivation}
because the wait task should complete once the timeout expires,
but found System.Threading.Tasks.Task+DelayPromise {Status=RanToCompletion}.
at FluentAssertions.Primitives.ReferenceTypeAssertions`2.BeSameAs(TSubject expected, String because, Object[] becauseArgs)
Affected runs
A scan of the last 18 failed dotnet-build-and-test runs found 7 occurrences of this exact failure, roughly 39% of the failed runs in that sample.
Every occurrence is on the net472 / windows-latest leg of dotnet-test, and every occurrence is a merge_group run. No pull_request occurrences were found in the sample.
Suspected cause
The test races two timers against each other and then asserts which one won by object identity:
https://github.com/microsoft/agent-framework/blob/main/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InputWaiterTests.cs#L110-L123
Task waitTask = this._waiter.WaitForInputAsync(TimeSpan.FromMilliseconds(300));
Task completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(5)));
completed.Should().BeSameAs(waitTask, "the wait task should complete once the timeout expires");
await waitTask;
WaitForInputAsync(TimeSpan) delegates straight to SemaphoreSlim.WaitAsync(timeout, cancellationToken), so the 300 ms completion is scheduled through the timer queue and then resumed on the thread pool. The assertion only holds if that 300 ms continuation is observed before the 5 s guard fires, which gives the test a fixed wall clock budget of 4.7 s of tolerable scheduling delay.
The net472 integration leg is the slowest and most loaded job in the matrix, so thread pool starvation and GC pauses there can push the 300 ms continuation past the 5 s guard. When that happens Task.Delay wins the WhenAny and BeSameAs fails. This is a test harness timing problem rather than a defect in InputWaiter itself: the semaphore does honor the timeout, the test just cannot guarantee it observes it in time.
Mitigation
A follow up PR quarantines this test with [Fact(Skip = "...")] and links this issue, matching what was done for #5845.
Next steps
- Remove the wall clock race instead of widening the guard. The assertion does not need to compare task identity at all: awaiting
waitTask directly is enough to prove the timeout releases the wait, and a hang would be caught by the test runner timeout rather than by a hand rolled Task.Delay guard.
- If an explicit bound is still wanted, note that
Task.WaitAsync(TimeSpan) is not available on net472, so any replacement has to work on .NET Framework as well.
- Re-enable the test once the assertion no longer depends on scheduling latency.
[".NET"]
Summary
Microsoft.Agents.AI.Workflows.UnitTests.InputWaiterTests.InputWaiter_WaitForInputAsync_CompletesWhenTimeoutExpiresAsyncfails intermittently in themerge_group, blocking the queue for PRs that do not touch the workflows code at all. The most recent occurrence blocked PR #7337, which only adds a sample.Failure signature
Affected runs
A scan of the last 18 failed
dotnet-build-and-testruns found 7 occurrences of this exact failure, roughly 39% of the failed runs in that sample.Every occurrence is on the
net472/windows-latestleg ofdotnet-test, and every occurrence is amerge_grouprun. Nopull_requestoccurrences were found in the sample.Suspected cause
The test races two timers against each other and then asserts which one won by object identity:
https://github.com/microsoft/agent-framework/blob/main/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InputWaiterTests.cs#L110-L123
WaitForInputAsync(TimeSpan)delegates straight toSemaphoreSlim.WaitAsync(timeout, cancellationToken), so the 300 ms completion is scheduled through the timer queue and then resumed on the thread pool. The assertion only holds if that 300 ms continuation is observed before the 5 s guard fires, which gives the test a fixed wall clock budget of 4.7 s of tolerable scheduling delay.The
net472integration leg is the slowest and most loaded job in the matrix, so thread pool starvation and GC pauses there can push the 300 ms continuation past the 5 s guard. When that happensTask.Delaywins theWhenAnyandBeSameAsfails. This is a test harness timing problem rather than a defect inInputWaiteritself: the semaphore does honor the timeout, the test just cannot guarantee it observes it in time.Mitigation
A follow up PR quarantines this test with
[Fact(Skip = "...")]and links this issue, matching what was done for #5845.Next steps
- Remove the wall clock race instead of widening the guard. The assertion does not need to compare task identity at all: awaiting
- If an explicit bound is still wanted, note that
- Re-enable the test once the assertion no longer depends on scheduling latency.
[".NET"]waitTaskdirectly is enough to prove the timeout releases the wait, and a hang would be caught by the test runner timeout rather than by a hand rolledTask.Delayguard.Task.WaitAsync(TimeSpan)is not available onnet472, so any replacement has to work on .NET Framework as well.