// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; namespace Microsoft.Agents.AI.Hosting.UnitTests; public class HostApplicationBuilderWorkflowExtensionsTests { /// /// Verifies that providing a null builder to AddWorkflow throws an ArgumentNullException. /// [Fact] public void AddWorkflow_NullBuilder_ThrowsArgumentNullException() => Assert.Throws( () => HostApplicationBuilderWorkflowExtensions.AddWorkflow( null!, "workflow", (sp, key) => CreateTestWorkflow(key))); /// /// Verifies that AddWorkflow throws ArgumentNullException for null name. /// [Fact] public void AddWorkflow_NullName_ThrowsArgumentNullException() { var builder = new HostApplicationBuilder(); var exception = Assert.Throws(() => builder.AddWorkflow(null!, (sp, key) => CreateTestWorkflow(key))); Assert.Equal("name", exception.ParamName); } /// /// Verifies that AddWorkflow throws ArgumentNullException for null factory delegate. /// [Fact] public void AddWorkflow_NullFactory_ThrowsArgumentNullException() { var builder = new HostApplicationBuilder(); var exception = Assert.Throws(() => builder.AddWorkflow("workflowName", null!)); Assert.Equal("createWorkflowDelegate", exception.ParamName); } /// /// Verifies that AddWorkflow returns the IHostWorkflowBuilder instance. /// [Fact] public void AddWorkflow_ValidParameters_ReturnsBuilder() { var builder = new HostApplicationBuilder(); var result = builder.AddWorkflow("workflowName", (sp, key) => CreateTestWorkflow(key)); Assert.NotNull(result); Assert.IsType(result, exactMatch: false); } /// /// Verifies that AddWorkflow registers the workflow as a keyed singleton service by default. /// [Fact] public void AddWorkflow_RegistersKeyedSingleton() { var builder = new HostApplicationBuilder(); const string WorkflowName = "testWorkflow"; builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key)); var descriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == WorkflowName && d.ServiceType == typeof(Workflow)); Assert.NotNull(descriptor); Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); } /// /// Verifies that AddWorkflow can be called multiple times with different workflow names. /// [Fact] public void AddWorkflow_MultipleCalls_RegistersMultipleWorkflows() { var builder = new HostApplicationBuilder(); builder.AddWorkflow("workflow1", (sp, key) => CreateTestWorkflow(key)); builder.AddWorkflow("workflow2", (sp, key) => CreateTestWorkflow(key)); builder.AddWorkflow("workflow3", (sp, key) => CreateTestWorkflow(key)); var workflowDescriptors = builder.Services .Where(d => d.ServiceType == typeof(Workflow) && d.ServiceKey is string) .ToList(); Assert.Equal(3, workflowDescriptors.Count); Assert.Contains(workflowDescriptors, d => (string)d.ServiceKey! == "workflow1"); Assert.Contains(workflowDescriptors, d => (string)d.ServiceKey! == "workflow2"); Assert.Contains(workflowDescriptors, d => (string)d.ServiceKey! == "workflow3"); } /// /// Verifies that a handoff workflow can be named from the DI workflow key. /// [Fact] public void AddWorkflow_HandoffWorkflowWithName_ResolvesWorkflow() { var builder = new HostApplicationBuilder(); const string WorkflowName = "handoffWorkflow"; var mockAgent = new Mock(); mockAgent.Setup(a => a.Name).Returns("handoffAgent"); #pragma warning disable MAAIW001 // This test covers hosting handoff workflows. builder.AddWorkflow(WorkflowName, (sp, key) => AgentWorkflowBuilder.CreateHandoffBuilderWith(mockAgent.Object) .WithName(key) .Build()); #pragma warning restore MAAIW001 var workflow = builder.Build().Services.GetRequiredKeyedService(WorkflowName); Assert.Equal(WorkflowName, workflow.Name); } /// /// Verifies that AddWorkflow handles empty strings for name. /// [Fact] public void AddWorkflow_EmptyName_ThrowsArgumentException() { var builder = new HostApplicationBuilder(); var result = builder.AddWorkflow("", (sp, key) => CreateTestWorkflow(key)); Assert.NotNull(result); } /// /// Verifies that AddWorkflow with special characters in name works correctly for valid names. /// [Theory] [InlineData("workflow_name")] // underscore is allowed [InlineData("Workflow123")] // alphanumeric is allowed [InlineData("_workflow")] // can start with underscore [InlineData("workflow-name")] // dash is allowed [InlineData("workflow.name")] // period is allowed [InlineData("workflow:type")] // colon is allowed [InlineData("my.workflow_1:type-name")] // complex valid name public void AddWorkflow_ValidSpecialCharactersInName_Succeeds(string name) { var builder = new HostApplicationBuilder(); var result = builder.AddWorkflow(name, (sp, key) => CreateTestWorkflow(key)); var descriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == name && d.ServiceType == typeof(Workflow)); Assert.NotNull(descriptor); } /// /// Verifies that AddAsAIAgent without a name parameter uses the workflow name as the agent name. /// [Fact] public void AddAsAIAgent_WithoutName_UsesWorkflowName() { var builder = new HostApplicationBuilder(); const string WorkflowName = "testWorkflow"; var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key)); var agentBuilder = workflowBuilder.AddAsAIAgent(); Assert.NotNull(agentBuilder); // Verify workflow is registered with workflow name var workflowDescriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == WorkflowName && d.ServiceType == typeof(Workflow)); Assert.NotNull(workflowDescriptor); // Verify agent is registered with workflow name var agentDescriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == WorkflowName && d.ServiceType == typeof(AIAgent)); Assert.NotNull(agentDescriptor); } /// /// Verifies that AddAsAIAgent with a name parameter uses that name instead of the workflow name. /// [Fact] public void AddAsAIAgent_WithName_UsesProvidedName() { var builder = new HostApplicationBuilder(); const string WorkflowName = "testWorkflow"; const string AgentName = "testAgent"; var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key)); var agentBuilder = workflowBuilder.AddAsAIAgent(AgentName); Assert.NotNull(agentBuilder); // Verify workflow is registered with workflow name var workflowDescriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == WorkflowName && d.ServiceType == typeof(Workflow)); Assert.NotNull(workflowDescriptor); // Verify agent is registered with agent name (not workflow name) var agentDescriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == AgentName && d.ServiceType == typeof(AIAgent)); Assert.NotNull(agentDescriptor); // Verify no agent registered with workflow name var wrongAgentDescriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == WorkflowName && d.ServiceType == typeof(AIAgent)); Assert.NotSame(workflowDescriptor, wrongAgentDescriptor); } /// /// Verifies that AddAsAIAgent correctly retrieves the workflow using the workflow name, not the agent name. /// [Fact] public void AddAsAIAgent_WithDifferentName_RetrievesWorkflowCorrectly() { var builder = new HostApplicationBuilder(); const string WorkflowName = "myWorkflow"; const string AgentName = "myAgent"; var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key)); workflowBuilder.AddAsAIAgent(AgentName); var serviceProvider = builder.Build().Services; // Act - Get the agent using the agent name var agent = serviceProvider.GetRequiredKeyedService(AgentName); Assert.NotNull(agent); Assert.Equal(AgentName, agent.Name); // Verify that we can still get the workflow using the workflow name var workflow = serviceProvider.GetRequiredKeyedService(WorkflowName); Assert.NotNull(workflow); Assert.Equal(WorkflowName, workflow.Name); } /// /// Verifies that AddAsAIAgent returns IHostedAgentBuilder with correct name. /// [Fact] public void AddAsAIAgent_ReturnsHostedAgentBuilder() { var builder = new HostApplicationBuilder(); const string WorkflowName = "testWorkflow"; const string AgentName = "testAgent"; var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key)); var agentBuilder = workflowBuilder.AddAsAIAgent(AgentName); Assert.NotNull(agentBuilder); Assert.IsType(agentBuilder, exactMatch: false); Assert.Equal(AgentName, agentBuilder.Name); } /// /// Verifies that AddAsAIAgent without name returns IHostedAgentBuilder with workflow name. /// [Fact] public void AddAsAIAgent_WithoutName_ReturnsHostedAgentBuilderWithWorkflowName() { var builder = new HostApplicationBuilder(); const string WorkflowName = "testWorkflow"; var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key)); var agentBuilder = workflowBuilder.AddAsAIAgent(); Assert.NotNull(agentBuilder); Assert.IsType(agentBuilder, exactMatch: false); Assert.Equal(WorkflowName, agentBuilder.Name); } /// /// Verifies that AddAsAIAgent can chain multiple agents from the same workflow. /// [Fact] public void AddAsAIAgent_MultipleAgents_FromSameWorkflow() { var builder = new HostApplicationBuilder(); const string WorkflowName = "testWorkflow"; var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key)); var agentBuilder1 = workflowBuilder.AddAsAIAgent("agent1"); var agentBuilder2 = workflowBuilder.AddAsAIAgent("agent2"); Assert.NotNull(agentBuilder1); Assert.NotNull(agentBuilder2); // Verify both agents are registered var agentDescriptor1 = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == "agent1" && d.ServiceType == typeof(AIAgent)); var agentDescriptor2 = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == "agent2" && d.ServiceType == typeof(AIAgent)); Assert.NotNull(agentDescriptor1); Assert.NotNull(agentDescriptor2); // Verify workflow is registered only once var workflowDescriptors = builder.Services.Where( d => (d.ServiceKey as string) == WorkflowName && d.ServiceType == typeof(Workflow)).ToList(); Assert.Single(workflowDescriptors); } /// /// Verifies that AddAsAIAgent with null name behaves the same as the parameterless overload. /// [Fact] public void AddAsAIAgent_WithNullName_UsesWorkflowName() { var builder = new HostApplicationBuilder(); const string WorkflowName = "testWorkflow"; var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key)); var agentBuilder = workflowBuilder.AddAsAIAgent(name: null); Assert.NotNull(agentBuilder); Assert.Equal(WorkflowName, agentBuilder.Name); // Verify agent is registered with workflow name var agentDescriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == WorkflowName && d.ServiceType == typeof(AIAgent)); Assert.NotNull(agentDescriptor); } /// /// Verifies that AddAsAIAgent with empty string name uses empty string as agent name. /// [Fact] public void AddAsAIAgent_WithEmptyName_UsesEmptyStringAsAgentName() { var builder = new HostApplicationBuilder(); const string WorkflowName = "testWorkflow"; var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key)); var agentBuilder = workflowBuilder.AddAsAIAgent(name: ""); Assert.NotNull(agentBuilder); Assert.Equal("", agentBuilder.Name); // Verify agent is registered with empty string name var agentDescriptor = builder.Services.FirstOrDefault( d => d.ServiceKey is string s && s.Length == 0 && d.ServiceType == typeof(AIAgent)); Assert.NotNull(agentDescriptor); } /// /// Verifies that AddWorkflow registers with the specified scoped lifetime. /// [Fact] public void AddWorkflow_WithScopedLifetime_RegistersKeyedScoped() { // Arrange var builder = new HostApplicationBuilder(); const string WorkflowName = "scopedWorkflow"; // Act builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key), ServiceLifetime.Scoped); // Assert var descriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == WorkflowName && d.ServiceType == typeof(Workflow)); Assert.NotNull(descriptor); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } /// /// Verifies that AddWorkflow registers with the specified transient lifetime. /// [Fact] public void AddWorkflow_WithTransientLifetime_RegistersKeyedTransient() { // Arrange var builder = new HostApplicationBuilder(); const string WorkflowName = "transientWorkflow"; // Act builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key), ServiceLifetime.Transient); // Assert var descriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == WorkflowName && d.ServiceType == typeof(Workflow)); Assert.NotNull(descriptor); Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime); } /// /// Verifies that AddAsAIAgent respects the lifetime parameter. /// [Theory] [InlineData(ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Transient)] public void AddAsAIAgent_RespectsLifetime(ServiceLifetime lifetime) { // Arrange var builder = new HostApplicationBuilder(); const string WorkflowName = "testWorkflow"; var workflowBuilder = builder.AddWorkflow(WorkflowName, (sp, key) => CreateTestWorkflow(key)); // Act var agentBuilder = workflowBuilder.AddAsAIAgent("agent", lifetime); // Assert var descriptor = builder.Services.FirstOrDefault( d => (d.ServiceKey as string) == "agent" && d.ServiceType == typeof(AIAgent)); Assert.NotNull(descriptor); Assert.Equal(lifetime, descriptor.Lifetime); Assert.Equal(lifetime, agentBuilder.Lifetime); } /// /// Helper method to create a simple test workflow with a given name. /// private static Workflow CreateTestWorkflow(string name) { // Create a simple workflow using AgentWorkflowBuilder var mockAgent = new Mock(); mockAgent.Setup(a => a.Name).Returns("testAgent"); return AgentWorkflowBuilder.BuildSequential(workflowName: name, agents: [mockAgent.Object]); } }