// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; using Microsoft.Agents.AI.Hosting.OpenAI.Tests; namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; /// /// Tests for OpenAI Responses API model serialization and deserialization. /// These tests verify that our models correctly serialize to and deserialize from JSON /// matching the OpenAI wire format, without testing actual API implementation behavior. /// public sealed class OpenAIResponsesSerializationTests : ConformanceTestBase { #region Request Serialization Tests [Fact] public void Deserialize_BasicRequest_Success() { // Arrange string json = LoadTraceFile("basic/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.Equal("gpt-4o-mini", request.Model); Assert.NotNull(request.Input); Assert.Equal(100, request.MaxOutputTokens); } [Fact] public void Deserialize_BasicRequest_RoundTrip() { // Arrange string originalJson = LoadTraceFile("basic/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(originalJson, Responses.ResponsesJsonContext.Default.CreateResponse); string reserializedJson = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse); CreateResponse? roundtripped = JsonSerializer.Deserialize(reserializedJson, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(roundtripped); Assert.Equal(request.Model, roundtripped.Model); Assert.Equal(request.MaxOutputTokens, roundtripped.MaxOutputTokens); } [Fact] public void Deserialize_StreamingRequest_HasStreamFlag() { // Arrange string json = LoadTraceFile("streaming/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.True(request.Stream); Assert.Equal(200, request.MaxOutputTokens); } [Fact] public void Deserialize_ConversationRequest_HasPreviousResponseId() { // Arrange string json = LoadTraceFile("conversation/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.PreviousResponseId); Assert.StartsWith("resp_", request.PreviousResponseId); } [Fact] public void Deserialize_MetadataRequest_HasAllParameters() { // Arrange string json = LoadTraceFile("metadata/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.Metadata); Assert.Equal(3, request.Metadata.Count); Assert.Equal("test_user_123", request.Metadata["user_id"]); Assert.Equal("session_456", request.Metadata["session_id"]); Assert.Equal("conformance_test", request.Metadata["purpose"]); Assert.NotNull(request.Instructions); Assert.Equal("Respond in a friendly, educational tone.", request.Instructions); Assert.Equal(0.7, request.Temperature); Assert.Equal(0.9, request.TopP); Assert.Equal(150, request.MaxOutputTokens); } [Fact] public void Deserialize_ToolCallRequest_HasToolDefinitions() { // Arrange string json = LoadTraceFile("tool_call/request.json"); // Act // CreateResponse doesn't have Tools property - it uses dynamic JSON using var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Assert Assert.True(root.TryGetProperty("tools", out var tools)); Assert.Equal(JsonValueKind.Array, tools.ValueKind); Assert.Equal(1, tools.GetArrayLength()); var tool = tools[0]; Assert.Equal("function", tool.GetProperty("type").GetString()); Assert.Equal("get_weather", tool.GetProperty("name").GetString()); Assert.True(tool.TryGetProperty("description", out _)); Assert.True(tool.TryGetProperty("parameters", out var parameters)); Assert.Equal("object", parameters.GetProperty("type").GetString()); } [Fact] public void Serialize_CreateMinimalRequest_MatchesFormat() { // Arrange var request = new CreateResponse { Model = "gpt-4o-mini", Input = ResponseInput.FromText("Hello") }; // Act string json = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Assert Assert.Equal("gpt-4o-mini", root.GetProperty("model").GetString()); Assert.True(root.TryGetProperty("input", out var input)); // Input can be string or object - verify one exists Assert.True(input.ValueKind == JsonValueKind.String || input.ValueKind == JsonValueKind.Object); } [Fact] public void Serialize_CreateRequestWithOptions_IncludesAllFields() { // Arrange var request = new CreateResponse { Model = "gpt-4o-mini", Input = ResponseInput.FromText("Test input"), MaxOutputTokens = 100, Temperature = 0.7, TopP = 0.9, Stream = false, Instructions = "Test instructions", Metadata = new Dictionary { ["key1"] = "value1", ["key2"] = "value2" } }; // Act string json = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Assert Assert.Equal("gpt-4o-mini", root.GetProperty("model").GetString()); Assert.Equal(100, root.GetProperty("max_output_tokens").GetInt32()); Assert.Equal(0.7, root.GetProperty("temperature").GetDouble()); Assert.Equal(0.9, root.GetProperty("top_p").GetDouble()); Assert.False(root.GetProperty("stream").GetBoolean()); Assert.Equal("Test instructions", root.GetProperty("instructions").GetString()); var metadata = root.GetProperty("metadata"); Assert.Equal("value1", metadata.GetProperty("key1").GetString()); Assert.Equal("value2", metadata.GetProperty("key2").GetString()); } [Fact] public void Serialize_NullableFields_AreOmittedWhenNull() { // Arrange var request = new CreateResponse { Model = "gpt-4o-mini", Input = ResponseInput.FromText("Test") }; // Act string json = JsonSerializer.Serialize(request, Responses.ResponsesJsonContext.Default.CreateResponse); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; // Assert - Optional fields should not be present when null Assert.False(root.TryGetProperty("previous_response_id", out _) && root.GetProperty("previous_response_id").ValueKind != JsonValueKind.Null, "previous_response_id should be omitted or null"); Assert.False(root.TryGetProperty("instructions", out _) && root.GetProperty("instructions").ValueKind != JsonValueKind.Null, "instructions should be omitted or null"); } [Fact] public void Deserialize_ImageInputRequest_HasImageData() { // Arrange string json = LoadTraceFile("image_input/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.Input); } [Fact] public void Deserialize_ImageInputStreamingRequest_HasStreamAndImage() { // Arrange string json = LoadTraceFile("image_input_streaming/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.True(request.Stream); Assert.NotNull(request.Input); } [Fact] public void Deserialize_JsonOutputRequest_HasJsonSchema() { // Arrange string json = LoadTraceFile("json_output/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.Input); Assert.NotNull(request.Text); Assert.NotNull(request.Text.Format); Assert.IsType(request.Text.Format); var jsonSchemaFormat = (ResponseTextFormatConfigurationJsonSchema)request.Text.Format; Assert.Equal("json_schema", jsonSchemaFormat.Type); Assert.NotNull(jsonSchemaFormat.Name); Assert.NotNull(jsonSchemaFormat.Schema); } [Fact] public void Deserialize_JsonOutputStreamingRequest_HasJsonSchemaAndStream() { // Arrange string json = LoadTraceFile("json_output_streaming/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.True(request.Stream); Assert.NotNull(request.Input); Assert.NotNull(request.Text); Assert.NotNull(request.Text.Format); Assert.IsType(request.Text.Format); var jsonSchemaFormat = (ResponseTextFormatConfigurationJsonSchema)request.Text.Format; Assert.Equal("json_schema", jsonSchemaFormat.Type); } [Fact] public void Deserialize_ReasoningRequest_HasReasoningConfiguration() { // Arrange string json = LoadTraceFile("reasoning/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.Reasoning); } [Fact] public void Deserialize_ReasoningStreamingRequest_HasReasoningAndStream() { // Arrange string json = LoadTraceFile("reasoning_streaming/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.True(request.Stream); Assert.NotNull(request.Reasoning); } [Fact] public void Deserialize_RefusalRequest_CanBeDeserialized() { // Arrange string json = LoadTraceFile("refusal/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.NotNull(request.Input); } [Fact] public void Deserialize_RefusalStreamingRequest_HasStream() { // Arrange string json = LoadTraceFile("refusal_streaming/request.json"); // Act CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); // Assert Assert.NotNull(request); Assert.True(request.Stream); Assert.NotNull(request.Input); } [Fact] public void Deserialize_AllRequests_CanBeDeserialized() { // Arrange string[] requestPaths = [ "basic/request.json", "streaming/request.json", "conversation/request.json", "metadata/request.json", "tool_call/request.json", "image_input/request.json", "image_input_streaming/request.json", "json_output/request.json", "json_output_streaming/request.json", "reasoning/request.json", "reasoning_streaming/request.json", "refusal/request.json", "refusal_streaming/request.json" ]; foreach (var path in requestPaths) { string json = LoadTraceFile(path); // Act & Assert - Should not throw CreateResponse? request = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.CreateResponse); Assert.NotNull(request); Assert.NotNull(request.Input); } } #endregion #region Response Deserialization Tests [Fact] public void Deserialize_BasicResponse_Success() { // Arrange string json = LoadTraceFile("basic/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.StartsWith("resp_", response.Id); Assert.Equal("response", response.Object); Assert.True(response.CreatedAt > 0); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Model); Assert.StartsWith("gpt-4o-mini", response.Model); } [Fact] public void Deserialize_BasicResponse_HasCorrectOutput() { // Arrange string json = LoadTraceFile("basic/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Output); Assert.Single(response.Output); var outputItem = response.Output[0]; Assert.NotNull(outputItem); // Verify it's a message type using var doc = JsonDocument.Parse(JsonSerializer.Serialize(outputItem, Responses.ResponsesJsonContext.Default.ItemResource)); var root = doc.RootElement; Assert.Equal("message", root.GetProperty("type").GetString()); } [Fact] public void Deserialize_BasicResponse_HasCorrectUsage() { // Arrange string json = LoadTraceFile("basic/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Usage); Assert.True(response.Usage.InputTokens > 0); Assert.True(response.Usage.OutputTokens > 0); Assert.Equal(response.Usage.InputTokens + response.Usage.OutputTokens, response.Usage.TotalTokens); Assert.NotNull(response.Usage.InputTokensDetails); Assert.NotNull(response.Usage.OutputTokensDetails); } [Fact] public void Deserialize_ConversationResponse_HasPreviousResponseId() { // Arrange string json = LoadTraceFile("conversation/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.PreviousResponseId); Assert.StartsWith("resp_", response.PreviousResponseId); Assert.NotEqual(response.Id, response.PreviousResponseId); } [Fact] public void Deserialize_MetadataResponse_PreservesMetadata() { // Arrange string json = LoadTraceFile("metadata/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Metadata); Assert.Equal("test_user_123", response.Metadata["user_id"]); Assert.Equal("session_456", response.Metadata["session_id"]); Assert.Equal("conformance_test", response.Metadata["purpose"]); } [Fact] public void Deserialize_MetadataResponse_HasIncompleteStatus() { // Arrange string json = LoadTraceFile("metadata/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Incomplete, response.Status); Assert.NotNull(response.IncompleteDetails); Assert.Equal("max_output_tokens", response.IncompleteDetails.Reason); } [Fact] public void Deserialize_MetadataResponse_HasInstructions() { // Arrange string json = LoadTraceFile("metadata/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Instructions); Assert.Equal("Respond in a friendly, educational tone.", response.Instructions); } [Fact] public void Deserialize_MetadataResponse_HasModelParameters() { // Arrange string json = LoadTraceFile("metadata/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(0.7, response.Temperature); Assert.Equal(0.9, response.TopP); Assert.Equal(150, response.MaxOutputTokens); } [Fact] public void Deserialize_ToolCallResponse_HasFunctionCall() { // Arrange string json = LoadTraceFile("tool_call/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Output); Assert.Single(response.Output); // Verify the output is a function_call type using var doc = JsonDocument.Parse(JsonSerializer.Serialize(response.Output[0], Responses.ResponsesJsonContext.Default.ItemResource)); var root = doc.RootElement; Assert.Equal("function_call", root.GetProperty("type").GetString()); Assert.Equal("get_weather", root.GetProperty("name").GetString()); Assert.True(root.TryGetProperty("arguments", out var args)); Assert.True(root.TryGetProperty("call_id", out var callId)); Assert.StartsWith("call_", callId.GetString()); } [Fact] public void Deserialize_ToolCallResponse_HasToolDefinitions() { // Arrange string json = LoadTraceFile("tool_call/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Tools); Assert.Single(response.Tools); var tool = response.Tools[0]; Assert.Equal(JsonValueKind.Object, tool.ValueKind); var toolObj = tool; Assert.Equal("function", toolObj.GetProperty("type").GetString()); Assert.Equal("get_weather", toolObj.GetProperty("name").GetString()); Assert.True(toolObj.TryGetProperty("parameters", out var parameters)); Assert.Equal("object", parameters.GetProperty("type").GetString()); } [Fact] public void Deserialize_ImageInputResponse_HasImageInInput() { // Arrange string json = LoadTraceFile("image_input/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Output); } [Fact] public void Deserialize_JsonOutputResponse_HasStructuredOutput() { // Arrange string json = LoadTraceFile("json_output/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Output); Assert.NotNull(response.Text); Assert.NotNull(response.Text.Format); Assert.IsType(response.Text.Format); var jsonSchemaFormat = (ResponseTextFormatConfigurationJsonSchema)response.Text.Format; Assert.Equal("json_schema", jsonSchemaFormat.Type); } [Fact] public void Deserialize_ReasoningResponse_HasReasoningItems() { // Arrange string json = LoadTraceFile("reasoning/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Output); Assert.NotNull(response.Reasoning); } [Fact] public void Deserialize_RefusalResponse_HasRefusalContent() { // Arrange string json = LoadTraceFile("refusal/response.json"); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.Equal(ResponseStatus.Completed, response.Status); Assert.NotNull(response.Output); } [Fact] public void Deserialize_AllResponses_HaveRequiredFields() { // Arrange string[] responsePaths = [ "basic/response.json", "conversation/response.json", "metadata/response.json", "tool_call/response.json", "image_input/response.json", "json_output/response.json", "reasoning/response.json", "refusal/response.json" ]; foreach (var path in responsePaths) { string json = LoadTraceFile(path); // Act Response? response = JsonSerializer.Deserialize(json, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(response.Id); Assert.Equal("response", response.Object); Assert.True(response.CreatedAt > 0, $"Response from {path} should have created_at"); Assert.NotNull(response.Model); Assert.NotNull(response.Output); } } [Fact] public void Deserialize_ResponseRoundTrip_PreservesData() { // Arrange string originalJson = LoadTraceFile("basic/response.json"); // Act - Deserialize and re-serialize Response? response = JsonSerializer.Deserialize(originalJson, Responses.ResponsesJsonContext.Default.Response); string reserializedJson = JsonSerializer.Serialize(response, Responses.ResponsesJsonContext.Default.Response); Response? roundtripped = JsonSerializer.Deserialize(reserializedJson, Responses.ResponsesJsonContext.Default.Response); // Assert Assert.NotNull(response); Assert.NotNull(roundtripped); Assert.Equal(response.Id, roundtripped.Id); Assert.Equal(response.CreatedAt, roundtripped.CreatedAt); Assert.Equal(response.Status, roundtripped.Status); Assert.Equal(response.Model, roundtripped.Model); } #endregion #region Streaming Event Deserialization Tests [Fact] public void ParseStreamingEvents_BasicFormat_Success() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); // Assert Assert.NotEmpty(events); Assert.All(events, evt => { Assert.True(evt.TryGetProperty("type", out var type)); Assert.True(evt.TryGetProperty("sequence_number", out var seqNum)); Assert.Equal(JsonValueKind.Number, seqNum.ValueKind); }); } [Fact] public void ParseStreamingEvents_HasCorrectEventTypes() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); var eventTypes = events.Select(e => e.GetProperty("type").GetString()).ToHashSet(); // Assert Assert.Contains("response.created", eventTypes); Assert.Contains("response.in_progress", eventTypes); Assert.Contains("response.output_item.added", eventTypes); Assert.Contains("response.content_part.added", eventTypes); Assert.Contains("response.output_text.delta", eventTypes); Assert.Contains("response.output_text.done", eventTypes); Assert.Contains("response.content_part.done", eventTypes); Assert.Contains("response.output_item.done", eventTypes); } [Fact] public void ParseStreamingEvents_DeserializeCreatedEvent_Success() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var createdEventJson = events.First(e => e.GetProperty("type").GetString() == "response.created"); // Act string jsonString = createdEventJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); Assert.IsType(evt); var created = (StreamingResponseCreated)evt; Assert.Equal(0, created.SequenceNumber); Assert.NotNull(created.Response); Assert.NotNull(created.Response.Id); Assert.StartsWith("resp_", created.Response.Id); } [Fact] public void ParseStreamingEvents_DeserializeInProgressEvent_Success() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var inProgressEventJson = events.First(e => e.GetProperty("type").GetString() == "response.in_progress"); // Act string jsonString = inProgressEventJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); Assert.IsType(evt); var inProgress = (StreamingResponseInProgress)evt; Assert.Equal(1, inProgress.SequenceNumber); Assert.NotNull(inProgress.Response); Assert.Equal(ResponseStatus.InProgress, inProgress.Response.Status); } [Fact] public void ParseStreamingEvents_DeserializeOutputItemAdded_Success() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var itemAddedJson = events.First(e => e.GetProperty("type").GetString() == "response.output_item.added"); // Act string jsonString = itemAddedJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); Assert.IsType(evt); var itemAdded = (StreamingOutputItemAdded)evt; Assert.Equal(0, itemAdded.OutputIndex); Assert.NotNull(itemAdded.Item); } [Fact] public void ParseStreamingEvents_DeserializeContentPartAdded_Success() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var partAddedJson = events.First(e => e.GetProperty("type").GetString() == "response.content_part.added"); // Act string jsonString = partAddedJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); Assert.IsType(evt); var partAdded = (StreamingContentPartAdded)evt; Assert.NotNull(partAdded.ItemId); Assert.Equal(0, partAdded.OutputIndex); Assert.Equal(0, partAdded.ContentIndex); Assert.NotNull(partAdded.Part); } [Fact] public void ParseStreamingEvents_DeserializeTextDelta_Success() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var textDeltaJson = events.First(e => e.GetProperty("type").GetString() == "response.output_text.delta"); // Act string jsonString = textDeltaJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); Assert.IsType(evt); var textDelta = (StreamingOutputTextDelta)evt; Assert.NotNull(textDelta.ItemId); Assert.Equal(0, textDelta.OutputIndex); Assert.Equal(0, textDelta.ContentIndex); Assert.NotNull(textDelta.Delta); } [Fact] public void ParseStreamingEvents_AccumulateTextDeltas_MatchesFinalText() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); // Act var deltas = new List(); string? finalText = null; foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); if (evt is StreamingOutputTextDelta delta) { deltas.Add(delta.Delta); } else if (evt is StreamingOutputTextDone done) { finalText = done.Text; } } // Assert Assert.NotEmpty(deltas); Assert.NotNull(finalText); string accumulated = string.Concat(deltas); Assert.Equal(accumulated, finalText); } [Fact] public void ParseStreamingEvents_SequenceNumbersAreSequential() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); // Act var sequenceNumbers = new List(); foreach (var eventJson in events) { string jsonString = eventJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); sequenceNumbers.Add(evt.SequenceNumber); } // Assert Assert.NotEmpty(sequenceNumbers); Assert.Equal(0, sequenceNumbers.First()); for (int i = 0; i < sequenceNumbers.Count; i++) { Assert.Equal(i, sequenceNumbers[i]); } } [Fact] public void ParseStreamingEvents_FinalEvent_IsTerminalState() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); var events = ParseSseEventsFromContent(sseContent); var lastEventJson = events.Last(); // Act string jsonString = lastEventJson.GetRawText(); StreamingResponseEvent? evt = JsonSerializer.Deserialize(jsonString, Responses.ResponsesJsonContext.Default.StreamingResponseEvent); // Assert Assert.NotNull(evt); // Should be one of the terminal events bool isTerminal = evt is StreamingResponseCompleted || evt is StreamingResponseIncomplete || evt is StreamingResponseFailed; Assert.True(isTerminal, $"Expected terminal event, got: {evt.GetType().Name}"); } [Fact] public void ParseStreamingEvents_ImageInputStreaming_HasImageEvents() { // Arrange string sseContent = LoadTraceFile("image_input_streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); // Assert Assert.NotEmpty(events); Assert.All(events, evt => { StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } [Fact] public void ParseStreamingEvents_JsonOutputStreaming_HasJsonSchemaEvents() { // Arrange string sseContent = LoadTraceFile("json_output_streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); // Assert Assert.NotEmpty(events); Assert.All(events, evt => { StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } [Fact] public void ParseStreamingEvents_ReasoningStreaming_HasReasoningEvents() { // Arrange string sseContent = LoadTraceFile("reasoning_streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); var eventTypes = events.Select(e => e.GetProperty("type").GetString()).ToHashSet(); // Assert Assert.NotEmpty(events); // Should have reasoning-related events Assert.Contains("response.created", eventTypes); Assert.All(events, evt => { StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } [Fact] public void ParseStreamingEvents_RefusalStreaming_HasRefusalEvents() { // Arrange string sseContent = LoadTraceFile("refusal_streaming/response.txt"); // Act var events = ParseSseEventsFromContent(sseContent); var eventTypes = events.Select(e => e.GetProperty("type").GetString()).ToHashSet(); // Assert Assert.NotEmpty(events); // Should have refusal-related events Assert.All(events, evt => { StreamingResponseEvent? parsed = JsonSerializer.Deserialize(evt.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); Assert.NotNull(parsed); }); } [Fact] public void ParseStreamingEvents_AllStreamingTraces_CanBeDeserialized() { // Arrange string[] streamingPaths = [ "streaming/response.txt", "image_input_streaming/response.txt", "json_output_streaming/response.txt", "reasoning_streaming/response.txt", "refusal_streaming/response.txt" ]; foreach (var path in streamingPaths) { string sseContent = LoadTraceFile(path); // Act & Assert foreach (var eventJson in ParseSseEventsFromContent(sseContent)) { // Should not throw StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); } } } [Fact] public void ParseStreamingEvents_AllEvents_CanBeDeserialized() { // Arrange string sseContent = LoadTraceFile("streaming/response.txt"); // Act & Assert foreach (var eventJson in ParseSseEventsFromContent(sseContent)) { // Should not throw StreamingResponseEvent? evt = JsonSerializer.Deserialize(eventJson.GetRawText(), Responses.ResponsesJsonContext.Default.StreamingResponseEvent); Assert.NotNull(evt); // Verify polymorphic deserialization worked Assert.True( evt is StreamingResponseCreated || evt is StreamingResponseInProgress || evt is StreamingResponseCompleted || evt is StreamingResponseIncomplete || evt is StreamingResponseFailed || evt is StreamingOutputItemAdded || evt is StreamingOutputItemDone || evt is StreamingContentPartAdded || evt is StreamingContentPartDone || evt is StreamingOutputTextDelta || evt is StreamingOutputTextDone || evt is StreamingFunctionCallArgumentsDelta || evt is StreamingFunctionCallArgumentsDone, $"Unknown event type: {evt.GetType().Name}"); } } /// /// Helper to parse SSE events from a streaming response content string. /// private static List ParseSseEventsFromContent(string sseContent) { var events = new List(); var lines = sseContent.Split('\n'); for (int i = 0; i < lines.Length; i++) { var line = lines[i].TrimEnd('\r'); if (line.StartsWith("event: ", StringComparison.Ordinal)) { // Next line should have the data if (i + 1 < lines.Length) { var dataLine = lines[i + 1].TrimEnd('\r'); if (dataLine.StartsWith("data: ", StringComparison.Ordinal)) { var jsonData = dataLine.Substring("data: ".Length); var doc = JsonDocument.Parse(jsonData); events.Add(doc.RootElement.Clone()); } } } } return events; } #endregion }