mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
9eb8f70c95
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>
125 lines
4.7 KiB
C#
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;
|
|
}
|
|
}
|