// 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 Moq;
namespace Microsoft.Agents.AI.UnitTests;
///
/// Unit tests for FunctionCallMiddlewareAgent functionality.
///
public sealed class FunctionInvocationDelegatingAgentTests
{
#region Basic Functionality Tests
///
/// Tests that FunctionCallMiddlewareAgent can be created with valid parameters.
///
[Fact]
public void Constructor_ValidParameters_CreatesInstance()
{
// Arrange
var mockChatClient = new Mock();
var innerAgent = new ChatClientAgent(mockChatClient.Object);
static ValueTask CallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
=> next(context, cancellationToken);
// Act
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, CallbackAsync);
// Assert
Assert.NotNull(middleware);
Assert.Equal(innerAgent.Id, middleware.Id);
Assert.Equal(innerAgent.Name, middleware.Name);
Assert.Equal(innerAgent.Description, middleware.Description);
}
///
/// Tests that constructor throws ArgumentNullException for null inner agent.
///
[Fact]
public void Constructor_NullInnerAgent_ThrowsArgumentNullException()
{
// Arrange
static ValueTask CallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
=> next(context, cancellationToken);
// Act & Assert
Assert.Throws(() => new FunctionInvocationDelegatingAgent(null!, CallbackAsync));
}
#endregion
#region Function Invocation Tests
///
/// Tests that middleware is invoked when functions are called during agent execution without options.
///
[Fact]
public async Task RunAsync_WithFunctionCall_NoOptions_InvokesMiddlewareAsync()
{
// Arrange
var executionOrder = new List();
var testFunction = AIFunctionFactory.Create(() =>
{
executionOrder.Add("Function-Executed");
return "Function result";
}, "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
var innerAgent = new ChatClientAgent(mockChatClient.Object, tools: [testFunction]);
var messages = new List { new(ChatRole.User, "Test message") };
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
executionOrder.Add("Middleware-Pre");
var result = await next(context, cancellationToken);
executionOrder.Add("Middleware-Post");
return result;
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
await middleware.RunAsync(messages, null, null, CancellationToken.None);
// Assert
Assert.Contains("Middleware-Pre", executionOrder);
Assert.Contains("Function-Executed", executionOrder);
Assert.Contains("Middleware-Post", executionOrder);
// Verify execution order
var middlewarePreIndex = executionOrder.IndexOf("Middleware-Pre");
var functionIndex = executionOrder.IndexOf("Function-Executed");
var middlewarePostIndex = executionOrder.IndexOf("Middleware-Post");
Assert.True(middlewarePreIndex < functionIndex);
Assert.True(functionIndex < middlewarePostIndex);
}
///
/// Tests that middleware is invoked when functions are called during agent execution without options.
///
[Fact]
public async Task RunAsync_WithFunctionCall_AgentRunOptions_InvokesMiddlewareAsync()
{
// Arrange
var executionOrder = new List();
var testFunction = AIFunctionFactory.Create(() =>
{
executionOrder.Add("Function-Executed");
return "Function result";
}, "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
var innerAgent = new ChatClientAgent(mockChatClient.Object, tools: [testFunction]);
var messages = new List { new(ChatRole.User, "Test message") };
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
executionOrder.Add("Middleware-Pre");
var result = await next(context, cancellationToken);
executionOrder.Add("Middleware-Post");
return result;
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
await middleware.RunAsync(messages, null, new AgentRunOptions(), CancellationToken.None);
// Assert
Assert.Contains("Middleware-Pre", executionOrder);
Assert.Contains("Function-Executed", executionOrder);
Assert.Contains("Middleware-Post", executionOrder);
// Verify execution order
var middlewarePreIndex = executionOrder.IndexOf("Middleware-Pre");
var functionIndex = executionOrder.IndexOf("Function-Executed");
var middlewarePostIndex = executionOrder.IndexOf("Middleware-Post");
Assert.True(middlewarePreIndex < functionIndex);
Assert.True(functionIndex < middlewarePostIndex);
}
///
/// Tests that middleware is invoked when functions are called during agent execution without options.
///
[Fact]
public async Task RunAsync_WithFunctionCall_CustomAgentRunOptions_ThrowsNotSupportedAsync()
{
// Arrange
var executionOrder = new List();
var testFunction = AIFunctionFactory.Create(() =>
{
executionOrder.Add("Function-Executed");
return "Function result";
}, "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
var innerAgent = new ChatClientAgent(mockChatClient.Object, tools: [testFunction]);
var messages = new List { new(ChatRole.User, "Test message") };
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
executionOrder.Add("Middleware-Pre");
var result = await next(context, cancellationToken);
executionOrder.Add("Middleware-Post");
return result;
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
await Assert.ThrowsAsync(() =>
middleware.RunAsync(messages, null, new CustomAgentRunOptions(), CancellationToken.None));
}
///
/// Tests that middleware is invoked when functions are called during agent execution.
///
[Fact]
public async Task RunAsync_WithFunctionCall_InvokesMiddlewareAsync()
{
// Arrange
var executionOrder = new List();
var testFunction = AIFunctionFactory.Create(() =>
{
executionOrder.Add("Function-Executed");
return "Function result";
}, "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
executionOrder.Add("Middleware-Pre");
var result = await next(context, cancellationToken);
executionOrder.Add("Middleware-Post");
return result;
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
await middleware.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.Contains("Middleware-Pre", executionOrder);
Assert.Contains("Function-Executed", executionOrder);
Assert.Contains("Middleware-Post", executionOrder);
// Verify execution order
var middlewarePreIndex = executionOrder.IndexOf("Middleware-Pre");
var functionIndex = executionOrder.IndexOf("Function-Executed");
var middlewarePostIndex = executionOrder.IndexOf("Middleware-Post");
Assert.True(middlewarePreIndex < functionIndex);
Assert.True(functionIndex < middlewarePostIndex);
}
///
/// Tests that multiple function calls trigger middleware for each invocation.
///
[Fact]
public async Task RunAsync_WithMultipleFunctionCalls_InvokesMiddlewareForEachAsync()
{
// Arrange
var executionOrder = new List();
var function1 = AIFunctionFactory.Create(() =>
{
executionOrder.Add("Function1-Executed");
return "Function1 result";
}, "Function1", "First test function");
var function2 = AIFunctionFactory.Create(() =>
{
executionOrder.Add("Function2-Executed");
return "Function2 result";
}, "Function2", "Second test function");
var functionCall1 = new FunctionCallContent("call_1", "Function1", new Dictionary());
var functionCall2 = new FunctionCallContent("call_2", "Function2", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall1, functionCall2);
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
executionOrder.Add($"Middleware-Pre-{context.Function.Name}");
var result = await next(context, cancellationToken);
executionOrder.Add($"Middleware-Post-{context.Function.Name}");
return result;
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [function1, function2] });
await middleware.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.Contains("Middleware-Pre-Function1", executionOrder);
Assert.Contains("Function1-Executed", executionOrder);
Assert.Contains("Middleware-Post-Function1", executionOrder);
Assert.Contains("Middleware-Pre-Function2", executionOrder);
Assert.Contains("Function2-Executed", executionOrder);
Assert.Contains("Middleware-Post-Function2", executionOrder);
}
#endregion
#region Context Validation Tests
///
/// Tests that FunctionInvocationContext contains correct values during middleware execution.
///
[Fact]
public async Task RunAsync_MiddlewareContext_ContainsCorrectValuesAsync()
{
// Arrange
var testFunction = AIFunctionFactory.Create(() => "Function result", "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary { ["param"] = "value" });
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
FunctionInvocationContext? capturedContext = null;
AIAgent? capturedAgent = null;
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
capturedContext = context;
capturedAgent = agent;
return await next(context, cancellationToken);
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
await middleware.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.NotNull(capturedContext);
Assert.Equal("TestFunction", capturedContext.Function.Name);
Assert.Same(innerAgent, capturedAgent); // The agent passed should be the inner agent
Assert.NotNull(capturedContext.Arguments);
// Note: Additional context properties would need to be verified based on actual FunctionInvocationContext structure
}
#endregion
#region AIAgentBuilder Use Method Tests
///
/// Verify that AIAgentBuilder.Use method works correctly with function invocation middleware.
///
[Fact]
public async Task AIAgentBuilder_Use_FunctionInvocationMiddleware_WorksCorrectlyAsync()
{
// Arrange
var mockChatClient = new Mock();
var testFunction = AIFunctionFactory.Create(() => "test result", name: "TestFunction");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var executionOrder = new List();
// Mock the chat client to return a function call, then a response
mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, [functionCall])));
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
// Act
var agent = new AIAgentBuilder(innerAgent)
.Use((agent, context, next, cancellationToken) =>
{
executionOrder.Add("Middleware-Pre");
var result = next(context, cancellationToken);
executionOrder.Add("Middleware-Post");
return result;
})
.Build();
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
await agent.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.Contains("Middleware-Pre", executionOrder);
Assert.Contains("Middleware-Post", executionOrder);
}
///
/// Verify that multiple function invocation middleware are executed.
///
[Fact]
public async Task AIAgentBuilder_Use_MultipleFunctionMiddleware_BothExecuteAsync()
{
// Arrange
var mockChatClient = new Mock();
var testFunction = AIFunctionFactory.Create(() => "test result", name: "TestFunction");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var firstMiddlewareExecuted = false;
var secondMiddlewareExecuted = false;
// Mock the chat client to return a function call, then a response
mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, [functionCall])));
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
// Act
var agent = new AIAgentBuilder(innerAgent)
.Use((agent, context, next, cancellationToken) =>
{
firstMiddlewareExecuted = true;
return next(context, cancellationToken);
})
.Use((agent, context, next, cancellationToken) =>
{
secondMiddlewareExecuted = true;
return next(context, cancellationToken);
})
.Build();
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
await agent.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.True(firstMiddlewareExecuted, "First middleware should have executed");
Assert.True(secondMiddlewareExecuted, "Second middleware should have executed");
}
///
/// Verify that AIAgentBuilder.Use method throws InvalidOperationException when inner agent is doesn't use a FunctinInvocking.
///
[Fact]
public void AIAgentBuilder_Use_NonFICCEnabledAgent_ThrowsInvalidOperationException()
{
// Arrange
var mockAgent = new Mock();
// Act & Assert
var builder = new AIAgentBuilder(mockAgent.Object);
var exception = Assert.Throws(() =>
{
builder.Use((agent, context, next, cancellationToken) => next(context, cancellationToken));
builder.Build();
});
}
///
/// Verify that AIAgentBuilder.Use method throws InvalidOperationException when inner agent is doesn't use a FunctinInvokingChatClient.
///
[Fact]
public void AIAgentBuilder_Use_NonFICCDecoratedChatClientInAgent_ThrowsInvalidOperationException()
{
// Arrange
var mockChatClient = new Mock();
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions() { UseProvidedChatClientAsIs = true });
// Act & Assert
var builder = new AIAgentBuilder(agent);
var exception = Assert.Throws(() =>
{
builder.Use((agent, context, next, cancellationToken) => next(context, cancellationToken));
builder.Build();
});
}
///
/// Tests function invocation middleware when FunctionInvokingChatClient.CurrentContext is null (direct function invocation).
///
[Fact]
public async Task RunAsync_DirectFunctionInvocation_MiddlewareHandlesNullCurrentContextAsync()
{
// Arrange
var executionOrder = new List();
var capturedContext = new List();
var testFunction = AIFunctionFactory.Create(() =>
{
executionOrder.Add("Function-Executed");
return "Function result";
}, "TestFunction", "A test function");
var mockChatClient = new Mock();
// Setup mock to directly invoke the function (bypassing FunctionInvokingChatClient)
mockChatClient.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
.Returns, ChatOptions, CancellationToken>(async (messages, options, ct) =>
{
// Directly invoke the function to simulate null CurrentContext scenario
if (options?.Tools?.FirstOrDefault() is AIFunction function)
{
executionOrder.Add("Direct-Function-Invocation");
await function.InvokeAsync([], ct);
}
return new ChatResponse([new ChatMessage(ChatRole.Assistant, "Response after direct invocation")]);
});
var innerAgent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
UseProvidedChatClientAsIs = true
});
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
executionOrder.Add("Middleware-Pre");
capturedContext.Add(context);
var result = await next(context, cancellationToken);
executionOrder.Add("Middleware-Post");
return result;
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
var messages = new List { new(ChatRole.User, "Test message") };
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
await middleware.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.Contains("Direct-Function-Invocation", executionOrder);
Assert.Contains("Middleware-Pre", executionOrder);
Assert.Contains("Function-Executed", executionOrder);
Assert.Contains("Middleware-Post", executionOrder);
// Verify that the context was created with Iteration = -1 (indicating no ambient context)
Assert.Single(capturedContext);
Assert.Equal(0, capturedContext[0].Iteration);
Assert.Equal("TestFunction", capturedContext[0].Function.Name);
Assert.NotNull(capturedContext[0].Arguments);
}
#endregion
#region Error Handling Tests
///
/// Tests that exceptions thrown by middleware during pre-invocation surface to the caller.
///
[Fact]
public async Task RunAsync_MiddlewareThrowsPreInvocation_ExceptionSurfacesAsync()
{
// Arrange
var testFunction = AIFunctionFactory.Create(() => "Function result", "TestFunction", "A test function");
var mockChatClient = new Mock();
mockChatClient.Setup(c => c.GetResponseAsync(
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync(() => new ChatResponse([
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_123", "TestFunction", new Dictionary())])
]));
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
var expectedException = new InvalidOperationException("Pre-invocation error");
ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
throw expectedException;
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act & Assert
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
var actualException = await Assert.ThrowsAsync(
() => middleware.RunAsync(messages, null, options, CancellationToken.None));
Assert.Same(expectedException, actualException);
}
///
/// Tests that exceptions thrown by the function are handled by middleware.
///
[Fact]
public async Task RunAsync_FunctionThrowsException_MiddlewareCanHandleAsync()
{
// Arrange
var functionException = new InvalidOperationException("Function error");
string ThrowingFunction() => throw functionException;
var testFunction = AIFunctionFactory.Create(ThrowingFunction, "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
var middlewareHandledException = false;
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
try
{
return await next(context, cancellationToken);
}
catch (InvalidOperationException)
{
middlewareHandledException = true;
return "Error handled by middleware";
}
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
await middleware.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.True(middlewareHandledException);
}
#endregion
#region Result Modification Tests
///
/// Tests that middleware can modify function results.
///
[Fact]
public async Task RunAsync_MiddlewareModifiesResult_ModifiedResultUsedAsync()
{
// Arrange
var testFunction = AIFunctionFactory.Create(() => "Original result", "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
const string ModifiedResult = "Modified by middleware";
static async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
await next(context, cancellationToken);
return ModifiedResult; // Return the modified result instead of setting context property
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
var response = await middleware.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.NotNull(response);
// The modified result should be reflected in the response messages
var functionResultContent = response.Messages
.SelectMany(m => m.Contents)
.OfType()
.FirstOrDefault();
Assert.NotNull(functionResultContent);
Assert.Equal(ModifiedResult, functionResultContent.Result);
}
#endregion
#region Middleware Chaining Tests
///
/// Tests execution order with multiple function middleware instances in a chain.
///
[Fact]
public async Task RunAsync_MultipleFunctionMiddleware_ExecutesInCorrectOrderAsync()
{
// Arrange
var executionOrder = new List();
var testFunction = AIFunctionFactory.Create(() =>
{
executionOrder.Add("Function-Executed");
return "Function result";
}, "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = new Mock();
// Setup sequence: first call returns function call, subsequent calls return final response
var responseWithFunctionCall = new ChatResponse([
new ChatMessage(ChatRole.Assistant, [functionCall])
]);
var finalResponse = new ChatResponse([
new ChatMessage(ChatRole.Assistant, "Final response")
]);
mockChatClient.SetupSequence(c => c.GetResponseAsync(
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync(responseWithFunctionCall)
.ReturnsAsync(finalResponse);
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
async ValueTask FirstMiddlewareAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
executionOrder.Add("First-Pre");
var result = await next(context, cancellationToken);
executionOrder.Add("First-Post");
return result;
}
async ValueTask SecondMiddlewareAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
executionOrder.Add("Second-Pre");
var result = await next(context, cancellationToken);
executionOrder.Add("Second-Post");
return result;
}
// Create nested middleware chain
var firstMiddleware = new FunctionInvocationDelegatingAgent(innerAgent, FirstMiddlewareAsync);
var secondMiddleware = new FunctionInvocationDelegatingAgent(firstMiddleware, SecondMiddlewareAsync);
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
await secondMiddleware.RunAsync(messages, null, options, CancellationToken.None);
// Assert
var expectedOrder = new[] { "First-Pre", "Second-Pre", "Function-Executed", "Second-Post", "First-Post" };
Assert.Equal(expectedOrder, executionOrder);
}
///
/// Tests that function middleware works correctly when combined with running middleware.
///
[Fact]
public async Task RunAsync_FunctionMiddlewareWithRunningMiddleware_BothExecuteAsync()
{
// Arrange
var executionOrder = new List();
var testFunction = AIFunctionFactory.Create(() =>
{
executionOrder.Add("Function-Executed");
return "Function result";
}, "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
async Task RunningMiddlewareCallbackAsync(IEnumerable messages, AgentSession? session, AgentRunOptions? options, AIAgent innerAgent, CancellationToken cancellationToken)
{
executionOrder.Add("Running-Pre");
var result = await innerAgent.RunAsync(messages, session, options, cancellationToken);
executionOrder.Add("Running-Post");
return result;
}
async ValueTask FunctionMiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
executionOrder.Add("Function-Pre");
var result = await next(context, cancellationToken);
executionOrder.Add("Function-Post");
return result;
}
// Create middleware chain: Function -> Running -> Inner using AIAgentBuilder
var runningMiddleware = new AIAgentBuilder(innerAgent)
.Use(RunningMiddlewareCallbackAsync, null)
.Build();
var functionMiddleware = new FunctionInvocationDelegatingAgent(runningMiddleware, FunctionMiddlewareCallbackAsync);
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
await functionMiddleware.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.Contains("Running-Pre", executionOrder);
Assert.Contains("Running-Post", executionOrder);
Assert.Contains("Function-Pre", executionOrder);
Assert.Contains("Function-Post", executionOrder);
Assert.Contains("Function-Executed", executionOrder);
}
#endregion
#region Streaming Tests
///
/// Tests that function middleware works correctly with streaming responses.
///
[Fact]
public async Task RunStreamingAsync_WithFunctionCall_InvokesMiddlewareAsync()
{
// Arrange
var executionOrder = new List();
var testFunction = AIFunctionFactory.Create(() =>
{
executionOrder.Add("Function-Executed");
return "Function result";
}, "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
// Setup streaming response with function calls
var streamingResponse = new ChatResponseUpdate[]
{
new() { Contents = [functionCall] }, // Include function call in streaming response
new() { Contents = [new TextContent("Streaming response")] }
};
mockChatClient.Setup(c => c.GetStreamingResponseAsync(
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.Returns(streamingResponse.ToAsyncEnumerable());
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
executionOrder.Add("Middleware-Pre");
var result = await next(context, cancellationToken);
executionOrder.Add("Middleware-Post");
return result;
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
var responseUpdates = new List();
await foreach (var update in middleware.RunStreamingAsync(messages, null, options, CancellationToken.None))
{
responseUpdates.Add(update);
}
// Assert
Assert.NotEmpty(responseUpdates);
Assert.Contains("Middleware-Pre", executionOrder);
Assert.Contains("Function-Executed", executionOrder);
Assert.Contains("Middleware-Post", executionOrder);
}
#endregion
#region Edge Cases
///
/// Tests that middleware is not invoked when no function calls are made.
///
[Fact]
public async Task RunAsync_NoFunctionCalls_MiddlewareNotInvokedAsync()
{
// Arrange
var middlewareInvoked = false;
var mockChatClient = CreateMockChatClient(
new ChatResponse([new ChatMessage(ChatRole.Assistant, "Regular response")]));
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
middlewareInvoked = true;
return await next(context, cancellationToken);
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
await middleware.RunAsync(messages, null, null, CancellationToken.None);
// Assert
Assert.False(middlewareInvoked);
}
///
/// Tests that middleware handles cancellation tokens correctly.
///
[Fact]
public async Task RunAsync_CancellationToken_PropagatedToMiddlewareAsync()
{
// Arrange
var testFunction = AIFunctionFactory.Create(() => "Function result", "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
var cancellationTokenSource = new CancellationTokenSource();
var expectedToken = cancellationTokenSource.Token;
CancellationToken? capturedToken = null;
async ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
capturedToken = cancellationToken;
return await next(context, cancellationToken);
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
await middleware.RunAsync(messages, null, options, expectedToken);
// Assert
Assert.Equal(expectedToken, capturedToken);
}
///
/// Tests that middleware can prevent function execution by not calling next().
///
[Fact]
public async Task RunAsync_MiddlewareDoesNotCallNext_FunctionNotExecutedAsync()
{
// Arrange
var functionExecuted = false;
var testFunction = AIFunctionFactory.Create(() =>
{
functionExecuted = true;
return "Function result";
}, "TestFunction", "A test function");
var functionCall = new FunctionCallContent("call_123", "TestFunction", new Dictionary());
var mockChatClient = CreateMockChatClientWithFunctionCalls(functionCall);
var innerAgent = new ChatClientAgent(mockChatClient.Object);
var messages = new List { new(ChatRole.User, "Test message") };
static ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
{
// Don't call next() - this should prevent function execution
// Return the blocked result directly
return new ValueTask("Blocked by middleware");
}
var middleware = new FunctionInvocationDelegatingAgent(innerAgent, MiddlewareCallbackAsync);
// Act
var options = new ChatClientAgentRunOptions(new ChatOptions { Tools = [testFunction] });
var response = await middleware.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.False(functionExecuted);
Assert.NotNull(response);
// Verify the middleware result is used
var functionResultContent = response.Messages
.SelectMany(m => m.Contents)
.OfType()
.FirstOrDefault();
Assert.NotNull(functionResultContent);
Assert.Equal("Blocked by middleware", functionResultContent.Result);
}
#endregion
#region Options Preservation Tests
///
/// Tests that FunctionInvocationDelegatingAgent preserves all original AgentRunOptions properties
/// when converting base AgentRunOptions to ChatClientAgentRunOptions.
///
[Fact]
public async Task RunAsync_WithBaseAgentRunOptions_PreservesAllOriginalOptionsAsync()
{
// Arrange
AgentRunOptions? capturedOptions = null;
var responseFormat = ChatResponseFormat.Json;
var additionalProperties = new AdditionalPropertiesDictionary { ["key1"] = "value1" };
Mock mockChatClient = new();
var chatClientAgent = new ChatClientAgent(mockChatClient.Object);
// Wrap the inner agent in a spy that captures the converted options and returns a dummy response
var spyAgent = new AnonymousDelegatingAIAgent(
chatClientAgent,
runFunc: (messages, session, options, innerAgent, ct) =>
{
capturedOptions = options;
return Task.FromResult(new AgentResponse(new ChatResponse(new ChatMessage(ChatRole.Assistant, "test")) { ResponseId = "test" }));
},
runStreamingFunc: null);
static ValueTask MiddlewareCallbackAsync(AIAgent agent, FunctionInvocationContext context, Func> next, CancellationToken cancellationToken)
=> next(context, cancellationToken);
var middleware = new FunctionInvocationDelegatingAgent(spyAgent, MiddlewareCallbackAsync);
var originalOptions = new AgentRunOptions
{
ResponseFormat = responseFormat,
AllowBackgroundResponses = true,
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
AdditionalProperties = additionalProperties,
};
// Act
await middleware.RunAsync([new(ChatRole.User, "Test")], null, originalOptions, CancellationToken.None);
// Assert - All original properties were preserved on the converted options
Assert.NotNull(capturedOptions);
Assert.IsType(capturedOptions);
Assert.Same(responseFormat, capturedOptions.ResponseFormat);
Assert.True(capturedOptions.AllowBackgroundResponses);
Assert.Same(originalOptions.ContinuationToken, capturedOptions.ContinuationToken);
Assert.Same(additionalProperties, capturedOptions.AdditionalProperties);
}
#endregion
///
/// Creates a mock IChatClient with predefined responses for testing.
///
/// The responses to return in sequence.
/// A configured mock IChatClient.
private static Mock CreateMockChatClient(params ChatResponse[] responses)
{
var mockChatClient = new Mock();
var responseQueue = new Queue(responses);
mockChatClient.Setup(c => c.GetResponseAsync(
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync(() => responseQueue.Count > 0 ? responseQueue.Dequeue() : responses.LastOrDefault() ?? CreateDefaultResponse());
return mockChatClient;
}
///
/// Creates a mock IChatClient that returns responses with function calls for testing function middleware.
///
/// The function calls to include in responses.
/// A configured mock IChatClient.
private static Mock CreateMockChatClientWithFunctionCalls(params FunctionCallContent[] functionCalls)
{
var mockChatClient = new Mock();
var responseWithFunctionCalls = new ChatResponse([
new ChatMessage(ChatRole.Assistant, functionCalls.Cast().ToList())
]);
mockChatClient.Setup(c => c.GetResponseAsync(
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync(responseWithFunctionCalls);
return mockChatClient;
}
///
/// Creates a default ChatResponse for fallback scenarios.
///
/// A default ChatResponse.
private static ChatResponse CreateDefaultResponse()
{
return new ChatResponse([new ChatMessage(ChatRole.Assistant, "Default response")]);
}
///
/// Custom AgentRunOptions class for testing
///
private sealed class CustomAgentRunOptions : AgentRunOptions;
}