// 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
{
///
/// Verifies that providing a null builder to AddAIAgent throws an ArgumentNullException.
///
[Fact]
public void AddAIAgent_NullBuilder_ThrowsArgumentNullException() => Assert.Throws(
() => AgentHostingServiceCollectionExtensions.AddAIAgent(null!, "agent", "instructions"));
///
/// Verifies that AddAIAgent without chat client key throws ArgumentNullException for null name.
///
[Fact]
public void AddAIAgent_NullName_ThrowsArgumentNullException()
{
var services = new ServiceCollection();
var exception = Assert.Throws(() => services.AddAIAgent(null!, "instructions"));
Assert.Equal("name", exception.ParamName);
}
///
/// Verifies that AddAIAgent without chat client key allows null instructions.
///
[Fact]
public void AddAIAgent_NullInstructions_AllowsNull()
{
var services = new ServiceCollection();
var result = services.AddAIAgent("agentName", (string)null!);
Assert.NotNull(result);
}
///
/// Verifies that AddAIAgent with chat client key throws ArgumentNullException for null name.
///
[Fact]
public void AddAIAgentWithKey_NullName_ThrowsArgumentNullException()
{
var services = new ServiceCollection();
var exception = Assert.Throws(() => services.AddAIAgent(null!, "instructions", "key"));
Assert.Equal("name", exception.ParamName);
}
///
/// Verifies that AddAIAgent with chat client key allows null instructions.
///
[Fact]
public void AddAIAgentWithKey_NullInstructions_AllowsNull()
{
var services = new ServiceCollection();
var result = services.AddAIAgent("agentName", null, "key");
Assert.NotNull(result);
}
///
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null builder.
///
[Fact]
public void AddAIAgentWithFactory_NullBuilder_ThrowsArgumentNullException() =>
Assert.Throws(() =>
AgentHostingServiceCollectionExtensions.AddAIAgent(null!, "agentName", (sp, key) => new Mock().Object));
///
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null name.
///
[Fact]
public void AddAIAgentWithFactory_NullName_ThrowsArgumentNullException()
{
var services = new ServiceCollection();
var exception = Assert.Throws(() => services.AddAIAgent(null!, (sp, key) => new Mock().Object));
Assert.Equal("name", exception.ParamName);
}
///
/// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null factory.
///
[Fact]
public void AddAIAgentWithFactory_NullFactory_ThrowsArgumentNullException()
{
var services = new ServiceCollection();
var exception = Assert.Throws(() => services.AddAIAgent("agentName", (Func)null!));
Assert.Equal("createAgentDelegate", exception.ParamName);
}
///
/// Verifies that AddAIAgent with factory delegate returns the same builder instance.
///
[Fact]
public void AddAIAgentWithFactory_ValidParameters_ReturnsBuilder()
{
var services = new ServiceCollection();
var mockAgent = new Mock();
var result = services.AddAIAgent("agentName", (sp, key) => mockAgent.Object);
Assert.NotNull(result);
}
///
/// Verifies that AddAIAgent registers the agent as a keyed singleton service by default.
///
[Fact]
public void AddAIAgent_RegistersKeyedSingleton()
{
var services = new ServiceCollection();
var mockAgent = new Mock();
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);
}
///
/// Verifies that AddAIAgent can be called multiple times with different agent names.
///
[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");
}
///
/// Verifies that AddAIAgent handles empty strings for name.
///
[Fact]
public void AddAIAgent_EmptyName_ThrowsArgumentException()
{
var services = new ServiceCollection();
Assert.Throws(() => services.AddAIAgent("", "instructions"));
}
///
/// Verifies that AddAIAgent allows empty strings for instructions.
///
[Fact]
public void AddAIAgent_EmptyInstructions_Succeeds()
{
var services = new ServiceCollection();
var result = services.AddAIAgent("agentName", "");
Assert.NotNull(result);
}
///
/// Verifies that AddAIAgent without chat client key calls the overload with null key.
///
[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);
}
///
/// Verifies that AddAIAgent with special characters in name works correctly for valid names.
///
[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);
}
///
/// Verifies that AddAIAgent registers with the specified scoped lifetime.
///
[Fact]
public void AddAIAgent_WithScopedLifetime_RegistersKeyedScoped()
{
// Arrange
var services = new ServiceCollection();
var mockAgent = new Mock();
const string AgentName = "scopedAgent";
// Act
var result = services.AddAIAgent(AgentName, (sp, key) => mockAgent.Object, ServiceLifetime.Scoped);
// Assert
var descriptor = services.FirstOrDefault(
d => (d.ServiceKey as string) == AgentName &&
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime);
Assert.Equal(ServiceLifetime.Scoped, result.Lifetime);
}
///
/// Verifies that AddAIAgent registers with the specified transient lifetime.
///
[Fact]
public void AddAIAgent_WithTransientLifetime_RegistersKeyedTransient()
{
// Arrange
var services = new ServiceCollection();
var mockAgent = new Mock();
const string AgentName = "transientAgent";
// Act
var result = services.AddAIAgent(AgentName, (sp, key) => mockAgent.Object, ServiceLifetime.Transient);
// Assert
var descriptor = services.FirstOrDefault(
d => (d.ServiceKey as string) == AgentName &&
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
Assert.Equal(ServiceLifetime.Transient, descriptor.Lifetime);
Assert.Equal(ServiceLifetime.Transient, result.Lifetime);
}
///
/// Verifies that the builder exposes the correct lifetime for default registration.
///
[Fact]
public void AddAIAgent_DefaultLifetime_BuilderExposesSingleton()
{
// Arrange
var services = new ServiceCollection();
var mockAgent = new Mock();
// Act
var result = services.AddAIAgent("agentName", (sp, key) => mockAgent.Object);
// Assert
Assert.Equal(ServiceLifetime.Singleton, result.Lifetime);
}
///
/// Verifies that AddAIAgent with instructions overload respects the lifetime parameter.
///
[Theory]
[InlineData(ServiceLifetime.Singleton)]
[InlineData(ServiceLifetime.Scoped)]
[InlineData(ServiceLifetime.Transient)]
public void AddAIAgent_InstructionsOverload_RespectsLifetime(ServiceLifetime lifetime)
{
// Arrange
var services = new ServiceCollection();
// Act
var result = services.AddAIAgent("agent", "instructions", lifetime);
// Assert
var descriptor = services.FirstOrDefault(
d => (d.ServiceKey as string) == "agent" &&
d.ServiceType == typeof(AIAgent));
Assert.NotNull(descriptor);
Assert.Equal(lifetime, descriptor.Lifetime);
Assert.Equal(lifetime, result.Lifetime);
}
}