mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
33f84f9ed2
* Improve fidelity of OpenAI Responses server and add Conversations * Merge * nit * Undo prior change * Undo prior change * Review feedback * Review feedback * Fix test * Use simpler JsonDocument approach for polymorphic deserialization * More review feedback * dotnet format
1202 lines
56 KiB
C#
1202 lines
56 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
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;
|
|
|
|
namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests;
|
|
|
|
/// <summary>
|
|
/// Conformance tests for OpenAI Conversations API implementation behavior.
|
|
/// Tests use real API traces to ensure our implementation produces responses
|
|
/// that match OpenAI's wire format when processing actual requests through the server.
|
|
/// </summary>
|
|
public sealed class OpenAIConversationsConformanceTests : IAsyncDisposable
|
|
{
|
|
private const string TracesBasePath = "ConformanceTraces/Conversations";
|
|
private WebApplication? _app;
|
|
private HttpClient? _httpClient;
|
|
|
|
/// <summary>
|
|
/// Loads a JSON file from the conformance traces directory.
|
|
/// </summary>
|
|
private static string LoadTraceFile(string relativePath)
|
|
{
|
|
var fullPath = Path.Combine(TracesBasePath, relativePath);
|
|
|
|
if (!File.Exists(fullPath))
|
|
{
|
|
throw new FileNotFoundException($"Conformance trace file not found: {fullPath}");
|
|
}
|
|
|
|
return File.ReadAllText(fullPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a JSON document from the conformance traces directory.
|
|
/// </summary>
|
|
private static JsonDocument LoadTraceDocument(string relativePath)
|
|
{
|
|
var json = LoadTraceFile(relativePath);
|
|
return JsonDocument.Parse(json);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that a JSON element exists (property is present, value can be null).
|
|
/// </summary>
|
|
private static void AssertJsonPropertyExists(JsonElement element, string propertyName)
|
|
{
|
|
if (!element.TryGetProperty(propertyName, out _))
|
|
{
|
|
Assert.Fail($"Expected property '{propertyName}' not found in JSON");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that a JSON element has a specific string value.
|
|
/// </summary>
|
|
private static void AssertJsonPropertyEquals(JsonElement element, string propertyName, string expectedValue)
|
|
{
|
|
AssertJsonPropertyExists(element, propertyName);
|
|
var actualValue = element.GetProperty(propertyName).GetString();
|
|
|
|
if (actualValue != expectedValue)
|
|
{
|
|
Assert.Fail($"Property '{propertyName}': expected '{expectedValue}', got '{actualValue}'");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that a JSON element has a specific boolean value.
|
|
/// </summary>
|
|
private static void AssertJsonPropertyEquals(JsonElement element, string propertyName, bool expectedValue)
|
|
{
|
|
AssertJsonPropertyExists(element, propertyName);
|
|
var actualValue = element.GetProperty(propertyName).GetBoolean();
|
|
|
|
if (actualValue != expectedValue)
|
|
{
|
|
Assert.Fail($"Property '{propertyName}': expected {expectedValue}, got {actualValue}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a test server with Conversations API.
|
|
/// </summary>
|
|
private async Task<HttpClient> CreateTestServerAsync(string agentName, string instructions, string responseText)
|
|
{
|
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseTestServer();
|
|
|
|
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient(responseText);
|
|
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
|
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
|
|
builder.AddOpenAIConversations();
|
|
builder.AddOpenAIResponses();
|
|
|
|
this._app = builder.Build();
|
|
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
|
|
this._app.MapOpenAIConversations();
|
|
this._app.MapOpenAIResponses(agent);
|
|
|
|
await this._app.StartAsync();
|
|
|
|
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
|
|
?? throw new InvalidOperationException("TestServer not found");
|
|
|
|
this._httpClient = testServer.CreateClient();
|
|
return this._httpClient;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a test server with a stateful mock that returns different responses for each call.
|
|
/// </summary>
|
|
private async Task<HttpClient> CreateTestServerWithStatefulMockAsync(string agentName, string instructions, string[] responseTexts)
|
|
{
|
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseTestServer();
|
|
|
|
IChatClient mockChatClient = new TestHelpers.StatefulMockChatClient(responseTexts);
|
|
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
|
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
|
|
builder.AddOpenAIConversations();
|
|
builder.AddOpenAIResponses();
|
|
|
|
this._app = builder.Build();
|
|
|
|
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
|
|
this._app.MapOpenAIConversations();
|
|
this._app.MapOpenAIResponses(agent);
|
|
|
|
await this._app.StartAsync();
|
|
|
|
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
|
|
?? throw new InvalidOperationException("TestServer not found");
|
|
|
|
this._httpClient = testServer.CreateClient();
|
|
return this._httpClient;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a test server with a tool call mock.
|
|
/// </summary>
|
|
private async Task<HttpClient> CreateTestServerWithToolCallAsync(string agentName, string instructions, string functionName, string arguments)
|
|
{
|
|
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
|
builder.WebHost.UseTestServer();
|
|
|
|
IChatClient mockChatClient = new TestHelpers.ToolCallMockChatClient(functionName, arguments);
|
|
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
|
builder.AddAIAgent(agentName, instructions, chatClientServiceKey: "chat-client");
|
|
builder.AddOpenAIConversations();
|
|
builder.AddOpenAIResponses();
|
|
|
|
this._app = builder.Build();
|
|
|
|
AIAgent agent = this._app.Services.GetRequiredKeyedService<AIAgent>(agentName);
|
|
this._app.MapOpenAIConversations();
|
|
this._app.MapOpenAIResponses(agent);
|
|
|
|
await this._app.StartAsync();
|
|
|
|
TestServer testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
|
|
?? throw new InvalidOperationException("TestServer not found");
|
|
|
|
this._httpClient = testServer.CreateClient();
|
|
return this._httpClient;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a POST request with JSON content to the test server.
|
|
/// </summary>
|
|
private static async Task<HttpResponseMessage> SendPostRequestAsync(HttpClient client, string path, string requestJson)
|
|
{
|
|
using StringContent content = new(requestJson, Encoding.UTF8, "application/json");
|
|
return await client.PostAsync(new Uri(path, UriKind.Relative), content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a GET request to the test server.
|
|
/// </summary>
|
|
private static async Task<HttpResponseMessage> SendGetRequestAsync(HttpClient client, string path)
|
|
{
|
|
return await client.GetAsync(new Uri(path, UriKind.Relative));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a DELETE request to the test server.
|
|
/// </summary>
|
|
private static async Task<HttpResponseMessage> SendDeleteRequestAsync(HttpClient client, string path)
|
|
{
|
|
return await client.DeleteAsync(new Uri(path, UriKind.Relative));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the response JSON and returns a JsonDocument.
|
|
/// </summary>
|
|
private static async Task<JsonDocument> ParseResponseAsync(HttpResponseMessage response)
|
|
{
|
|
string responseJson = await response.Content.ReadAsStringAsync();
|
|
return JsonDocument.Parse(responseJson);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BasicConversationCreateAsync()
|
|
{
|
|
// Arrange
|
|
string requestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("basic-agent", "You are a helpful assistant.", "The capital of France is Paris.");
|
|
|
|
// Act
|
|
HttpResponseMessage httpResponse = await SendPostRequestAsync(client, "/v1/conversations", requestJson);
|
|
using var responseDoc = await ParseResponseAsync(httpResponse);
|
|
var response = responseDoc.RootElement;
|
|
|
|
// Parse the request
|
|
using var requestDoc = JsonDocument.Parse(requestJson);
|
|
var request = requestDoc.RootElement;
|
|
|
|
// Assert - Request has metadata
|
|
AssertJsonPropertyExists(request, "metadata");
|
|
var requestMetadata = request.GetProperty("metadata");
|
|
Assert.Equal(JsonValueKind.Object, requestMetadata.ValueKind);
|
|
|
|
// Assert - Response metadata
|
|
AssertJsonPropertyExists(response, "id");
|
|
AssertJsonPropertyEquals(response, "object", "conversation");
|
|
AssertJsonPropertyExists(response, "created_at");
|
|
var id = response.GetProperty("id").GetString();
|
|
Assert.NotNull(id);
|
|
Assert.StartsWith("conv_", id);
|
|
var createdAt = response.GetProperty("created_at").GetInt64();
|
|
Assert.True(createdAt > 0, "created_at should be a positive unix timestamp");
|
|
|
|
// Assert - Response preserves metadata
|
|
AssertJsonPropertyExists(response, "metadata");
|
|
var responseMetadata = response.GetProperty("metadata");
|
|
Assert.Equal(JsonValueKind.Object, responseMetadata.ValueKind);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BasicConversationWithMessagesAsync()
|
|
{
|
|
// Arrange
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
string firstMessageRequestJson = LoadTraceFile("basic/first_message_request.json");
|
|
string secondMessageRequestJson = LoadTraceFile("basic/second_message_request.json");
|
|
using var firstMessageExpectedDoc = LoadTraceDocument("basic/first_message_response.json");
|
|
using var secondMessageExpectedDoc = LoadTraceDocument("basic/second_message_response.json");
|
|
|
|
// Get expected response texts
|
|
string firstExpectedText = firstMessageExpectedDoc.RootElement.GetProperty("output")[0]
|
|
.GetProperty("content")[0]
|
|
.GetProperty("text").GetString()!;
|
|
string secondExpectedText = secondMessageExpectedDoc.RootElement.GetProperty("output")[0]
|
|
.GetProperty("content")[0]
|
|
.GetProperty("text").GetString()!;
|
|
|
|
// Create a stateful mock that returns different responses for each call
|
|
HttpClient client = await this.CreateTestServerWithStatefulMockAsync(
|
|
"basic-agent",
|
|
"You are a helpful assistant.",
|
|
[firstExpectedText, secondExpectedText]);
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Act - Send first message (using Responses API with conversation parameter)
|
|
// Update the request JSON with the actual conversation ID
|
|
using var firstMsgDoc = JsonDocument.Parse(firstMessageRequestJson);
|
|
var firstMsgRequest = JsonSerializer.Serialize(new
|
|
{
|
|
model = firstMsgDoc.RootElement.GetProperty("model").GetString(),
|
|
conversation = conversationId,
|
|
input = firstMsgDoc.RootElement.GetProperty("input").GetString(),
|
|
max_output_tokens = firstMsgDoc.RootElement.GetProperty("max_output_tokens").GetInt32()
|
|
});
|
|
|
|
HttpResponseMessage firstMsgResponse = await SendPostRequestAsync(client, "/basic-agent/v1/responses", firstMsgRequest);
|
|
using var firstMsgResponseDoc = await ParseResponseAsync(firstMsgResponse);
|
|
var firstResponse = firstMsgResponseDoc.RootElement;
|
|
|
|
// Assert - First response has conversation reference
|
|
AssertJsonPropertyExists(firstResponse, "conversation");
|
|
var conversationRef = firstResponse.GetProperty("conversation");
|
|
|
|
// The conversation reference can be either a string (just the ID) or an object with an id property
|
|
if (conversationRef.ValueKind == JsonValueKind.String)
|
|
{
|
|
var refId = conversationRef.GetString();
|
|
Assert.Equal(conversationId, refId);
|
|
}
|
|
else if (conversationRef.ValueKind == JsonValueKind.Object)
|
|
{
|
|
AssertJsonPropertyEquals(conversationRef, "id", conversationId);
|
|
}
|
|
else
|
|
{
|
|
Assert.Fail($"Expected conversation to be either a string or an object, but got {conversationRef.ValueKind}");
|
|
}
|
|
|
|
// Assert - First response has output
|
|
AssertJsonPropertyExists(firstResponse, "output");
|
|
var firstOutput = firstResponse.GetProperty("output");
|
|
Assert.True(firstOutput.GetArrayLength() > 0);
|
|
|
|
// Assert - First response status is completed
|
|
AssertJsonPropertyEquals(firstResponse, "status", "completed");
|
|
|
|
// Act - Send second message
|
|
using var secondMsgDoc = JsonDocument.Parse(secondMessageRequestJson);
|
|
var secondMsgRequest = JsonSerializer.Serialize(new
|
|
{
|
|
model = secondMsgDoc.RootElement.GetProperty("model").GetString(),
|
|
conversation = conversationId,
|
|
input = secondMsgDoc.RootElement.GetProperty("input").GetString(),
|
|
max_output_tokens = secondMsgDoc.RootElement.GetProperty("max_output_tokens").GetInt32()
|
|
});
|
|
|
|
HttpResponseMessage secondMsgResponse = await SendPostRequestAsync(client, "/basic-agent/v1/responses", secondMsgRequest);
|
|
using var secondMsgResponseDoc = await ParseResponseAsync(secondMsgResponse);
|
|
var secondResponse = secondMsgResponseDoc.RootElement;
|
|
|
|
// Assert - Second response has conversation reference
|
|
AssertJsonPropertyExists(secondResponse, "conversation");
|
|
var secondConversationRef = secondResponse.GetProperty("conversation");
|
|
|
|
if (secondConversationRef.ValueKind == JsonValueKind.String)
|
|
{
|
|
var refId = secondConversationRef.GetString();
|
|
Assert.Equal(conversationId, refId);
|
|
}
|
|
else if (secondConversationRef.ValueKind == JsonValueKind.Object)
|
|
{
|
|
AssertJsonPropertyEquals(secondConversationRef, "id", conversationId);
|
|
}
|
|
else
|
|
{
|
|
Assert.Fail($"Expected conversation to be either a string or an object, but got {secondConversationRef.ValueKind}");
|
|
}
|
|
|
|
// Assert - Second response has output
|
|
AssertJsonPropertyExists(secondResponse, "output");
|
|
var secondOutput = secondResponse.GetProperty("output");
|
|
Assert.True(secondOutput.GetArrayLength() > 0);
|
|
|
|
// Assert - Second response status is completed
|
|
AssertJsonPropertyEquals(secondResponse, "status", "completed");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateConversationWithItemsAsync()
|
|
{
|
|
// Arrange
|
|
string requestJson = LoadTraceFile("create_with_items/create_request.json");
|
|
using var expectedResponseDoc = LoadTraceDocument("create_with_items/create_response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("items-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Act
|
|
HttpResponseMessage httpResponse = await SendPostRequestAsync(client, "/v1/conversations", requestJson);
|
|
using var responseDoc = await ParseResponseAsync(httpResponse);
|
|
var response = responseDoc.RootElement;
|
|
|
|
// Parse the request
|
|
using var requestDoc = JsonDocument.Parse(requestJson);
|
|
var request = requestDoc.RootElement;
|
|
|
|
// Assert - Request has items array
|
|
AssertJsonPropertyExists(request, "items");
|
|
var requestItems = request.GetProperty("items");
|
|
Assert.Equal(JsonValueKind.Array, requestItems.ValueKind);
|
|
Assert.True(requestItems.GetArrayLength() > 0);
|
|
|
|
// Assert - Response has conversation structure
|
|
AssertJsonPropertyExists(response, "id");
|
|
AssertJsonPropertyEquals(response, "object", "conversation");
|
|
AssertJsonPropertyExists(response, "created_at");
|
|
AssertJsonPropertyExists(response, "metadata");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddItemsToConversationAsync()
|
|
{
|
|
// Arrange
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
string addItemsRequestJson = LoadTraceFile("add_items/request.json");
|
|
using var expectedResponseDoc = LoadTraceDocument("add_items/response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("add-items-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Act - Create conversation first
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Act - Add items
|
|
HttpResponseMessage addItemsResponse = await SendPostRequestAsync(client, $"/v1/conversations/{conversationId}/items", addItemsRequestJson);
|
|
using var addItemsDoc = await ParseResponseAsync(addItemsResponse);
|
|
var response = addItemsDoc.RootElement;
|
|
|
|
// Parse the request
|
|
using var requestDoc = JsonDocument.Parse(addItemsRequestJson);
|
|
var request = requestDoc.RootElement;
|
|
|
|
// Assert - Request has items array
|
|
AssertJsonPropertyExists(request, "items");
|
|
var requestItems = request.GetProperty("items");
|
|
Assert.Equal(JsonValueKind.Array, requestItems.ValueKind);
|
|
var itemCount = requestItems.GetArrayLength();
|
|
Assert.True(itemCount > 0);
|
|
|
|
// Assert - Response has data array with created items
|
|
AssertJsonPropertyExists(response, "data");
|
|
var responseData = response.GetProperty("data");
|
|
Assert.Equal(JsonValueKind.Array, responseData.ValueKind);
|
|
Assert.Equal(itemCount, responseData.GetArrayLength());
|
|
|
|
// Assert - Each item has required fields
|
|
foreach (var item in responseData.EnumerateArray())
|
|
{
|
|
AssertJsonPropertyExists(item, "id");
|
|
AssertJsonPropertyEquals(item, "type", "message");
|
|
AssertJsonPropertyExists(item, "content");
|
|
AssertJsonPropertyExists(item, "role");
|
|
var itemId = item.GetProperty("id").GetString();
|
|
Assert.NotNull(itemId);
|
|
Assert.StartsWith("msg_", itemId);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListItemsInConversationAsync()
|
|
{
|
|
// Arrange
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
using var expectedResponseDoc = LoadTraceDocument("list_items/response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("list-items-agent", "You are a helpful assistant.", "The capital of France is Paris.");
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Act - List items
|
|
HttpResponseMessage listResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items");
|
|
using var listDoc = await ParseResponseAsync(listResponse);
|
|
var response = listDoc.RootElement;
|
|
|
|
// Assert - Response has list structure
|
|
AssertJsonPropertyEquals(response, "object", "list");
|
|
AssertJsonPropertyExists(response, "data");
|
|
AssertJsonPropertyExists(response, "first_id");
|
|
AssertJsonPropertyExists(response, "last_id");
|
|
AssertJsonPropertyExists(response, "has_more");
|
|
|
|
var data = response.GetProperty("data");
|
|
Assert.Equal(JsonValueKind.Array, data.ValueKind);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RetrieveConversationAsync()
|
|
{
|
|
// Arrange
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
using var expectedResponseDoc = LoadTraceDocument("retrieve_conversation/response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("retrieve-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var createdConversation = createDoc.RootElement;
|
|
string conversationId = createdConversation.GetProperty("id").GetString()!;
|
|
|
|
// Act - Retrieve conversation
|
|
HttpResponseMessage retrieveResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}");
|
|
using var retrieveDoc = await ParseResponseAsync(retrieveResponse);
|
|
var response = retrieveDoc.RootElement;
|
|
|
|
// Assert - Response has conversation structure
|
|
AssertJsonPropertyExists(response, "id");
|
|
AssertJsonPropertyEquals(response, "object", "conversation");
|
|
AssertJsonPropertyExists(response, "created_at");
|
|
AssertJsonPropertyExists(response, "metadata");
|
|
var id = response.GetProperty("id").GetString();
|
|
Assert.Equal(conversationId, id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RetrieveItemAsync()
|
|
{
|
|
// Arrange
|
|
string createRequestJson = LoadTraceFile("create_with_items/create_request.json");
|
|
using var expectedResponseDoc = LoadTraceDocument("retrieve_item/response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("retrieve-item-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Act - Create conversation with items
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Act - List items to get an item ID
|
|
HttpResponseMessage listResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items");
|
|
using var listDoc = await ParseResponseAsync(listResponse);
|
|
var listResult = listDoc.RootElement;
|
|
var items = listResult.GetProperty("data");
|
|
Assert.True(items.GetArrayLength() > 0, "Should have at least one item");
|
|
string itemId = items[0].GetProperty("id").GetString()!;
|
|
|
|
// Act - Retrieve specific item
|
|
HttpResponseMessage retrieveResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items/{itemId}");
|
|
using var retrieveDoc = await ParseResponseAsync(retrieveResponse);
|
|
var response = retrieveDoc.RootElement;
|
|
|
|
// Assert - Response has item structure
|
|
AssertJsonPropertyExists(response, "id");
|
|
AssertJsonPropertyEquals(response, "type", "message");
|
|
AssertJsonPropertyExists(response, "content");
|
|
AssertJsonPropertyExists(response, "role");
|
|
var id = response.GetProperty("id").GetString();
|
|
Assert.Equal(itemId, id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateConversationAsync()
|
|
{
|
|
// Arrange
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
string updateRequestJson = LoadTraceFile("update_conversation/request.json");
|
|
using var expectedResponseDoc = LoadTraceDocument("update_conversation/response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("update-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Act - Update conversation
|
|
HttpResponseMessage updateResponse = await SendPostRequestAsync(client, $"/v1/conversations/{conversationId}", updateRequestJson);
|
|
using var updateDoc = await ParseResponseAsync(updateResponse);
|
|
var response = updateDoc.RootElement;
|
|
|
|
// Parse the request
|
|
using var requestDoc = JsonDocument.Parse(updateRequestJson);
|
|
var request = requestDoc.RootElement;
|
|
|
|
// Assert - Request has metadata
|
|
AssertJsonPropertyExists(request, "metadata");
|
|
var requestMetadata = request.GetProperty("metadata");
|
|
|
|
// Assert - Response preserves updated metadata
|
|
AssertJsonPropertyExists(response, "id");
|
|
AssertJsonPropertyEquals(response, "object", "conversation");
|
|
AssertJsonPropertyExists(response, "metadata");
|
|
var responseMetadata = response.GetProperty("metadata");
|
|
|
|
// Verify metadata was updated
|
|
foreach (var prop in requestMetadata.EnumerateObject())
|
|
{
|
|
Assert.True(responseMetadata.TryGetProperty(prop.Name, out var value));
|
|
Assert.Equal(prop.Value.GetString(), value.GetString());
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteConversationAsync()
|
|
{
|
|
// Arrange
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
using var expectedResponseDoc = LoadTraceDocument("delete_conversation/response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("delete-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Act - Delete conversation
|
|
HttpResponseMessage deleteResponse = await SendDeleteRequestAsync(client, $"/v1/conversations/{conversationId}");
|
|
using var deleteDoc = await ParseResponseAsync(deleteResponse);
|
|
var response = deleteDoc.RootElement;
|
|
|
|
// Assert - Delete response structure
|
|
AssertJsonPropertyExists(response, "id");
|
|
AssertJsonPropertyEquals(response, "object", "conversation.deleted");
|
|
AssertJsonPropertyEquals(response, "deleted", true);
|
|
var id = response.GetProperty("id").GetString();
|
|
Assert.Equal(conversationId, id);
|
|
|
|
// Assert - Conversation is actually deleted
|
|
HttpResponseMessage retrieveResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}");
|
|
Assert.Equal(System.Net.HttpStatusCode.NotFound, retrieveResponse.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteItemAsync()
|
|
{
|
|
// Arrange
|
|
string createRequestJson = LoadTraceFile("create_with_items/create_request.json");
|
|
using var expectedResponseDoc = LoadTraceDocument("delete_item/response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("delete-item-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Act - Create conversation with items
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Act - List items to get an item ID
|
|
HttpResponseMessage listResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items");
|
|
using var listDoc = await ParseResponseAsync(listResponse);
|
|
var listResult = listDoc.RootElement;
|
|
var items = listResult.GetProperty("data");
|
|
Assert.True(items.GetArrayLength() > 0, "Should have at least one item");
|
|
string itemId = items[0].GetProperty("id").GetString()!;
|
|
|
|
// Act - Delete item
|
|
HttpResponseMessage deleteResponse = await SendDeleteRequestAsync(client, $"/v1/conversations/{conversationId}/items/{itemId}");
|
|
using var deleteDoc = await ParseResponseAsync(deleteResponse);
|
|
var response = deleteDoc.RootElement;
|
|
|
|
// Assert - Delete response structure
|
|
AssertJsonPropertyExists(response, "id");
|
|
AssertJsonPropertyEquals(response, "object", "conversation.item.deleted");
|
|
AssertJsonPropertyEquals(response, "deleted", true);
|
|
var id = response.GetProperty("id").GetString();
|
|
Assert.Equal(itemId, id);
|
|
|
|
// Assert - Item is actually deleted
|
|
HttpResponseMessage retrieveResponse = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items/{itemId}");
|
|
Assert.Equal(System.Net.HttpStatusCode.NotFound, retrieveResponse.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ErrorConversationNotFoundAsync()
|
|
{
|
|
// Arrange
|
|
using var expectedResponseDoc = LoadTraceDocument("error_conversation_not_found/response.json");
|
|
const string NonExistentConversationId = "conv_nonexistent123456789";
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("error-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Act
|
|
HttpResponseMessage response = await SendGetRequestAsync(client, $"/v1/conversations/{NonExistentConversationId}");
|
|
using var responseDoc = await ParseResponseAsync(response);
|
|
var responseJson = responseDoc.RootElement;
|
|
|
|
// Assert - Response is 404
|
|
Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode);
|
|
|
|
// Assert - Error response structure
|
|
AssertJsonPropertyExists(responseJson, "error");
|
|
var error = responseJson.GetProperty("error");
|
|
AssertJsonPropertyExists(error, "message");
|
|
AssertJsonPropertyExists(error, "type");
|
|
var errorMessage = error.GetProperty("message").GetString();
|
|
Assert.NotNull(errorMessage);
|
|
Assert.Contains("not found", errorMessage, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ErrorItemNotFoundAsync()
|
|
{
|
|
// Arrange
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
using var expectedResponseDoc = LoadTraceDocument("error_item_not_found/response.json");
|
|
const string NonExistentItemId = "msg_nonexistent123456789";
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("error-item-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Act - Try to retrieve non-existent item
|
|
HttpResponseMessage response = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items/{NonExistentItemId}");
|
|
using var responseDoc = await ParseResponseAsync(response);
|
|
var responseJson = responseDoc.RootElement;
|
|
|
|
// Assert - Response is 404
|
|
Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode);
|
|
|
|
// Assert - Error response structure
|
|
AssertJsonPropertyExists(responseJson, "error");
|
|
var error = responseJson.GetProperty("error");
|
|
AssertJsonPropertyExists(error, "message");
|
|
AssertJsonPropertyExists(error, "type");
|
|
var errorMessage = error.GetProperty("message").GetString();
|
|
Assert.NotNull(errorMessage);
|
|
Assert.Contains("not found", errorMessage, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ErrorInvalidJsonAsync()
|
|
{
|
|
// Arrange
|
|
string invalidJson = LoadTraceFile("error_invalid_json/request.txt");
|
|
using var expectedResponseDoc = LoadTraceDocument("error_invalid_json/response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("error-json-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Act
|
|
using StringContent content = new(invalidJson, Encoding.UTF8, "application/json");
|
|
HttpResponseMessage response = await client.PostAsync(new Uri("/v1/conversations", UriKind.Relative), content);
|
|
|
|
// Assert - Response is 400
|
|
Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ErrorDeleteAlreadyDeletedAsync()
|
|
{
|
|
// Arrange
|
|
using var expectedResponseDoc = LoadTraceDocument("error_delete_already_deleted/response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("delete-twice-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Create a conversation
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
string conversationId = createDoc.RootElement.GetProperty("id").GetString()!;
|
|
|
|
// Delete the conversation
|
|
await SendDeleteRequestAsync(client, $"/v1/conversations/{conversationId}");
|
|
|
|
// Act - Try to delete again
|
|
HttpResponseMessage response = await SendDeleteRequestAsync(client, $"/v1/conversations/{conversationId}");
|
|
using var responseDoc = await ParseResponseAsync(response);
|
|
var responseJson = responseDoc.RootElement;
|
|
|
|
// Assert - Should return 404
|
|
Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode);
|
|
|
|
// Assert - Error response structure
|
|
AssertJsonPropertyExists(responseJson, "error");
|
|
var error = responseJson.GetProperty("error");
|
|
AssertJsonPropertyExists(error, "message");
|
|
AssertJsonPropertyExists(error, "type");
|
|
var errorMessage = error.GetProperty("message").GetString();
|
|
Assert.NotNull(errorMessage);
|
|
Assert.Contains("not found", errorMessage, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ErrorInvalidLimitAsync()
|
|
{
|
|
// Arrange
|
|
using var expectedResponseDoc = LoadTraceDocument("error_invalid_limit/response.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("invalid-limit-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Create a conversation
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
string conversationId = createDoc.RootElement.GetProperty("id").GetString()!;
|
|
|
|
// Act - Request items with invalid limit (e.g., negative or too large)
|
|
HttpResponseMessage response = await SendGetRequestAsync(client, $"/v1/conversations/{conversationId}/items?limit=-1");
|
|
using var responseDoc = await ParseResponseAsync(response);
|
|
var responseJson = responseDoc.RootElement;
|
|
|
|
// Assert - Should return 400
|
|
Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode);
|
|
|
|
// Assert - Error response structure
|
|
AssertJsonPropertyExists(responseJson, "error");
|
|
var error = responseJson.GetProperty("error");
|
|
AssertJsonPropertyExists(error, "message");
|
|
AssertJsonPropertyExists(error, "type");
|
|
var errorMessage = error.GetProperty("message").GetString();
|
|
Assert.NotNull(errorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ToolCallFullScenarioAsync()
|
|
{
|
|
// Arrange - Full test for tool call scenario through Conversations and Responses API
|
|
string createRequestJson = LoadTraceFile("tool_call/create_conversation_request.json");
|
|
string firstMessageRequestJson = LoadTraceFile("tool_call/first_message_request.json");
|
|
using var messageExpectedDoc = LoadTraceDocument("tool_call/first_message_response.json");
|
|
|
|
// Extract function call details from expected response
|
|
var expectedOutput = messageExpectedDoc.RootElement.GetProperty("output")[0];
|
|
string functionName = expectedOutput.GetProperty("name").GetString()!;
|
|
string arguments = expectedOutput.GetProperty("arguments").GetString()!;
|
|
|
|
// Create server with proper tool call mock
|
|
HttpClient client = await this.CreateTestServerWithToolCallAsync("tool-call-agent", "You are a helpful assistant.", functionName, arguments);
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Act - Send message with tools through Responses API
|
|
using var msgDoc = JsonDocument.Parse(firstMessageRequestJson);
|
|
var msgRequest = JsonSerializer.Serialize(new
|
|
{
|
|
model = msgDoc.RootElement.GetProperty("model").GetString(),
|
|
conversation = conversationId,
|
|
input = msgDoc.RootElement.GetProperty("input"),
|
|
tools = msgDoc.RootElement.GetProperty("tools"),
|
|
max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32()
|
|
});
|
|
|
|
HttpResponseMessage msgResponse = await SendPostRequestAsync(client, "/tool-call-agent/v1/responses", msgRequest);
|
|
using var msgResponseDoc = await ParseResponseAsync(msgResponse);
|
|
var response = msgResponseDoc.RootElement;
|
|
|
|
// Assert - Response has conversation reference
|
|
AssertJsonPropertyExists(response, "conversation");
|
|
AssertJsonPropertyEquals(response, "status", "completed");
|
|
|
|
// Assert - Response has function call output
|
|
AssertJsonPropertyExists(response, "output");
|
|
var output = response.GetProperty("output");
|
|
Assert.True(output.GetArrayLength() > 0);
|
|
|
|
// Assert - Output contains function call
|
|
var outputItem = output[0];
|
|
AssertJsonPropertyEquals(outputItem, "type", "function_call");
|
|
AssertJsonPropertyEquals(outputItem, "name", functionName);
|
|
AssertJsonPropertyExists(outputItem, "arguments");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ImageInputFullScenarioAsync()
|
|
{
|
|
// Arrange - Full test for image input scenario through Conversations and Responses API
|
|
string createRequestJson = LoadTraceFile("image_input/create_conversation_request.json");
|
|
string firstMessageRequestJson = LoadTraceFile("image_input/first_message_request.json");
|
|
using var createExpectedDoc = LoadTraceDocument("image_input/create_conversation_response.json");
|
|
using var messageExpectedDoc = LoadTraceDocument("image_input/first_message_response.json");
|
|
|
|
// Get expected response text
|
|
string expectedText = messageExpectedDoc.RootElement.GetProperty("output")[0]
|
|
.GetProperty("content")[0]
|
|
.GetProperty("text").GetString()!;
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("image-input-agent", "You are a helpful assistant.", expectedText);
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Parse the image input request to verify structure
|
|
using var requestDoc = JsonDocument.Parse(firstMessageRequestJson);
|
|
var request = requestDoc.RootElement;
|
|
|
|
// Assert - Request structure with image content (validates we're testing the right scenario)
|
|
AssertJsonPropertyExists(request, "input");
|
|
var input = request.GetProperty("input");
|
|
Assert.Equal(JsonValueKind.Array, input.ValueKind);
|
|
|
|
var message = input[0];
|
|
AssertJsonPropertyExists(message, "content");
|
|
var content = message.GetProperty("content");
|
|
Assert.True(content.GetArrayLength() > 1, "Should have text and image content");
|
|
|
|
// Assert - Has input_image content type
|
|
JsonElement? imagePart = content.EnumerateArray()
|
|
.Where(part => part.GetProperty("type").GetString() == "input_image")
|
|
.Cast<JsonElement?>()
|
|
.FirstOrDefault();
|
|
bool hasImage = imagePart.HasValue;
|
|
if (hasImage)
|
|
{
|
|
AssertJsonPropertyExists(imagePart!.Value, "image_url");
|
|
}
|
|
Assert.True(hasImage, "Request should have input_image content");
|
|
|
|
// Act - Send message with image through Responses API
|
|
using var msgDoc = JsonDocument.Parse(firstMessageRequestJson);
|
|
var msgRequest = JsonSerializer.Serialize(new
|
|
{
|
|
model = msgDoc.RootElement.GetProperty("model").GetString(),
|
|
conversation = conversationId,
|
|
input = msgDoc.RootElement.GetProperty("input"),
|
|
max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32()
|
|
});
|
|
|
|
HttpResponseMessage msgResponse = await SendPostRequestAsync(client, "/image-input-agent/v1/responses", msgRequest);
|
|
using var msgResponseDoc = await ParseResponseAsync(msgResponse);
|
|
var response = msgResponseDoc.RootElement;
|
|
|
|
// Assert - Response has conversation reference (validates integration)
|
|
AssertJsonPropertyExists(response, "conversation");
|
|
AssertJsonPropertyEquals(response, "status", "completed");
|
|
|
|
// Assert - Response has output (validates the system processed the request successfully)
|
|
AssertJsonPropertyExists(response, "output");
|
|
var output = response.GetProperty("output");
|
|
Assert.True(output.GetArrayLength() > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ImageInputStreamingScenarioAsync()
|
|
{
|
|
// Arrange - Test streaming response with image input through Conversations + Responses API
|
|
string createRequestJson = LoadTraceFile("image_input_streaming/create_conversation_request.json");
|
|
string firstMessageRequestJson = LoadTraceFile("image_input_streaming/first_message_request.json");
|
|
string expectedResponseSse = LoadTraceFile("image_input_streaming/first_message_response.txt");
|
|
|
|
// Extract expected text from SSE events
|
|
var expectedEvents = ParseSseEventsFromContent(expectedResponseSse);
|
|
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
|
|
string expectedText = string.Concat(deltaEvents.Select(e => e.GetProperty("delta").GetString()));
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("image-streaming-agent", "You are a helpful assistant.", expectedText);
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Prepare streaming request with conversation
|
|
using var msgDoc = JsonDocument.Parse(firstMessageRequestJson);
|
|
var msgRequest = JsonSerializer.Serialize(new
|
|
{
|
|
model = msgDoc.RootElement.GetProperty("model").GetString(),
|
|
conversation = conversationId,
|
|
input = msgDoc.RootElement.GetProperty("input"),
|
|
stream = true,
|
|
max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32()
|
|
});
|
|
|
|
// Act - Send streaming request
|
|
HttpResponseMessage streamResponse = await SendPostRequestAsync(client, "/image-streaming-agent/v1/responses", msgRequest);
|
|
|
|
// Assert - Response should be SSE format (validates streaming works with image input)
|
|
Assert.Equal("text/event-stream", streamResponse.Content.Headers.ContentType?.MediaType);
|
|
|
|
string responseSse = await streamResponse.Content.ReadAsStringAsync();
|
|
var events = ParseSseEventsFromContent(responseSse);
|
|
|
|
// Assert - Has expected event types (validates proper streaming event structure)
|
|
var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!);
|
|
Assert.Contains("response.created", eventTypes);
|
|
Assert.Contains("response.output_text.delta", eventTypes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RefusalStreamingScenarioAsync()
|
|
{
|
|
// Arrange - Test streaming response with refusal through Conversations + Responses API
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
string firstMessageRequestJson = LoadTraceFile("refusal_streaming/first_message_request.json");
|
|
string expectedResponseSse = LoadTraceFile("refusal_streaming/first_message_response.txt");
|
|
|
|
// Extract expected text from SSE events
|
|
var expectedEvents = ParseSseEventsFromContent(expectedResponseSse);
|
|
var deltaEvents = expectedEvents.Where(e => e.GetProperty("type").GetString() == "response.output_text.delta").ToList();
|
|
string expectedText = string.Concat(deltaEvents.Select(e => e.GetProperty("delta").GetString()));
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("refusal-streaming-agent", "You are a helpful assistant.", expectedText);
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Prepare streaming request with conversation
|
|
using var msgDoc = JsonDocument.Parse(firstMessageRequestJson);
|
|
var msgRequest = JsonSerializer.Serialize(new
|
|
{
|
|
model = msgDoc.RootElement.GetProperty("model").GetString(),
|
|
conversation = conversationId,
|
|
input = msgDoc.RootElement.GetProperty("input"),
|
|
stream = true,
|
|
max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32()
|
|
});
|
|
|
|
// Act - Send streaming request
|
|
HttpResponseMessage streamResponse = await SendPostRequestAsync(client, "/refusal-streaming-agent/v1/responses", msgRequest);
|
|
|
|
// Assert - Response should be SSE format
|
|
Assert.Equal("text/event-stream", streamResponse.Content.Headers.ContentType?.MediaType);
|
|
|
|
string responseSse = await streamResponse.Content.ReadAsStringAsync();
|
|
var events = ParseSseEventsFromContent(responseSse);
|
|
|
|
// Assert - Has expected event types (conformance check)
|
|
var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!);
|
|
Assert.Contains("response.created", eventTypes);
|
|
Assert.Contains("response.output_text.delta", eventTypes);
|
|
|
|
// Assert - Text contains refusal (validates refusal content is in streaming output)
|
|
var doneEvent = events.First(e => e.GetProperty("type").GetString() == "response.output_text.done");
|
|
var finalText = doneEvent.GetProperty("text").GetString();
|
|
Assert.NotNull(finalText);
|
|
Assert.Contains("can't assist", finalText, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ToolCallStreamingScenarioAsync()
|
|
{
|
|
// Arrange - Test streaming response with tool call through Conversations + Responses API
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
string firstMessageRequestJson = LoadTraceFile("tool_call_streaming/first_message_request.json");
|
|
|
|
// Use tool call details from the non-streaming test
|
|
using var messageExpectedDoc = LoadTraceDocument("tool_call/first_message_response.json");
|
|
var expectedOutput = messageExpectedDoc.RootElement.GetProperty("output")[0];
|
|
string functionName = expectedOutput.GetProperty("name").GetString()!;
|
|
string arguments = expectedOutput.GetProperty("arguments").GetString()!;
|
|
|
|
HttpClient client = await this.CreateTestServerWithToolCallAsync("tool-streaming-agent", "You are a helpful assistant.", functionName, arguments);
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Prepare streaming request with conversation
|
|
using var msgDoc = JsonDocument.Parse(firstMessageRequestJson);
|
|
var msgRequest = JsonSerializer.Serialize(new
|
|
{
|
|
model = msgDoc.RootElement.GetProperty("model").GetString(),
|
|
conversation = conversationId,
|
|
input = msgDoc.RootElement.GetProperty("input"),
|
|
tools = msgDoc.RootElement.GetProperty("tools"),
|
|
stream = true,
|
|
max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32()
|
|
});
|
|
|
|
// Act - Send streaming request
|
|
HttpResponseMessage streamResponse = await SendPostRequestAsync(client, "/tool-streaming-agent/v1/responses", msgRequest);
|
|
|
|
// Assert - Response should be SSE format
|
|
Assert.Equal("text/event-stream", streamResponse.Content.Headers.ContentType?.MediaType);
|
|
|
|
string responseSse = await streamResponse.Content.ReadAsStringAsync();
|
|
var events = ParseSseEventsFromContent(responseSse);
|
|
|
|
// Assert - Has expected event types for function call streaming
|
|
var eventTypes = events.ConvertAll(e => e.GetProperty("type").GetString()!);
|
|
Assert.Contains("response.created", eventTypes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RefusalFullScenarioAsync()
|
|
{
|
|
// Arrange - Full test for refusal scenario through Conversations and Responses API
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
string firstMessageRequestJson = LoadTraceFile("refusal/first_message_request.json");
|
|
using var createExpectedDoc = LoadTraceDocument("refusal/create_conversation_response.json");
|
|
using var messageExpectedDoc = LoadTraceDocument("refusal/first_message_response.json");
|
|
|
|
// Get expected response text (refusal message)
|
|
string expectedText = messageExpectedDoc.RootElement.GetProperty("output")[0]
|
|
.GetProperty("content")[0]
|
|
.GetProperty("text").GetString()!;
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("refusal-agent", "You are a helpful assistant.", expectedText);
|
|
|
|
// Act - Create conversation
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
var conversation = createDoc.RootElement;
|
|
string conversationId = conversation.GetProperty("id").GetString()!;
|
|
|
|
// Parse the refusal request to verify structure
|
|
using var requestDoc = JsonDocument.Parse(firstMessageRequestJson);
|
|
var request = requestDoc.RootElement;
|
|
|
|
// Assert - Request structure (input can be string or array depending on the request format)
|
|
AssertJsonPropertyExists(request, "input");
|
|
var input = request.GetProperty("input");
|
|
Assert.True(input.ValueKind is JsonValueKind.String or JsonValueKind.Array);
|
|
|
|
// Act - Send message through Responses API
|
|
using var msgDoc = JsonDocument.Parse(firstMessageRequestJson);
|
|
var msgRequest = JsonSerializer.Serialize(new
|
|
{
|
|
model = msgDoc.RootElement.GetProperty("model").GetString(),
|
|
conversation = conversationId,
|
|
input = msgDoc.RootElement.GetProperty("input"),
|
|
max_output_tokens = msgDoc.RootElement.GetProperty("max_output_tokens").GetInt32()
|
|
});
|
|
|
|
HttpResponseMessage msgResponse = await SendPostRequestAsync(client, "/refusal-agent/v1/responses", msgRequest);
|
|
using var msgResponseDoc = await ParseResponseAsync(msgResponse);
|
|
var response = msgResponseDoc.RootElement;
|
|
|
|
// Assert - Response has conversation reference (validates integration)
|
|
AssertJsonPropertyExists(response, "conversation");
|
|
// Assert - Refusals should be completed, not failed (important behavioral validation)
|
|
AssertJsonPropertyEquals(response, "status", "completed");
|
|
|
|
// Assert - Response has output with refusal (validates structure)
|
|
AssertJsonPropertyExists(response, "output");
|
|
var output = response.GetProperty("output");
|
|
Assert.True(output.GetArrayLength() > 0);
|
|
|
|
var outputMessage = output[0];
|
|
var outputContent = outputMessage.GetProperty("content");
|
|
var textContent = outputContent[0];
|
|
var text = textContent.GetProperty("text").GetString();
|
|
Assert.NotNull(text);
|
|
// Validate refusal pattern (confirms we're testing the right scenario)
|
|
Assert.Contains("can't assist", text, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ErrorMissingRequiredFieldAsync()
|
|
{
|
|
// Arrange
|
|
string requestJson = LoadTraceFile("error_missing_required_field/request.json");
|
|
|
|
HttpClient client = await this.CreateTestServerAsync("missing-field-agent", "You are a helpful assistant.", "Test response");
|
|
|
|
// Create a conversation first
|
|
string createRequestJson = LoadTraceFile("basic/create_conversation_request.json");
|
|
HttpResponseMessage createResponse = await SendPostRequestAsync(client, "/v1/conversations", createRequestJson);
|
|
using var createDoc = await ParseResponseAsync(createResponse);
|
|
string conversationId = createDoc.RootElement.GetProperty("id").GetString()!;
|
|
|
|
// Act - Send request with missing required field (role is missing)
|
|
HttpResponseMessage response = await SendPostRequestAsync(client, $"/v1/conversations/{conversationId}/items", requestJson);
|
|
|
|
// Assert - System should reject the request with a client error status code
|
|
// We accept 400 (Bad Request) or 422 (Unprocessable Entity) as both indicate validation failure
|
|
Assert.True(
|
|
response.StatusCode is System.Net.HttpStatusCode.BadRequest or
|
|
System.Net.HttpStatusCode.UnprocessableEntity,
|
|
$"Expected 400 or 422 status code for missing required field, but got {(int)response.StatusCode} ({response.StatusCode})");
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
this._httpClient?.Dispose();
|
|
if (this._app != null)
|
|
{
|
|
await this._app.DisposeAsync();
|
|
}
|
|
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper to parse SSE events from 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) && 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;
|
|
}
|
|
}
|