// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Events;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
///
/// Tests for .
///
public sealed class InvokeFunctionToolExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
#region Step Naming Convention Tests
[Fact]
public void InvokeFunctionToolThrowsWhenModelInvalid() =>
// Arrange, Act & Assert
Assert.Throws(() => new InvokeFunctionToolExecutor(new InvokeFunctionTool(), new MockAgentProvider().Object, this.State));
[Fact]
public void InvokeFunctionToolNamingConvention()
{
// Arrange
string testId = this.CreateActionId().Value;
// Act
string externalInputStep = InvokeFunctionToolExecutor.Steps.ExternalInput(testId);
string resumeStep = InvokeFunctionToolExecutor.Steps.Resume(testId);
// Assert
Assert.Equal($"{testId}_{nameof(InvokeFunctionToolExecutor.Steps.ExternalInput)}", externalInputStep);
Assert.Equal($"{testId}_{nameof(InvokeFunctionToolExecutor.Steps.Resume)}", resumeStep);
}
#endregion
#region ExecuteAsync Tests
[Fact]
public async Task InvokeFunctionToolExecuteWithoutApprovalAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolExecuteWithoutApprovalAsync),
functionName: "simple_function",
requireApproval: false);
// Act and Assert
await this.ExecuteTestAsync(model);
}
[Fact]
public async Task InvokeFunctionToolExecuteWithArgumentsAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolExecuteWithArgumentsAsync),
functionName: "get_weather",
argumentKey: "location",
argumentValue: "Seattle");
// Act and Assert
await this.ExecuteTestAsync(model);
}
[Fact]
public async Task InvokeFunctionToolExecuteWithRequireApprovalAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolExecuteWithRequireApprovalAsync),
functionName: "approval_function",
requireApproval: true);
// Act and Assert
await this.ExecuteTestAsync(model);
}
[Fact]
public async Task InvokeFunctionToolExecuteWithEmptyConversationIdAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolExecuteWithEmptyConversationIdAsync),
functionName: "test_function",
conversationId: "");
// Act and Assert
await this.ExecuteTestAsync(model);
}
[Fact]
public async Task InvokeFunctionToolExecuteWithNullArgumentsAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolExecuteWithNullArgumentsAsync),
functionName: "no_args_function",
argumentKey: null);
// Act and Assert
await this.ExecuteTestAsync(model);
}
[Fact]
public async Task InvokeFunctionToolExecuteWithNullRequireApprovalAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolExecuteWithNullRequireApprovalAsync),
functionName: "test_function",
requireApproval: null);
// Act and Assert
await this.ExecuteTestAsync(model);
}
[Fact]
public async Task InvokeFunctionToolExecuteWithNullConversationIdAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolExecuteWithNullConversationIdAsync),
functionName: "test_function",
conversationId: null);
// Act and Assert
await this.ExecuteTestAsync(model);
}
#endregion
#region CaptureResponseAsync Tests
[Fact]
public async Task InvokeFunctionToolCaptureResponseWithNoOutputConfiguredAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolCaptureResponseWithNoOutputConfiguredAsync),
functionName: "test_function");
MockAgentProvider mockAgentProvider = new();
InvokeFunctionToolExecutor action = new(model, mockAgentProvider.Object, this.State);
FunctionResultContent functionResult = new(action.Id, "Result without output");
ExternalInputResponse response = new(new ChatMessage(ChatRole.Tool, [functionResult]));
// Act
WorkflowEvent[] events = await this.ExecuteCaptureResponseTestAsync(action, response);
// Assert
VerifyModel(model, action);
Assert.NotEmpty(events);
}
[Fact]
public async Task InvokeFunctionToolCaptureResponseWithEmptyMessagesAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolCaptureResponseWithEmptyMessagesAsync),
functionName: "test_function");
MockAgentProvider mockAgentProvider = new();
InvokeFunctionToolExecutor action = new(model, mockAgentProvider.Object, this.State);
// Empty response
ExternalInputResponse response = new([]);
// Act
WorkflowEvent[] events = await this.ExecuteCaptureResponseTestAsync(action, response);
// Assert
VerifyModel(model, action);
Assert.NotEmpty(events);
}
[Fact]
public async Task InvokeFunctionToolCaptureResponseWithConversationIdAsync()
{
// Arrange
this.State.InitializeSystem();
const string ConversationId = "TestConversationId";
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolCaptureResponseWithConversationIdAsync),
functionName: "test_function",
conversationId: ConversationId);
MockAgentProvider mockAgentProvider = new();
InvokeFunctionToolExecutor action = new(model, mockAgentProvider.Object, this.State);
FunctionResultContent functionResult = new(action.Id, "Result for conversation");
ExternalInputResponse response = new(new ChatMessage(ChatRole.Tool, [functionResult]));
// Act
WorkflowEvent[] events = await this.ExecuteCaptureResponseTestAsync(action, response);
// Assert
VerifyModel(model, action);
Assert.NotEmpty(events);
}
[Fact]
public async Task InvokeFunctionToolCaptureResponseWithNonMatchingResultAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolCaptureResponseWithNonMatchingResultAsync),
functionName: "test_function");
MockAgentProvider mockAgentProvider = new();
InvokeFunctionToolExecutor action = new(model, mockAgentProvider.Object, this.State);
// Use a different call ID that doesn't match the action ID
FunctionResultContent functionResult = new("different_call_id", "Different result");
ExternalInputResponse response = new(new ChatMessage(ChatRole.Tool, [functionResult]));
// Act
WorkflowEvent[] events = await this.ExecuteCaptureResponseTestAsync(action, response);
// Assert
VerifyModel(model, action);
Assert.NotEmpty(events);
}
[Fact]
public async Task InvokeFunctionToolCaptureResponseWithMultipleFunctionResultsAsync()
{
// Arrange
this.State.InitializeSystem();
InvokeFunctionTool model = this.CreateModel(
displayName: nameof(InvokeFunctionToolCaptureResponseWithMultipleFunctionResultsAsync),
functionName: "test_function",
conversationId: "TestConversation");
MockAgentProvider mockAgentProvider = new();
InvokeFunctionToolExecutor action = new(model, mockAgentProvider.Object, this.State);
// Multiple function results - the matching one should be captured
FunctionResultContent nonMatchingResult = new("other_call_id", "Other result");
FunctionResultContent matchingResult = new(action.Id, "Matching result");
ExternalInputResponse response = new(new ChatMessage(ChatRole.Tool, [nonMatchingResult, matchingResult]));
// Act
WorkflowEvent[] events = await this.ExecuteCaptureResponseTestAsync(action, response);
// Assert
VerifyModel(model, action);
Assert.NotEmpty(events);
}
#endregion
#region Helper Methods
private async Task ExecuteTestAsync(InvokeFunctionTool model)
{
MockAgentProvider mockAgentProvider = new();
InvokeFunctionToolExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
WorkflowEvent[] events = await this.ExecuteAsync(action, isDiscrete: false);
// Assert
VerifyModel(model, action);
VerifyInvocationEvent(events);
// IsDiscreteAction should be false for InvokeFunction
VerifyIsDiscrete(action, isDiscrete: false);
}
private async Task ExecuteCaptureResponseTestAsync(
InvokeFunctionToolExecutor action,
ExternalInputResponse response)
{
return await this.ExecuteAsync(
action,
InvokeFunctionToolExecutor.Steps.ExternalInput(action.Id),
(context, _, cancellationToken) => action.CaptureResponseAsync(context, response, cancellationToken));
}
private InvokeFunctionTool CreateModel(
string displayName,
string functionName,
bool? requireApproval = false,
string? conversationId = null,
string? argumentKey = null,
string? argumentValue = null)
{
InvokeFunctionTool.Builder builder = new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
FunctionName = new StringExpression.Builder(StringExpression.Literal(functionName)),
RequireApproval = requireApproval != null ? new BoolExpression.Builder(BoolExpression.Literal(requireApproval.Value)) : null
};
if (conversationId is not null)
{
builder.ConversationId = new StringExpression.Builder(StringExpression.Literal(conversationId));
}
if (argumentKey is not null && argumentValue is not null)
{
builder.Arguments.Add(argumentKey, ValueExpression.Literal(new StringDataValue(argumentValue)));
}
return AssignParent(builder);
}
#endregion
}