// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#if NET
using Microsoft.Agents.AI.Tools.Shell;
#endif
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
public class HarnessAgentTests
{
private const int TestMaxContextWindowTokens = 100_000;
private const int TestMaxOutputTokens = 10_000;
///
/// Creates a HarnessAgent with all default features disabled to isolate tests for specific behaviors.
///
private static HarnessAgentOptions CreateAllDisabledOptions() => new()
{
DisableToolApproval = true,
DisableOpenTelemetry = true,
DisableFileMemory = true,
DisableFileAccess = true,
DisableWebSearch = true,
DisableTodoProvider = true,
DisableAgentModeProvider = true,
DisableAgentSkillsProvider = true,
};
#region Constructor Validation
///
/// Verify that the constructor throws when chatClient is null.
///
[Fact]
public void Constructor_ThrowsWhenChatClientIsNull()
{
// Act & Assert
Assert.Throws(() => new HarnessAgent(null!, TestMaxContextWindowTokens, TestMaxOutputTokens));
}
///
/// Verify that the constructor throws when MaxContextWindowTokens is invalid (zero).
///
[Fact]
public void Constructor_ThrowsWhenMaxContextWindowTokensIsZero()
{
// Arrange
var chatClient = new Mock().Object;
// Act & Assert
Assert.Throws(() => new HarnessAgent(chatClient, 0, TestMaxOutputTokens));
}
///
/// Verify that the constructor throws when MaxOutputTokens equals MaxContextWindowTokens.
///
[Fact]
public void Constructor_ThrowsWhenMaxOutputTokensEqualsContextWindow()
{
// Arrange
var chatClient = new Mock().Object;
// Act & Assert
Assert.Throws(() => new HarnessAgent(chatClient, 100_000, 100_000));
}
///
/// Verify that the constructor succeeds when options is null.
///
[Fact]
public void Constructor_SucceedsWhenOptionsIsNull()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens);
// Assert
Assert.NotNull(agent);
}
#endregion
#region Agent Identity
///
/// Verify that Name and Description are passed through to the inner agent.
///
[Fact]
public void NameAndDescription_ArePassedThrough()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.Name = "TestAgent";
options.Description = "A test agent";
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
// Assert
Assert.Equal("TestAgent", agent.Name);
Assert.Equal("A test agent", agent.Description);
}
///
/// Verify that Id is passed through to the inner agent.
///
[Fact]
public void Id_IsPassedThrough()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.Id = "my-agent-id";
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
// Assert
Assert.Equal("my-agent-id", agent.Id);
}
#endregion
#region Instructions
///
/// Verify that default instructions are used when none are provided.
///
[Fact]
public void Instructions_DefaultsToBuiltInInstructions()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
Assert.Equal(HarnessAgent.DefaultInstructions, innerAgent!.Instructions);
}
///
/// Verify that default instructions are used when options is provided but neither HarnessInstructions nor ChatOptions.Instructions is set.
///
[Fact]
public void Instructions_DefaultsWhenBothNull()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.ChatOptions = new ChatOptions { Temperature = 0.5f };
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
Assert.Equal(HarnessAgent.DefaultInstructions, innerAgent!.Instructions);
}
///
/// Verify that ChatOptions.Instructions is appended to the default HarnessInstructions.
///
[Fact]
public void Instructions_CombinesDefaultHarnessWithAgentInstructions()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.ChatOptions = new ChatOptions { Instructions = "You are a custom assistant." };
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
var expected = $"{HarnessAgent.DefaultInstructions}\n\nYou are a custom assistant.";
Assert.Equal(expected, innerAgent!.Instructions);
}
///
/// Verify that custom HarnessInstructions replaces the default.
///
[Fact]
public void Instructions_CustomHarnessInstructionsReplacesDefault()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.HarnessInstructions = "Custom harness rules.";
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
Assert.Equal("Custom harness rules.", innerAgent!.Instructions);
}
///
/// Verify that custom HarnessInstructions and ChatOptions.Instructions are combined.
///
[Fact]
public void Instructions_CombinesCustomHarnessWithAgentInstructions()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.HarnessInstructions = "Custom harness rules.";
options.ChatOptions = new ChatOptions { Instructions = "You are a research agent." };
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
Assert.Equal("Custom harness rules.\n\nYou are a research agent.", innerAgent!.Instructions);
}
///
/// Verify that empty HarnessInstructions omits harness portion, using only agent instructions.
///
[Fact]
public void Instructions_EmptyHarnessInstructionsUsesOnlyAgentInstructions()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.HarnessInstructions = string.Empty;
options.ChatOptions = new ChatOptions { Instructions = "Agent only instructions." };
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
Assert.Equal("Agent only instructions.", innerAgent!.Instructions);
}
///
/// Verify that empty HarnessInstructions with no agent instructions results in empty string.
///
[Fact]
public void Instructions_EmptyHarnessInstructionsWithNoAgentInstructions()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.HarnessInstructions = string.Empty;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
Assert.Equal(string.Empty, innerAgent!.Instructions);
}
#endregion
#region ChatHistoryProvider
///
/// Verify that the default ChatHistoryProvider is InMemoryChatHistoryProvider when none is specified.
///
[Fact]
public void ChatHistoryProvider_DefaultsToInMemory()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
Assert.IsType(innerAgent!.ChatHistoryProvider);
}
///
/// Verify that a custom ChatHistoryProvider is used when provided.
///
[Fact]
public void ChatHistoryProvider_UsesCustomProviderWhenSpecified()
{
// Arrange
var chatClient = new Mock().Object;
var customProvider = new InMemoryChatHistoryProvider();
var options = CreateAllDisabledOptions();
options.ChatHistoryProvider = customProvider;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
Assert.Same(customProvider, innerAgent!.ChatHistoryProvider);
}
#endregion
#region ChatClient Pipeline
///
/// Verify that the inner agent's ChatClient includes FunctionInvokingChatClient in the pipeline.
///
[Fact]
public void Pipeline_IncludesFunctionInvokingChatClient()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
var ficc = innerAgent!.ChatClient.GetService();
Assert.NotNull(ficc);
}
///
/// Verify that the inner agent's ChatClient pipeline includes more than just the raw chat client,
/// confirming that per-service-call persistence and other decorators have been applied.
///
[Fact]
public void Pipeline_HasDecoratedChatClient()
{
// Arrange
var mockClient = new Mock();
var rawClient = mockClient.Object;
// Act
var agent = new HarnessAgent(rawClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
// Assert — the pipeline wraps the raw client, so the outer client is not the same object.
Assert.NotNull(innerAgent);
Assert.NotSame(rawClient, innerAgent!.ChatClient);
}
#endregion
#region AIContextProviders
///
/// Verify that additional AIContextProviders from options are passed to the inner ChatClientAgent.
///
[Fact]
public void AIContextProviders_ArePassedToInnerAgent()
{
// Arrange
var chatClient = new Mock().Object;
var customProvider = new TodoProvider();
var options = CreateAllDisabledOptions();
options.AIContextProviders = [customProvider];
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert — the custom provider should appear in the inner agent's AIContextProviders.
Assert.NotNull(innerAgent);
Assert.NotNull(innerAgent!.AIContextProviders);
Assert.Contains(customProvider, innerAgent.AIContextProviders!);
}
///
/// Verify that when all default providers are disabled and no user AIContextProviders are specified,
/// the inner agent has an empty providers list.
///
[Fact]
public void AIContextProviders_IsEmptyWhenAllDisabledAndNoneSpecified()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
Assert.NotNull(innerAgent!.AIContextProviders);
Assert.Empty(innerAgent.AIContextProviders!);
}
#endregion
#region ChatOptions and Tools
///
/// Verify that tools from ChatOptions are passed to the model during invocation.
///
[Fact]
public async Task ChatOptions_ToolsArePreservedAsync()
{
// Arrange
var tool = AIFunctionFactory.Create(() => "test", "TestTool");
var mockClient = new Mock();
ChatOptions? capturedOptions = null;
mockClient
.Setup(c => c.GetResponseAsync(
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.Callback, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));
var options = CreateAllDisabledOptions();
options.ChatOptions = new ChatOptions { Tools = [tool] };
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var session = await agent.CreateSessionAsync();
// Act
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);
// Assert — verify the tool was included in the ChatOptions passed to the model.
Assert.NotNull(capturedOptions);
Assert.NotNull(capturedOptions!.Tools);
Assert.Contains(capturedOptions.Tools, t => t == tool);
}
///
/// Verify that the source ChatOptions are cloned and not modified.
///
[Fact]
public void ChatOptions_SourceIsNotModified()
{
// Arrange
var chatClient = new Mock().Object;
var sourceChatOptions = new ChatOptions
{
Instructions = "original instructions",
Temperature = 0.7f,
};
// Act
_ = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions
{
ChatOptions = sourceChatOptions,
});
// Assert — source ChatOptions should not be mutated.
Assert.Equal("original instructions", sourceChatOptions.Instructions);
Assert.Equal(0.7f, sourceChatOptions.Temperature);
}
#endregion
#region GetService
///
/// Verify that GetService returns the HarnessAgent for its own type.
///
[Fact]
public void GetService_ReturnsSelfForHarnessAgentType()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
// Assert
Assert.Same(agent, agent.GetService());
}
///
/// Verify that GetService returns the inner ChatClientAgent.
///
[Fact]
public void GetService_ReturnsInnerChatClientAgent()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
// Assert
Assert.NotNull(agent.GetService());
}
#endregion
#region RunAsync Delegation
///
/// Verify that RunAsync delegates to the inner ChatClientAgent.
///
[Fact]
public async Task RunAsync_DelegatesToInnerAgentAsync()
{
// Arrange
var mockClient = new Mock();
mockClient
.Setup(c => c.GetResponseAsync(
It.IsAny>(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello!")));
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var session = await agent.CreateSessionAsync();
// Act
var response = await agent.RunAsync(
[new ChatMessage(ChatRole.User, "Hi")],
session);
// Assert
Assert.NotNull(response);
Assert.True(response.Messages.Any());
}
#endregion
#region DefaultInstructions
///
/// Verify that DefaultInstructions is a non-empty public constant.
///
[Fact]
public void DefaultInstructions_IsNonEmpty()
{
// Assert
Assert.False(string.IsNullOrWhiteSpace(HarnessAgent.DefaultInstructions));
}
#endregion
#region AsHarnessAgent Extension Method
///
/// Verify that AsHarnessAgent creates a HarnessAgent with default options.
///
[Fact]
public void AsHarnessAgent_CreatesAgentWithDefaults()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens);
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
Assert.Equal(HarnessAgent.DefaultInstructions, agent.GetService()!.Instructions);
}
///
/// Verify that AsHarnessAgent passes options through to the HarnessAgent.
///
[Fact]
public void AsHarnessAgent_PassesOptionsThrough()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.Name = "ExtensionAgent";
options.ChatOptions = new ChatOptions { Instructions = "Custom instructions" };
// Act
var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.Equal("ExtensionAgent", agent.Name);
Assert.NotNull(innerAgent);
var expected = $"{HarnessAgent.DefaultInstructions}\n\nCustom instructions";
Assert.Equal(expected, innerAgent!.Instructions);
}
///
/// Verify that AsHarnessAgent throws when chatClient is null.
///
[Fact]
public void AsHarnessAgent_ThrowsWhenChatClientIsNull()
{
// Act & Assert
Assert.Throws(() => ((IChatClient)null!).AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens));
}
#endregion
#region Feature: ToolApproval
///
/// Verify that ToolApprovalAgent is included in the pipeline by default.
///
[Fact]
public void ToolApproval_IncludedByDefault()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableToolApproval = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
// Assert
Assert.NotNull(agent.GetService());
}
///
/// Verify that ToolApprovalAgent is excluded when disabled.
///
[Fact]
public void ToolApproval_ExcludedWhenDisabled()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
// Assert
Assert.Null(agent.GetService());
}
#endregion
#region Feature: OpenTelemetry
///
/// Verify that OpenTelemetryAgent is included in the pipeline by default.
///
[Fact]
public void OpenTelemetry_IncludedByDefault()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableOpenTelemetry = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
// Assert
Assert.NotNull(agent.GetService());
}
///
/// Verify that OpenTelemetryAgent is excluded when disabled.
///
[Fact]
public void OpenTelemetry_ExcludedWhenDisabled()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
// Assert
Assert.Null(agent.GetService());
}
///
/// Verify that a custom OpenTelemetrySourceName is accepted without error.
///
[Fact]
public void OpenTelemetry_CustomSourceNameIsAccepted()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableOpenTelemetry = false;
options.OpenTelemetrySourceName = "MyApp.AgentTracing";
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
// Assert
Assert.NotNull(agent.GetService());
}
#endregion
#region Feature: WebSearch
///
/// Verify that HostedWebSearchTool is added to ChatOptions.Tools by default.
///
[Fact]
public async Task WebSearch_IncludedByDefaultAsync()
{
// Arrange
var mockClient = new Mock();
ChatOptions? capturedOptions = null;
mockClient
.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
.Callback, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));
var options = CreateAllDisabledOptions();
options.DisableWebSearch = false;
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var session = await agent.CreateSessionAsync();
// Act
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);
// Assert
Assert.NotNull(capturedOptions?.Tools);
Assert.Contains(capturedOptions!.Tools!, t => t is HostedWebSearchTool);
}
///
/// Verify that HostedWebSearchTool is not added when disabled.
///
[Fact]
public async Task WebSearch_ExcludedWhenDisabledAsync()
{
// Arrange
var mockClient = new Mock();
ChatOptions? capturedOptions = null;
mockClient
.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
.Callback, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var session = await agent.CreateSessionAsync();
// Act
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);
// Assert
Assert.NotNull(capturedOptions);
if (capturedOptions!.Tools != null)
{
Assert.DoesNotContain(capturedOptions.Tools, t => t is HostedWebSearchTool);
}
}
///
/// Verify that user-provided tools are preserved alongside the default HostedWebSearchTool.
///
[Fact]
public async Task WebSearch_CoexistsWithUserToolsAsync()
{
// Arrange
var mockClient = new Mock();
ChatOptions? capturedOptions = null;
mockClient
.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
.Callback, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));
var userTool = AIFunctionFactory.Create(() => "test", "UserTool");
var options = CreateAllDisabledOptions();
options.DisableWebSearch = false;
options.ChatOptions = new ChatOptions { Tools = [userTool] };
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var session = await agent.CreateSessionAsync();
// Act
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);
// Assert
Assert.NotNull(capturedOptions?.Tools);
Assert.Contains(capturedOptions!.Tools!, t => t is HostedWebSearchTool);
Assert.Contains(capturedOptions.Tools!, t => t == userTool);
}
#endregion
#region Feature: TodoProvider
///
/// Verify that TodoProvider is included in AIContextProviders by default.
///
[Fact]
public void TodoProvider_IncludedByDefault()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableTodoProvider = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is TodoProvider);
}
///
/// Verify that TodoProvider is excluded when disabled.
///
[Fact]
public void TodoProvider_ExcludedWhenDisabled()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
if (innerAgent!.AIContextProviders != null)
{
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is TodoProvider);
}
}
#endregion
#region Feature: AgentModeProvider
///
/// Verify that AgentModeProvider is included in AIContextProviders by default.
///
[Fact]
public void AgentModeProvider_IncludedByDefault()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableAgentModeProvider = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentModeProvider);
}
///
/// Verify that AgentModeProvider is excluded when disabled.
///
[Fact]
public void AgentModeProvider_ExcludedWhenDisabled()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
if (innerAgent!.AIContextProviders != null)
{
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is AgentModeProvider);
}
}
///
/// Verify that custom AgentModeProviderOptions are passed through.
///
[Fact]
public void AgentModeProvider_UsesCustomOptions()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableAgentModeProvider = false;
options.AgentModeProviderOptions = new AgentModeProviderOptions
{
Modes =
[
new AgentModeProviderOptions.AgentMode("custom-mode", "A custom mode for testing"),
],
DefaultMode = "custom-mode",
};
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert — AgentModeProvider should be present (we can't easily inspect its internal options,
// but we verify it is created and present).
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentModeProvider);
}
#endregion
#region Feature: FileMemoryProvider
///
/// Verify that FileMemoryProvider is included in AIContextProviders by default.
///
[Fact]
public void FileMemoryProvider_IncludedByDefault()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableFileMemory = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileMemoryProvider);
}
///
/// Verify that FileMemoryProvider is excluded when disabled.
///
[Fact]
public void FileMemoryProvider_ExcludedWhenDisabled()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
if (innerAgent!.AIContextProviders != null)
{
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is FileMemoryProvider);
}
}
///
/// Verify that a custom FileMemoryStore is used when provided.
///
[Fact]
public void FileMemoryProvider_UsesCustomStore()
{
// Arrange
var chatClient = new Mock().Object;
var customStore = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableFileMemory = false;
options.FileMemoryStore = customStore;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert — FileMemoryProvider should be present with the custom store.
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileMemoryProvider);
}
#endregion
#region Feature: FileAccessProvider
///
/// Verify that FileAccessProvider is included in AIContextProviders by default.
///
[Fact]
public void FileAccessProvider_IncludedByDefault()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableFileAccess = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileAccessProvider);
}
///
/// Verify that FileAccessProvider is excluded when disabled.
///
[Fact]
public void FileAccessProvider_ExcludedWhenDisabled()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
if (innerAgent!.AIContextProviders != null)
{
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is FileAccessProvider);
}
}
///
/// Verify that a custom FileAccessStore is used when provided.
///
[Fact]
public void FileAccessProvider_UsesCustomStore()
{
// Arrange
var chatClient = new Mock().Object;
var customStore = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableFileAccess = false;
options.FileAccessStore = customStore;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert — FileAccessProvider should be present with the custom store.
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileAccessProvider);
}
#endregion
#region Feature: AgentSkillsProvider
///
/// Verify that AgentSkillsProvider is included in AIContextProviders by default.
///
[Fact]
public void AgentSkillsProvider_IncludedByDefault()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableAgentSkillsProvider = false;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentSkillsProvider);
}
///
/// Verify that AgentSkillsProvider is excluded when disabled.
///
[Fact]
public void AgentSkillsProvider_ExcludedWhenDisabled()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
if (innerAgent!.AIContextProviders != null)
{
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is AgentSkillsProvider);
}
}
///
/// Verify that a custom AgentSkillsSource is used when provided.
///
[Fact]
public void AgentSkillsProvider_UsesCustomSource()
{
// Arrange
var chatClient = new Mock().Object;
var customSource = new Mock().Object;
var options = CreateAllDisabledOptions();
options.DisableAgentSkillsProvider = false;
options.AgentSkillsSource = customSource;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert — AgentSkillsProvider should be present.
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentSkillsProvider);
}
#endregion
#region Feature: MaximumIterationsPerRequest
///
/// Verify that MaximumIterationsPerRequest configures the FunctionInvokingChatClient.
///
[Fact]
public void MaximumIterationsPerRequest_ConfiguresFunctionInvokingChatClient()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.MaximumIterationsPerRequest = 42;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
var ficc = innerAgent!.ChatClient.GetService();
// Assert
Assert.NotNull(ficc);
Assert.Equal(42, ficc!.MaximumIterationsPerRequest);
}
///
/// Verify that the default MaximumIterationsPerRequest is used when not set.
///
[Fact]
public void MaximumIterationsPerRequest_UsesDefaultWhenNotSet()
{
// Arrange
var chatClient = new Mock().Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
var innerAgent = agent.GetService();
var ficc = innerAgent!.ChatClient.GetService();
// Assert — default is not 0 and not our custom value.
Assert.NotNull(ficc);
Assert.NotEqual(0, ficc!.MaximumIterationsPerRequest);
}
#endregion
#region Feature: All Defaults Enabled
///
/// Verify that when no options are provided, all default features are enabled.
///
[Fact]
public async Task AllDefaults_AllFeaturesEnabledAsync()
{
// Arrange
var mockClient = new Mock();
ChatOptions? capturedOptions = null;
mockClient
.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
.Callback, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Done")));
// Act
var agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens);
var innerAgent = agent.GetService();
// Assert — agent wrappers
Assert.NotNull(agent.GetService());
Assert.NotNull(agent.GetService());
// Assert — default context providers
Assert.NotNull(innerAgent);
Assert.NotNull(innerAgent!.AIContextProviders);
var providers = innerAgent.AIContextProviders!.ToList();
Assert.Contains(providers, p => p is TodoProvider);
Assert.Contains(providers, p => p is AgentModeProvider);
Assert.Contains(providers, p => p is FileMemoryProvider);
Assert.Contains(providers, p => p is FileAccessProvider);
Assert.Contains(providers, p => p is AgentSkillsProvider);
// Assert — HostedWebSearchTool is present in the tools sent to the model
var session = await agent.CreateSessionAsync();
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);
Assert.NotNull(capturedOptions?.Tools);
Assert.Contains(capturedOptions!.Tools!, t => t is HostedWebSearchTool);
}
#endregion
#region Feature: BackgroundAgentsProvider
///
/// Verify that BackgroundAgentsProvider is included when BackgroundAgents are specified.
///
[Fact]
public void BackgroundAgentsProvider_IncludedWhenAgentsSpecified()
{
// Arrange
var chatClient = new Mock().Object;
var bgAgentMock = new Mock();
bgAgentMock.Setup(a => a.Name).Returns("TestBackgroundAgent");
var options = CreateAllDisabledOptions();
options.BackgroundAgents = [bgAgentMock.Object];
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is BackgroundAgentsProvider);
}
///
/// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is null.
///
[Fact]
public void BackgroundAgentsProvider_ExcludedWhenAgentsNull()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.BackgroundAgents = null;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
if (innerAgent!.AIContextProviders != null)
{
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider);
}
}
///
/// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is an empty collection.
///
[Fact]
public void BackgroundAgentsProvider_ExcludedWhenAgentsEmpty()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.BackgroundAgents = Array.Empty();
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
if (innerAgent!.AIContextProviders != null)
{
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider);
}
}
///
/// Verify that BackgroundAgentsProviderOptions is passed through when specified.
///
[Fact]
public async Task BackgroundAgentsProvider_UsesProvidedOptionsAsync()
{
// Arrange
var chatClient = new Mock().Object;
var bgAgentMock = new Mock();
bgAgentMock.Setup(a => a.Name).Returns("TestBackgroundAgent");
bgAgentMock.Setup(a => a.Description).Returns("A test background agent");
var providerOptions = new BackgroundAgentsProviderOptions
{
Instructions = "Custom instructions with {background_agents} list.",
};
var options = CreateAllDisabledOptions();
options.BackgroundAgents = [bgAgentMock.Object];
options.BackgroundAgentsProviderOptions = providerOptions;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
var bgProvider = innerAgent!.AIContextProviders!.OfType().Single();
#pragma warning disable MAAI001
var invokingContext = new AIContextProvider.InvokingContext(
new Mock().Object,
new Mock().Object,
new AIContext());
#pragma warning restore MAAI001
AIContext result = await bgProvider.InvokingAsync(invokingContext);
// Assert — custom instructions template is used and agent info is included
Assert.NotNull(result.Instructions);
Assert.Contains("Custom instructions with", result.Instructions);
Assert.Contains("TestBackgroundAgent", result.Instructions);
}
///
/// Verify that multiple background agents are all passed to the provider.
///
[Fact]
public async Task BackgroundAgentsProvider_IncludesMultipleAgentsAsync()
{
// Arrange
var chatClient = new Mock().Object;
var agent1Mock = new Mock();
agent1Mock.Setup(a => a.Name).Returns("Agent1");
agent1Mock.Setup(a => a.Description).Returns("First agent");
var agent2Mock = new Mock();
agent2Mock.Setup(a => a.Name).Returns("Agent2");
agent2Mock.Setup(a => a.Description).Returns("Second agent");
var options = CreateAllDisabledOptions();
options.BackgroundAgents = [agent1Mock.Object, agent2Mock.Object];
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
var bgProvider = innerAgent!.AIContextProviders!.OfType().Single();
#pragma warning disable MAAI001
var invokingContext = new AIContextProvider.InvokingContext(
new Mock().Object,
new Mock().Object,
new AIContext());
#pragma warning restore MAAI001
AIContext result = await bgProvider.InvokingAsync(invokingContext);
// Assert — both agents appear in the provider's instructions
Assert.NotNull(result.Instructions);
Assert.Contains("Agent1", result.Instructions);
Assert.Contains("First agent", result.Instructions);
Assert.Contains("Agent2", result.Instructions);
Assert.Contains("Second agent", result.Instructions);
}
#endregion
#if NET
#region Feature: ShellEnvironmentProvider
///
/// Verify that ShellEnvironmentProvider is included when ShellExecutor is provided.
///
[Fact]
public void ShellEnvironmentProvider_IncludedWhenExecutorProvided()
{
// Arrange
var chatClient = new Mock().Object;
var executorMock = new Mock();
executorMock.Setup(e => e.AsAIFunction(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(AIFunctionFactory.Create(() => "test", "run_shell"));
var options = CreateAllDisabledOptions();
options.ShellExecutor = executorMock.Object;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is ShellEnvironmentProvider);
}
///
/// Verify that ShellEnvironmentProvider is not included when ShellExecutor is null.
///
[Fact]
public void ShellEnvironmentProvider_ExcludedWhenExecutorNull()
{
// Arrange
var chatClient = new Mock().Object;
var options = CreateAllDisabledOptions();
options.ShellExecutor = null;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert
Assert.NotNull(innerAgent);
Assert.NotNull(innerAgent!.AIContextProviders);
Assert.DoesNotContain(innerAgent.AIContextProviders!, p => p is ShellEnvironmentProvider);
}
///
/// Verify that the shell tool AIFunction is added to ChatOptions.Tools when ShellExecutor is provided.
///
[Fact]
public async Task ShellExecutor_ToolAddedToChatOptionsAsync()
{
// Arrange
ChatOptions? capturedOptions = null;
var chatClientMock = new Mock();
chatClientMock
.Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
.Callback, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "done")));
var executorMock = new Mock();
executorMock.Setup(e => e.AsAIFunction(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(AIFunctionFactory.Create(() => "shell output", "run_shell"));
var options = CreateAllDisabledOptions();
options.DisableWebSearch = true;
options.ShellExecutor = executorMock.Object;
// Act
var agent = new HarnessAgent(chatClientMock.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var session = await agent.CreateSessionAsync();
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session);
// Assert — the shell tool should be present
Assert.NotNull(capturedOptions?.Tools);
Assert.Contains(capturedOptions!.Tools!, t => t is AIFunction f && f.Name == "run_shell");
}
///
/// Verify that ShellEnvironmentProvider is present when ShellEnvironmentProviderOptions is also specified.
///
[Fact]
public void ShellEnvironmentProvider_PresentWhenOptionsProvided()
{
// Arrange
var chatClient = new Mock().Object;
var executorMock = new Mock();
executorMock.Setup(e => e.AsAIFunction(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(AIFunctionFactory.Create(() => "test", "run_shell"));
var envOptions = new ShellEnvironmentProviderOptions
{
ProbeTools = ["git", "python"],
};
var options = CreateAllDisabledOptions();
options.ShellExecutor = executorMock.Object;
options.ShellEnvironmentProviderOptions = envOptions;
// Act
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
var innerAgent = agent.GetService();
// Assert — provider should exist (options wiring is validated by the provider's behavior)
Assert.NotNull(innerAgent?.AIContextProviders);
Assert.Contains(innerAgent!.AIContextProviders!, p => p is ShellEnvironmentProvider);
}
#endregion
#endif
}