// 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;
}
}
}