test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled

This commit is contained in:
2026-01-24 03:05:12 +11:00
parent f78f2388b3
commit 539852f81c
2584 changed files with 287471 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
namespace Microsoft.Agents.AI.Hosting.UnitTests;
public class AgentHostingServiceCollectionExtensionsTests
{
/// <summary>
/// Verifies that providing a null builder to AddAIAgent throws an ArgumentNullException.
/// </summary>
[Fact]
public void AddAIAgent_NullBuilder_ThrowsArgumentNullException() => Assert.Throws<ArgumentNullException>(
() => AgentHostingServiceCollectionExtensions.AddAIAgent(null!, "agent", "instructions"));
/// <summary>
/// Verifies that AddAIAgent without chat client key throws ArgumentNullException for null name.
/// </summary>
[Fact]
public void AddAIAgent_NullName_ThrowsArgumentNullException()
{
var services = new ServiceCollection();
var exception = Assert.Throws<ArgumentNullException>(() => services.AddAIAgent(null!, "instructions"));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verifies that AddAIAgent without chat client key allows null instructions.
/// </summary>
[Fact]
public void AddAIAgent_NullInstructions_AllowsNull()
{
var services = new ServiceCollection();
var result = services.AddAIAgent("agentName", (string)null!);
Assert.NotNull(result);
}
/// <summary>
/// Verifies that AddAIAgent with chat client key throws ArgumentNullException for null name.
/// </summary>
[Fact]
public void AddAIAgentWithKey_NullName_ThrowsArgumentNullException()
{
var services = new ServiceCollection();
var exception = Assert.Throws<ArgumentNullException>(() => services.AddAIAgent(null!, "instructions", "key"));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verifies that AddAIAgent with chat client key allows null instructions.
/// </summary>
[Fact]
public void AddAIAgentWithKey_NullInstructions_AllowsNull()
{
var services = new ServiceCollection();
var result = services.AddAIAgent("agentName", null, "key");
Assert.NotNull(result);
}
/// <summary>
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null builder.
/// </summary>
[Fact]
public void AddAIAgentWithFactory_NullBuilder_ThrowsArgumentNullException() =>
Assert.Throws<ArgumentNullException>(() =>
AgentHostingServiceCollectionExtensions.AddAIAgent(null!, "agentName", (sp, key) => new Mock<AIAgent>().Object));
/// <summary>
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null name.
/// </summary>
[Fact]
public void AddAIAgentWithFactory_NullName_ThrowsArgumentNullException()
{
var services = new ServiceCollection();
var exception = Assert.Throws<ArgumentNullException>(() => services.AddAIAgent(null!, (sp, key) => new Mock<AIAgent>().Object));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null factory.
/// </summary>
[Fact]
public void AddAIAgentWithFactory_NullFactory_ThrowsArgumentNullException()
{
var services = new ServiceCollection();
var exception = Assert.Throws<ArgumentNullException>(() => services.AddAIAgent("agentName", (Func<IServiceProvider, string, AIAgent>)null!));
Assert.Equal("createAgentDelegate", exception.ParamName);
}
/// <summary>
/// Verifies that AddAIAgent with factory delegate returns the same builder instance.
/// </summary>
[Fact]
public void AddAIAgentWithFactory_ValidParameters_ReturnsBuilder()
{
var services = new ServiceCollection();
var mockAgent = new Mock<AIAgent>();
var result = services.AddAIAgent("agentName", (sp, key) => mockAgent.Object);
Assert.NotNull(result);
}
/// <summary>
/// Verifies that AddAIAgent registers the agent as a keyed singleton service.
/// </summary>
[Fact]
public void AddAIAgent_RegistersKeyedSingleton()
{
var services = new ServiceCollection();
var mockAgent = new Mock<AIAgent>();
const string AgentName = "testAgent";
services.AddAIAgent(AgentName, (sp, key) => mockAgent.Object);
var descriptor = services.FirstOrDefault(
d => (d.ServiceKey as string) == AgentName &&
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime);
}
/// <summary>
/// Verifies that AddAIAgent can be called multiple times with different agent names.
/// </summary>
[Fact]
public void AddAIAgent_MultipleCalls_RegistersMultipleAgents()
{
var services = new ServiceCollection();
services.AddAIAgent("agent1", "instructions1");
services.AddAIAgent("agent2", "instructions2");
services.AddAIAgent("agent3", "instructions3");
var agentDescriptors = services
.Where(d => d.ServiceType == typeof(AIAgent) && d.ServiceKey is string)
.ToList();
Assert.Equal(3, agentDescriptors.Count);
Assert.Contains(agentDescriptors, d => (string)d.ServiceKey! == "agent1");
Assert.Contains(agentDescriptors, d => (string)d.ServiceKey! == "agent2");
Assert.Contains(agentDescriptors, d => (string)d.ServiceKey! == "agent3");
}
/// <summary>
/// Verifies that AddAIAgent handles empty strings for name.
/// </summary>
[Fact]
public void AddAIAgent_EmptyName_ThrowsArgumentException()
{
var services = new ServiceCollection();
Assert.Throws<ArgumentException>(() => services.AddAIAgent("", "instructions"));
}
/// <summary>
/// Verifies that AddAIAgent allows empty strings for instructions.
/// </summary>
[Fact]
public void AddAIAgent_EmptyInstructions_Succeeds()
{
var services = new ServiceCollection();
var result = services.AddAIAgent("agentName", "");
Assert.NotNull(result);
}
/// <summary>
/// Verifies that AddAIAgent without chat client key calls the overload with null key.
/// </summary>
[Fact]
public void AddAIAgent_WithoutKey_CallsOverloadWithNullKey()
{
var builder = new HostApplicationBuilder();
var result = builder.AddAIAgent("agentName", "instructions");
// The agent should be registered (proving the method chain worked)
var descriptor = builder.Services.FirstOrDefault(
d => d.ServiceKey is "agentName" &&
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
}
/// <summary>
/// Verifies that AddAIAgent with special characters in name works correctly for valid names.
/// </summary>
[Theory]
[InlineData("agent_name")] // underscore is allowed
[InlineData("Agent123")] // alphanumeric is allowed
[InlineData("_agent")] // can start with underscore
[InlineData("agent-name")] // dash is allowed
[InlineData("agent.name")] // period is allowed
[InlineData("agent:type")] // colon is allowed
[InlineData("my.agent_1:type-name")] // complex valid name
public void AddAIAgent_ValidSpecialCharactersInName_Succeeds(string name)
{
var builder = new HostApplicationBuilder();
var result = builder.AddAIAgent(name, "instructions");
var descriptor = builder.Services.FirstOrDefault(
d => (d.ServiceKey as string) == name &&
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
}
}

View File

@@ -0,0 +1,238 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
namespace Microsoft.Agents.AI.Hosting.UnitTests;
public class HostApplicationBuilderAgentExtensionsTests
{
/// <summary>
/// Verifies that providing a null builder to AddAIAgent throws an ArgumentNullException.
/// </summary>
[Fact]
public void AddAIAgent_NullBuilder_ThrowsArgumentNullException() =>
// Act & Assert
Assert.Throws<ArgumentNullException>(
() => HostApplicationBuilderAgentExtensions.AddAIAgent(null!, "agent", "instructions"));
/// <summary>
/// Verifies that AddAIAgent without chat client key throws ArgumentNullException for null name.
/// </summary>
[Fact]
public void AddAIAgent_NullName_ThrowsArgumentNullException()
{
// Arrange
var builder = new HostApplicationBuilder();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddAIAgent(null!, "instructions"));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verifies that AddAIAgent without chat client key allows null instructions.
/// </summary>
[Fact]
public void AddAIAgent_NullInstructions_AllowsNull()
{
var builder = new HostApplicationBuilder();
var result = builder.AddAIAgent("agentName", (string)null!);
Assert.NotNull(result);
}
/// <summary>
/// Verifies that AddAIAgent with chat client key throws ArgumentNullException for null name.
/// </summary>
[Fact]
public void AddAIAgentWithKey_NullName_ThrowsArgumentNullException()
{
// Arrange
var builder = new HostApplicationBuilder();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddAIAgent(null!, "instructions", "key"));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verifies that AddAIAgent with chat client key allows null instructions.
/// </summary>
[Fact]
public void AddAIAgentWithKey_NullInstructions_AllowsNull()
{
var builder = new HostApplicationBuilder();
var result = builder.AddAIAgent("agentName", null, "key");
Assert.NotNull(result);
}
/// <summary>
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null builder.
/// </summary>
[Fact]
public void AddAIAgentWithFactory_NullBuilder_ThrowsArgumentNullException() =>
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
HostApplicationBuilderAgentExtensions.AddAIAgent(
null!,
"agentName",
(sp, key) => new Mock<AIAgent>().Object));
/// <summary>
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null name.
/// </summary>
[Fact]
public void AddAIAgentWithFactory_NullName_ThrowsArgumentNullException()
{
// Arrange
var builder = new HostApplicationBuilder();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddAIAgent(null!, (sp, key) => new Mock<AIAgent>().Object));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null factory.
/// </summary>
[Fact]
public void AddAIAgentWithFactory_NullFactory_ThrowsArgumentNullException()
{
// Arrange
var builder = new HostApplicationBuilder();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddAIAgent("agentName", (Func<IServiceProvider, string, AIAgent>)null!));
Assert.Equal("createAgentDelegate", exception.ParamName);
}
/// <summary>
/// Verifies that AddAIAgent with factory delegate returns the same builder instance.
/// </summary>
[Fact]
public void AddAIAgentWithFactory_ValidParameters_ReturnsBuilder()
{
var builder = new HostApplicationBuilder();
var mockAgent = new Mock<AIAgent>();
var result = builder.AddAIAgent("agentName", (sp, key) => mockAgent.Object);
Assert.NotNull(result);
}
/// <summary>
/// Verifies that AddAIAgent registers the agent as a keyed singleton service.
/// </summary>
[Fact]
public void AddAIAgent_RegistersKeyedSingleton()
{
// Arrange
var builder = new HostApplicationBuilder();
var mockAgent = new Mock<AIAgent>();
const string AgentName = "testAgent";
// Act
builder.AddAIAgent(AgentName, (sp, key) => mockAgent.Object);
// Assert
var descriptor = builder.Services.FirstOrDefault(
d => (d.ServiceKey as string) == AgentName &&
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime);
}
/// <summary>
/// Verifies that AddAIAgent can be called multiple times with different agent names.
/// </summary>
[Fact]
public void AddAIAgent_MultipleCalls_RegistersMultipleAgents()
{
// Arrange
var builder = new HostApplicationBuilder();
// Act
builder.AddAIAgent("agent1", "instructions1");
builder.AddAIAgent("agent2", "instructions2");
builder.AddAIAgent("agent3", "instructions3");
// Assert
var agentDescriptors = builder.Services
.Where(d => d.ServiceType == typeof(AIAgent) && d.ServiceKey is string)
.ToList();
Assert.Equal(3, agentDescriptors.Count);
Assert.Contains(agentDescriptors, d => (string)d.ServiceKey! == "agent1");
Assert.Contains(agentDescriptors, d => (string)d.ServiceKey! == "agent2");
Assert.Contains(agentDescriptors, d => (string)d.ServiceKey! == "agent3");
}
/// <summary>
/// Verifies that AddAIAgent handles empty strings for name.
/// </summary>
[Fact]
public void AddAIAgent_EmptyName_ThrowsArgumentException()
{
// Arrange
var builder = new HostApplicationBuilder();
// Act & Assert
Assert.Throws<ArgumentException>(() =>
builder.AddAIAgent("", "instructions"));
}
/// <summary>
/// Verifies that AddAIAgent allows empty strings for instructions.
/// </summary>
[Fact]
public void AddAIAgent_EmptyInstructions_Succeeds()
{
var builder = new HostApplicationBuilder();
var result = builder.AddAIAgent("agentName", "");
Assert.NotNull(result);
}
/// <summary>
/// Verifies that AddAIAgent without chat client key calls the overload with null key.
/// </summary>
[Fact]
public void AddAIAgent_WithoutKey_CallsOverloadWithNullKey()
{
var builder = new HostApplicationBuilder();
var result = builder.AddAIAgent("agentName", "instructions");
// The agent should be registered (proving the method chain worked)
var descriptor = builder.Services.FirstOrDefault(
d => d.ServiceKey is "agentName" &&
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
}
/// <summary>
/// Verifies that AddAIAgent with special characters in name works correctly for valid names.
/// </summary>
[Theory]
[InlineData("agent_name")] // underscore is allowed
[InlineData("Agent123")] // alphanumeric is allowed
[InlineData("_agent")] // can start with underscore
[InlineData("agent-name")] // dash is allowed
[InlineData("agent.name")] // period is allowed
[InlineData("agent:type")] // colon is allowed
[InlineData("my.agent_1:type-name")] // complex valid name
public void AddAIAgent_ValidSpecialCharactersInName_Succeeds(string name)
{
var builder = new HostApplicationBuilder();
var result = builder.AddAIAgent(name, "instructions");
var descriptor = builder.Services.FirstOrDefault(
d => (d.ServiceKey as string) == name &&
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
}
}

View File

@@ -0,0 +1,342 @@
// 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
{
/// <summary>
/// Verifies that providing a null builder to AddWorkflow throws an ArgumentNullException.
/// </summary>
[Fact]
public void AddWorkflow_NullBuilder_ThrowsArgumentNullException() =>
Assert.Throws<ArgumentNullException>(
() => HostApplicationBuilderWorkflowExtensions.AddWorkflow(
null!,
"workflow",
(sp, key) => CreateTestWorkflow(key)));
/// <summary>
/// Verifies that AddWorkflow throws ArgumentNullException for null name.
/// </summary>
[Fact]
public void AddWorkflow_NullName_ThrowsArgumentNullException()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddWorkflow(null!, (sp, key) => CreateTestWorkflow(key)));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verifies that AddWorkflow throws ArgumentNullException for null factory delegate.
/// </summary>
[Fact]
public void AddWorkflow_NullFactory_ThrowsArgumentNullException()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddWorkflow("workflowName", null!));
Assert.Equal("createWorkflowDelegate", exception.ParamName);
}
/// <summary>
/// Verifies that AddWorkflow returns the IHostWorkflowBuilder instance.
/// </summary>
[Fact]
public void AddWorkflow_ValidParameters_ReturnsBuilder()
{
var builder = new HostApplicationBuilder();
var result = builder.AddWorkflow("workflowName", (sp, key) => CreateTestWorkflow(key));
Assert.NotNull(result);
Assert.IsType<IHostedWorkflowBuilder>(result, exactMatch: false);
}
/// <summary>
/// Verifies that AddWorkflow registers the workflow as a keyed singleton service.
/// </summary>
[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);
}
/// <summary>
/// Verifies that AddWorkflow can be called multiple times with different workflow names.
/// </summary>
[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");
}
/// <summary>
/// Verifies that AddWorkflow handles empty strings for name.
/// </summary>
[Fact]
public void AddWorkflow_EmptyName_ThrowsArgumentException()
{
var builder = new HostApplicationBuilder();
var result = builder.AddWorkflow("", (sp, key) => CreateTestWorkflow(key));
Assert.NotNull(result);
}
/// <summary>
/// Verifies that AddWorkflow with special characters in name works correctly for valid names.
/// </summary>
[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);
}
/// <summary>
/// Verifies that AddAsAIAgent without a name parameter uses the workflow name as the agent name.
/// </summary>
[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);
}
/// <summary>
/// Verifies that AddAsAIAgent with a name parameter uses that name instead of the workflow name.
/// </summary>
[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);
}
/// <summary>
/// Verifies that AddAsAIAgent correctly retrieves the workflow using the workflow name, not the agent name.
/// </summary>
[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<AIAgent>(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<Workflow>(WorkflowName);
Assert.NotNull(workflow);
Assert.Equal(WorkflowName, workflow.Name);
}
/// <summary>
/// Verifies that AddAsAIAgent returns IHostedAgentBuilder with correct name.
/// </summary>
[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<IHostedAgentBuilder>(agentBuilder, exactMatch: false);
Assert.Equal(AgentName, agentBuilder.Name);
}
/// <summary>
/// Verifies that AddAsAIAgent without name returns IHostedAgentBuilder with workflow name.
/// </summary>
[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<IHostedAgentBuilder>(agentBuilder, exactMatch: false);
Assert.Equal(WorkflowName, agentBuilder.Name);
}
/// <summary>
/// Verifies that AddAsAIAgent can chain multiple agents from the same workflow.
/// </summary>
[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);
}
/// <summary>
/// Verifies that AddAsAIAgent with null name behaves the same as the parameterless overload.
/// </summary>
[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);
}
/// <summary>
/// Verifies that AddAsAIAgent with empty string name uses empty string as agent name.
/// </summary>
[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);
}
/// <summary>
/// Helper method to create a simple test workflow with a given name.
/// </summary>
private static Workflow CreateTestWorkflow(string name)
{
// Create a simple workflow using AgentWorkflowBuilder
var mockAgent = new Mock<AIAgent>();
mockAgent.Setup(a => a.Name).Returns("testAgent");
return AgentWorkflowBuilder.BuildSequential(workflowName: name, agents: [mockAgent.Object]);
}
}

View File

@@ -0,0 +1,283 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Agents.AI.Hosting.UnitTests;
/// <summary>
/// Unit tests for AI tool registration extensions on <see cref="IHostedAgentBuilder"/>.
/// </summary>
public sealed class HostedAgentBuilderToolsExtensionsTests
{
[Fact]
public void WithAITool_ThrowsWhenBuilderIsNull()
{
var tool = new DummyAITool();
Assert.Throws<ArgumentNullException>(() => HostedAgentBuilderExtensions.WithAITool(null!, tool));
}
[Fact]
public void WithAITool_ThrowsWhenToolIsNull()
{
var services = new ServiceCollection();
var builder = services.AddAIAgent("test-agent", "Test instructions");
Assert.Throws<ArgumentNullException>(() => builder.WithAITool(tool: null!));
}
[Fact]
public void WithAITools_ThrowsWhenBuilderIsNull()
{
var tools = new[] { new DummyAITool() };
Assert.Throws<ArgumentNullException>(() => HostedAgentBuilderExtensions.WithAITools(null!, tools));
}
[Fact]
public void WithAITools_ThrowsWhenToolsArrayIsNull()
{
var services = new ServiceCollection();
var builder = services.AddAIAgent("test-agent", "Test instructions");
Assert.Throws<ArgumentNullException>(() => builder.WithAITools(null!));
}
[Fact]
public void RegisteredTools_ResolvesAllToolsForAgent()
{
var services = new ServiceCollection();
services.AddSingleton<IChatClient>(new MockChatClient());
var builder = services.AddAIAgent("test-agent", "Test instructions");
var tool1 = new DummyAITool();
var tool2 = new DummyAITool();
builder
.WithAITool(tool1)
.WithAITool(tool2);
var serviceProvider = services.BuildServiceProvider();
var agent1Tools = ResolveToolsFromAgent(serviceProvider, "test-agent");
Assert.Contains(tool1, agent1Tools);
Assert.Contains(tool2, agent1Tools);
var agent1ToolsDI = ResolveToolsFromDI(serviceProvider, "test-agent");
Assert.Contains(tool1, agent1ToolsDI);
Assert.Contains(tool2, agent1ToolsDI);
}
[Fact]
public void RegisteredTools_IsolatedPerAgent()
{
var services = new ServiceCollection();
services.AddSingleton<IChatClient>(new MockChatClient());
var builder1 = services.AddAIAgent("agent1", "Agent 1 instructions");
var builder2 = services.AddAIAgent("agent2", "Agent 2 instructions");
var tool1 = new DummyAITool();
var tool2 = new DummyAITool();
var tool3 = new DummyAITool();
builder1
.WithAITool(tool1)
.WithAITool(tool2);
builder2
.WithAITool(tool3);
var serviceProvider = services.BuildServiceProvider();
var agent1Tools = ResolveToolsFromAgent(serviceProvider, "agent1");
var agent2Tools = ResolveToolsFromAgent(serviceProvider, "agent2");
var agent1ToolsDI = ResolveToolsFromDI(serviceProvider, "agent1");
var agent2ToolsDI = ResolveToolsFromDI(serviceProvider, "agent2");
Assert.Contains(tool1, agent1Tools);
Assert.Contains(tool2, agent1Tools);
Assert.Contains(tool1, agent1ToolsDI);
Assert.Contains(tool2, agent1ToolsDI);
Assert.Contains(tool3, agent2Tools);
Assert.Contains(tool3, agent2ToolsDI);
}
private static IList<AITool> ResolveToolsFromAgent(IServiceProvider serviceProvider, string name)
{
var agent = serviceProvider.GetRequiredKeyedService<AIAgent>(name) as ChatClientAgent;
Assert.NotNull(agent?.ChatOptions?.Tools);
return agent.ChatOptions.Tools;
}
private static List<AITool> ResolveToolsFromDI(IServiceProvider serviceProvider, string name)
{
var tools = serviceProvider.GetKeyedServices<AITool>(name);
Assert.NotNull(tools);
return tools.ToList();
}
[Fact]
public void WithAIToolFactory_ThrowsWhenBuilderIsNull()
{
Assert.Throws<ArgumentNullException>(() => HostedAgentBuilderExtensions.WithAITool(null!, CreateTool));
static AITool CreateTool(IServiceProvider _) => new DummyAITool();
}
[Fact]
public void WithAIToolFactory_ThrowsWhenFactoryIsNull()
{
var services = new ServiceCollection();
var builder = services.AddAIAgent("test-agent", "Test instructions");
Assert.Throws<ArgumentNullException>(() => builder.WithAITool(factory: null!));
}
[Fact]
public void WithAIToolFactory_RegistersToolFromFactory()
{
var services = new ServiceCollection();
services.AddSingleton<IChatClient>(new MockChatClient());
DummyAITool? createdTool = null;
var builder = services.AddAIAgent("test-agent", "Test instructions");
builder.WithAITool(sp =>
{
createdTool = new DummyAITool();
return createdTool;
});
var serviceProvider = services.BuildServiceProvider();
var tools = ResolveToolsFromDI(serviceProvider, "test-agent");
Assert.Single(tools);
Assert.Same(createdTool, tools[0]);
}
[Fact]
public void WithAIToolFactory_CanAccessServicesFromFactory()
{
var services = new ServiceCollection();
var mockChatClient = new MockChatClient();
services.AddSingleton<IChatClient>(mockChatClient);
IChatClient? resolvedChatClient = null;
var builder = services.AddAIAgent("test-agent", "Test instructions");
builder.WithAITool(sp =>
{
resolvedChatClient = sp.GetService<IChatClient>();
return new DummyAITool();
});
var serviceProvider = services.BuildServiceProvider();
_ = ResolveToolsFromDI(serviceProvider, "test-agent");
Assert.Same(mockChatClient, resolvedChatClient);
}
[Fact]
public void WithAIToolFactory_ToolsAreIsolatedPerAgent()
{
var services = new ServiceCollection();
services.AddSingleton<IChatClient>(new MockChatClient());
var tool1 = new DummyAITool();
var tool2 = new DummyAITool();
var builder1 = services.AddAIAgent("agent1", "Agent 1 instructions");
var builder2 = services.AddAIAgent("agent2", "Agent 2 instructions");
builder1.WithAITool(_ => tool1);
builder2.WithAITool(_ => tool2);
var serviceProvider = services.BuildServiceProvider();
var agent1Tools = ResolveToolsFromDI(serviceProvider, "agent1");
var agent2Tools = ResolveToolsFromDI(serviceProvider, "agent2");
Assert.Single(agent1Tools);
Assert.Contains(tool1, agent1Tools);
Assert.DoesNotContain(tool2, agent1Tools);
Assert.Single(agent2Tools);
Assert.Contains(tool2, agent2Tools);
Assert.DoesNotContain(tool1, agent2Tools);
}
[Fact]
public void WithAIToolFactory_CanCombineWithDirectToolRegistration()
{
var services = new ServiceCollection();
services.AddSingleton<IChatClient>(new MockChatClient());
var directTool = new DummyAITool();
var factoryTool = new DummyAITool();
var builder = services.AddAIAgent("test-agent", "Test instructions");
builder
.WithAITool(directTool)
.WithAITool(_ => factoryTool);
var serviceProvider = services.BuildServiceProvider();
var tools = ResolveToolsFromDI(serviceProvider, "test-agent");
Assert.Equal(2, tools.Count);
Assert.Contains(directTool, tools);
Assert.Contains(factoryTool, tools);
}
[Fact]
public void WithAIToolFactory_ToolsAvailableOnAgent()
{
var services = new ServiceCollection();
services.AddSingleton<IChatClient>(new MockChatClient());
var factoryTool = new DummyAITool();
var builder = services.AddAIAgent("test-agent", "Test instructions");
builder.WithAITool(_ => factoryTool);
var serviceProvider = services.BuildServiceProvider();
var agentTools = ResolveToolsFromAgent(serviceProvider, "test-agent");
Assert.Contains(factoryTool, agentTools);
}
/// <summary>
/// Dummy AITool implementation for testing.
/// </summary>
private sealed class DummyAITool : AITool;
/// <summary>
/// Mock chat client for testing.
/// </summary>
private sealed class MockChatClient : IChatClient
{
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public object? GetService(Type serviceType, object? serviceKey = null)
{
return null;
}
public void Dispose()
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting\Microsoft.Agents.AI.Hosting.csproj" />
</ItemGroup>
</Project>