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