// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Anthropic; using Anthropic.Core; using Anthropic.Services; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Anthropic.UnitTests.Extensions; /// /// Unit tests for the AnthropicClientExtensions class. /// public sealed class AnthropicClientExtensionsTests { /// /// Test custom chat client that can be used to verify clientFactory functionality. /// private sealed class TestChatClient : IChatClient { private readonly IChatClient _innerClient; public TestChatClient(IChatClient innerClient) { this._innerClient = innerClient; } public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => this._innerClient.GetResponseAsync(messages, options, cancellationToken); public async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { await foreach (var update in this._innerClient.GetStreamingResponseAsync(messages, options, cancellationToken)) { yield return update; } } public object? GetService(Type serviceType, object? serviceKey = null) { // Return this instance when requested if (serviceType == typeof(TestChatClient)) { return this; } return this._innerClient.GetService(serviceType, serviceKey); } public void Dispose() => this._innerClient.Dispose(); } /// /// Creates a test ChatClient implementation for testing. /// private sealed class TestAnthropicChatClient : IAnthropicClient { public TestAnthropicChatClient() { } public HttpClient HttpClient { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public string BaseUrl { get => "http://localhost"; init => throw new NotImplementedException(); } public bool ResponseValidation { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public int? MaxRetries { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public TimeSpan? Timeout { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public string? ApiKey { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public string? AuthToken { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public string? WebhookKey { get => throw new NotImplementedException(); init => throw new NotImplementedException(); } public IAnthropicClientWithRawResponse WithRawResponse => throw new NotImplementedException(); public IMessageService Messages => throw new NotImplementedException(); public IModelService Models => throw new NotImplementedException(); public IBetaService Beta => throw new NotImplementedException(); public IAnthropicClient WithOptions(Func modifier) { throw new NotImplementedException(); } public void Dispose() { } } /// /// Verify that CreateAIAgent with clientFactory parameter correctly applies the factory. /// [Fact] public void CreateAIAgent_WithClientFactory_AppliesFactoryCorrectly() { // Arrange var chatClient = new TestAnthropicChatClient(); var testChatClient = new TestChatClient(chatClient.AsIChatClient()); // Act var agent = chatClient.AsAIAgent( model: "test-model", instructions: "Test instructions", name: "Test Agent", description: "Test description", clientFactory: (innerClient) => testChatClient); // Assert Assert.NotNull(agent); Assert.Equal("Test Agent", agent.Name); Assert.Equal("Test description", agent.Description); // Verify that the custom chat client can be retrieved from the agent's service collection var retrievedTestClient = agent.GetService(); Assert.NotNull(retrievedTestClient); Assert.Same(testChatClient, retrievedTestClient); } /// /// Verify that CreateAIAgent with clientFactory using AsBuilder pattern works correctly. /// [Fact] public void CreateAIAgent_WithClientFactoryUsingAsBuilder_AppliesFactoryCorrectly() { // Arrange var chatClient = new TestAnthropicChatClient(); TestChatClient? testChatClient = null; // Act var agent = chatClient.AsAIAgent( model: "test-model", instructions: "Test instructions", clientFactory: (innerClient) => innerClient.AsBuilder().Use((innerClient) => testChatClient = new TestChatClient(innerClient)).Build()); // Assert Assert.NotNull(agent); // Verify that the custom chat client can be retrieved from the agent's service collection var retrievedTestClient = agent.GetService(); Assert.NotNull(retrievedTestClient); Assert.Same(testChatClient, retrievedTestClient); } /// /// Verify that CreateAIAgent with options and clientFactory parameter correctly applies the factory. /// [Fact] public void CreateAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly() { // Arrange var chatClient = new TestAnthropicChatClient(); var testChatClient = new TestChatClient(chatClient.AsIChatClient()); var options = new ChatClientAgentOptions { Name = "Test Agent", Description = "Test description", ChatOptions = new() { Instructions = "Test instructions" } }; // Act var agent = chatClient.AsAIAgent( options, clientFactory: (innerClient) => testChatClient); // Assert Assert.NotNull(agent); Assert.Equal("Test Agent", agent.Name); Assert.Equal("Test description", agent.Description); // Verify that the custom chat client can be retrieved from the agent's service collection var retrievedTestClient = agent.GetService(); Assert.NotNull(retrievedTestClient); Assert.Same(testChatClient, retrievedTestClient); } /// /// Verify that CreateAIAgent without clientFactory works normally. /// [Fact] public void CreateAIAgent_WithoutClientFactory_WorksNormally() { // Arrange var chatClient = new TestAnthropicChatClient(); // Act var agent = chatClient.AsAIAgent( model: "test-model", instructions: "Test instructions", name: "Test Agent"); // Assert Assert.NotNull(agent); Assert.Equal("Test Agent", agent.Name); // Verify that no TestChatClient is available since no factory was provided var retrievedTestClient = agent.GetService(); Assert.Null(retrievedTestClient); } /// /// Verify that CreateAIAgent with null clientFactory works normally. /// [Fact] public void CreateAIAgent_WithNullClientFactory_WorksNormally() { // Arrange var chatClient = new TestAnthropicChatClient(); // Act var agent = chatClient.AsAIAgent( model: "test-model", instructions: "Test instructions", name: "Test Agent", clientFactory: null); // Assert Assert.NotNull(agent); Assert.Equal("Test Agent", agent.Name); // Verify that no TestChatClient is available since no factory was provided var retrievedTestClient = agent.GetService(); Assert.Null(retrievedTestClient); } /// /// Verify that CreateAIAgent throws ArgumentNullException when client is null. /// [Fact] public void CreateAIAgent_WithNullClient_ThrowsArgumentNullException() { // Act & Assert var exception = Assert.Throws(() => ((TestAnthropicChatClient)null!).AsAIAgent("test-model")); Assert.Equal("client", exception.ParamName); } /// /// Verify that CreateAIAgent with options throws ArgumentNullException when options is null. /// [Fact] public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException() { // Arrange var chatClient = new TestAnthropicChatClient(); // Act & Assert var exception = Assert.Throws(() => chatClient.AsAIAgent((ChatClientAgentOptions)null!)); Assert.Equal("options", exception.ParamName); } /// /// Verify that CreateAIAgent with tools correctly assigns tools to ChatOptions. /// [Fact] public void CreateAIAgent_WithTools_AssignsToolsCorrectly() { // Arrange var chatClient = new TestAnthropicChatClient(); IList tools = [AIFunctionFactory.Create(() => "test result", "TestFunction", "A test function")]; // Act var agent = chatClient.AsAIAgent( model: "test-model", name: "Test Agent", tools: tools); // Assert Assert.NotNull(agent); Assert.Equal("Test Agent", agent.Name); // When tools are provided, ChatOptions is created but instructions remain null Assert.Null(agent.Instructions); // Verify that tools are registered in the FunctionInvokingChatClient var functionInvokingClient = agent.GetService(); Assert.NotNull(functionInvokingClient); Assert.NotNull(functionInvokingClient.AdditionalTools); Assert.Contains(functionInvokingClient.AdditionalTools, t => t is AIFunction func && func.Name == "TestFunction"); } /// /// Verify that CreateAIAgent with explicit defaultMaxTokens uses the provided value. /// [Fact] public async Task CreateAIAgent_WithExplicitMaxTokens_UsesProvidedValueAsync() { // Arrange int capturedMaxTokens = 0; var handler = new CapturingHttpHandler(request => { // Parse the request body to capture max_tokens var content = request.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); if (content is not null) { var json = System.Text.Json.JsonDocument.Parse(content); if (json.RootElement.TryGetProperty("max_tokens", out var maxTokens)) { capturedMaxTokens = maxTokens.GetInt32(); } } }); var client = new AnthropicClient { HttpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost") }, ApiKey = "test-key" }; // Act var agent = client.AsAIAgent( model: "claude-haiku-4-5", name: "Test Agent", defaultMaxTokens: 8192); // Invoke the agent to trigger the request var session = await agent.CreateSessionAsync(); try { await agent.RunAsync("Test message", session); } catch { // Expected to fail since we're using a test handler } // Assert Assert.Equal(8192, capturedMaxTokens); } /// /// HTTP handler that captures requests for verification. /// private sealed class CapturingHttpHandler : HttpMessageHandler { private readonly Action _captureRequest; public CapturingHttpHandler(Action captureRequest) { this._captureRequest = captureRequest; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { this._captureRequest(request); return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) { Content = new StringContent("{\"error\": \"test\"}") }); } } /// /// Verify that CreateAIAgent with tools and instructions correctly assigns both. /// [Fact] public void CreateAIAgent_WithToolsAndInstructions_AssignsBothCorrectly() { // Arrange var chatClient = new TestAnthropicChatClient(); IList tools = [AIFunctionFactory.Create(() => "test result", "TestFunction", "A test function")]; // Act var agent = chatClient.AsAIAgent( model: "test-model", name: "Test Agent", instructions: "Test instructions", tools: tools); // Assert Assert.NotNull(agent); Assert.Equal("Test Agent", agent.Name); Assert.Equal("Test instructions", agent.Instructions); // Verify that tools are registered in the FunctionInvokingChatClient var functionInvokingClient = agent.GetService(); Assert.NotNull(functionInvokingClient); Assert.NotNull(functionInvokingClient.AdditionalTools); Assert.Contains(functionInvokingClient.AdditionalTools, t => t is AIFunction func && func.Name == "TestFunction"); } /// /// Verify that CreateAIAgent with empty tools list does not assign tools. /// [Fact] public void CreateAIAgent_WithEmptyTools_DoesNotAssignTools() { // Arrange var chatClient = new TestAnthropicChatClient(); IList tools = []; // Act var agent = chatClient.AsAIAgent( model: "test-model", name: "Test Agent", tools: tools); // Assert Assert.NotNull(agent); Assert.Equal("Test Agent", agent.Name); // With empty tools and no instructions, agent instructions remain null Assert.Null(agent.Instructions); // Verify that FunctionInvokingChatClient has no additional tools assigned var functionInvokingClient = agent.GetService(); Assert.NotNull(functionInvokingClient); Assert.True(functionInvokingClient.AdditionalTools is null or { Count: 0 }); } /// /// Verify that CreateAIAgent with null instructions does not set instructions. /// [Fact] public void CreateAIAgent_WithNullInstructions_DoesNotSetInstructions() { // Arrange var chatClient = new TestAnthropicChatClient(); // Act var agent = chatClient.AsAIAgent( model: "test-model", name: "Test Agent", instructions: null); // Assert Assert.NotNull(agent); Assert.Equal("Test Agent", agent.Name); Assert.Null(agent.Instructions); } /// /// Verify that CreateAIAgent with whitespace instructions does not set instructions. /// [Fact] public void CreateAIAgent_WithWhitespaceInstructions_DoesNotSetInstructions() { // Arrange var chatClient = new TestAnthropicChatClient(); // Act var agent = chatClient.AsAIAgent( model: "test-model", name: "Test Agent", instructions: " "); // Assert Assert.NotNull(agent); Assert.Equal("Test Agent", agent.Name); Assert.Null(agent.Instructions); } }