// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using OpenAI.Responses; namespace Microsoft.Agents.AI.OpenAI.UnitTests.Extensions; /// /// Unit tests for the class. /// public sealed class OpenAIResponseClientExtensionsTests { /// /// 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 ResponsesClient implementation for testing. /// private sealed class TestOpenAIResponseClient : ResponsesClient { public TestOpenAIResponseClient() { } } /// /// Verify that CreateAIAgent with clientFactory parameter correctly applies the factory. /// [Fact] public void CreateAIAgent_WithClientFactory_AppliesFactoryCorrectly() { // Arrange var responseClient = new TestOpenAIResponseClient(); var testChatClient = new TestChatClient(responseClient.AsIChatClient()); // Act var agent = responseClient.AsAIAgent( 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 without clientFactory works normally. /// [Fact] public void CreateAIAgent_WithoutClientFactory_WorksNormally() { // Arrange var responseClient = new TestOpenAIResponseClient(); // Act var agent = responseClient.AsAIAgent( 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 responseClient = new TestOpenAIResponseClient(); // Act var agent = responseClient.AsAIAgent( 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(() => ((ResponsesClient)null!).AsAIAgent()); Assert.Equal("client", exception.ParamName); } /// /// Verify that CreateAIAgent with options throws ArgumentNullException when options is null. /// [Fact] public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException() { // Arrange var responseClient = new TestOpenAIResponseClient(); // Act & Assert var exception = Assert.Throws(() => responseClient.AsAIAgent((ChatClientAgentOptions)null!)); Assert.Equal("options", exception.ParamName); } /// /// Verify that CreateAIAgent with services parameter correctly passes it through to the ChatClientAgent. /// [Fact] public void CreateAIAgent_WithServices_PassesServicesToAgent() { // Arrange var responseClient = new TestOpenAIResponseClient(); var serviceProvider = new TestServiceProvider(); // Act var agent = responseClient.AsAIAgent( instructions: "Test instructions", name: "Test Agent", services: serviceProvider); // Assert Assert.NotNull(agent); // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient var chatClient = agent.GetService(); Assert.NotNull(chatClient); var functionInvokingClient = chatClient.GetService(); Assert.NotNull(functionInvokingClient); Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient)); } /// /// Verify that CreateAIAgent with options and services parameter correctly passes it through to the ChatClientAgent. /// [Fact] public void CreateAIAgent_WithOptionsAndServices_PassesServicesToAgent() { // Arrange var responseClient = new TestOpenAIResponseClient(); var serviceProvider = new TestServiceProvider(); var options = new ChatClientAgentOptions { Name = "Test Agent", ChatOptions = new() { Instructions = "Test instructions" } }; // Act var agent = responseClient.AsAIAgent(options, services: serviceProvider); // Assert Assert.NotNull(agent); Assert.Equal("Test Agent", agent.Name); // Verify the IServiceProvider was passed through to the FunctionInvokingChatClient var chatClient = agent.GetService(); Assert.NotNull(chatClient); var functionInvokingClient = chatClient.GetService(); Assert.NotNull(functionInvokingClient); Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient)); } /// /// Verify that CreateAIAgent with both clientFactory and services works correctly. /// [Fact] public void CreateAIAgent_WithClientFactoryAndServices_AppliesBothCorrectly() { // Arrange var responseClient = new TestOpenAIResponseClient(); var serviceProvider = new TestServiceProvider(); var testChatClient = new TestChatClient(responseClient.AsIChatClient()); // Act var agent = responseClient.AsAIAgent( instructions: "Test instructions", name: "Test Agent", clientFactory: (innerClient) => testChatClient, services: serviceProvider); // Assert Assert.NotNull(agent); // Verify the custom chat client was applied var retrievedTestClient = agent.GetService(); Assert.NotNull(retrievedTestClient); Assert.Same(testChatClient, retrievedTestClient); // Verify the IServiceProvider was passed through var chatClient = agent.GetService(); Assert.NotNull(chatClient); var functionInvokingClient = chatClient.GetService(); Assert.NotNull(functionInvokingClient); Assert.Same(serviceProvider, GetFunctionInvocationServices(functionInvokingClient)); } /// /// Verify that AsIChatClientWithStoredOutputDisabled throws ArgumentNullException when client is null. /// [Fact] public void AsIChatClientWithStoredOutputDisabled_WithNullClient_ThrowsArgumentNullException() { // Act & Assert var exception = Assert.Throws(() => ((ResponsesClient)null!).AsIChatClientWithStoredOutputDisabled()); Assert.Equal("responseClient", exception.ParamName); } /// /// Verify that AsIChatClientWithStoredOutputDisabled wraps the original ResponsesClient, /// which remains accessible via the service chain. /// [Fact] public void AsIChatClientWithStoredOutputDisabled_InnerResponsesClientIsAccessible() { // Arrange var responseClient = new TestOpenAIResponseClient(); // Act var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(); // Assert - the inner ResponsesClient should be accessible via GetService var innerClient = chatClient.GetService(); Assert.NotNull(innerClient); Assert.Same(responseClient, innerClient); } /// /// Verify that AsIChatClientWithStoredOutputDisabled with includeReasoningEncryptedContent false /// wraps the original ResponsesClient, which remains accessible via the service chain. /// [Fact] public void AsIChatClientWithStoredOutputDisabled_WithIncludeReasoningFalse_InnerResponsesClientIsAccessible() { // Arrange var responseClient = new TestOpenAIResponseClient(); // Act var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(includeReasoningEncryptedContent: false); // Assert - the inner ResponsesClient should be accessible via GetService var innerClient = chatClient.GetService(); Assert.NotNull(innerClient); Assert.Same(responseClient, innerClient); } /// /// Verify that AsIChatClientWithStoredOutputDisabled with default parameter (includeReasoningEncryptedContent = true) /// configures StoredOutputEnabled to false and includes ReasoningEncryptedContent in IncludedProperties. /// [Fact] public void AsIChatClientWithStoredOutputDisabled_Default_ConfiguresStoredOutputDisabledWithReasoningEncryptedContent() { // Arrange var responseClient = new TestOpenAIResponseClient(); // Act var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(); // Assert var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient); Assert.NotNull(createResponseOptions); Assert.False(createResponseOptions.StoredOutputEnabled); Assert.Contains(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties); } /// /// Verify that AsIChatClientWithStoredOutputDisabled with includeReasoningEncryptedContent explicitly set to true /// configures StoredOutputEnabled to false and includes ReasoningEncryptedContent in IncludedProperties. /// [Fact] public void AsIChatClientWithStoredOutputDisabled_WithIncludeReasoningTrue_ConfiguresStoredOutputDisabledWithReasoningEncryptedContent() { // Arrange var responseClient = new TestOpenAIResponseClient(); // Act var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(includeReasoningEncryptedContent: true); // Assert var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient); Assert.NotNull(createResponseOptions); Assert.False(createResponseOptions.StoredOutputEnabled); Assert.Contains(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties); } /// /// Verify that AsIChatClientWithStoredOutputDisabled with includeReasoningEncryptedContent set to false /// configures StoredOutputEnabled to false and does not include ReasoningEncryptedContent in IncludedProperties. /// [Fact] public void AsIChatClientWithStoredOutputDisabled_WithIncludeReasoningFalse_ConfiguresStoredOutputDisabledWithoutReasoningEncryptedContent() { // Arrange var responseClient = new TestOpenAIResponseClient(); // Act var chatClient = responseClient.AsIChatClientWithStoredOutputDisabled(includeReasoningEncryptedContent: false); // Assert var createResponseOptions = GetCreateResponseOptionsFromPipeline(chatClient); Assert.NotNull(createResponseOptions); Assert.False(createResponseOptions.StoredOutputEnabled); Assert.DoesNotContain(IncludedResponseProperty.ReasoningEncryptedContent, createResponseOptions.IncludedProperties); } /// /// A simple test IServiceProvider implementation for testing. /// private sealed class TestServiceProvider : IServiceProvider { public object? GetService(Type serviceType) => null; } /// /// Uses reflection to access the FunctionInvocationServices property which is not public. /// private static IServiceProvider? GetFunctionInvocationServices(FunctionInvokingChatClient client) { var property = typeof(FunctionInvokingChatClient).GetProperty( "FunctionInvocationServices", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); return property?.GetValue(client) as IServiceProvider; } /// /// Extracts the produced by the ConfigureOptions pipeline /// by using reflection to access the configure action and invoking it on a test . /// private static CreateResponseOptions? GetCreateResponseOptionsFromPipeline(IChatClient chatClient) { // The ConfigureOptionsChatClient stores the configure action in a private field. var configureField = chatClient.GetType().GetField("_configureOptions", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(configureField); var configureAction = configureField.GetValue(chatClient) as Action; Assert.NotNull(configureAction); var options = new ChatOptions(); configureAction(options); Assert.NotNull(options.RawRepresentationFactory); return options.RawRepresentationFactory(chatClient) as CreateResponseOptions; } }