// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; namespace Microsoft.Extensions.AI.Agents.Hosting.UnitTests; public class HostApplicationBuilderAgentExtensionsTests { /// /// Verifies that providing a null builder to AddAIAgent throws an ArgumentNullException. /// [Fact] public void AddAIAgent_NullBuilder_ThrowsArgumentNullException() { // Act & Assert Assert.Throws( () => HostApplicationBuilderAgentExtensions.AddAIAgent(null!, "agent", "instructions")); } /// /// Verifies that AddAIAgent with valid parameters returns the same builder instance. /// /// The chat client key to use, or null to use the default service. [Theory] [InlineData(null)] [InlineData("customKey")] public void AddAIAgent_ValidParameters_ReturnsBuilder(string? chatClientKey) { // Arrange var builder = new HostApplicationBuilder(); // Act var result = builder.AddAIAgent("agentName", "instructions", chatClientKey); // Assert Assert.Same(builder, result); } /// /// Verifies that AddAIAgent without chat client key throws ArgumentNullException for null name. /// [Fact] public void AddAIAgent_NullName_ThrowsArgumentNullException() { // Arrange var builder = new HostApplicationBuilder(); // Act & Assert var exception = Assert.Throws(() => builder.AddAIAgent(null!, "instructions")); Assert.Equal("name", exception.ParamName); } /// /// Verifies that AddAIAgent without chat client key allows null instructions. /// [Fact] public void AddAIAgent_NullInstructions_AllowsNull() { // Arrange var builder = new HostApplicationBuilder(); // Act var result = builder.AddAIAgent("agentName", (string)null!); // Assert Assert.Same(builder, result); } /// /// Verifies that AddAIAgent with chat client key throws ArgumentNullException for null name. /// [Fact] public void AddAIAgentWithKey_NullName_ThrowsArgumentNullException() { // Arrange var builder = new HostApplicationBuilder(); // Act & Assert var exception = Assert.Throws(() => builder.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() { // Arrange var builder = new HostApplicationBuilder(); // Act var result = builder.AddAIAgent("agentName", null!, "key"); // Assert Assert.Same(builder, result); } /// /// Verifies that AddAIAgent with factory delegate throws ArgumentNullException for null builder. /// [Fact] public void AddAIAgentWithFactory_NullBuilder_ThrowsArgumentNullException() { // Act & Assert Assert.Throws(() => HostApplicationBuilderAgentExtensions.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() { // Arrange var builder = new HostApplicationBuilder(); // Act & Assert var exception = Assert.Throws(() => builder.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() { // Arrange var builder = new HostApplicationBuilder(); // Act & Assert var exception = Assert.Throws(() => builder.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() { // Arrange var builder = new HostApplicationBuilder(); var mockAgent = new Mock(); // Act var result = builder.AddAIAgent("agentName", (sp, key) => mockAgent.Object); // Assert Assert.Same(builder, result); } /// /// Verifies that AddAIAgent registers the agent as a keyed singleton service. /// [Fact] public void AddAIAgent_RegistersKeyedSingleton() { // Arrange var builder = new HostApplicationBuilder(); var mockAgent = new Mock(); 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); } /// /// Verifies that AddAIAgent can be called multiple times with different agent names. /// [Fact] public void AddAIAgent_MultipleCalls_RegistersMultipleAgents() { // Arrange var builder = new HostApplicationBuilder(); // Act builder.AddAIAgent("agent1", "instructions1") .AddAIAgent("agent2", "instructions2") .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"); } /// /// Verifies that AddAIAgent handles empty strings for name. /// [Fact] public void AddAIAgent_EmptyName_ThrowsArgumentException() { // Arrange var builder = new HostApplicationBuilder(); // Act & Assert Assert.Throws(() => builder.AddAIAgent("", "instructions")); } /// /// Verifies that AddAIAgent allows empty strings for instructions. /// [Fact] public void AddAIAgent_EmptyInstructions_Succeeds() { // Arrange var builder = new HostApplicationBuilder(); // Act var result = builder.AddAIAgent("agentName", ""); // Assert Assert.Same(builder, result); } /// /// Verifies that AddAIAgent with whitespace name throws ArgumentException. /// [Theory] [InlineData(" ")] [InlineData("\t")] [InlineData(" agent ")] public void AddAIAgent_WhitespaceName_ThrowsArgumentException(string name) { // Arrange var builder = new HostApplicationBuilder(); // Act & Assert var exception = Assert.Throws(() => builder.AddAIAgent(name, "instructions")); Assert.Contains("Invalid type", exception.Message); } /// /// Verifies that AddAIAgent without chat client key calls the overload with null key. /// [Fact] public void AddAIAgent_WithoutKey_CallsOverloadWithNullKey() { // Arrange var builder = new HostApplicationBuilder(); // Act var result = builder.AddAIAgent("agentName", "instructions"); // Assert Assert.Same(builder, result); // The agent should be registered (proving the method chain worked) var descriptor = builder.Services.FirstOrDefault( d => d.ServiceKey as string == "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) { // Arrange var builder = new HostApplicationBuilder(); // Act var result = builder.AddAIAgent(name, "instructions"); // Assert Assert.Same(builder, result); var descriptor = builder.Services.FirstOrDefault( d => d.ServiceKey as string == name && d.ServiceType == typeof(AIAgent)); Assert.NotNull(descriptor); } /// /// Verifies that AddAIAgent with invalid special characters throws ArgumentException. /// [Theory] [InlineData("特殊字符")] // non-ASCII not allowed [InlineData("123agent")] // cannot start with number [InlineData("agent@name")] // @ not allowed [InlineData("agent/name")] // / not allowed [InlineData("agent name")] // space not allowed [InlineData(".agent")] // cannot start with period [InlineData("-agent")] // cannot start with dash [InlineData(":agent")] // cannot start with colon public void AddAIAgent_InvalidSpecialCharactersInName_ThrowsArgumentException(string name) { // Arrange var builder = new HostApplicationBuilder(); // Act & Assert var exception = Assert.Throws(() => builder.AddAIAgent(name, "instructions")); Assert.Contains("Invalid type", exception.Message); } }