Files
agent-framework/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/InputWaiterTests.cs
T
Jacob Alber 9eb8f70c95 test: reshuffle .NET Workflow tests in preparation for Outputs overhaul
Phase 1 of the .NET Workflows outputs overhaul (see
working/implementation-plan.md). Pure moves/renames in
dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests; no production code
changes, no new test cases. The split keeps each orchestration mode in
its own source file so the upcoming tag-aware and orchestration-default
test additions land on clean diffs.

Renames:
* WorkflowBuilderSmokeTests.cs -> WorkflowBuilderTests.cs (with class
  rename to match). The scope is no longer "smoke"-only once subsequent
  phases add tag-aware builder tests.
* InputWaiterAndOutputFilterTests.cs -> InputWaiterTests.cs +
  OutputFilterTests.cs. The file already declared the two test classes
  separately; this split simply gives each its own file so the
  output-filter cases have a dedicated home for tag-aware additions.

Split of AgentWorkflowBuilderTests.cs:
* AgentWorkflowBuilderTests.cs is now the outer
  `public static partial class AgentWorkflowBuilderTests` holding the
  shared test helpers (DoubleEchoAgent + session + WithBarrier variant,
  WorkflowRunResult, RunWorkflow* methods) bumped from `private` to
  `internal` so the new top-level GroupChatWorkflowBuilderTests in the
  same assembly can reach them.
* AgentWorkflowBuilder.SequentialTests.cs (nested SequentialTests):
  BuildSequential_InvalidArguments_Throws,
  BuildSequential_AgentsRunInOrderAsync.
* AgentWorkflowBuilder.ConcurrentTests.cs (nested ConcurrentTests):
  BuildConcurrent_InvalidArguments_Throws,
  BuildConcurrent_AgentsRunInParallelAsync.

Sequential and Concurrent are kept as nested classes because they're
modes of the same `AgentWorkflowBuilder` static factory and do not
produce dedicated builder types.

New file:
* GroupChatWorkflowBuilderTests.cs (top-level): the existing
  BuildGroupChat_* and GroupChatManager_* cases moved out of the old
  AgentWorkflowBuilderTests file. They exercise the
  `GroupChatWorkflowBuilder` type (returned by
  `AgentWorkflowBuilder.CreateGroupChatBuilderWith`), so a dedicated
  top-level test class - matching the convention reserved by the plan
  for HandoffWorkflowBuilderTests / MagenticWorkflowBuilderTests - is
  the right home. Cross-class helper references qualify with
  `AgentWorkflowBuilderTests.DoubleEchoAgent` and
  `AgentWorkflowBuilderTests.RunWorkflowAsync`.

The outer partial class is `static` (and nested classes carry the
instance test methods) because the outer holds only static helpers;
this satisfies CA1052 without suppressions and is invisible to xUnit
discovery, which finds tests on the nested classes as
`AgentWorkflowBuilderTests.SequentialTests.*` etc.

Validation: `dotnet build` clean on both target frameworks; all 547
tests in Microsoft.Agents.AI.Workflows.UnitTests pass on net10.0.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 16:44:08 -04:00

125 lines
4.7 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Agents.AI.Workflows.Execution;
namespace Microsoft.Agents.AI.Workflows.UnitTests;
public sealed class InputWaiterTests : IDisposable
{
private readonly InputWaiter _waiter = new();
public void Dispose()
{
this._waiter.Dispose();
GC.SuppressFinalize(this);
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_CompletesAfterSignalAsync()
{
this._waiter.SignalInput();
// WaitForInputAsync should complete immediately since input was already signaled
Task waitTask = this._waiter.WaitForInputAsync(CancellationToken.None);
Task completed = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(1)));
completed.Should().BeSameAs(waitTask, "the wait task should complete before the timeout");
await waitTask;
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_BlocksUntilSignaledAsync()
{
// Use the no-timeout overload so that the wait can only be released by SignalInput.
// A finite timeout would make this test's logic racy: the component correctly
// honors the timeout, but if the test thread is starved of CPU time (CI load,
// GC pause) long enough for the timeout to fire, waitTask completes before
// SignalInput is called and the "should not complete before signaled" assertion
// flakes. Timeout behavior is covered separately below.
Task waitTask = this._waiter.WaitForInputAsync(CancellationToken.None);
Task completedBeforeSignal = await Task.WhenAny(waitTask, Task.Delay(100));
completedBeforeSignal.Should().NotBeSameAs(
waitTask,
"the waiter should not complete before input is signaled");
this._waiter.SignalInput();
Task completedAfterSignal = await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(1)));
completedAfterSignal.Should().BeSameAs(
waitTask,
"the wait task should complete after being signaled");
await waitTask;
}
[Fact]
public void InputWaiter_SignalInput_DoubleSignalDoesNotThrow()
{
// Binary semaphore behavior: double signal should be idempotent
FluentActions.Invoking(() =>
{
this._waiter.SignalInput();
this._waiter.SignalInput();
}).Should().NotThrow("double signaling should be handled gracefully");
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_RespectsCancellationAsync()
{
using CancellationTokenSource cts = new();
Task waitTask = this._waiter.WaitForInputAsync(cts.Token);
cts.Cancel();
Func<Task> act = () => waitTask;
await act.Should().ThrowAsync<OperationCanceledException>();
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_DoesNotCompleteWhenNotSignaledAsync()
{
using CancellationTokenSource cts = new();
Task waitTask = this._waiter.WaitForInputAsync(cts.Token);
Task completed = await Task.WhenAny(waitTask, Task.Delay(100));
completed.Should().NotBeSameAs(waitTask, "the wait task should not complete when input is not signaled");
// Cancel and observe the pending task to avoid an unobserved exception on Dispose
cts.Cancel();
try { await waitTask; }
catch (OperationCanceledException) { }
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_CanBeSignaledMultipleTimesSequentiallyAsync()
{
// First signal/wait cycle
this._waiter.SignalInput();
await this._waiter.WaitForInputAsync(TimeSpan.FromSeconds(1));
// Second signal/wait cycle
this._waiter.SignalInput();
await this._waiter.WaitForInputAsync(TimeSpan.FromSeconds(1));
}
[Fact]
public async Task InputWaiter_WaitForInputAsync_CompletesWhenTimeoutExpiresAsync()
{
// Verify that a finite timeout releases the block even without a signal.
// We only assert that it *does* complete (within a generous outer bound);
// we intentionally do not assert that it stays blocked until the timeout,
// because that would re-introduce the same wall-clock flakiness
// described in BlocksUntilSignaledAsync (see comment on that test).
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;
}
}