From d1d69af48299a051786a6d05add4d17a8cbd1f43 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Thu, 19 Jun 2025 10:12:24 +0100
Subject: [PATCH] Add tests for ChatClientAgent with OpenAI responses (#82)
* Add tests for ChatClientAgent with OpenAI responses
* Address PR comments
* Fix typo
---
dotnet/agent-framework-dotnet.slnx | 3 +
.../OpenAIResponse.IntegrationTests.csproj | 17 +++
.../OpenAIResponseFixture.cs | 107 ++++++++++++++++++
.../OpenAIResponseInvokeStreamingTests.cs | 13 +++
.../OpenAIResponseInvokeTests.cs | 13 +++
5 files changed, 153 insertions(+)
create mode 100644 dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj
create mode 100644 dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
create mode 100644 dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeStreamingTests.cs
create mode 100644 dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeTests.cs
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index f0bfc8efe7..9dc111f7d5 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -14,6 +14,9 @@
+
+
+
diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj
new file mode 100644
index 0000000000..0aabad91c8
--- /dev/null
+++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponse.IntegrationTests.csproj
@@ -0,0 +1,17 @@
+
+
+
+ $(ProjectsTargetFrameworks)
+ $(ProjectsDebugTargetFrameworks)
+ True
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
new file mode 100644
index 0000000000..c1a7b9edbf
--- /dev/null
+++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AgentConformance.IntegrationTests.Support;
+using AgentConformanceTests;
+using Microsoft.Agents;
+using Microsoft.Extensions.AI;
+using OpenAI;
+using OpenAI.Responses;
+using Shared.IntegrationTests;
+
+namespace OpenAIResponse.IntegrationTests;
+
+public class OpenAIResponseFixture(bool store) : AgentFixture
+{
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+ private OpenAIResponseClient _openAIResponseClient;
+ private IChatClient _chatClient;
+ private Agent _agent;
+#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+
+ public override Agent Agent => this._agent;
+
+ public override async Task> GetChatHistoryAsync(AgentThread thread)
+ {
+ if (thread is not ChatClientAgentThread chatClientThread)
+ {
+ throw new InvalidOperationException("The thread must be of type ChatClientAgentThread to retrieve chat history.");
+ }
+
+ if (store)
+ {
+ var inputItems = await this._openAIResponseClient.GetResponseInputItemsAsync(chatClientThread.Id).ToListAsync();
+ var response = await this._openAIResponseClient.GetResponseAsync(chatClientThread.Id);
+ var responseItem = response.Value.OutputItems.FirstOrDefault()!;
+
+ // Take the messages that were the chat history leading up to the current response
+ // remove the instruction messages, and reverse the order so that the most recent message is last.
+ var previousMessages = inputItems
+ .Select(ConvertToChatMessage)
+ .Where(x => x.Text != "You are a helpful assistant.")
+ .Reverse();
+
+ // Convert the response item to a chat message.
+ var responseMessage = ConvertToChatMessage(responseItem);
+
+ // Concatenate the previous messages with the response message to get a full chat history
+ // that includes the current response.
+ return previousMessages
+ .Concat([responseMessage])
+ .ToList();
+ }
+
+ return await chatClientThread.GetMessagesAsync().ToListAsync();
+ }
+
+ private static ChatMessage ConvertToChatMessage(ResponseItem item)
+ {
+ if (item is MessageResponseItem messageResponseItem)
+ {
+ var role = messageResponseItem.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant;
+ return new ChatMessage(role, messageResponseItem.Content.FirstOrDefault()?.Text);
+ }
+
+ throw new NotSupportedException("This test currently only supports text messages");
+ }
+
+ public override Task DeleteThreadAsync(AgentThread thread)
+ {
+ // Chat Completion does not require/support deleting threads, so this is a no-op.
+ return Task.CompletedTask;
+ }
+
+ public override Task InitializeAsync()
+ {
+ var config = TestConfiguration.LoadSection();
+
+ this._openAIResponseClient = new OpenAIClient(config.ApiKey)
+ .GetOpenAIResponseClient(config.ChatModelId);
+ this._chatClient = this._openAIResponseClient
+ .AsIChatClient();
+
+ var options = new ChatClientAgentOptions
+ {
+ Name = "HelpfulAssistant",
+ Instructions = "You are a helpful assistant.",
+ ChatOptions = new ChatOptions
+ {
+ RawRepresentationFactory = new Func((_) => new ResponseCreationOptions() { StoredOutputEnabled = store })
+ },
+ };
+
+ this._agent =
+ new ChatClientAgent(this._chatClient, options);
+
+ return Task.CompletedTask;
+ }
+
+ public override Task DisposeAsync()
+ {
+ this._chatClient.Dispose();
+ return Task.CompletedTask;
+ }
+}
diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeStreamingTests.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeStreamingTests.cs
new file mode 100644
index 0000000000..49d13af615
--- /dev/null
+++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeStreamingTests.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using AgentConformance.IntegrationTests;
+
+namespace OpenAIResponse.IntegrationTests;
+
+public class OpenAIResponseStoreTrueInvokeStreamingTests() : RunStreamingAsyncTests(() => new(store: true))
+{
+}
+
+public class OpenAIResponseStoreFalseInvokeStreamingTests() : RunStreamingAsyncTests(() => new(store: false))
+{
+}
diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeTests.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeTests.cs
new file mode 100644
index 0000000000..a0135b3551
--- /dev/null
+++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseInvokeTests.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using AgentConformance.IntegrationTests;
+
+namespace OpenAIResponse.IntegrationTests;
+
+public class OpenAIResponseStoreTrueInvokeTests() : RunAsyncTests(() => new(store: true))
+{
+}
+
+public class OpenAIResponseStoreFalseInvokeTests() : RunAsyncTests(() => new(store: false))
+{
+}