// 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; public class HarnessAgentTests { private const int TestMaxContextWindowTokens = 100_000; private const int TestMaxOutputTokens = 10_000; #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; // Act var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions { Name = "TestAgent", Description = "A test agent", }); // 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; // Act var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions { Id = "my-agent-id", }); // 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); 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 ChatOptions.Instructions is null. /// [Fact] public void Instructions_DefaultsWhenChatOptionsInstructionsIsNull() { // Arrange var chatClient = new Mock().Object; // Act var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions { ChatOptions = new ChatOptions { Temperature = 0.5f }, }); var innerAgent = agent.GetService(); // Assert Assert.NotNull(innerAgent); Assert.Equal(HarnessAgent.DefaultInstructions, innerAgent!.Instructions); } /// /// Verify that ChatOptions.Instructions overrides the defaults. /// [Fact] public void Instructions_CanBeOverriddenViaChatOptions() { // Arrange var chatClient = new Mock().Object; // Act var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions { ChatOptions = new ChatOptions { Instructions = "You are a custom assistant." }, }); var innerAgent = agent.GetService(); // Assert Assert.NotNull(innerAgent); Assert.Equal("You are a custom assistant.", 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); 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(); // Act var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions { ChatHistoryProvider = customProvider, }); 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); 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); 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, /// not merged into the chat client builder pipeline. /// [Fact] public void AIContextProviders_ArePassedToInnerAgent() { // Arrange var chatClient = new Mock().Object; var todoProvider = new TodoProvider(); // Act var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions { AIContextProviders = [todoProvider], }); var innerAgent = agent.GetService(); // Assert — the TodoProvider should appear in the inner agent's AIContextProviders. Assert.NotNull(innerAgent); Assert.NotNull(innerAgent!.AIContextProviders); Assert.Contains(todoProvider, innerAgent.AIContextProviders!); } /// /// Verify that when no AIContextProviders are specified, the inner agent has no additional providers. /// [Fact] public void AIContextProviders_IsNullWhenNoneSpecified() { // Arrange var chatClient = new Mock().Object; // Act var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens); var innerAgent = agent.GetService(); // Assert Assert.NotNull(innerAgent); Assert.Null(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 agent = new HarnessAgent(mockClient.Object, TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions { ChatOptions = new ChatOptions { Tools = [tool], }, }); 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); // 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); // 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); 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; // Act var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens, new HarnessAgentOptions { Name = "ExtensionAgent", ChatOptions = new ChatOptions { Instructions = "Custom instructions" }, }); var innerAgent = agent.GetService(); // Assert Assert.Equal("ExtensionAgent", agent.Name); Assert.NotNull(innerAgent); Assert.Equal("Custom instructions", 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 }