// 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 (lone FCC = HITL request) + 1 text = 3 // Total: 7 output items Assert.Equal(7, events.OfType().Count()); Assert.Single(events.OfType()); 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; } }