mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
bda40ba0e1
* Add shell support to the HarnessAgent * Address PR comments * Address PR comments
1464 lines
51 KiB
C#
1464 lines
51 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// Creates a HarnessAgent with all default features disabled to isolate tests for specific behaviors.
|
|
/// </summary>
|
|
private static HarnessAgentOptions CreateAllDisabledOptions() => new()
|
|
{
|
|
DisableToolApproval = true,
|
|
DisableOpenTelemetry = true,
|
|
DisableFileMemory = true,
|
|
DisableFileAccess = true,
|
|
DisableWebSearch = true,
|
|
DisableTodoProvider = true,
|
|
DisableAgentModeProvider = true,
|
|
DisableAgentSkillsProvider = true,
|
|
};
|
|
|
|
#region Constructor Validation
|
|
|
|
/// <summary>
|
|
/// Verify that the constructor throws when chatClient is null.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Constructor_ThrowsWhenChatClientIsNull()
|
|
{
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => new HarnessAgent(null!, TestMaxContextWindowTokens, TestMaxOutputTokens));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that the constructor throws when MaxContextWindowTokens is invalid (zero).
|
|
/// </summary>
|
|
[Fact]
|
|
public void Constructor_ThrowsWhenMaxContextWindowTokensIsZero()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new HarnessAgent(chatClient, 0, TestMaxOutputTokens));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that the constructor throws when MaxOutputTokens equals MaxContextWindowTokens.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Constructor_ThrowsWhenMaxOutputTokensEqualsContextWindow()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new HarnessAgent(chatClient, 100_000, 100_000));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that the constructor succeeds when options is null.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Constructor_SucceedsWhenOptionsIsNull()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens);
|
|
|
|
// Assert
|
|
Assert.NotNull(agent);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Agent Identity
|
|
|
|
/// <summary>
|
|
/// Verify that Name and Description are passed through to the inner agent.
|
|
/// </summary>
|
|
[Fact]
|
|
public void NameAndDescription_ArePassedThrough()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that Id is passed through to the inner agent.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Id_IsPassedThrough()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().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
|
|
|
|
/// <summary>
|
|
/// Verify that default instructions are used when none are provided.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Instructions_DefaultsToBuiltInInstructions()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
Assert.Equal(HarnessAgent.DefaultInstructions, innerAgent!.Instructions);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that default instructions are used when options is provided but neither HarnessInstructions nor ChatOptions.Instructions is set.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Instructions_DefaultsWhenBothNull()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.ChatOptions = new ChatOptions { Temperature = 0.5f };
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
Assert.Equal(HarnessAgent.DefaultInstructions, innerAgent!.Instructions);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that ChatOptions.Instructions is appended to the default HarnessInstructions.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Instructions_CombinesDefaultHarnessWithAgentInstructions()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().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<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
var expected = $"{HarnessAgent.DefaultInstructions}\n\nYou are a custom assistant.";
|
|
Assert.Equal(expected, innerAgent!.Instructions);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that custom HarnessInstructions replaces the default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Instructions_CustomHarnessInstructionsReplacesDefault()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.HarnessInstructions = "Custom harness rules.";
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
Assert.Equal("Custom harness rules.", innerAgent!.Instructions);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that custom HarnessInstructions and ChatOptions.Instructions are combined.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Instructions_CombinesCustomHarnessWithAgentInstructions()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().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<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
Assert.Equal("Custom harness rules.\n\nYou are a research agent.", innerAgent!.Instructions);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that empty HarnessInstructions omits harness portion, using only agent instructions.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Instructions_EmptyHarnessInstructionsUsesOnlyAgentInstructions()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().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<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
Assert.Equal("Agent only instructions.", innerAgent!.Instructions);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that empty HarnessInstructions with no agent instructions results in empty string.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Instructions_EmptyHarnessInstructionsWithNoAgentInstructions()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.HarnessInstructions = string.Empty;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
Assert.Equal(string.Empty, innerAgent!.Instructions);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ChatHistoryProvider
|
|
|
|
/// <summary>
|
|
/// Verify that the default ChatHistoryProvider is InMemoryChatHistoryProvider when none is specified.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ChatHistoryProvider_DefaultsToInMemory()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
Assert.IsType<InMemoryChatHistoryProvider>(innerAgent!.ChatHistoryProvider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that a custom ChatHistoryProvider is used when provided.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ChatHistoryProvider_UsesCustomProviderWhenSpecified()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var customProvider = new InMemoryChatHistoryProvider();
|
|
var options = CreateAllDisabledOptions();
|
|
options.ChatHistoryProvider = customProvider;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
Assert.Same(customProvider, innerAgent!.ChatHistoryProvider);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ChatClient Pipeline
|
|
|
|
/// <summary>
|
|
/// Verify that the inner agent's ChatClient includes FunctionInvokingChatClient in the pipeline.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Pipeline_IncludesFunctionInvokingChatClient()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
var ficc = innerAgent!.ChatClient.GetService<FunctionInvokingChatClient>();
|
|
Assert.NotNull(ficc);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Pipeline_HasDecoratedChatClient()
|
|
{
|
|
// Arrange
|
|
var mockClient = new Mock<IChatClient>();
|
|
var rawClient = mockClient.Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(rawClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// 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
|
|
|
|
/// <summary>
|
|
/// Verify that additional AIContextProviders from options are passed to the inner ChatClientAgent.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AIContextProviders_ArePassedToInnerAgent()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var customProvider = new TodoProvider();
|
|
var options = CreateAllDisabledOptions();
|
|
options.AIContextProviders = [customProvider];
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert — the custom provider should appear in the inner agent's AIContextProviders.
|
|
Assert.NotNull(innerAgent);
|
|
Assert.NotNull(innerAgent!.AIContextProviders);
|
|
Assert.Contains(customProvider, innerAgent.AIContextProviders!);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that when all default providers are disabled and no user AIContextProviders are specified,
|
|
/// the inner agent has an empty providers list.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AIContextProviders_IsEmptyWhenAllDisabledAndNoneSpecified()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
Assert.NotNull(innerAgent!.AIContextProviders);
|
|
Assert.Empty(innerAgent.AIContextProviders!);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ChatOptions and Tools
|
|
|
|
/// <summary>
|
|
/// Verify that tools from ChatOptions are passed to the model during invocation.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ChatOptions_ToolsArePreservedAsync()
|
|
{
|
|
// Arrange
|
|
var tool = AIFunctionFactory.Create(() => "test", "TestTool");
|
|
var mockClient = new Mock<IChatClient>();
|
|
ChatOptions? capturedOptions = null;
|
|
mockClient
|
|
.Setup(c => c.GetResponseAsync(
|
|
It.IsAny<IEnumerable<ChatMessage>>(),
|
|
It.IsAny<ChatOptions>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.Callback<IEnumerable<ChatMessage>, 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that the source ChatOptions are cloned and not modified.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ChatOptions_SourceIsNotModified()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().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
|
|
|
|
/// <summary>
|
|
/// Verify that GetService returns the HarnessAgent for its own type.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetService_ReturnsSelfForHarnessAgentType()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
|
|
// Assert
|
|
Assert.Same(agent, agent.GetService<HarnessAgent>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that GetService returns the inner ChatClientAgent.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetService_ReturnsInnerChatClientAgent()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
|
|
// Assert
|
|
Assert.NotNull(agent.GetService<ChatClientAgent>());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region RunAsync Delegation
|
|
|
|
/// <summary>
|
|
/// Verify that RunAsync delegates to the inner ChatClientAgent.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task RunAsync_DelegatesToInnerAgentAsync()
|
|
{
|
|
// Arrange
|
|
var mockClient = new Mock<IChatClient>();
|
|
mockClient
|
|
.Setup(c => c.GetResponseAsync(
|
|
It.IsAny<IEnumerable<ChatMessage>>(),
|
|
It.IsAny<ChatOptions>(),
|
|
It.IsAny<CancellationToken>()))
|
|
.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
|
|
|
|
/// <summary>
|
|
/// Verify that DefaultInstructions is a non-empty public constant.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DefaultInstructions_IsNonEmpty()
|
|
{
|
|
// Assert
|
|
Assert.False(string.IsNullOrWhiteSpace(HarnessAgent.DefaultInstructions));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AsHarnessAgent Extension Method
|
|
|
|
/// <summary>
|
|
/// Verify that AsHarnessAgent creates a HarnessAgent with default options.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AsHarnessAgent_CreatesAgentWithDefaults()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = chatClient.AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens);
|
|
|
|
// Assert
|
|
Assert.NotNull(agent);
|
|
Assert.IsType<HarnessAgent>(agent);
|
|
Assert.Equal(HarnessAgent.DefaultInstructions, agent.GetService<ChatClientAgent>()!.Instructions);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that AsHarnessAgent passes options through to the HarnessAgent.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AsHarnessAgent_PassesOptionsThrough()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().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<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.Equal("ExtensionAgent", agent.Name);
|
|
Assert.NotNull(innerAgent);
|
|
var expected = $"{HarnessAgent.DefaultInstructions}\n\nCustom instructions";
|
|
Assert.Equal(expected, innerAgent!.Instructions);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that AsHarnessAgent throws when chatClient is null.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AsHarnessAgent_ThrowsWhenChatClientIsNull()
|
|
{
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => ((IChatClient)null!).AsHarnessAgent(TestMaxContextWindowTokens, TestMaxOutputTokens));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Feature: ToolApproval
|
|
|
|
/// <summary>
|
|
/// Verify that ToolApprovalAgent is included in the pipeline by default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ToolApproval_IncludedByDefault()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.DisableToolApproval = false;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
|
|
// Assert
|
|
Assert.NotNull(agent.GetService<ToolApprovalAgent>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that ToolApprovalAgent is excluded when disabled.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ToolApproval_ExcludedWhenDisabled()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
|
|
// Assert
|
|
Assert.Null(agent.GetService<ToolApprovalAgent>());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Feature: OpenTelemetry
|
|
|
|
/// <summary>
|
|
/// Verify that OpenTelemetryAgent is included in the pipeline by default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void OpenTelemetry_IncludedByDefault()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.DisableOpenTelemetry = false;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
|
|
// Assert
|
|
Assert.NotNull(agent.GetService<OpenTelemetryAgent>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that OpenTelemetryAgent is excluded when disabled.
|
|
/// </summary>
|
|
[Fact]
|
|
public void OpenTelemetry_ExcludedWhenDisabled()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
|
|
// Assert
|
|
Assert.Null(agent.GetService<OpenTelemetryAgent>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that a custom OpenTelemetrySourceName is accepted without error.
|
|
/// </summary>
|
|
[Fact]
|
|
public void OpenTelemetry_CustomSourceNameIsAccepted()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().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<OpenTelemetryAgent>());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Feature: WebSearch
|
|
|
|
/// <summary>
|
|
/// Verify that HostedWebSearchTool is added to ChatOptions.Tools by default.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task WebSearch_IncludedByDefaultAsync()
|
|
{
|
|
// Arrange
|
|
var mockClient = new Mock<IChatClient>();
|
|
ChatOptions? capturedOptions = null;
|
|
mockClient
|
|
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
|
.Callback<IEnumerable<ChatMessage>, 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that HostedWebSearchTool is not added when disabled.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task WebSearch_ExcludedWhenDisabledAsync()
|
|
{
|
|
// Arrange
|
|
var mockClient = new Mock<IChatClient>();
|
|
ChatOptions? capturedOptions = null;
|
|
mockClient
|
|
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
|
.Callback<IEnumerable<ChatMessage>, 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that user-provided tools are preserved alongside the default HostedWebSearchTool.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task WebSearch_CoexistsWithUserToolsAsync()
|
|
{
|
|
// Arrange
|
|
var mockClient = new Mock<IChatClient>();
|
|
ChatOptions? capturedOptions = null;
|
|
mockClient
|
|
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
|
.Callback<IEnumerable<ChatMessage>, 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
|
|
|
|
/// <summary>
|
|
/// Verify that TodoProvider is included in AIContextProviders by default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TodoProvider_IncludedByDefault()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.DisableTodoProvider = false;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent?.AIContextProviders);
|
|
Assert.Contains(innerAgent!.AIContextProviders!, p => p is TodoProvider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that TodoProvider is excluded when disabled.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TodoProvider_ExcludedWhenDisabled()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
if (innerAgent!.AIContextProviders != null)
|
|
{
|
|
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is TodoProvider);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Feature: AgentModeProvider
|
|
|
|
/// <summary>
|
|
/// Verify that AgentModeProvider is included in AIContextProviders by default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AgentModeProvider_IncludedByDefault()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.DisableAgentModeProvider = false;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent?.AIContextProviders);
|
|
Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentModeProvider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that AgentModeProvider is excluded when disabled.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AgentModeProvider_ExcludedWhenDisabled()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
if (innerAgent!.AIContextProviders != null)
|
|
{
|
|
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is AgentModeProvider);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that custom AgentModeProviderOptions are passed through.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AgentModeProvider_UsesCustomOptions()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().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<ChatClientAgent>();
|
|
|
|
// 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
|
|
|
|
/// <summary>
|
|
/// Verify that FileMemoryProvider is included in AIContextProviders by default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FileMemoryProvider_IncludedByDefault()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.DisableFileMemory = false;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent?.AIContextProviders);
|
|
Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileMemoryProvider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that FileMemoryProvider is excluded when disabled.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FileMemoryProvider_ExcludedWhenDisabled()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
if (innerAgent!.AIContextProviders != null)
|
|
{
|
|
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is FileMemoryProvider);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that a custom FileMemoryStore is used when provided.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FileMemoryProvider_UsesCustomStore()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var customStore = new Mock<AgentFileStore>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.DisableFileMemory = false;
|
|
options.FileMemoryStore = customStore;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// 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
|
|
|
|
/// <summary>
|
|
/// Verify that FileAccessProvider is included in AIContextProviders by default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FileAccessProvider_IncludedByDefault()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.DisableFileAccess = false;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent?.AIContextProviders);
|
|
Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileAccessProvider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that FileAccessProvider is excluded when disabled.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FileAccessProvider_ExcludedWhenDisabled()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
if (innerAgent!.AIContextProviders != null)
|
|
{
|
|
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is FileAccessProvider);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that a custom FileAccessStore is used when provided.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FileAccessProvider_UsesCustomStore()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var customStore = new Mock<AgentFileStore>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.DisableFileAccess = false;
|
|
options.FileAccessStore = customStore;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// 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
|
|
|
|
/// <summary>
|
|
/// Verify that AgentSkillsProvider is included in AIContextProviders by default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AgentSkillsProvider_IncludedByDefault()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.DisableAgentSkillsProvider = false;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent?.AIContextProviders);
|
|
Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentSkillsProvider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that AgentSkillsProvider is excluded when disabled.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AgentSkillsProvider_ExcludedWhenDisabled()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
if (innerAgent!.AIContextProviders != null)
|
|
{
|
|
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is AgentSkillsProvider);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that a custom AgentSkillsSource is used when provided.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AgentSkillsProvider_UsesCustomSource()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var customSource = new Mock<AgentSkillsSource>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.DisableAgentSkillsProvider = false;
|
|
options.AgentSkillsSource = customSource;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert — AgentSkillsProvider should be present.
|
|
Assert.NotNull(innerAgent?.AIContextProviders);
|
|
Assert.Contains(innerAgent!.AIContextProviders!, p => p is AgentSkillsProvider);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Feature: MaximumIterationsPerRequest
|
|
|
|
/// <summary>
|
|
/// Verify that MaximumIterationsPerRequest configures the FunctionInvokingChatClient.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MaximumIterationsPerRequest_ConfiguresFunctionInvokingChatClient()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.MaximumIterationsPerRequest = 42;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
var ficc = innerAgent!.ChatClient.GetService<FunctionInvokingChatClient>();
|
|
|
|
// Assert
|
|
Assert.NotNull(ficc);
|
|
Assert.Equal(42, ficc!.MaximumIterationsPerRequest);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that the default MaximumIterationsPerRequest is used when not set.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MaximumIterationsPerRequest_UsesDefaultWhenNotSet()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, CreateAllDisabledOptions());
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
var ficc = innerAgent!.ChatClient.GetService<FunctionInvokingChatClient>();
|
|
|
|
// Assert — default is not 0 and not our custom value.
|
|
Assert.NotNull(ficc);
|
|
Assert.NotEqual(0, ficc!.MaximumIterationsPerRequest);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Feature: All Defaults Enabled
|
|
|
|
/// <summary>
|
|
/// Verify that when no options are provided, all default features are enabled.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task AllDefaults_AllFeaturesEnabledAsync()
|
|
{
|
|
// Arrange
|
|
var mockClient = new Mock<IChatClient>();
|
|
ChatOptions? capturedOptions = null;
|
|
mockClient
|
|
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
|
.Callback<IEnumerable<ChatMessage>, 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<ChatClientAgent>();
|
|
|
|
// Assert — agent wrappers
|
|
Assert.NotNull(agent.GetService<ToolApprovalAgent>());
|
|
Assert.NotNull(agent.GetService<OpenTelemetryAgent>());
|
|
|
|
// 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
|
|
|
|
/// <summary>
|
|
/// Verify that BackgroundAgentsProvider is included when BackgroundAgents are specified.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BackgroundAgentsProvider_IncludedWhenAgentsSpecified()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var bgAgentMock = new Mock<AIAgent>();
|
|
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<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent?.AIContextProviders);
|
|
Assert.Contains(innerAgent!.AIContextProviders!, p => p is BackgroundAgentsProvider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is null.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BackgroundAgentsProvider_ExcludedWhenAgentsNull()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.BackgroundAgents = null;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
if (innerAgent!.AIContextProviders != null)
|
|
{
|
|
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is an empty collection.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BackgroundAgentsProvider_ExcludedWhenAgentsEmpty()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.BackgroundAgents = Array.Empty<AIAgent>();
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
if (innerAgent!.AIContextProviders != null)
|
|
{
|
|
Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that BackgroundAgentsProviderOptions is passed through when specified.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task BackgroundAgentsProvider_UsesProvidedOptionsAsync()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var bgAgentMock = new Mock<AIAgent>();
|
|
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<ChatClientAgent>();
|
|
var bgProvider = innerAgent!.AIContextProviders!.OfType<BackgroundAgentsProvider>().Single();
|
|
|
|
#pragma warning disable MAAI001
|
|
var invokingContext = new AIContextProvider.InvokingContext(
|
|
new Mock<AIAgent>().Object,
|
|
new Mock<AgentSession>().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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that multiple background agents are all passed to the provider.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task BackgroundAgentsProvider_IncludesMultipleAgentsAsync()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var agent1Mock = new Mock<AIAgent>();
|
|
agent1Mock.Setup(a => a.Name).Returns("Agent1");
|
|
agent1Mock.Setup(a => a.Description).Returns("First agent");
|
|
var agent2Mock = new Mock<AIAgent>();
|
|
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<ChatClientAgent>();
|
|
var bgProvider = innerAgent!.AIContextProviders!.OfType<BackgroundAgentsProvider>().Single();
|
|
|
|
#pragma warning disable MAAI001
|
|
var invokingContext = new AIContextProvider.InvokingContext(
|
|
new Mock<AIAgent>().Object,
|
|
new Mock<AgentSession>().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
|
|
|
|
/// <summary>
|
|
/// Verify that ShellEnvironmentProvider is included when ShellExecutor is provided.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ShellEnvironmentProvider_IncludedWhenExecutorProvided()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var executorMock = new Mock<ShellExecutor>();
|
|
executorMock.Setup(e => e.AsAIFunction(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<bool>()))
|
|
.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<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent?.AIContextProviders);
|
|
Assert.Contains(innerAgent!.AIContextProviders!, p => p is ShellEnvironmentProvider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that ShellEnvironmentProvider is not included when ShellExecutor is null.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ShellEnvironmentProvider_ExcludedWhenExecutorNull()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var options = CreateAllDisabledOptions();
|
|
options.ShellExecutor = null;
|
|
|
|
// Act
|
|
var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options);
|
|
var innerAgent = agent.GetService<ChatClientAgent>();
|
|
|
|
// Assert
|
|
Assert.NotNull(innerAgent);
|
|
Assert.NotNull(innerAgent!.AIContextProviders);
|
|
Assert.DoesNotContain(innerAgent.AIContextProviders!, p => p is ShellEnvironmentProvider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that the shell tool AIFunction is added to ChatOptions.Tools when ShellExecutor is provided.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ShellExecutor_ToolAddedToChatOptionsAsync()
|
|
{
|
|
// Arrange
|
|
ChatOptions? capturedOptions = null;
|
|
var chatClientMock = new Mock<IChatClient>();
|
|
chatClientMock
|
|
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
|
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts)
|
|
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "done")));
|
|
|
|
var executorMock = new Mock<ShellExecutor>();
|
|
executorMock.Setup(e => e.AsAIFunction(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<bool>()))
|
|
.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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify that ShellEnvironmentProvider is present when ShellEnvironmentProviderOptions is also specified.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ShellEnvironmentProvider_PresentWhenOptionsProvided()
|
|
{
|
|
// Arrange
|
|
var chatClient = new Mock<IChatClient>().Object;
|
|
var executorMock = new Mock<ShellExecutor>();
|
|
executorMock.Setup(e => e.AsAIFunction(It.IsAny<string>(), It.IsAny<string?>(), It.IsAny<bool>()))
|
|
.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<ChatClientAgent>();
|
|
|
|
// 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
|
|
}
|