// 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.UnitTests; /// /// Unit tests for the class. /// public class AIAgentBuilderTests { /// /// Verify that constructor throws ArgumentNullException when innerAgent is null. /// [Fact] public void Constructor_WithNullInnerAgent_ThrowsArgumentNullException() { // Act & Assert Assert.Throws("innerAgent", () => new AIAgentBuilder((AIAgent)null!)); } /// /// Verify that constructor throws ArgumentNullException when innerAgentFactory is null. /// [Fact] public void Constructor_WithNullInnerAgentFactory_ThrowsArgumentNullException() { // Act & Assert Assert.Throws("innerAgentFactory", () => new AIAgentBuilder((Func)null!)); } /// /// Verify that Build returns the inner agent when no middleware is added. /// [Fact] public void Build_WithNoMiddleware_ReturnsInnerAgent() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act var result = builder.Build(); // Assert Assert.Same(mockAgent.Object, result); } /// /// Verify that Build works with factory function. /// [Fact] public void Build_WithFactory_ReturnsAgentFromFactory() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(_ => mockAgent.Object); // Act var result = builder.Build(); // Assert Assert.Same(mockAgent.Object, result); } /// /// Verify that Use with simple factory works correctly. /// [Fact] public void Use_WithSimpleFactory_AppliesMiddleware() { // Arrange var mockInnerAgent = new Mock(); var mockOuterAgent = new Mock(); var builder = new AIAgentBuilder(mockInnerAgent.Object); // Act var result = builder.Use(innerAgent => { Assert.Same(mockInnerAgent.Object, innerAgent); return mockOuterAgent.Object; }).Build(); // Assert Assert.Same(mockOuterAgent.Object, result); } /// /// Verify that Use with service provider factory works correctly. /// [Fact] public void Use_WithServiceProviderFactory_AppliesMiddleware() { // Arrange var mockInnerAgent = new Mock(); var mockOuterAgent = new Mock(); var mockServiceProvider = new Mock(); var builder = new AIAgentBuilder(mockInnerAgent.Object); // Act var result = builder.Use((innerAgent, services) => { Assert.Same(mockInnerAgent.Object, innerAgent); Assert.NotNull(services); return mockOuterAgent.Object; }).Build(mockServiceProvider.Object); // Assert Assert.Same(mockOuterAgent.Object, result); } /// /// Verify that multiple middleware are applied in correct order (first added is outermost). /// [Fact] public void Use_WithMultipleMiddleware_AppliesInCorrectOrder() { // Arrange var mockInnerAgent = new Mock(); var mockMiddleAgent = new Mock(); var mockOuterAgent = new Mock(); var builder = new AIAgentBuilder(mockInnerAgent.Object); // Act var result = builder .Use(innerAgent => { // First middleware added (will be outermost) - should receive result of second middleware Assert.Same(mockMiddleAgent.Object, innerAgent); return mockOuterAgent.Object; }) .Use(innerAgent => { // Second middleware added (will be applied first) - should receive the original inner agent Assert.Same(mockInnerAgent.Object, innerAgent); return mockMiddleAgent.Object; }) .Build(); // Assert // The result should be from the first middleware since it's the outermost Assert.Same(mockOuterAgent.Object, result); } /// /// Verify that Use throws ArgumentNullException when agentFactory is null. /// [Fact] public void Use_WithNullSimpleFactory_ThrowsArgumentNullException() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act & Assert Assert.Throws("agentFactory", () => builder.Use((Func)null!)); } /// /// Verify that Use throws ArgumentNullException when agentFactory with service provider is null. /// [Fact] public void Use_WithNullServiceProviderFactory_ThrowsArgumentNullException() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act & Assert Assert.Throws("agentFactory", () => builder.Use((Func)null!)); } /// /// Verify that Build throws InvalidOperationException when middleware returns null. /// [Fact] public void Build_WithMiddlewareReturningNull_ThrowsInvalidOperationException() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act & Assert var exception = Assert.Throws(() => builder.Use(_ => null!).Build()); Assert.Contains("returned null", exception.Message); Assert.Contains("AIAgentBuilder", exception.Message); } /// /// Verify that Build uses EmptyServiceProvider when services is null. /// [Fact] public void Build_WithNullServices_UsesEmptyServiceProvider() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); IServiceProvider? capturedServices = null; // Act builder.Use((agent, services) => { capturedServices = services; return agent; }).Build(null); // Assert Assert.NotNull(capturedServices); Assert.Null(capturedServices.GetService(typeof(string))); // EmptyServiceProvider returns null for everything } /// /// Verify that service provider is passed correctly to factories. /// [Fact] public void PassesServiceProviderToFactories() { // Arrange var expectedServiceProvider = new ServiceCollection().BuildServiceProvider(); var mockInnerAgent = new Mock(); var mockOuterAgent = new Mock(); var builder = new AIAgentBuilder(services => { Assert.Same(expectedServiceProvider, services); return mockInnerAgent.Object; }); builder.Use((innerAgent, serviceProvider) => { Assert.Same(expectedServiceProvider, serviceProvider); Assert.Same(mockInnerAgent.Object, innerAgent); return mockOuterAgent.Object; }); // Act var result = builder.Build(expectedServiceProvider); // Assert Assert.Same(mockOuterAgent.Object, result); } /// /// Verify that pipeline is built in the order added (first added is outermost). /// [Fact] public void BuildsPipelineInOrderAdded() { // Arrange var mockInnerAgent = new Mock(); var builder = new AIAgentBuilder(mockInnerAgent.Object) .Use(next => new InnerAgentCapturingAgent("First", next)) .Use(next => new InnerAgentCapturingAgent("Second", next)) .Use(next => new InnerAgentCapturingAgent("Third", next)); // Act var first = (InnerAgentCapturingAgent)builder.Build(); // Assert Assert.Equal("First", first.TestName); var second = (InnerAgentCapturingAgent)first.InnerAgent; Assert.Equal("Second", second.TestName); var third = (InnerAgentCapturingAgent)second.InnerAgent; Assert.Equal("Third", third.TestName); Assert.Same(mockInnerAgent.Object, third.InnerAgent); } /// /// Verify that factories cannot return null. /// [Fact] public void DoesNotAllowFactoriesToReturnNull() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); builder.Use(_ => null!); // Act & Assert var ex = Assert.Throws(() => builder.Build()); Assert.Contains("entry at index 0", ex.Message); } /// /// Verify that EmptyServiceProvider is used when no services are provided and supports keyed services. /// [Fact] public void UsesEmptyServiceProviderWhenNoServicesProvided() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act & Assert builder.Use((innerAgent, serviceProvider) => { Assert.Null(serviceProvider.GetService(typeof(object))); var keyedServiceProvider = Assert.IsType(serviceProvider, exactMatch: false); Assert.Null(keyedServiceProvider.GetKeyedService(typeof(object), "key")); Assert.Throws(() => keyedServiceProvider.GetRequiredKeyedService(typeof(object), "key")); return innerAgent; }); builder.Build(); } #region Delegate Overload Tests /// /// Verify that Use with shared delegate throws ArgumentNullException when sharedFunc is null. /// [Fact] public void Use_WithNullSharedFunc_ThrowsArgumentNullException() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act & Assert Assert.Throws("sharedFunc", () => builder.Use((Func, AgentSession?, AgentRunOptions?, Func, AgentSession?, AgentRunOptions?, CancellationToken, Task>, CancellationToken, Task>)null!)); } /// /// Verify that Use with both delegates null throws ArgumentNullException. /// [Fact] public void Use_WithBothDelegatesNull_ThrowsArgumentNullException() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act & Assert var exception = Assert.Throws(() => builder.Use(null, null)); Assert.Contains("runFunc", exception.Message); } /// /// Verify that Use with shared delegate creates AnonymousDelegatingAIAgent. /// [Fact] public void Use_WithSharedDelegate_CreatesAnonymousDelegatingAgent() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act var result = builder.Use((_, _, _, _, _) => Task.CompletedTask).Build(); // Assert Assert.IsType(result); } /// /// Verify that Use with runFunc only creates AnonymousDelegatingAIAgent. /// [Fact] public void Use_WithRunFuncOnly_CreatesAnonymousDelegatingAgent() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act var result = builder.Use((_, _, _, _, _) => Task.FromResult(new AgentResponse()), null).Build(); // Assert Assert.IsType(result); } /// /// Verify that Use with runStreamingFunc only creates AnonymousDelegatingAIAgent. /// [Fact] public void Use_WithStreamingFuncOnly_CreatesAnonymousDelegatingAgent() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act var result = builder.Use(null, (_, _, _, _, _) => AsyncEnumerable.Empty()).Build(); // Assert Assert.IsType(result); } /// /// Verify that Use with both delegates creates AnonymousDelegatingAIAgent. /// [Fact] public void Use_WithBothDelegates_CreatesAnonymousDelegatingAgent() { // Arrange var mockAgent = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); // Act var result = builder.Use( (_, _, _, _, _) => Task.FromResult(new AgentResponse()), (_, _, _, _, _) => AsyncEnumerable.Empty()).Build(); // Assert Assert.IsType(result); } /// /// Verify that Use with both delegates allows both to access AgentRunContext. /// [Fact] public async Task Use_WithBothDelegates_AllowsDelegateToAccessAgentRunContextAsync() { // Arrange var mockAgent = new Mock(); var mockSession = new Mock(); var builder = new AIAgentBuilder(mockAgent.Object); AIAgent? builtAgent = null; bool nonStreamingMiddlewareExecuted = false; bool streamingMiddlwareExecuted = true; builtAgent = builder.Use( (_, _, _, _, _) => { Assert.NotNull(AIAgent.CurrentRunContext); Assert.Same(builtAgent, AIAgent.CurrentRunContext.Agent); Assert.Same(mockSession.Object, AIAgent.CurrentRunContext.Session); nonStreamingMiddlewareExecuted = true; return Task.FromResult(new AgentResponse()); }, (_, _, _, _, _) => { Assert.NotNull(AIAgent.CurrentRunContext); Assert.Same(builtAgent, AIAgent.CurrentRunContext.Agent); Assert.Same(mockSession.Object, AIAgent.CurrentRunContext.Session); streamingMiddlwareExecuted = true; return AsyncEnumerable.Empty(); }).Build(); // Act await builtAgent.RunAsync("Input message", mockSession.Object); await foreach (var update in builtAgent.RunStreamingAsync("Input message", mockSession.Object)) { } // Assert Assert.True(nonStreamingMiddlewareExecuted); Assert.True(streamingMiddlwareExecuted); } #endregion /// /// Helper class for testing pipeline order. /// private sealed class InnerAgentCapturingAgent : DelegatingAIAgent { public string TestName { get; } public new AIAgent InnerAgent => base.InnerAgent; public InnerAgentCapturingAgent(string name, AIAgent innerAgent) : base(innerAgent) { this.TestName = name; } } }