diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 2bfb78486c..05979b090f 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -592,6 +592,7 @@ + diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs index 8d5d330471..c4130599a9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs @@ -19,7 +19,7 @@ namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// /// This policy is added at request time (per-call ) -/// by when invoking the wrapped +/// by when invoking the wrapped /// . It is only registered when an agent is /// resolved by the Foundry hosting layer. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj index af02c44aad..209edd9a82 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj @@ -44,7 +44,7 @@ - + diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs index 49eb745b6b..a84bbd3a6b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs @@ -210,7 +210,7 @@ internal static AIAgent ApplyOpenTelemetry(AIAgent agent) /// /// Attempts to wrap the agent's underlying - /// with a so every outgoing Responses-API request + /// with a so every outgoing Responses-API request /// carries the hosted-agent User-Agent segment. /// /// @@ -219,7 +219,7 @@ internal static AIAgent ApplyOpenTelemetry(AIAgent agent) /// /// exposes no ; /// the chat client is not backed by MEAI's internal OpenAIResponsesChatClient (e.g., a non-OpenAI provider or a custom impl); - /// the inner is already a . + /// the inner is already a . /// /// /// @@ -261,12 +261,12 @@ internal static AIAgent TryApplyUserAgent(AIAgent agent) } var current = field.GetValue(meaiInstance) as ResponsesClient; - if (current is null or DelegatingResponsesClient) + if (current is null or UserAgentResponsesClient) { return agent; } - field.SetValue(meaiInstance, new DelegatingResponsesClient(current)); + field.SetValue(meaiInstance, new UserAgentResponsesClient(current)); return agent; } diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/DelegatingResponsesClient.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/UserAgentResponsesClient.cs similarity index 94% rename from dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/DelegatingResponsesClient.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/UserAgentResponsesClient.cs index ebf878b49a..aaddfd89da 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/DelegatingResponsesClient.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/UserAgentResponsesClient.cs @@ -33,11 +33,11 @@ namespace Microsoft.Agents.AI.Foundry.Hosting; /// never expected to run; the throwing transport surfaces any unexpected escape route loudly. /// /// -internal sealed class DelegatingResponsesClient : ResponsesClient +internal sealed class UserAgentResponsesClient : ResponsesClient { private readonly ResponsesClient _inner; - public DelegatingResponsesClient(ResponsesClient inner) + public UserAgentResponsesClient(ResponsesClient inner) : base(BuildDummyPipeline(), new OpenAIClientOptions { Endpoint = inner?.Endpoint }) { this._inner = inner ?? throw new ArgumentNullException(nameof(inner)); @@ -104,7 +104,7 @@ private static ClientPipeline BuildDummyPipeline() private sealed class ThrowingTransport : PipelineTransport { private const string Message = - "DelegatingResponsesClient transport invoked bypassed the override-and-delegate design. This exception should be unreachable and should never be thrown following the correct usage of DelegatingResponsesClient."; + "UserAgentResponsesClient transport invoked bypassed the override-and-delegate design. This exception should be unreachable and should never be thrown following the correct usage of UserAgentResponsesClient."; protected override PipelineMessage CreateMessageCore() => throw new InvalidOperationException(Message); protected override void ProcessCore(PipelineMessage message) => throw new InvalidOperationException(Message); diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj b/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj index cd4200df75..71e7df373f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/Microsoft.Agents.AI.Foundry.csproj @@ -53,6 +53,7 @@ + diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTelemetryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTelemetryTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTelemetryTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTelemetryTests.cs index 48daf490ee..dcb4e7e212 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTelemetryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTelemetryTests.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; -using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; @@ -19,7 +18,7 @@ using OpenTelemetry.Trace; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; /// /// Tests that verify OTel spans are actually emitted and captured through the diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs index d7fc8fc884..eaf396eccc 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; -using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -17,7 +16,7 @@ using Moq; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; public class AgentFrameworkResponseHandlerTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerWorkflowTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerWorkflowTests.cs new file mode 100644 index 0000000000..5954ddafc5 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerWorkflowTests.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.AgentServer.Responses; +using Azure.AI.AgentServer.Responses.Models; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; + +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; + +/// +/// Unit tests for that verify behavior +/// when the registered agent is a workflow-backed . These exercise +/// real workflow builders and the in-process execution environment to drive the handler +/// through realistic streaming event patterns. +/// +public class AgentFrameworkResponseHandlerWorkflowTests +{ + [Fact] + public async Task SequentialWorkflow_SingleAgent_ProducesTextOutputAsync() + { + // Arrange: single-agent sequential workflow + var echoAgent = new StreamingTextAgent("echo", "Hello from the workflow!"); + var workflow = AgentWorkflowBuilder.BuildSequential("test-sequential", echoAgent); + var workflowAgent = workflow.AsAIAgent( + id: "workflow-agent", + name: "Test Workflow", + executionEnvironment: InProcessExecution.OffThread, + includeExceptionDetails: true); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Hello"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: should have lifecycle events + at least one text output + terminal + Assert.IsType(events[0]); + Assert.IsType(events[1]); + Assert.True(events.Count >= 4, $"Expected at least 4 events, got {events.Count}"); + + var lastEvent = events[^1]; + Assert.True( + lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, + $"Expected terminal event, got {lastEvent.GetType().Name}"); + } + + [Fact] + public async Task SequentialWorkflow_TwoAgents_ProducesOutputFromBothAsync() + { + // Arrange: two agents in sequence + var agent1 = new StreamingTextAgent("agent1", "First agent says hello"); + var agent2 = new StreamingTextAgent("agent2", "Second agent says goodbye"); + var workflow = AgentWorkflowBuilder.BuildSequential("test-sequential-2", agent1, agent2); + var workflowAgent = workflow.AsAIAgent( + id: "seq-workflow", + name: "Sequential Workflow", + executionEnvironment: InProcessExecution.OffThread, + includeExceptionDetails: true); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Process this"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: should have workflow action events for executor lifecycle + var lastEvent = events[^1]; + Assert.True( + lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, + $"Expected terminal event, got {lastEvent.GetType().Name}"); + + // Should have output item events (either text messages or workflow actions) + Assert.True(events.OfType().Any(), + "Expected at least one output item from the workflow"); + } + + [Fact] + public async Task Workflow_AgentThrowsException_ProducesErrorOutputAsync() + { + // Arrange: workflow with an agent that throws + var throwingAgent = new ThrowingStreamingAgent("thrower", new InvalidOperationException("Agent crashed")); + var workflow = AgentWorkflowBuilder.BuildSequential("test-error", throwingAgent); + var workflowAgent = workflow.AsAIAgent( + id: "error-workflow", + name: "Error Workflow", + executionEnvironment: InProcessExecution.OffThread, + includeExceptionDetails: true); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Trigger error"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: should have lifecycle events + error/failure indicator + Assert.IsType(events[0]); + Assert.IsType(events[1]); + + var lastEvent = events[^1]; + // Workflow errors surface as either Failed or Completed (depending on error handling) + Assert.True( + lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, + $"Expected terminal event, got {lastEvent.GetType().Name}"); + } + + [Fact] + public async Task Workflow_ExecutorEvents_ProduceWorkflowActionItemsAsync() + { + // Arrange + var agent = new StreamingTextAgent("test-agent", "Result"); + var workflow = AgentWorkflowBuilder.BuildSequential("test-actions", agent); + var workflowAgent = workflow.AsAIAgent( + id: "actions-workflow", + name: "Actions Workflow", + executionEnvironment: InProcessExecution.OffThread); + + var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Hello"); + + // Act + var events = await CollectEventsAsync(handler, request, context); + + // Assert: workflow should produce OutputItemAdded events for executor lifecycle + var addedEvents = events.OfType().ToList(); + Assert.True(addedEvents.Count >= 1, + $"Expected at least 1 output item added event, got {addedEvents.Count}"); + } + + [Fact] + public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectlyAsync() + { + // Arrange: workflow agent registered with a keyed service name + var agent = new StreamingTextAgent("inner", "Keyed workflow response"); + var workflow = AgentWorkflowBuilder.BuildSequential("keyed-wf", agent); + var workflowAgent = workflow.AsAIAgent( + id: "keyed-workflow", + name: "Keyed Workflow", + executionEnvironment: InProcessExecution.OffThread); + + var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); + services.AddKeyedSingleton("my-workflow", workflowAgent); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + var request = new CreateResponse { Model = "test", AgentReference = new AgentReference("my-workflow") }; + request.Input = CreateUserInput("Test keyed workflow"); + var mockContext = CreateMockContext(); + + // Act + var events = await CollectEventsAsync(handler, request, mockContext.Object); + + // Assert + Assert.IsType(events[0]); + Assert.True(events.Count >= 3, $"Expected at least 3 events, got {events.Count}"); + } + + private static (AgentFrameworkResponseHandler handler, CreateResponse request, ResponseContext context) + CreateHandlerWithAgent(AIAgent agent, string userMessage) + { + var services = new ServiceCollection(); + services.AddSingleton(new InMemoryAgentSessionStore()); + services.AddSingleton(agent); + services.AddSingleton>(NullLogger.Instance); + var sp = services.BuildServiceProvider(); + + var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); + var request = new CreateResponse { Model = "test" }; + request.Input = CreateUserInput(userMessage); + var mockContext = CreateMockContext(); + + return (handler, request, mockContext.Object); + } + + private static BinaryData CreateUserInput(string text) + { + return BinaryData.FromObjectAsJson(new[] + { + new { type = "message", id = "msg_in_1", status = "completed", role = "user", + content = new[] { new { type = "input_text", text } } + } + }); + } + + private static Mock CreateMockContext() + { + var mock = new Mock("resp_" + new string('0', 46)) { CallBase = true }; + mock.Setup(x => x.GetHistoryAsync(It.IsAny())) + .ReturnsAsync(Array.Empty()); + mock.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Array.Empty()); + return mock; + } + + private static async Task> CollectEventsAsync( + AgentFrameworkResponseHandler handler, + CreateResponse request, + ResponseContext context) + { + var events = new List(); + await foreach (var evt in handler.CreateAsync(request, context, CancellationToken.None)) + { + events.Add(evt); + } + + return events; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FakeAuthenticationTokenProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FakeAuthenticationTokenProvider.cs new file mode 100644 index 0000000000..96d542379a --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FakeAuthenticationTokenProvider.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; + +internal sealed class FakeAuthenticationTokenProvider : AuthenticationTokenProvider +{ + public override GetTokenOptions? CreateTokenOptions(IReadOnlyDictionary properties) + { + return new GetTokenOptions(new Dictionary()); + } + + public override AuthenticationToken GetToken(GetTokenOptions options, CancellationToken cancellationToken) + { + return new AuthenticationToken("token-value", "token-type", DateTimeOffset.UtcNow.AddHours(1)); + } + + public override ValueTask GetTokenAsync(GetTokenOptions options, CancellationToken cancellationToken) + { + return new ValueTask(this.GetToken(options, cancellationToken)); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryAIToolExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryAIToolExtensionsTests.cs similarity index 96% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryAIToolExtensionsTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryAIToolExtensionsTests.cs index f083a9946e..022fbf0bfe 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryAIToolExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryAIToolExtensionsTests.cs @@ -1,9 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using Microsoft.Agents.AI.Foundry.Hosting; -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; public class FoundryAIToolExtensionsTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxBearerTokenHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxBearerTokenHandlerTests.cs similarity index 98% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxBearerTokenHandlerTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxBearerTokenHandlerTests.cs index cea48d8eb0..e619445481 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxBearerTokenHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxBearerTokenHandlerTests.cs @@ -6,10 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Azure.Core; -using Microsoft.Agents.AI.Foundry.Hosting; using Moq; -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; public class FoundryToolboxBearerTokenHandlerTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxServiceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxServiceTests.cs similarity index 96% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxServiceTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxServiceTests.cs index 24f7433c4e..cdcdf5ee8e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxServiceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxServiceTests.cs @@ -4,11 +4,10 @@ using System.Threading; using System.Threading.Tasks; using Azure.Core; -using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.Options; using Moq; -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; public class FoundryToolboxServiceTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxTests.cs index 3f0a8a54dd..6c4be484a7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/FoundryToolboxTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxTests.cs @@ -9,13 +9,12 @@ using System.Threading.Tasks; using Azure.AI.Projects; using Azure.AI.Projects.Agents; -using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; #pragma warning disable OPENAI001 #pragma warning disable AAIP001 -namespace Microsoft.Agents.AI.Foundry.UnitTests; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; /// /// Unit tests for the class. diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/HostedOutboundUserAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedOutboundUserAgentTests.cs similarity index 98% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/HostedOutboundUserAgentTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedOutboundUserAgentTests.cs index 30c4bef9de..31981509e4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/HostedOutboundUserAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedOutboundUserAgentTests.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Azure.AI.Extensions.OpenAI; -using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.TestHost; @@ -18,7 +17,7 @@ #pragma warning disable OPENAI001, SCME0001, SCME0002, MEAI001 -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; /// /// End-to-end tests that exercise the FULL hosted ASP.NET Core pipeline: diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HttpHandlerAssert.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HttpHandlerAssert.cs new file mode 100644 index 0000000000..f8b3f5ba30 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HttpHandlerAssert.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; + +internal sealed class HttpHandlerAssert : HttpClientHandler +{ + private readonly Func? _assertion; + private readonly Func>? _assertionAsync; + + public HttpHandlerAssert(Func assertion) + { + this._assertion = assertion; + } + public HttpHandlerAssert(Func> assertionAsync) + { + this._assertionAsync = assertionAsync; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (this._assertionAsync is not null) + { + return await this._assertionAsync.Invoke(request); + } + + return this._assertion!.Invoke(request); + } + +#if NET + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + return this._assertion!(request); + } +#endif +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs index f10a015c4b..f4a46940e9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/InputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/InputConverterTests.cs @@ -3,11 +3,10 @@ using System; using System.Linq; using Azure.AI.AgentServer.Responses.Models; -using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; public class InputConverterTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj new file mode 100644 index 0000000000..fa28a32494 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj @@ -0,0 +1,35 @@ + + + + $(TargetFrameworksCore) + false + $(NoWarn);NU1605;NU1903 + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs similarity index 99% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs index 2b8abfefcb..5fa9a435a1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/OutputConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterTests.cs @@ -7,13 +7,12 @@ using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.AI.AgentServer.Responses.Models; -using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; using Moq; using MeaiTextContent = Microsoft.Extensions.AI.TextContent; -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; public class OutputConverterTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterWorkflowTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterWorkflowTests.cs new file mode 100644 index 0000000000..f991744292 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OutputConverterWorkflowTests.cs @@ -0,0 +1,213 @@ +// 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; + +/// +/// Unit tests for 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). +/// +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(); + 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().Count()); + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(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(); + 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().Count()); + Assert.Equal(3, events.OfType().Count()); + Assert.IsType(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(); + 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().Count()); + Assert.Single(events.OfType()); + Assert.IsType(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(); + 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().Count()); + Assert.Single(events.OfType()); + Assert.IsType(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 { ["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(); + 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().Count()); + Assert.Contains(events, e => e is ResponseFunctionCallArgumentsDoneEvent); + Assert.Equal(2, events.OfType().Count()); + Assert.IsType(events[^1]); + } + + private static (ResponseEventStream stream, Mock mockContext) CreateTestStream() + { + var mockContext = new Mock("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 ToAsync(IEnumerable source) + { + foreach (var item in source) + { + yield return item; + } + + await Task.CompletedTask; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ServiceCollectionExtensionsTests.cs similarity index 97% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ServiceCollectionExtensionsTests.cs index c5b9bf1701..bcab777af0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/ServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ServiceCollectionExtensionsTests.cs @@ -3,13 +3,12 @@ using System; using System.Linq; using Azure.AI.AgentServer.Responses; -using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Moq; using OpenAI.Responses; -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; public class ServiceCollectionExtensionsTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxRecordResponse.json b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/TestData/ToolboxRecordResponse.json similarity index 100% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxRecordResponse.json rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/TestData/ToolboxRecordResponse.json diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxVersionResponse.json b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/TestData/ToolboxVersionResponse.json similarity index 100% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxVersionResponse.json rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/TestData/ToolboxVersionResponse.json diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxVersionWithDecorationFields.json b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/TestData/ToolboxVersionWithDecorationFields.json similarity index 100% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestData/ToolboxVersionWithDecorationFields.json rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/TestData/ToolboxVersionWithDecorationFields.json diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/TestDataUtil.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/TestDataUtil.cs new file mode 100644 index 0000000000..c62f2aedc2 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/TestDataUtil.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; + +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; + +/// +/// Utility class for loading toolbox-related test data files. +/// +internal static class TestDataUtil +{ + private static readonly string s_toolboxRecordResponseJson = File.ReadAllText("TestData/ToolboxRecordResponse.json"); + private static readonly string s_toolboxVersionResponseJson = File.ReadAllText("TestData/ToolboxVersionResponse.json"); + private static readonly string s_toolboxVersionWithDecorationFieldsJson = File.ReadAllText("TestData/ToolboxVersionWithDecorationFields.json"); + + /// + /// Gets the toolbox record response JSON. + /// + public static string GetToolboxRecordResponseJson() => s_toolboxRecordResponseJson; + + /// + /// Gets the toolbox version response JSON. + /// + public static string GetToolboxVersionResponseJson() => s_toolboxVersionResponseJson; + + /// + /// Gets the toolbox version response JSON with decoration fields on tools. + /// + public static string GetToolboxVersionWithDecorationFieldsJson() => s_toolboxVersionWithDecorationFieldsJson; +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/DelegatingResponsesClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/UserAgentResponsesClientTests.cs similarity index 97% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/DelegatingResponsesClientTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/UserAgentResponsesClientTests.cs index 8ec8a7f570..c57bf6802e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/DelegatingResponsesClientTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/UserAgentResponsesClientTests.cs @@ -11,23 +11,22 @@ using System.Threading; using System.Threading.Tasks; using Azure.AI.Extensions.OpenAI; -using Microsoft.Agents.AI.Foundry.Hosting; using Microsoft.Extensions.AI; using OpenAI; using OpenAI.Responses; #pragma warning disable OPENAI001, SCME0001, SCME0002, MEAI001 -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; /// -/// Verifies that preserves user-supplied client options +/// Verifies that preserves user-supplied client options /// (Transport, RetryPolicy, UserAgentApplicationId, OrganizationId, ProjectId) and adds the /// hosted-agent User-Agent supplement on every outgoing request, including streaming. /// Covers both the Azure-flavored and the native OpenAI /// . /// -public sealed partial class DelegatingResponsesClientTests +public sealed partial class UserAgentResponsesClientTests { private const string TestEndpoint = "https://fake-foundry.example.com/api/projects/fake-prj"; private const string OpenAIEndpoint = "https://fake-openai.example.com/v1"; @@ -215,7 +214,7 @@ public async Task Polyfill_AncillaryProtocolMethod_AddsSupplementAsync(string me using var httpClient = new HttpClient(handler); #pragma warning restore CA5399 var inner = BuildOpenAIInner(httpClient, userAgentApplicationId: "MY_APP_ID"); - var wrapper = new DelegatingResponsesClient(inner); + var wrapper = new UserAgentResponsesClient(inner); // Act switch (method) @@ -326,7 +325,7 @@ private static IChatClient MakeWithDelegating(ResponsesClient inner) IChatClient meai = inner.AsIChatClient(Deployment); var meaiType = meai.GetType(); var field = meaiType.GetField("_responseClient", BindingFlags.NonPublic | BindingFlags.Instance)!; - field.SetValue(meai, new DelegatingResponsesClient(inner)); + field.SetValue(meai, new UserAgentResponsesClient(inner)); return meai; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/WorkflowTestAgents.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/WorkflowTestAgents.cs new file mode 100644 index 0000000000..7e10abc9eb --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/WorkflowTestAgents.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using MeaiTextContent = Microsoft.Extensions.AI.TextContent; + +namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; + +/// +/// A test agent that streams a single text update. +/// +internal sealed class StreamingTextAgent(string id, string responseText) : AIAgent +{ + public new string Id => id; + + protected override async IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + yield return new AgentResponseUpdate + { + MessageId = $"msg_{id}", + Contents = [new MeaiTextContent(responseText)] + }; + + await Task.CompletedTask; + } + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); +} + +/// +/// A test agent that always throws an exception during streaming. +/// +internal sealed class ThrowingStreamingAgent(string id, Exception exception) : AIAgent +{ + public new string Id => id; + + protected override IAsyncEnumerable RunCoreStreamingAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw exception; + + protected override Task RunCoreAsync( + IEnumerable messages, + AgentSession? session, + AgentRunOptions? options, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask CreateSessionCoreAsync( + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask SerializeSessionCoreAsync( + AgentSession session, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + protected override ValueTask DeserializeSessionCoreAsync( + JsonElement serializedState, + JsonSerializerOptions? jsonSerializerOptions, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs deleted file mode 100644 index d87406f815..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Hosting/WorkflowIntegrationTests.cs +++ /dev/null @@ -1,508 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.AgentServer.Responses; -using Azure.AI.AgentServer.Responses.Models; -using Microsoft.Agents.AI.Foundry.Hosting; -using Microsoft.Agents.AI.Workflows; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using MeaiTextContent = Microsoft.Extensions.AI.TextContent; - -namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting; - -/// -/// Integration tests that verify workflow execution through the -/// pipeline. -/// These use real workflow builders and the InProcessExecution environment -/// to produce authentic streaming event patterns. -/// -public class WorkflowIntegrationTests -{ - // ===== Sequential Workflow Tests ===== - - [Fact] - public async Task SequentialWorkflow_SingleAgent_ProducesTextOutputAsync() - { - // Arrange: single-agent sequential workflow - var echoAgent = new StreamingTextAgent("echo", "Hello from the workflow!"); - var workflow = AgentWorkflowBuilder.BuildSequential("test-sequential", echoAgent); - var workflowAgent = workflow.AsAIAgent( - id: "workflow-agent", - name: "Test Workflow", - executionEnvironment: InProcessExecution.OffThread, - includeExceptionDetails: true); - - var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Hello"); - - // Act - var events = await CollectEventsAsync(handler, request, context); - - // Assert: should have lifecycle events + at least one text output + terminal - Assert.IsType(events[0]); - Assert.IsType(events[1]); - Assert.True(events.Count >= 4, $"Expected at least 4 events, got {events.Count}"); - - var lastEvent = events[^1]; - Assert.True( - lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, - $"Expected terminal event, got {lastEvent.GetType().Name}"); - } - - [Fact] - public async Task SequentialWorkflow_TwoAgents_ProducesOutputFromBothAsync() - { - // Arrange: two agents in sequence - var agent1 = new StreamingTextAgent("agent1", "First agent says hello"); - var agent2 = new StreamingTextAgent("agent2", "Second agent says goodbye"); - var workflow = AgentWorkflowBuilder.BuildSequential("test-sequential-2", agent1, agent2); - var workflowAgent = workflow.AsAIAgent( - id: "seq-workflow", - name: "Sequential Workflow", - executionEnvironment: InProcessExecution.OffThread, - includeExceptionDetails: true); - - var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Process this"); - - // Act - var events = await CollectEventsAsync(handler, request, context); - - // Assert: should have workflow action events for executor lifecycle - var lastEvent = events[^1]; - Assert.True( - lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, - $"Expected terminal event, got {lastEvent.GetType().Name}"); - - // Should have output item events (either text messages or workflow actions) - Assert.True(events.OfType().Any(), - "Expected at least one output item from the workflow"); - } - - // ===== Workflow Error Propagation ===== - - [Fact] - public async Task Workflow_AgentThrowsException_ProducesErrorOutputAsync() - { - // Arrange: workflow with an agent that throws - var throwingAgent = new ThrowingStreamingAgent("thrower", new InvalidOperationException("Agent crashed")); - var workflow = AgentWorkflowBuilder.BuildSequential("test-error", throwingAgent); - var workflowAgent = workflow.AsAIAgent( - id: "error-workflow", - name: "Error Workflow", - executionEnvironment: InProcessExecution.OffThread, - includeExceptionDetails: true); - - var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Trigger error"); - - // Act - var events = await CollectEventsAsync(handler, request, context); - - // Assert: should have lifecycle events + error/failure indicator - Assert.IsType(events[0]); - Assert.IsType(events[1]); - - var lastEvent = events[^1]; - // Workflow errors surface as either Failed or Completed (depending on error handling) - Assert.True( - lastEvent is ResponseCompletedEvent || lastEvent is ResponseFailedEvent, - $"Expected terminal event, got {lastEvent.GetType().Name}"); - } - - // ===== Workflow Action Lifecycle Events ===== - - [Fact] - public async Task Workflow_ExecutorEvents_ProduceWorkflowActionItemsAsync() - { - // Arrange - var agent = new StreamingTextAgent("test-agent", "Result"); - var workflow = AgentWorkflowBuilder.BuildSequential("test-actions", agent); - var workflowAgent = workflow.AsAIAgent( - id: "actions-workflow", - name: "Actions Workflow", - executionEnvironment: InProcessExecution.OffThread); - - var (handler, request, context) = CreateHandlerWithAgent(workflowAgent, "Hello"); - - // Act - var events = await CollectEventsAsync(handler, request, context); - - // Assert: workflow should produce OutputItemAdded events for executor lifecycle - var addedEvents = events.OfType().ToList(); - Assert.True(addedEvents.Count >= 1, - $"Expected at least 1 output item added event, got {addedEvents.Count}"); - } - - // ===== Keyed Workflow Registration ===== - - [Fact] - public async Task WorkflowAgent_RegisteredWithKey_ResolvesCorrectlyAsync() - { - // Arrange: workflow agent registered with a keyed service name - var agent = new StreamingTextAgent("inner", "Keyed workflow response"); - var workflow = AgentWorkflowBuilder.BuildSequential("keyed-wf", agent); - var workflowAgent = workflow.AsAIAgent( - id: "keyed-workflow", - name: "Keyed Workflow", - executionEnvironment: InProcessExecution.OffThread); - - var services = new ServiceCollection(); - services.AddSingleton(new InMemoryAgentSessionStore()); - services.AddKeyedSingleton("my-workflow", workflowAgent); - var sp = services.BuildServiceProvider(); - - var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); - var request = new CreateResponse { Model = "test", AgentReference = new AgentReference("my-workflow") }; - request.Input = CreateUserInput("Test keyed workflow"); - var mockContext = CreateMockContext(); - - // Act - var events = await CollectEventsAsync(handler, request, mockContext.Object); - - // Assert - Assert.IsType(events[0]); - Assert.True(events.Count >= 3, $"Expected at least 3 events, got {events.Count}"); - } - - // ===== OutputConverter Direct Workflow Pattern Tests ===== - // These test the OutputConverter directly with update patterns that mirror real workflows. - - [Fact] - public async Task OutputConverter_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(); - 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().Count()); - Assert.Equal(2, events.OfType().Count()); - Assert.IsType(events[^1]); - } - - [Fact] - public async Task OutputConverter_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(); - 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().Count()); - Assert.Equal(3, events.OfType().Count()); - Assert.IsType(events[^1]); - } - - [Fact] - public async Task OutputConverter_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(); - 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().Count()); - Assert.Single(events.OfType()); - Assert.IsType(events[^1]); - } - - [Fact] - public async Task OutputConverter_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(); - 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().Count()); - Assert.Single(events.OfType()); - Assert.IsType(events[^1]); - } - - [Fact] - public async Task OutputConverter_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 { ["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(); - 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().Count()); - Assert.Contains(events, e => e is ResponseFunctionCallArgumentsDoneEvent); - Assert.Equal(2, events.OfType().Count()); - Assert.IsType(events[^1]); - } - - // ===== Helpers ===== - - private static (AgentFrameworkResponseHandler handler, CreateResponse request, ResponseContext context) - CreateHandlerWithAgent(AIAgent agent, string userMessage) - { - var services = new ServiceCollection(); - services.AddSingleton(new InMemoryAgentSessionStore()); - services.AddSingleton(agent); - services.AddSingleton>(NullLogger.Instance); - var sp = services.BuildServiceProvider(); - - var handler = new AgentFrameworkResponseHandler(sp, NullLogger.Instance); - var request = new CreateResponse { Model = "test" }; - request.Input = CreateUserInput(userMessage); - var mockContext = CreateMockContext(); - - return (handler, request, mockContext.Object); - } - - private static BinaryData CreateUserInput(string text) - { - return BinaryData.FromObjectAsJson(new[] - { - new { type = "message", id = "msg_in_1", status = "completed", role = "user", - content = new[] { new { type = "input_text", text } } - } - }); - } - - private static Mock CreateMockContext() - { - var mock = new Mock("resp_" + new string('0', 46)) { CallBase = true }; - mock.Setup(x => x.GetHistoryAsync(It.IsAny())) - .ReturnsAsync(Array.Empty()); - mock.Setup(x => x.GetInputItemsAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(Array.Empty()); - return mock; - } - - private static (ResponseEventStream stream, Mock mockContext) CreateTestStream() - { - var mockContext = new Mock("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 Task> CollectEventsAsync( - AgentFrameworkResponseHandler handler, - CreateResponse request, - ResponseContext context) - { - var events = new List(); - await foreach (var evt in handler.CreateAsync(request, context, CancellationToken.None)) - { - events.Add(evt); - } - - return events; - } - - private static async IAsyncEnumerable ToAsync(IEnumerable source) - { - foreach (var item in source) - { - yield return item; - } - - await Task.CompletedTask; - } - - // ===== Test Agent Types ===== - - /// - /// A test agent that streams a single text update. - /// - private sealed class StreamingTextAgent(string id, string responseText) : AIAgent - { - public new string Id => id; - - protected override async IAsyncEnumerable RunCoreStreamingAsync( - IEnumerable messages, - AgentSession? session, - AgentRunOptions? options, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - yield return new AgentResponseUpdate - { - MessageId = $"msg_{id}", - Contents = [new MeaiTextContent(responseText)] - }; - - await Task.CompletedTask; - } - - protected override Task RunCoreAsync( - IEnumerable messages, - AgentSession? session, - AgentRunOptions? options, - CancellationToken cancellationToken = default) => - throw new NotImplementedException(); - - protected override ValueTask CreateSessionCoreAsync( - CancellationToken cancellationToken = default) => - throw new NotImplementedException(); - - protected override ValueTask SerializeSessionCoreAsync( - AgentSession session, - JsonSerializerOptions? jsonSerializerOptions, - CancellationToken cancellationToken = default) => - throw new NotImplementedException(); - - protected override ValueTask DeserializeSessionCoreAsync( - JsonElement serializedState, - JsonSerializerOptions? jsonSerializerOptions, - CancellationToken cancellationToken = default) => - throw new NotImplementedException(); - } - - /// - /// A test agent that always throws an exception during streaming. - /// - private sealed class ThrowingStreamingAgent(string id, Exception exception) : AIAgent - { - public new string Id => id; - - protected override IAsyncEnumerable RunCoreStreamingAsync( - IEnumerable messages, - AgentSession? session, - AgentRunOptions? options, - CancellationToken cancellationToken = default) => - throw exception; - - protected override Task RunCoreAsync( - IEnumerable messages, - AgentSession? session, - AgentRunOptions? options, - CancellationToken cancellationToken = default) => - throw new NotImplementedException(); - - protected override ValueTask CreateSessionCoreAsync( - CancellationToken cancellationToken = default) => - throw new NotImplementedException(); - - protected override ValueTask SerializeSessionCoreAsync( - AgentSession session, - JsonSerializerOptions? jsonSerializerOptions, - CancellationToken cancellationToken = default) => - throw new NotImplementedException(); - - protected override ValueTask DeserializeSessionCoreAsync( - JsonElement serializedState, - JsonSerializerOptions? jsonSerializerOptions, - CancellationToken cancellationToken = default) => - throw new NotImplementedException(); - } -} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj index 2265719711..cfa5e7a11f 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/Microsoft.Agents.AI.Foundry.UnitTests.csproj @@ -7,33 +7,13 @@ - - - - - - - - - - - - - - - - - - - - @@ -50,15 +30,6 @@ Always - - Always - - - Always - - - Always - diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/RequestOptionsExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/RequestOptionsExtensionsTests.cs index 18824c4875..df5dd8ebae 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/RequestOptionsExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/RequestOptionsExtensionsTests.cs @@ -66,7 +66,7 @@ public async Task MeaiUserAgentPolicy_DoesNotAddFoundryHostingSegmentAsync() await pipeline.SendAsync(message); // Assert: the policy is MEAI-only; the foundry-hosting supplement is added elsewhere - // (by the polyfill DelegatingResponsesClient → HostedAgentUserAgentPolicy). + // (by the polyfill UserAgentResponsesClient → HostedAgentUserAgentPolicy). Assert.NotNull(handler.LastUserAgent); Assert.DoesNotContain("foundry-hosting/agent-framework-dotnet", handler.LastUserAgent); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs index 898e0293c7..3460362efd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/TestDataUtil.cs @@ -14,9 +14,6 @@ internal static class TestDataUtil private static readonly string s_agentResponseJson = File.ReadAllText("TestData/AgentResponse.json"); private static readonly string s_agentVersionResponseJson = File.ReadAllText("TestData/AgentVersionResponse.json"); private static readonly string s_openAIDefaultResponseJson = File.ReadAllText("TestData/OpenAIDefaultResponse.json"); - private static readonly string s_toolboxRecordResponseJson = File.ReadAllText("TestData/ToolboxRecordResponse.json"); - private static readonly string s_toolboxVersionResponseJson = File.ReadAllText("TestData/ToolboxVersionResponse.json"); - private static readonly string s_toolboxVersionWithDecorationFieldsJson = File.ReadAllText("TestData/ToolboxVersionWithDecorationFields.json"); private const string AgentDefinitionPlaceholder = "\"agent-definition-placeholder\""; @@ -165,19 +162,4 @@ private static string ApplyDescription(string json, string? description) } return json; } - - /// - /// Gets the toolbox record response JSON. - /// - public static string GetToolboxRecordResponseJson() => s_toolboxRecordResponseJson; - - /// - /// Gets the toolbox version response JSON. - /// - public static string GetToolboxVersionResponseJson() => s_toolboxVersionResponseJson; - - /// - /// Gets the toolbox version response JSON with decoration fields on tools. - /// - public static string GetToolboxVersionWithDecorationFieldsJson() => s_toolboxVersionWithDecorationFieldsJson; }