// 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.IsAssignableFrom(result);
}
///
/// Verifies that AddWorkflow registers the workflow as a keyed singleton service.
///
[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 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 providing a null builder to AddConcurrentWorkflow throws an ArgumentNullException.
///
[Fact]
public void AddConcurrentWorkflow_NullBuilder_ThrowsArgumentNullException()
{
Assert.Throws(() =>
HostApplicationBuilderWorkflowExtensions.AddConcurrentWorkflow(null!, "workflow", [null!]));
}
///
/// Verifies that AddConcurrentWorkflow throws ArgumentNullException for null name.
///
[Fact]
public void AddConcurrentWorkflow_NullName_ThrowsArgumentNullException()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws(() =>
builder.AddConcurrentWorkflow(null!, [new HostedAgentBuilder("test", builder)]));
Assert.Equal("name", exception.ParamName);
}
///
/// Verifies that AddConcurrentWorkflow throws ArgumentNullException for null agent builders.
///
[Fact]
public void AddConcurrentWorkflow_NullAgentBuilders_ThrowsArgumentNullException()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws(() =>
builder.AddConcurrentWorkflow("workflowName", null!));
Assert.Equal("agentBuilders", exception.ParamName);
}
///
/// Verifies that AddConcurrentWorkflow returns IHostWorkflowBuilder instance.
///
[Fact]
public void AddConcurrentWorkflow_ValidParameters_ReturnsBuilder()
{
var builder = new HostApplicationBuilder();
var result = builder.AddConcurrentWorkflow("concurrentWorkflow", [new HostedAgentBuilder("test", builder)]);
Assert.NotNull(result);
Assert.IsAssignableFrom(result);
}
///
/// Verifies that providing a null builder to AddSequentialWorkflow throws an ArgumentNullException.
///
[Fact]
public void AddSequentialWorkflow_NullBuilder_ThrowsArgumentNullException()
{
Assert.Throws(() =>
HostApplicationBuilderWorkflowExtensions.AddSequentialWorkflow(null!, "workflow", [null!]));
}
///
/// Verifies that AddSequentialWorkflow throws ArgumentNullException for null name.
///
[Fact]
public void AddSequentialWorkflow_NullName_ThrowsArgumentNullException()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws(() =>
builder.AddSequentialWorkflow(null!, [new HostedAgentBuilder("test", builder)]));
Assert.Equal("name", exception.ParamName);
}
///
/// Verifies that AddSequentialWorkflow throws ArgumentNullException for null agent builders.
///
[Fact]
public void AddSequentialWorkflow_NullAgentBuilders_ThrowsArgumentNullException()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws(() =>
builder.AddSequentialWorkflow("workflowName", null!));
Assert.Equal("agentBuilders", exception.ParamName);
}
[Fact]
public void AddSequentialWorkflow_EmptyAgentBuilders_Throws()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws(() =>
builder.AddSequentialWorkflow("sequentialWorkflow", Array.Empty()));
Assert.Equal("agentBuilders", exception.ParamName);
}
///
/// 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]);
}
}