// 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; using Moq; namespace Microsoft.Agents.AI.Hosting.UnitTests; /// /// Unit tests for AI tool registration extensions on . /// public sealed class HostedAgentBuilderToolsExtensionsTests { [Fact] public void WithAITool_ThrowsWhenBuilderIsNull() { var tool = new DummyAITool(); Assert.Throws(() => HostedAgentBuilderExtensions.WithAITool(null!, tool)); } [Fact] public void WithAITool_ThrowsWhenToolIsNull() { var services = new ServiceCollection(); var builder = services.AddAIAgent("test-agent", "Test instructions"); Assert.Throws(() => builder.WithAITool(tool: null!)); } [Fact] public void WithAITools_ThrowsWhenBuilderIsNull() { var tools = new[] { new DummyAITool() }; Assert.Throws(() => HostedAgentBuilderExtensions.WithAITools(null!, tools)); } [Fact] public void WithAITools_ThrowsWhenToolsArrayIsNull() { var services = new ServiceCollection(); var builder = services.AddAIAgent("test-agent", "Test instructions"); Assert.Throws(() => builder.WithAITools(null!)); } [Fact] public void RegisteredTools_ResolvesAllToolsForAgent() { var services = new ServiceCollection(); services.AddSingleton(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(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 ResolveToolsFromAgent(IServiceProvider serviceProvider, string name) { var agent = serviceProvider.GetRequiredKeyedService(name) as ChatClientAgent; Assert.NotNull(agent?.ChatOptions?.Tools); return agent.ChatOptions.Tools; } private static List ResolveToolsFromDI(IServiceProvider serviceProvider, string name) { var tools = serviceProvider.GetKeyedServices(name); Assert.NotNull(tools); return tools.ToList(); } [Fact] public void WithAIToolFactory_ThrowsWhenBuilderIsNull() { Assert.Throws(() => 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(() => builder.WithAITool(factory: null!)); } [Fact] public void WithAIToolFactory_RegistersToolFromFactory() { var services = new ServiceCollection(); services.AddSingleton(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(mockChatClient); IChatClient? resolvedChatClient = null; var builder = services.AddAIAgent("test-agent", "Test instructions"); builder.WithAITool(sp => { resolvedChatClient = sp.GetService(); 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(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(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(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); } /// /// Verifies that WithAITool factory method defaults to the agent's lifetime when no explicit lifetime is specified. /// [Theory] [InlineData(ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Transient)] public void WithAIToolFactory_DefaultsToAgentLifetime(ServiceLifetime agentLifetime) { // Arrange var services = new ServiceCollection(); var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, agentLifetime); // Act builder.WithAITool(_ => new DummyAITool()); // Assert var toolDescriptor = services.FirstOrDefault( d => (d.ServiceKey as string) == "test-agent" && d.ServiceType == typeof(AITool)); Assert.NotNull(toolDescriptor); Assert.Equal(agentLifetime, toolDescriptor.Lifetime); } /// /// Verifies that WithAITool factory method accepts an explicit lifetime override. /// [Fact] public void WithAIToolFactory_ExplicitLifetimeOverridesDefault() { // Arrange var services = new ServiceCollection(); var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, ServiceLifetime.Transient); // Act - Transient agent with Singleton tool is valid (longer-lived dependency) builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Singleton); // Assert var toolDescriptor = services.FirstOrDefault( d => (d.ServiceKey as string) == "test-agent" && d.ServiceType == typeof(AITool)); Assert.NotNull(toolDescriptor); Assert.Equal(ServiceLifetime.Singleton, toolDescriptor.Lifetime); } /// /// Verifies that WithAITool factory throws for singleton agent with scoped tool (captive dependency). /// [Fact] public void WithAIToolFactory_SingletonAgentWithScopedTool_ThrowsInvalidOperationException() { // Arrange var services = new ServiceCollection(); var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, ServiceLifetime.Singleton); // Act & Assert Assert.Throws(() => builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Scoped)); } /// /// Verifies that WithAITool factory throws for singleton agent with transient tool (captive dependency). /// [Fact] public void WithAIToolFactory_SingletonAgentWithTransientTool_ThrowsInvalidOperationException() { // Arrange var services = new ServiceCollection(); var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, ServiceLifetime.Singleton); // Act & Assert Assert.Throws(() => builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Transient)); } /// /// Verifies that WithAITool factory throws for scoped agent with transient tool (captive dependency). /// [Fact] public void WithAIToolFactory_ScopedAgentWithTransientTool_ThrowsInvalidOperationException() { // Arrange var services = new ServiceCollection(); var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, ServiceLifetime.Scoped); // Act & Assert Assert.Throws(() => builder.WithAITool(_ => new DummyAITool(), ServiceLifetime.Transient)); } /// /// Verifies all valid tool lifetime combinations do not throw. /// [Theory] [InlineData(ServiceLifetime.Singleton, ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Scoped, ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Scoped, ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Transient, ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Transient, ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Transient, ServiceLifetime.Transient)] public void WithAIToolFactory_ValidLifetimeCombinations_DoNotThrow(ServiceLifetime agentLifetime, ServiceLifetime toolLifetime) { // Arrange var services = new ServiceCollection(); var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, agentLifetime); // Act & Assert - should not throw builder.WithAITool(_ => new DummyAITool(), toolLifetime); } /// /// Verifies that ValidateToolLifetime correctly identifies all invalid combinations. /// [Theory] [InlineData(ServiceLifetime.Singleton, ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Singleton, ServiceLifetime.Transient)] [InlineData(ServiceLifetime.Scoped, ServiceLifetime.Transient)] public void ValidateToolLifetime_InvalidCombinations_Throw(ServiceLifetime agentLifetime, ServiceLifetime toolLifetime) { // Act & Assert Assert.Throws(() => HostedAgentBuilderExtensions.ValidateToolLifetime(agentLifetime, toolLifetime)); } /// /// Verifies that the WithSessionStore factory method defaults to Singleton regardless of agent lifetime. /// [Theory] [InlineData(ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Transient)] public void WithSessionStoreFactory_DefaultsToSingleton(ServiceLifetime agentLifetime) { // Arrange var services = new ServiceCollection(); var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, agentLifetime); // Act builder.WithSessionStore((sp, name) => new InMemoryAgentSessionStore()); // Assert var storeDescriptor = services.FirstOrDefault( d => (d.ServiceKey as string) == "test-agent" && d.ServiceType == typeof(AgentSessionStore)); Assert.NotNull(storeDescriptor); Assert.Equal(ServiceLifetime.Singleton, storeDescriptor.Lifetime); } /// /// Verifies that the WithSessionStore factory method accepts an explicit lifetime override. /// [Fact] public void WithSessionStoreFactory_ExplicitLifetimeOverridesDefault() { // Arrange var services = new ServiceCollection(); var builder = services.AddAIAgent("test-agent", (sp, key) => new Mock().Object, ServiceLifetime.Transient); // Act builder.WithSessionStore((sp, name) => new InMemoryAgentSessionStore(), ServiceLifetime.Singleton); // Assert var storeDescriptor = services.FirstOrDefault( d => (d.ServiceKey as string) == "test-agent" && d.ServiceType == typeof(AgentSessionStore)); Assert.NotNull(storeDescriptor); Assert.Equal(ServiceLifetime.Singleton, storeDescriptor.Lifetime); } /// /// Dummy AITool implementation for testing. /// private sealed class DummyAITool : AITool; /// /// Mock chat client for testing. /// private sealed class MockChatClient : IChatClient { public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable 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(); } } }