mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
fb97e93a01
* Foundry.Hosting.UnitTests: extract project from Foundry.UnitTests Move all Hosting/* tests, three toolbox TestData JSONs, and the FakeAuthenticationTokenProvider/HttpHandlerAssert/TestDataUtil helpers (trimmed to toolbox getters) into a new Microsoft.Agents.AI.Foundry.Hosting.UnitTests project. Add it to the slnx and grant the new assembly InternalsVisibleTo from Microsoft.Agents.AI.Foundry and Microsoft.Agents.AI.Foundry.Hosting. * Foundry.Hosting.UnitTests: align namespaces to assembly name Rename namespaces from Microsoft.Agents.AI.Foundry.UnitTests(.Hosting) to Microsoft.Agents.AI.Foundry.Hosting.UnitTests across all moved tests, the duplicated helpers, and the trimmed TestDataUtil. Also fixes the prior namespace inconsistency in FoundryToolboxTests. * Foundry.Hosting.UnitTests: split WorkflowIntegrationTests by SUT Replace the WorkflowIntegrationTests file (an IT-named file inside a UT project) with two SUT-focused files plus a shared test-doubles file: - AgentFrameworkResponseHandlerWorkflowTests.cs - the 5 handler-driven tests that exercise AgentFrameworkResponseHandler with a real workflow agent. - OutputConverterWorkflowTests.cs - the 5 OutputConverter tests driven by hand-crafted update sequences mirroring real workflow patterns. - WorkflowTestAgents.cs - StreamingTextAgent and ThrowingStreamingAgent extracted as internal types used by both files. * Foundry.UnitTests: trim Hosting-related conditionals and dead testdata Now that Hosting tests live in their own project: - drop the Compile Remove guard for the Hosting subfolder, - drop the .NETCoreApp-only PackageReferences (Azure.AI.AgentServer.Responses, Microsoft.AspNetCore.TestHost, OpenTelemetry, OpenTelemetry.Exporter.InMemory), - drop the conditional ProjectReference to Microsoft.Agents.AI.Foundry.Hosting, - delete the three Toolbox JSON files and the matching Toolbox getters in TestDataUtil. * Foundry.Hosting.UnitTests: drop redundant 'using Microsoft.Agents.AI.Foundry.Hosting' The new project namespace is Microsoft.Agents.AI.Foundry.Hosting.UnitTests, which already brings the parent Microsoft.Agents.AI.Foundry.Hosting namespace into scope. The explicit using statement is therefore redundant (IDE0005). Caught by 'dotnet format --verify-no-changes' running on Linux against the .NET 10 SDK. * Foundry.Hosting: drop InternalsVisibleTo to Foundry.UnitTests The non-hosting Foundry.UnitTests project no longer holds any Hosting tests after the split, so it doesn't need access to internal types in Microsoft.Agents.AI.Foundry.Hosting. Only Microsoft.Agents.AI.Foundry.Hosting.UnitTests needs it. * Foundry.Hosting: rename DelegatingResponsesClient to UserAgentResponsesClient Address westey-m's review feedback on PR #5453: `Delegating*` is conventionally reserved for inheritable base classes (mirroring `DelegatingHandler`) where consumers override one or two members. This polyfill is sealed and only injects the User-Agent supplement, so the new name reflects its actual purpose. Renamed via `git mv` to preserve history: * `src/Microsoft.Agents.AI.Foundry.Hosting/DelegatingResponsesClient.cs` to `UserAgentResponsesClient.cs` * `tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/DelegatingResponsesClientTests.cs` to `UserAgentResponsesClientTests.cs` Class, constructor, and all references updated across: * `src/.../UserAgentResponsesClient.cs` (class + constructor + internal log message) * `src/.../ServiceCollectionExtensions.cs` (cref + type check + instantiation) * `src/.../HostedAgentUserAgentPolicy.cs` (cref) * `tests/Foundry.UnitTests/RequestOptionsExtensionsTests.cs` (comment) * `tests/Foundry.Hosting.UnitTests/UserAgentResponsesClientTests.cs` (class + cref + instantiations)
214 lines
11 KiB
C#
214 lines
11 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Azure.AI.AgentServer.Responses;
|
|
using Azure.AI.AgentServer.Responses.Models;
|
|
using Microsoft.Agents.AI.Workflows;
|
|
using Microsoft.Extensions.AI;
|
|
using Moq;
|
|
using MeaiTextContent = Microsoft.Extensions.AI.TextContent;
|
|
|
|
namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="OutputConverter"/> driven directly by hand-crafted update
|
|
/// sequences that mirror the patterns produced by real workflow executions
|
|
/// (sequential, group chat, code executor, sub-workflow, mixed content types).
|
|
/// </summary>
|
|
public class OutputConverterWorkflowTests
|
|
{
|
|
[Fact]
|
|
public async Task SequentialWorkflowPattern_ProducesCorrectEventsAsync()
|
|
{
|
|
// Simulate what WorkflowSession produces for a 2-agent sequential workflow
|
|
var (stream, _) = CreateTestStream();
|
|
var updates = new[]
|
|
{
|
|
// Superstep 1: Agent 1
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_1", "start") },
|
|
new AgentResponseUpdate { MessageId = "msg_a1", Contents = [new MeaiTextContent("Agent 1 output")] },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_1", null) },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) },
|
|
// Superstep 2: Agent 2
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("agent_2", "start") },
|
|
new AgentResponseUpdate { MessageId = "msg_a2", Contents = [new MeaiTextContent("Agent 2 output")] },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("agent_2", null) },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) },
|
|
};
|
|
|
|
var events = new List<ResponseStreamEvent>();
|
|
await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream))
|
|
{
|
|
events.Add(evt);
|
|
}
|
|
|
|
// 4 workflow action items + 2 text messages = 6 output items
|
|
Assert.Equal(6, events.OfType<ResponseOutputItemAddedEvent>().Count());
|
|
Assert.Equal(2, events.OfType<ResponseTextDeltaEvent>().Count());
|
|
Assert.IsType<ResponseCompletedEvent>(events[^1]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GroupChatPattern_ProducesCorrectEventsAsync()
|
|
{
|
|
// Simulate round-robin group chat: agent1 → agent2 → agent1 → terminate
|
|
var (stream, _) = CreateTestStream();
|
|
var updates = new[]
|
|
{
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("chat_agent_1", "turn") },
|
|
new AgentResponseUpdate { MessageId = "msg_gc_1", Contents = [new MeaiTextContent("Agent 1 turn 1")] },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("chat_agent_1", null) },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("chat_agent_2", "turn") },
|
|
new AgentResponseUpdate { MessageId = "msg_gc_2", Contents = [new MeaiTextContent("Agent 2 turn 1")] },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("chat_agent_2", null) },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(3) },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("chat_agent_1", "turn") },
|
|
new AgentResponseUpdate { MessageId = "msg_gc_3", Contents = [new MeaiTextContent("Agent 1 turn 2")] },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("chat_agent_1", null) },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(3) },
|
|
};
|
|
|
|
var events = new List<ResponseStreamEvent>();
|
|
await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream))
|
|
{
|
|
events.Add(evt);
|
|
}
|
|
|
|
// 6 workflow actions + 3 text messages = 9 output items
|
|
Assert.Equal(9, events.OfType<ResponseOutputItemAddedEvent>().Count());
|
|
Assert.Equal(3, events.OfType<ResponseTextDeltaEvent>().Count());
|
|
Assert.IsType<ResponseCompletedEvent>(events[^1]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CodeExecutorPattern_ProducesCorrectEventsAsync()
|
|
{
|
|
// Simulate a code-based FunctionExecutor: invoked → completed, no text content
|
|
// (code executors don't produce AgentResponseUpdateEvent, just executor lifecycle)
|
|
var (stream, _) = CreateTestStream();
|
|
var updates = new[]
|
|
{
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("uppercase_fn", "hello") },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("uppercase_fn", "HELLO") },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) },
|
|
// Second executor uses the output
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(2) },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("format_agent", "start") },
|
|
new AgentResponseUpdate { MessageId = "msg_fmt", Contents = [new MeaiTextContent("Formatted: HELLO")] },
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("format_agent", null) },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(2) },
|
|
};
|
|
|
|
var events = new List<ResponseStreamEvent>();
|
|
await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream))
|
|
{
|
|
events.Add(evt);
|
|
}
|
|
|
|
// 4 workflow actions + 1 text message = 5 output items
|
|
Assert.Equal(5, events.OfType<ResponseOutputItemAddedEvent>().Count());
|
|
Assert.Single(events.OfType<ResponseTextDeltaEvent>());
|
|
Assert.IsType<ResponseCompletedEvent>(events[^1]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubworkflowPattern_ProducesCorrectEventsAsync()
|
|
{
|
|
// Simulate a parent workflow that invokes a sub-workflow executor
|
|
var (stream, _) = CreateTestStream();
|
|
var updates = new[]
|
|
{
|
|
new AgentResponseUpdate { RawRepresentation = new WorkflowStartedEvent("parent") },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepStartedEvent(1) },
|
|
// Sub-workflow executor invoked
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("sub_workflow_host", "start") },
|
|
// Inner agent within sub-workflow produces text (unwrapped by WorkflowSession)
|
|
new AgentResponseUpdate { MessageId = "msg_sub_1", Contents = [new MeaiTextContent("Sub-workflow agent output")] },
|
|
// Sub-workflow executor completed
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("sub_workflow_host", null) },
|
|
new AgentResponseUpdate { RawRepresentation = new SuperStepCompletedEvent(1) },
|
|
};
|
|
|
|
var events = new List<ResponseStreamEvent>();
|
|
await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream))
|
|
{
|
|
events.Add(evt);
|
|
}
|
|
|
|
// 2 workflow actions + 1 text message = 3 output items
|
|
Assert.Equal(3, events.OfType<ResponseOutputItemAddedEvent>().Count());
|
|
Assert.Single(events.OfType<ResponseTextDeltaEvent>());
|
|
Assert.IsType<ResponseCompletedEvent>(events[^1]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WorkflowWithMultipleContentTypes_HandlesAllCorrectlyAsync()
|
|
{
|
|
// Simulate a workflow producing reasoning, text, function calls, and usage
|
|
var (stream, _) = CreateTestStream();
|
|
var updates = new[]
|
|
{
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("planner", "start") },
|
|
// Reasoning
|
|
new AgentResponseUpdate { Contents = [new TextReasoningContent("Let me think about this...")] },
|
|
// Function call (tool use)
|
|
new AgentResponseUpdate
|
|
{
|
|
Contents = [new FunctionCallContent("call_search", "web_search",
|
|
new Dictionary<string, object?> { ["query"] = "latest news" })]
|
|
},
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("planner", null) },
|
|
// Next executor uses tool result
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorInvokedEvent("writer", "start") },
|
|
new AgentResponseUpdate { MessageId = "msg_w1", Contents = [new MeaiTextContent("Based on my research, ")] },
|
|
new AgentResponseUpdate { MessageId = "msg_w1", Contents = [new MeaiTextContent("here are the findings.")] },
|
|
new AgentResponseUpdate
|
|
{
|
|
Contents = [new UsageContent(new UsageDetails { InputTokenCount = 500, OutputTokenCount = 200, TotalTokenCount = 700 })]
|
|
},
|
|
new AgentResponseUpdate { RawRepresentation = new ExecutorCompletedEvent("writer", null) },
|
|
};
|
|
|
|
var events = new List<ResponseStreamEvent>();
|
|
await foreach (var evt in OutputConverter.ConvertUpdatesToEventsAsync(ToAsync(updates), stream))
|
|
{
|
|
events.Add(evt);
|
|
}
|
|
|
|
// Workflow actions: 4 (2 invoked + 2 completed)
|
|
// Content: 1 reasoning + 1 function call + 1 text message = 3
|
|
// Total: 7 output items
|
|
Assert.Equal(7, events.OfType<ResponseOutputItemAddedEvent>().Count());
|
|
Assert.Contains(events, e => e is ResponseFunctionCallArgumentsDoneEvent);
|
|
Assert.Equal(2, events.OfType<ResponseTextDeltaEvent>().Count());
|
|
Assert.IsType<ResponseCompletedEvent>(events[^1]);
|
|
}
|
|
|
|
private static (ResponseEventStream stream, Mock<ResponseContext> mockContext) CreateTestStream()
|
|
{
|
|
var mockContext = new Mock<ResponseContext>("resp_" + new string('0', 46)) { CallBase = true };
|
|
var request = new CreateResponse { Model = "test-model" };
|
|
var stream = new ResponseEventStream(mockContext.Object, request);
|
|
return (stream, mockContext);
|
|
}
|
|
|
|
private static async IAsyncEnumerable<T> ToAsync<T>(IEnumerable<T> source)
|
|
{
|
|
foreach (var item in source)
|
|
{
|
|
yield return item;
|
|
}
|
|
|
|
await Task.CompletedTask;
|
|
}
|
|
}
|