// Copyright (c) Microsoft. All rights reserved. using System; using System.ClientModel; using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenAI; using OpenAI.Responses; namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; /// /// Integration tests that start a web server and use the OpenAI Responses SDK client to verify protocol compatibility. /// These tests validate both streaming and non-streaming request scenarios. /// public sealed class OpenAIResponsesIntegrationTests : IAsyncDisposable { private WebApplication? _app; private HttpClient? _httpClient; public async ValueTask DisposeAsync() { this._httpClient?.Dispose(); if (this._app != null) { await this._app.DisposeAsync(); } } /// /// Verifies that streaming responses work correctly with the OpenAI SDK client. /// [Fact] public async Task CreateResponseStreaming_WithSimpleMessage_ReturnsStreamingUpdatesAsync() { // Arrange const string AgentName = "streaming-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "One Two Three"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Count to 3"); // Assert List updates = []; StringBuilder contentBuilder = new(); await foreach (StreamingResponseUpdate update in streamingResult) { updates.Add(update); if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { contentBuilder.Append(textDelta.Delta); } } Assert.NotEmpty(updates); // Verify we got various streaming update types Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate); Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate); Assert.Contains(updates, u => u is StreamingResponseOutputTextDeltaUpdate); // Verify content was received string content = contentBuilder.ToString(); Assert.Equal(ExpectedResponse, content); } /// /// Verifies that non-streaming responses work correctly with the OpenAI SDK client. /// [Fact] public async Task CreateResponse_WithSimpleMessage_ReturnsCompleteResponseAsync() { // Arrange const string AgentName = "non-streaming-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Hello! How can I help you today?"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Hello"); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Id); // Verify content string content = response.GetOutputText(); Assert.Equal(ExpectedResponse, content); } /// /// Verifies that streaming responses can handle multiple content chunks. /// [Fact] public async Task CreateResponseStreaming_WithMultipleChunks_StreamsAllContentAsync() { // Arrange const string AgentName = "multi-chunk-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "This is a test response with multiple words"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert List updates = []; StringBuilder contentBuilder = new(); await foreach (StreamingResponseUpdate update in streamingResult) { updates.Add(update); if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { contentBuilder.Append(textDelta.Delta); } } // Verify all content was received string receivedContent = contentBuilder.ToString(); Assert.Equal(ExpectedResponse, receivedContent); // Verify multiple content chunks were received List contentUpdates = updates.OfType().ToList(); Assert.True(contentUpdates.Count > 1, "Expected multiple content chunks in streaming response"); } /// /// Verifies that multiple agents can be accessed via the same server. /// [Fact] public async Task CreateResponse_WithMultipleAgents_EachAgentRespondsCorrectlyAsync() { // Arrange const string Agent1Name = "agent-one"; const string Agent1Instructions = "You are agent one."; const string Agent1Response = "Response from agent one"; const string Agent2Name = "agent-two"; const string Agent2Instructions = "You are agent two."; const string Agent2Response = "Response from agent two"; this._httpClient = await this.CreateTestServerWithMultipleAgentsAsync( (Agent1Name, Agent1Instructions, Agent1Response), (Agent2Name, Agent2Instructions, Agent2Response)); ResponsesClient responseClient1 = this.CreateResponseClient(Agent1Name); ResponsesClient responseClient2 = this.CreateResponseClient(Agent2Name); // Act ResponseResult response1 = await responseClient1.CreateResponseAsync("test-model", "Hello"); ResponseResult response2 = await responseClient2.CreateResponseAsync("test-model", "Hello"); // Assert string content1 = response1.GetOutputText(); string content2 = response2.GetOutputText(); Assert.Equal(Agent1Response, content1); Assert.Equal(Agent2Response, content2); Assert.NotEqual(content1, content2); } /// /// Verifies that streaming and non-streaming work correctly for the same agent. /// [Fact] public async Task CreateResponse_SameAgentStreamingAndNonStreaming_BothWorkCorrectlyAsync() { // Arrange const string AgentName = "dual-mode-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "This is the response"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act - Non-streaming ResponseResult nonStreamingResponse = await responseClient.CreateResponseAsync("test-model", "Test"); // Act - Streaming AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); StringBuilder streamingContent = new(); await foreach (StreamingResponseUpdate update in streamingResult) { if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { streamingContent.Append(textDelta.Delta); } } // Assert string nonStreamingContent = nonStreamingResponse.GetOutputText(); Assert.Equal(ExpectedResponse, nonStreamingContent); Assert.Equal(ExpectedResponse, streamingContent.ToString()); } /// /// Verifies that the response status is correctly set for completed responses. /// [Fact] public async Task CreateResponse_CompletedResponse_HasCorrectStatusAsync() { // Arrange const string AgentName = "status-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Complete"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test"); // Assert Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Id); Assert.Equal(ExpectedResponse, response.GetOutputText()); } /// /// Verifies that streaming responses contain the expected event sequence. /// [Fact] public async Task CreateResponseStreaming_VerifyEventSequence_ContainsExpectedEventsAsync() { // Arrange const string AgentName = "event-sequence-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Test response with multiple words"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert List updates = []; await foreach (StreamingResponseUpdate update in streamingResult) { updates.Add(update); } // Verify event sequence Assert.NotEmpty(updates); // First event should be created Assert.IsType(updates[0]); // Last event should be completed StreamingResponseUpdate lastUpdate = updates[^1]; Assert.IsType(lastUpdate); // Should contain text delta events in between List textDeltas = updates.Where(u => u is StreamingResponseOutputTextDeltaUpdate).ToList(); Assert.NotEmpty(textDeltas); } /// /// Verifies that streaming responses properly handle empty responses. /// [Fact] public async Task CreateResponseStreaming_EmptyResponse_HandlesGracefullyAsync() { // Arrange const string AgentName = "empty-response-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = ""; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert List updates = []; await foreach (StreamingResponseUpdate update in streamingResult) { updates.Add(update); } // Should still receive created and completed events Assert.NotEmpty(updates); Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate); Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate); } /// /// Verifies that non-streaming responses include proper metadata. /// [Fact] public async Task CreateResponse_IncludesMetadata_HasRequiredFieldsAsync() { // Arrange const string AgentName = "metadata-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Response with metadata"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test"); // Assert Assert.NotNull(response.Id); Assert.NotNull(response.Model); Assert.NotEqual(default, response.CreatedAt); Assert.Equal(ResponseStatus.Completed, response.Status); } /// /// Verifies that streaming responses handle very long text correctly. /// [Fact] public async Task CreateResponseStreaming_LongText_StreamsAllContentAsync() { // Arrange const string AgentName = "long-text-agent"; const string Instructions = "You are a helpful assistant."; string expectedResponse = string.Join(" ", Enumerable.Range(1, 100).Select(i => $"Word{i}")); this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, expectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Generate long text"); // Assert StringBuilder contentBuilder = new(); await foreach (StreamingResponseUpdate update in streamingResult) { if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { contentBuilder.Append(textDelta.Delta); } } string receivedContent = contentBuilder.ToString(); Assert.Equal(expectedResponse, receivedContent); } /// /// Verifies that streaming responses properly track output indices. /// [Fact] public async Task CreateResponseStreaming_OutputIndices_AreConsistentAsync() { // Arrange const string AgentName = "output-index-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Test output index"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert List outputIndices = []; await foreach (StreamingResponseUpdate update in streamingResult) { if (update is StreamingResponseOutputItemAddedUpdate itemAdded) { outputIndices.Add(itemAdded.OutputIndex); } if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { outputIndices.Add(textDelta.OutputIndex); } } // All output indices should be the same (first output) Assert.NotEmpty(outputIndices); Assert.All(outputIndices, index => Assert.Equal(0, index)); } /// /// Verifies that streaming responses handle single-word responses correctly. /// [Fact] public async Task CreateResponseStreaming_SingleWord_StreamsCorrectlyAsync() { // Arrange const string AgentName = "single-word-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Hello"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert StringBuilder contentBuilder = new(); await foreach (StreamingResponseUpdate update in streamingResult) { if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { contentBuilder.Append(textDelta.Delta); } } Assert.Equal(ExpectedResponse, contentBuilder.ToString()); } /// /// Verifies that streaming responses preserve special characters and formatting. /// [Fact] public async Task CreateResponseStreaming_SpecialCharacters_PreservesFormattingAsync() { // Arrange const string AgentName = "special-chars-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Hello! How are you? I'm fine. 100% great!"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert StringBuilder contentBuilder = new(); await foreach (StreamingResponseUpdate update in streamingResult) { if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { contentBuilder.Append(textDelta.Delta); } } Assert.Equal(ExpectedResponse, contentBuilder.ToString()); } /// /// Verifies that non-streaming responses handle special characters correctly. /// [Fact] public async Task CreateResponse_SpecialCharacters_PreservesContentAsync() { // Arrange const string AgentName = "special-chars-nonstreaming-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Symbols: @#$%^&*() Quotes: \"Hello\" 'World' Unicode: 你好 🌍"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test"); // Assert string content = response.GetOutputText(); Assert.Equal(ExpectedResponse, content); } /// /// Verifies that streaming responses include item IDs consistently. /// [Fact] public async Task CreateResponseStreaming_ItemIds_AreConsistentAsync() { // Arrange const string AgentName = "item-id-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Testing item IDs"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert List itemIds = []; await foreach (StreamingResponseUpdate update in streamingResult) { if (update is StreamingResponseOutputItemAddedUpdate itemAdded) { itemIds.Add(itemAdded.Item.Id); } if (update is StreamingResponseOutputTextDeltaUpdate textDelta && !string.IsNullOrEmpty(textDelta.ItemId)) { itemIds.Add(textDelta.ItemId); } } // All item IDs should be the same within a single response Assert.NotEmpty(itemIds); Assert.All(itemIds, id => Assert.Equal(itemIds[0], id)); } /// /// Verifies that multiple sequential non-streaming requests work correctly. /// [Fact] public async Task CreateResponse_MultipleSequentialRequests_AllSucceedAsync() { // Arrange const string AgentName = "sequential-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Response"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act & Assert - Make 5 sequential requests for (int i = 0; i < 5; i++) { ResponseResult response = await responseClient.CreateResponseAsync("test-model", $"Request {i}"); Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.Equal(ExpectedResponse, response.GetOutputText()); } } /// /// Verifies that multiple sequential streaming requests work correctly. /// [Fact] public async Task CreateResponseStreaming_MultipleSequentialRequests_AllStreamCorrectlyAsync() { // Arrange const string AgentName = "sequential-streaming-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Streaming response"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act & Assert - Make 3 sequential streaming requests for (int i = 0; i < 3; i++) { AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", $"Request {i}"); StringBuilder contentBuilder = new(); await foreach (StreamingResponseUpdate update in streamingResult) { if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { contentBuilder.Append(textDelta.Delta); } } Assert.Equal(ExpectedResponse, contentBuilder.ToString()); } } /// /// Verifies that response IDs are unique across multiple requests. /// [Fact] public async Task CreateResponse_MultipleRequests_GenerateUniqueIdsAsync() { // Arrange const string AgentName = "unique-id-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Response"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act List responseIds = []; for (int i = 0; i < 10; i++) { ResponseResult response = await responseClient.CreateResponseAsync("test-model", $"Request {i}"); responseIds.Add(response.Id); } // Assert Assert.Equal(10, responseIds.Count); Assert.Equal(responseIds.Count, responseIds.Distinct().Count()); // All IDs should be unique } /// /// Verifies that streaming responses track sequence numbers correctly. /// [Fact] public async Task CreateResponseStreaming_SequenceNumbers_AreMonotonicallyIncreasingAsync() { // Arrange const string AgentName = "sequence-number-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Test sequence numbers with multiple words"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert List sequenceNumbers = []; await foreach (StreamingResponseUpdate update in streamingResult) { sequenceNumbers.Add(update.SequenceNumber); } // Verify sequence numbers are monotonically increasing starting from 0 Assert.NotEmpty(sequenceNumbers); Assert.Equal(0, sequenceNumbers[0]); for (int i = 1; i < sequenceNumbers.Count; i++) { Assert.True(sequenceNumbers[i] > sequenceNumbers[i - 1], $"Sequence number {sequenceNumbers[i]} should be greater than {sequenceNumbers[i - 1]}"); } } /// /// Verifies that non-streaming responses have correct model information. /// [Fact] public async Task CreateResponse_ModelInformation_IsCorrectAsync() { // Arrange const string AgentName = "model-info-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Test model info"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test"); // Assert Assert.NotNull(response.Model); Assert.NotEmpty(response.Model); } /// /// Verifies that streaming responses properly handle responses with punctuation. /// [Fact] public async Task CreateResponseStreaming_Punctuation_PreservesContentAsync() { // Arrange const string AgentName = "punctuation-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Hello, world! How are you today? I'm doing well."; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert StringBuilder contentBuilder = new(); await foreach (StreamingResponseUpdate update in streamingResult) { if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { contentBuilder.Append(textDelta.Delta); } } Assert.Equal(ExpectedResponse, contentBuilder.ToString()); } /// /// Verifies that non-streaming responses work with very short input. /// [Fact] public async Task CreateResponse_ShortInput_ReturnsValidResponseAsync() { // Arrange const string AgentName = "short-input-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "OK"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Hi"); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.Equal(ExpectedResponse, response.GetOutputText()); } /// /// Verifies that streaming responses contain content index information. /// [Fact] public async Task CreateResponseStreaming_ContentIndices_AreConsistentAsync() { // Arrange const string AgentName = "content-index-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Test content indices"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert List contentIndices = []; await foreach (StreamingResponseUpdate update in streamingResult) { if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { contentIndices.Add(textDelta.ContentIndex); } } // All content indices should be the same for a single text response Assert.NotEmpty(contentIndices); Assert.All(contentIndices, index => Assert.Equal(0, index)); } /// /// Verifies that non-streaming responses handle newlines correctly. /// [Fact] public async Task CreateResponse_Newlines_PreservesFormattingAsync() { // Arrange const string AgentName = "newline-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Line 1\nLine 2\nLine 3"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Test"); // Assert string content = response.GetOutputText(); Assert.Equal(ExpectedResponse, content); Assert.Contains("\n", content); } /// /// Verifies that streaming responses handle newlines correctly. /// [Fact] public async Task CreateResponseStreaming_Newlines_PreservesFormattingAsync() { // Arrange const string AgentName = "newline-streaming-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "First line\nSecond line\nThird line"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert StringBuilder contentBuilder = new(); await foreach (StreamingResponseUpdate update in streamingResult) { if (update is StreamingResponseOutputTextDeltaUpdate textDelta) { contentBuilder.Append(textDelta.Delta); } } string content = contentBuilder.ToString(); Assert.Equal(ExpectedResponse, content); Assert.Contains("\n", content); } /// /// Verifies that responses with image content are properly handled in non-streaming mode. /// [Fact] public async Task CreateResponse_ImageContent_ReturnsCorrectlyAsync() { // Arrange const string AgentName = "image-content-agent"; const string Instructions = "You are a helpful assistant."; const string ImageUrl = "https://example.com/test-image.png"; this._httpClient = await this.CreateTestServerWithCustomClientAsync( agentName: AgentName, instructions: Instructions, chatClient: new TestHelpers.ImageContentMockChatClient(ImageUrl)); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Show me an image"); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Id); } /// /// Verifies that responses with image content stream correctly. /// [Fact] public async Task CreateResponseStreaming_ImageContent_StreamsCorrectlyAsync() { // Arrange const string AgentName = "image-streaming-agent"; const string Instructions = "You are a helpful assistant."; const string ImageUrl = "https://example.com/test-image.png"; this._httpClient = await this.CreateTestServerWithCustomClientAsync( agentName: AgentName, instructions: Instructions, chatClient: new TestHelpers.ImageContentMockChatClient(ImageUrl)); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Show me an image"); // Assert List updates = []; await foreach (StreamingResponseUpdate update in streamingResult) { updates.Add(update); } Assert.NotEmpty(updates); Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate); Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate); } /// /// Verifies that responses with audio content are properly handled. /// [Fact] public async Task CreateResponse_AudioContent_ReturnsCorrectlyAsync() { // Arrange const string AgentName = "audio-content-agent"; const string Instructions = "You are a helpful assistant."; const string AudioData = "base64_audio_data_here"; const string Transcript = "This is the audio transcript"; this._httpClient = await this.CreateTestServerWithCustomClientAsync( agentName: AgentName, instructions: Instructions, chatClient: new TestHelpers.AudioContentMockChatClient(AudioData, Transcript)); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Generate audio"); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Id); } /// /// Verifies that responses with audio content stream correctly. /// [Fact] public async Task CreateResponseStreaming_AudioContent_StreamsCorrectlyAsync() { // Arrange const string AgentName = "audio-streaming-agent"; const string Instructions = "You are a helpful assistant."; const string AudioData = "base64_audio_data"; const string Transcript = "Audio transcript"; this._httpClient = await this.CreateTestServerWithCustomClientAsync( agentName: AgentName, instructions: Instructions, chatClient: new TestHelpers.AudioContentMockChatClient(AudioData, Transcript)); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Generate audio"); // Assert List updates = []; await foreach (StreamingResponseUpdate update in streamingResult) { updates.Add(update); } Assert.NotEmpty(updates); Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate); Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate); } /// /// Verifies that responses with function calls are properly handled. /// [Fact] public async Task CreateResponse_FunctionCall_ReturnsCorrectlyAsync() { // Arrange const string AgentName = "function-call-agent"; const string Instructions = "You are a helpful assistant."; const string FunctionName = "get_weather"; const string Arguments = "{\"location\":\"Seattle\"}"; this._httpClient = await this.CreateTestServerWithCustomClientAsync( agentName: AgentName, instructions: Instructions, chatClient: new TestHelpers.FunctionCallMockChatClient(FunctionName, Arguments)); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "What's the weather?"); // Assert Assert.NotNull(response); Assert.NotNull(response.Id); } /// /// Verifies that responses with function calls stream correctly. /// [Fact] public async Task CreateResponseStreaming_FunctionCall_StreamsCorrectlyAsync() { // Arrange const string AgentName = "function-call-streaming-agent"; const string Instructions = "You are a helpful assistant."; const string FunctionName = "calculate"; const string Arguments = "{\"expression\":\"2+2\"}"; this._httpClient = await this.CreateTestServerWithCustomClientAsync( agentName: AgentName, instructions: Instructions, chatClient: new TestHelpers.FunctionCallMockChatClient(FunctionName, Arguments)); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Calculate 2+2"); // Assert List updates = []; await foreach (StreamingResponseUpdate update in streamingResult) { updates.Add(update); } Assert.NotEmpty(updates); Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate); } /// /// Verifies that responses with mixed content types are properly handled. /// [Fact] public async Task CreateResponse_MixedContent_ReturnsCorrectlyAsync() { // Arrange const string AgentName = "mixed-content-agent"; const string Instructions = "You are a helpful assistant."; this._httpClient = await this.CreateTestServerWithCustomClientAsync( agentName: AgentName, instructions: Instructions, chatClient: new TestHelpers.MixedContentMockChatClient()); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act ResponseResult response = await responseClient.CreateResponseAsync("test-model", "Show me various content"); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Id); } /// /// Verifies that responses with mixed content types stream correctly. /// [Fact] public async Task CreateResponseStreaming_MixedContent_StreamsCorrectlyAsync() { // Arrange const string AgentName = "mixed-streaming-agent"; const string Instructions = "You are a helpful assistant."; this._httpClient = await this.CreateTestServerWithCustomClientAsync( agentName: AgentName, instructions: Instructions, chatClient: new TestHelpers.MixedContentMockChatClient()); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Show me various content"); // Assert List updates = []; await foreach (StreamingResponseUpdate update in streamingResult) { updates.Add(update); } Assert.NotEmpty(updates); Assert.Contains(updates, u => u is StreamingResponseCreatedUpdate); Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate); // Should have multiple output item added events due to different content types List itemAddedUpdates = updates.Where(u => u is StreamingResponseOutputItemAddedUpdate).ToList(); Assert.NotEmpty(itemAddedUpdates); } /// /// Verifies that streaming text content includes proper done events. /// [Fact] public async Task CreateResponseStreaming_TextDone_IncludesDoneEventAsync() { // Arrange const string AgentName = "text-done-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Complete text response"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert List updates = []; await foreach (StreamingResponseUpdate update in streamingResult) { updates.Add(update); } // Should contain completed event (text done is represented by completed status) Assert.Contains(updates, u => u is StreamingResponseCompletedUpdate); } /// /// Verifies that content part added events are included in streaming responses. /// [Fact] public async Task CreateResponseStreaming_ContentPartAdded_IncludesEventAsync() { // Arrange const string AgentName = "content-part-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Response with content parts"; this._httpClient = await this.CreateTestServerAsync(AgentName, Instructions, ExpectedResponse); ResponsesClient responseClient = this.CreateResponseClient(AgentName); // Act AsyncCollectionResult streamingResult = responseClient.CreateResponseStreamingAsync("test-model", "Test"); // Assert List updates = []; await foreach (StreamingResponseUpdate update in streamingResult) { updates.Add(update); } // Should contain content part added event Assert.Contains(updates, u => u is StreamingResponseContentPartAddedUpdate); } /// /// Verifies that when a client provides a conversation ID, the underlying IChatClient /// does NOT receive that conversation ID via ChatOptions.ConversationId. /// This ensures that the host's conversation management is separate from the IChatClient's /// conversation handling (if any). /// [Fact] public async Task CreateResponse_WithConversationId_DoesNotForwardConversationIdToIChatClientAsync() { // Arrange const string AgentName = "conversation-id-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Response"; this._httpClient = await this.CreateTestServerWithConversationsAsync(AgentName, Instructions, ExpectedResponse); var mockChatClient = this.ResolveMockChatClient(); // First, create a conversation var createConversationRequest = new { metadata = new { agent_id = AgentName } }; string createConvJson = System.Text.Json.JsonSerializer.Serialize(createConversationRequest); using StringContent createConvContent = new(createConvJson, Encoding.UTF8, "application/json"); HttpResponseMessage createConvResponse = await this._httpClient.PostAsync( new Uri("/v1/conversations", UriKind.Relative), createConvContent); Assert.True(createConvResponse.IsSuccessStatusCode, $"Create conversation failed: {createConvResponse.StatusCode}"); string convResponseJson = await createConvResponse.Content.ReadAsStringAsync(); using var convDoc = System.Text.Json.JsonDocument.Parse(convResponseJson); string conversationId = convDoc.RootElement.GetProperty("id").GetString()!; // Act - Send request with conversation ID using raw HTTP // (OpenAI SDK doesn't expose ConversationId directly on CreateResponseOptions) var requestBody = new { input = "Test", agent = new { name = AgentName }, conversation = conversationId, stream = false }; string requestJson = System.Text.Json.JsonSerializer.Serialize(requestBody); using StringContent content = new(requestJson, Encoding.UTF8, "application/json"); HttpResponseMessage httpResponse = await this._httpClient.PostAsync( new Uri($"/{AgentName}/v1/responses", UriKind.Relative), content); // Assert - Response is successful Assert.True(httpResponse.IsSuccessStatusCode, $"Response status: {httpResponse.StatusCode}"); // Assert - The IChatClient should have received ChatOptions, but without the ConversationId set Assert.NotNull(mockChatClient.LastChatOptions); Assert.Null(mockChatClient.LastChatOptions.ConversationId); } /// /// Verifies that when a client provides a conversation ID in streaming mode, the underlying /// IChatClient does NOT receive that conversation ID via ChatOptions.ConversationId. /// [Fact] public async Task CreateResponseStreaming_WithConversationId_DoesNotForwardConversationIdToIChatClientAsync() { // Arrange const string AgentName = "conversation-streaming-agent"; const string Instructions = "You are a helpful assistant."; const string ExpectedResponse = "Streaming response"; this._httpClient = await this.CreateTestServerWithConversationsAsync(AgentName, Instructions, ExpectedResponse); var mockChatClient = this.ResolveMockChatClient(); // First, create a conversation var createConversationRequest = new { metadata = new { agent_id = AgentName } }; string createConvJson = System.Text.Json.JsonSerializer.Serialize(createConversationRequest); using StringContent createConvContent = new(createConvJson, Encoding.UTF8, "application/json"); HttpResponseMessage createConvResponse = await this._httpClient.PostAsync( new Uri("/v1/conversations", UriKind.Relative), createConvContent); Assert.True(createConvResponse.IsSuccessStatusCode, $"Create conversation failed: {createConvResponse.StatusCode}"); string convResponseJson = await createConvResponse.Content.ReadAsStringAsync(); using var convDoc = System.Text.Json.JsonDocument.Parse(convResponseJson); string conversationId = convDoc.RootElement.GetProperty("id").GetString()!; // Act - Send streaming request with conversation ID using raw HTTP var requestBody = new { input = "Test", agent = new { name = AgentName }, conversation = conversationId, stream = true }; string requestJson = System.Text.Json.JsonSerializer.Serialize(requestBody); using StringContent content = new(requestJson, Encoding.UTF8, "application/json"); HttpResponseMessage httpResponse = await this._httpClient.PostAsync( new Uri($"/{AgentName}/v1/responses", UriKind.Relative), content); // Assert - Response is successful and is SSE Assert.True(httpResponse.IsSuccessStatusCode, $"Response status: {httpResponse.StatusCode}"); Assert.Equal("text/event-stream", httpResponse.Content.Headers.ContentType?.MediaType); // Consume the SSE stream to complete the request string sseContent = await httpResponse.Content.ReadAsStringAsync(); // Verify streaming completed successfully by checking for response.completed event Assert.Contains("response.completed", sseContent); // Assert - The IChatClient should have received ChatOptions, but without the ConversationId set Assert.NotNull(mockChatClient.LastChatOptions); Assert.Null(mockChatClient.LastChatOptions.ConversationId); } /// /// Verifies that conversation history is passed to the agent on subsequent requests. /// This test reproduces the bug described in GitHub issue #3484. /// [Fact] public async Task CreateResponse_WithConversation_SecondRequestIncludesPriorMessagesAsync() { // Arrange const string AgentName = "memory-agent"; const string Instructions = "You are a helpful assistant."; const string AgentResponse = "Nice to meet you Alice"; var mockChatClient = new TestHelpers.ConversationMemoryMockChatClient(AgentResponse); this._httpClient = await this.CreateTestServerWithCustomClientAndConversationsAsync( AgentName, Instructions, mockChatClient); // Create a conversation string createConvJson = System.Text.Json.JsonSerializer.Serialize( new { metadata = new { agent_id = AgentName } }); using StringContent createConvContent = new(createConvJson, Encoding.UTF8, "application/json"); HttpResponseMessage createConvResponse = await this._httpClient.PostAsync( new Uri("/v1/conversations", UriKind.Relative), createConvContent); Assert.True(createConvResponse.IsSuccessStatusCode); string convJson = await createConvResponse.Content.ReadAsStringAsync(); using var convDoc = System.Text.Json.JsonDocument.Parse(convJson); string conversationId = convDoc.RootElement.GetProperty("id").GetString()!; // Act - First message await this.SendRawResponseAsync(AgentName, "My name is Alice", conversationId, stream: false); // Act - Second message in same conversation await this.SendRawResponseAsync(AgentName, "What is my name?", conversationId, stream: false); // Assert Assert.Equal(2, mockChatClient.CallHistory.Count); // First call: should have 1 message (just the user input) Assert.Single(mockChatClient.CallHistory[0]); Assert.Equal(ChatRole.User, mockChatClient.CallHistory[0][0].Role); // Second call: should have 3 messages (prior user + prior assistant + new user) Assert.Equal(3, mockChatClient.CallHistory[1].Count); Assert.Equal(ChatRole.User, mockChatClient.CallHistory[1][0].Role); Assert.Equal(ChatRole.Assistant, mockChatClient.CallHistory[1][1].Role); Assert.Equal(ChatRole.User, mockChatClient.CallHistory[1][2].Role); } private async Task SendRawResponseAsync( string agentName, string input, string conversationId, bool stream) { var requestBody = new { input, agent = new { name = agentName }, conversation = conversationId, stream }; string json = System.Text.Json.JsonSerializer.Serialize(requestBody); using StringContent content = new(json, Encoding.UTF8, "application/json"); HttpResponseMessage response = await this._httpClient!.PostAsync( new Uri($"/{agentName}/v1/responses", UriKind.Relative), content); Assert.True(response.IsSuccessStatusCode, $"Response failed: {response.StatusCode}"); // Consume the full response body to ensure execution completes await response.Content.ReadAsStringAsync(); return response; } private ResponsesClient CreateResponseClient(string agentName) { return new ResponsesClient( credential: new ApiKeyCredential("test-api-key"), options: new OpenAIClientOptions { Endpoint = new Uri(this._httpClient!.BaseAddress!, $"/{agentName}/v1/"), Transport = new HttpClientPipelineTransport(this._httpClient) }); } private TestHelpers.SimpleMockChatClient ResolveMockChatClient() { ArgumentNullException.ThrowIfNull(this._app, nameof(this._app)); var chatClient = this._app.Services.GetRequiredKeyedService("chat-client"); if (chatClient is not TestHelpers.SimpleMockChatClient mockChatClient) { throw new InvalidOperationException("Mock chat client not found or of incorrect type."); } return mockChatClient; } private async Task CreateTestServerAsync(string agentName, string instructions, string responseText = "Test response") { WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText); builder.Services.AddKeyedSingleton("chat-client", mockChatClient); builder.AddOpenAIResponses(); builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client"); this._app = builder.Build(); AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); this._app.MapOpenAIResponses(agent); await this._app.StartAsync(); TestServer testServer = this._app.Services.GetRequiredService() as TestServer ?? throw new InvalidOperationException("TestServer not found"); return testServer.CreateClient(); } private async Task CreateTestServerWithConversationsAsync(string agentName, string instructions, string responseText = "Test response") { WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText); builder.Services.AddKeyedSingleton("chat-client", mockChatClient); builder.AddOpenAIResponses(); builder.AddOpenAIConversations(); builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client"); this._app = builder.Build(); AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); this._app.MapOpenAIResponses(agent); this._app.MapOpenAIConversations(); await this._app.StartAsync(); TestServer testServer = this._app.Services.GetRequiredService() as TestServer ?? throw new InvalidOperationException("TestServer not found"); return testServer.CreateClient(); } private async Task CreateTestServerWithCustomClientAndConversationsAsync(string agentName, string instructions, IChatClient chatClient) { WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); builder.Services.AddKeyedSingleton($"chat-client-{agentName}", chatClient); builder.AddAIAgent(agentName, instructions, chatClientServiceKey: $"chat-client-{agentName}"); builder.AddOpenAIResponses(); builder.AddOpenAIConversations(); this._app = builder.Build(); AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); this._app.MapOpenAIResponses(agent); this._app.MapOpenAIConversations(); await this._app.StartAsync(); TestServer testServer = this._app.Services.GetRequiredService() as TestServer ?? throw new InvalidOperationException("TestServer not found"); return testServer.CreateClient(); } private async Task CreateTestServerWithCustomClientAsync(string agentName, string instructions, IChatClient chatClient) { WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); builder.Services.AddKeyedSingleton($"chat-client-{agentName}", chatClient); builder.AddAIAgent(agentName, instructions, chatClientServiceKey: $"chat-client-{agentName}"); builder.AddOpenAIResponses(); this._app = builder.Build(); AIAgent agent = this._app.Services.GetRequiredKeyedService(agentName); this._app.MapOpenAIResponses(agent); await this._app.StartAsync(); TestServer testServer = this._app.Services.GetRequiredService() as TestServer ?? throw new InvalidOperationException("TestServer not found"); return testServer.CreateClient(); } private async Task CreateTestServerWithMultipleAgentsAsync( params (string Name, string Instructions, string ResponseText)[] agents) { WebApplicationBuilder builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); foreach ((string name, string instructions, string responseText) in agents) { IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText); builder.Services.AddKeyedSingleton($"chat-client-{name}", mockChatClient); builder.AddAIAgent(name, instructions, chatClientServiceKey: $"chat-client-{name}"); } builder.AddOpenAIResponses(); this._app = builder.Build(); foreach ((string name, string _, string _) in agents) { AIAgent agent = this._app.Services.GetRequiredKeyedService(name); this._app.MapOpenAIResponses(agent); } await this._app.StartAsync(); TestServer testServer = this._app.Services.GetRequiredService() as TestServer ?? throw new InvalidOperationException("TestServer not found"); return testServer.CreateClient(); } }