Files
agent-framework/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs
T
Reuben Bond 103c7e7105 .NET: Improve fidelity of OpenAI Responses hosting (#1550)
* Improve conformance of OpenAI Responses API serving

* Update dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseExtensions.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Update dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentRunResponseExtensions.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Sort packages

* Relax adherence where acceptable

* nit

* PromptCacheKey is not obsolete

* format

---------

Co-authored-by: Stephen Toub <stoub@microsoft.com>
2025-10-22 17:48:44 +00:00

1094 lines
38 KiB
C#

// 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;
/// <summary>
/// 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.
/// </summary>
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<string, string>
{
["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<ResponseTextFormatConfigurationJsonSchema>(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<ResponseTextFormatConfigurationJsonSchema>(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<ResponseTextFormatConfigurationJsonSchema>(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<StreamingResponseCreated>(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<StreamingResponseInProgress>(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<StreamingOutputItemAdded>(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<StreamingContentPartAdded>(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<StreamingOutputTextDelta>(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>();
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<int>();
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}");
}
}
/// <summary>
/// Helper to parse SSE events from a streaming response content string.
/// </summary>
private static List<JsonElement> ParseSseEventsFromContent(string sseContent)
{
var events = new List<JsonElement>();
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
}