.NET: Improve unit test coverage for Microsoft.Agents.AI.OpenAI (#3349)

* Initial plan

* Add unit tests for Microsoft.Agents.AI.OpenAI to improve code coverage

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Address code review feedback: remove unused using directives

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Fix format issues: file encoding and remove unused using directives

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Fix redundant cast error by using named parameter

Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>

* Remove excessive inline comments per PR review feedback

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
This commit is contained in:
Copilot
2026-01-22 16:48:52 +00:00
committed by GitHub
Unverified
parent 9a37411dc1
commit 958e6d27ce
7 changed files with 1144 additions and 0 deletions
@@ -17,6 +17,10 @@
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.AI.OpenAI.UnitTests" />
</ItemGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework OpenAI</Title>
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
namespace Microsoft.Agents.AI.OpenAI.UnitTests.ChatClient;
/// <summary>
/// Unit tests for the <see cref="AsyncStreamingChatCompletionUpdateCollectionResult"/> class.
/// </summary>
public sealed class AsyncStreamingChatCompletionUpdateCollectionResultTests
{
/// <summary>
/// Verify that GetContinuationToken returns null.
/// </summary>
[Fact]
public void GetContinuationToken_ReturnsNull()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
AsyncCollectionResult<StreamingChatCompletionUpdate> collectionResult = new AsyncStreamingChatCompletionUpdateCollectionResult(updates);
// Act
ContinuationToken? token = collectionResult.GetContinuationToken(null!);
// Assert
Assert.Null(token);
}
/// <summary>
/// Verify that GetRawPagesAsync returns a single page.
/// </summary>
[Fact]
public async Task GetRawPagesAsync_ReturnsSinglePageAsync()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
AsyncCollectionResult<StreamingChatCompletionUpdate> collectionResult = new AsyncStreamingChatCompletionUpdateCollectionResult(updates);
// Act
List<ClientResult> pages = [];
await foreach (ClientResult page in collectionResult.GetRawPagesAsync())
{
pages.Add(page);
}
// Assert
Assert.Single(pages);
}
/// <summary>
/// Verify that iterating through the collection yields streaming updates.
/// </summary>
[Fact]
public async Task IterateCollection_YieldsUpdatesAsync()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
AsyncCollectionResult<StreamingChatCompletionUpdate> collectionResult = new AsyncStreamingChatCompletionUpdateCollectionResult(updates);
// Act
List<StreamingChatCompletionUpdate> results = [];
await foreach (StreamingChatCompletionUpdate update in collectionResult)
{
results.Add(update);
}
// Assert
Assert.Single(results);
}
/// <summary>
/// Verify that iterating through the collection with multiple updates yields all updates.
/// </summary>
[Fact]
public async Task IterateCollection_WithMultipleUpdates_YieldsAllUpdatesAsync()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateMultipleTestUpdatesAsync();
AsyncCollectionResult<StreamingChatCompletionUpdate> collectionResult = new AsyncStreamingChatCompletionUpdateCollectionResult(updates);
// Act
List<StreamingChatCompletionUpdate> results = [];
await foreach (StreamingChatCompletionUpdate update in collectionResult)
{
results.Add(update);
}
// Assert
Assert.Equal(3, results.Count);
}
private static async IAsyncEnumerable<AgentResponseUpdate> CreateTestUpdatesAsync()
{
yield return new AgentResponseUpdate(ChatRole.Assistant, "test");
await Task.CompletedTask;
}
private static async IAsyncEnumerable<AgentResponseUpdate> CreateMultipleTestUpdatesAsync()
{
yield return new AgentResponseUpdate(ChatRole.Assistant, "first");
yield return new AgentResponseUpdate(ChatRole.Assistant, "second");
yield return new AgentResponseUpdate(ChatRole.Assistant, "third");
await Task.CompletedTask;
}
}
@@ -0,0 +1,190 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using OpenAI.Responses;
namespace Microsoft.Agents.AI.OpenAI.UnitTests.ChatClient;
/// <summary>
/// Unit tests for the <see cref="AsyncStreamingResponseUpdateCollectionResult"/> class.
/// </summary>
public sealed class AsyncStreamingResponseUpdateCollectionResultTests
{
/// <summary>
/// Verify that GetContinuationToken returns null.
/// </summary>
[Fact]
public void GetContinuationToken_ReturnsNull()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
AsyncCollectionResult<StreamingResponseUpdate> collectionResult = new AsyncStreamingResponseUpdateCollectionResult(updates);
// Act
ContinuationToken? token = collectionResult.GetContinuationToken(null!);
// Assert
Assert.Null(token);
}
/// <summary>
/// Verify that GetRawPagesAsync returns a single page.
/// </summary>
[Fact]
public async Task GetRawPagesAsync_ReturnsSinglePageAsync()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
AsyncCollectionResult<StreamingResponseUpdate> collectionResult = new AsyncStreamingResponseUpdateCollectionResult(updates);
// Act
List<ClientResult> pages = [];
await foreach (ClientResult page in collectionResult.GetRawPagesAsync())
{
pages.Add(page);
}
// Assert
Assert.Single(pages);
}
/// <summary>
/// Verify that iterating through the collection yields streaming updates when RawRepresentation is a StreamingResponseUpdate.
/// </summary>
[Fact]
public async Task IterateCollection_WithStreamingResponseUpdateRawRepresentation_YieldsUpdatesAsync()
{
// Arrange
StreamingResponseUpdate rawUpdate = CreateStreamingResponseUpdate();
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesWithRawRepresentationAsync(rawUpdate);
AsyncCollectionResult<StreamingResponseUpdate> collectionResult = new AsyncStreamingResponseUpdateCollectionResult(updates);
// Act
List<StreamingResponseUpdate> results = [];
await foreach (StreamingResponseUpdate update in collectionResult)
{
results.Add(update);
}
// Assert
Assert.Single(results);
Assert.Same(rawUpdate, results[0]);
}
/// <summary>
/// Verify that iterating through the collection yields updates when RawRepresentation is a ChatResponseUpdate containing a StreamingResponseUpdate.
/// </summary>
[Fact]
public async Task IterateCollection_WithChatResponseUpdateContainingStreamingResponseUpdate_YieldsUpdatesAsync()
{
// Arrange
StreamingResponseUpdate rawUpdate = CreateStreamingResponseUpdate();
ChatResponseUpdate chatResponseUpdate = new() { RawRepresentation = rawUpdate };
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesWithChatResponseUpdateAsync(chatResponseUpdate);
AsyncCollectionResult<StreamingResponseUpdate> collectionResult = new AsyncStreamingResponseUpdateCollectionResult(updates);
// Act
List<StreamingResponseUpdate> results = [];
await foreach (StreamingResponseUpdate update in collectionResult)
{
results.Add(update);
}
// Assert
Assert.Single(results);
Assert.Same(rawUpdate, results[0]);
}
/// <summary>
/// Verify that iterating through the collection skips updates when RawRepresentation is not a StreamingResponseUpdate.
/// </summary>
[Fact]
public async Task IterateCollection_WithNonStreamingResponseUpdateRawRepresentation_SkipsUpdateAsync()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
AsyncCollectionResult<StreamingResponseUpdate> collectionResult = new AsyncStreamingResponseUpdateCollectionResult(updates);
// Act
List<StreamingResponseUpdate> results = [];
await foreach (StreamingResponseUpdate update in collectionResult)
{
results.Add(update);
}
// Assert
Assert.Empty(results);
}
/// <summary>
/// Verify that iterating through the collection skips updates when RawRepresentation is a ChatResponseUpdate without StreamingResponseUpdate.
/// </summary>
[Fact]
public async Task IterateCollection_WithChatResponseUpdateWithoutStreamingResponseUpdate_SkipsUpdateAsync()
{
// Arrange
ChatResponseUpdate chatResponseUpdate = new() { RawRepresentation = "not a streaming update" };
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesWithChatResponseUpdateAsync(chatResponseUpdate);
AsyncCollectionResult<StreamingResponseUpdate> collectionResult = new AsyncStreamingResponseUpdateCollectionResult(updates);
// Act
List<StreamingResponseUpdate> results = [];
await foreach (StreamingResponseUpdate update in collectionResult)
{
results.Add(update);
}
// Assert
Assert.Empty(results);
}
private static async IAsyncEnumerable<AgentResponseUpdate> CreateTestUpdatesAsync()
{
yield return new AgentResponseUpdate(ChatRole.Assistant, "test");
await Task.CompletedTask;
}
private static async IAsyncEnumerable<AgentResponseUpdate> CreateTestUpdatesWithRawRepresentationAsync(object rawRepresentation)
{
AgentResponseUpdate update = new(ChatRole.Assistant, "test")
{
RawRepresentation = rawRepresentation
};
yield return update;
await Task.CompletedTask;
}
private static async IAsyncEnumerable<AgentResponseUpdate> CreateTestUpdatesWithChatResponseUpdateAsync(ChatResponseUpdate chatResponseUpdate)
{
AgentResponseUpdate update = new(ChatRole.Assistant, "test")
{
RawRepresentation = chatResponseUpdate
};
yield return update;
await Task.CompletedTask;
}
private static StreamingResponseUpdate CreateStreamingResponseUpdate()
{
const string Json = """
{
"type": "response.output_item.added",
"sequence_number": 1,
"output_index": 0,
"item": {
"id": "item_abc123",
"type": "message",
"status": "in_progress",
"role": "assistant",
"content": []
}
}
""";
return System.ClientModel.Primitives.ModelReaderWriter.Read<StreamingResponseUpdate>(BinaryData.FromString(Json))!;
}
}
@@ -0,0 +1,154 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.OpenAI.UnitTests.ChatClient;
/// <summary>
/// Unit tests for the <see cref="StreamingUpdatePipelineResponse"/> class.
/// </summary>
public sealed class StreamingUpdatePipelineResponseTests
{
/// <summary>
/// Verify that Status property returns 200.
/// </summary>
[Fact]
public void Status_ReturnsOkStatus()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
PipelineResponse response = new StreamingUpdatePipelineResponse(updates);
// Act
int status = response.Status;
// Assert
Assert.Equal(200, status);
}
/// <summary>
/// Verify that ReasonPhrase property returns "OK".
/// </summary>
[Fact]
public void ReasonPhrase_ReturnsOk()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
PipelineResponse response = new StreamingUpdatePipelineResponse(updates);
// Act
string reasonPhrase = response.ReasonPhrase;
// Assert
Assert.Equal("OK", reasonPhrase);
}
/// <summary>
/// Verify that ContentStream getter returns null.
/// </summary>
[Fact]
public void ContentStream_Get_ReturnsNull()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
PipelineResponse response = new StreamingUpdatePipelineResponse(updates);
// Act
System.IO.Stream? contentStream = response.ContentStream;
// Assert
Assert.Null(contentStream);
}
/// <summary>
/// Verify that ContentStream setter is a no-op.
/// </summary>
[Fact]
public void ContentStream_Set_IsNoOp()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
PipelineResponse response = new StreamingUpdatePipelineResponse(updates);
var testStream = new System.IO.MemoryStream();
// Act
response.ContentStream = testStream;
// Assert
Assert.Null(response.ContentStream);
testStream.Dispose();
}
/// <summary>
/// Verify that Content property returns empty BinaryData.
/// </summary>
[Fact]
public void Content_ReturnsEmptyBinaryData()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
PipelineResponse response = new StreamingUpdatePipelineResponse(updates);
// Act
BinaryData content = response.Content;
// Assert
Assert.NotNull(content);
Assert.Equal(string.Empty, content.ToString());
}
/// <summary>
/// Verify that BufferContent throws NotSupportedException.
/// </summary>
[Fact]
public void BufferContent_ThrowsNotSupportedException()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
PipelineResponse response = new StreamingUpdatePipelineResponse(updates);
// Act & Assert
var exception = Assert.Throws<NotSupportedException>(() => response.BufferContent());
Assert.Contains("Buffering content is not supported", exception.Message);
}
/// <summary>
/// Verify that BufferContentAsync throws NotSupportedException.
/// </summary>
[Fact]
public async Task BufferContentAsync_ThrowsNotSupportedExceptionAsync()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
PipelineResponse response = new StreamingUpdatePipelineResponse(updates);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotSupportedException>(
async () => await response.BufferContentAsync());
Assert.Contains("Buffering content asynchronously is not supported", exception.Message);
}
/// <summary>
/// Verify that Dispose does not throw.
/// </summary>
[Fact]
public void Dispose_DoesNotThrow()
{
// Arrange
IAsyncEnumerable<AgentResponseUpdate> updates = CreateTestUpdatesAsync();
PipelineResponse response = new StreamingUpdatePipelineResponse(updates);
// Act & Assert
response.Dispose();
}
private static async IAsyncEnumerable<AgentResponseUpdate> CreateTestUpdatesAsync()
{
yield return new AgentResponseUpdate(Microsoft.Extensions.AI.ChatRole.Assistant, "test");
await Task.CompletedTask;
}
}
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using OpenAI.Responses;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
using ChatRole = Microsoft.Extensions.AI.ChatRole;
using OpenAIChatMessage = OpenAI.Chat.ChatMessage;
@@ -208,4 +209,167 @@ public sealed class AIAgentWithOpenAIExtensionsTests
yield return await Task.FromResult(update);
}
}
#region ResponseItem overload tests
/// <summary>
/// Verify that RunAsync with ResponseItem throws ArgumentNullException when agent is null.
/// </summary>
[Fact]
public async Task RunAsync_ResponseItem_WithNullAgent_ThrowsArgumentNullExceptionAsync()
{
// Arrange
AIAgent? agent = null;
IEnumerable<ResponseItem> messages = [ResponseItem.CreateUserMessageItem("Test message")];
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => agent!.RunAsync(messages));
Assert.Equal("agent", exception.ParamName);
}
/// <summary>
/// Verify that RunAsync with ResponseItem throws ArgumentNullException when messages is null.
/// </summary>
[Fact]
public async Task RunAsync_ResponseItem_WithNullMessages_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
IEnumerable<ResponseItem>? messages = null;
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(
() => mockAgent.Object.RunAsync(messages!));
Assert.Equal("messages", exception.ParamName);
}
/// <summary>
/// Verify that the RunAsync with ResponseItem extension method calls the underlying agent's RunAsync with converted messages and parameters.
/// </summary>
[Fact]
public async Task RunAsync_ResponseItem_CallsUnderlyingAgentAsync()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
var mockThread = new Mock<AgentThread>();
var options = new AgentRunOptions();
var cancellationToken = new CancellationToken(false);
const string TestMessageText = "Hello, assistant!";
const string ResponseText = "This is the assistant's response.";
IEnumerable<ResponseItem> responseItemMessages = [ResponseItem.CreateUserMessageItem(TestMessageText)];
var responseMessage = new ChatMessage(ChatRole.Assistant, [new TextContent(ResponseText)]);
mockAgent
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentThread?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new AgentResponse([responseMessage]));
// Act
ResponseResult result = await mockAgent.Object.RunAsync(responseItemMessages, mockThread.Object, options, cancellationToken);
// Assert
mockAgent.Protected()
.Verify("RunCoreAsync",
Times.Once(),
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
mockThread.Object,
options,
cancellationToken
);
Assert.NotNull(result);
}
/// <summary>
/// Verify that RunStreamingAsync with ResponseItem throws ArgumentNullException when agent is null.
/// </summary>
[Fact]
public void RunStreamingAsync_ResponseItem_WithNullAgent_ThrowsArgumentNullException()
{
// Arrange
AIAgent? agent = null;
IEnumerable<ResponseItem> messages = [ResponseItem.CreateUserMessageItem("Test message")];
// Act & Assert
Assert.Throws<ArgumentNullException>(
"agent",
() => agent!.RunStreamingAsync(messages));
}
/// <summary>
/// Verify that RunStreamingAsync with ResponseItem throws ArgumentNullException when messages is null.
/// </summary>
[Fact]
public void RunStreamingAsync_ResponseItem_WithNullMessages_ThrowsArgumentNullException()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
IEnumerable<ResponseItem>? messages = null;
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(
() => mockAgent.Object.RunStreamingAsync(messages!));
Assert.Equal("messages", exception.ParamName);
}
/// <summary>
/// Verify that the RunStreamingAsync with ResponseItem extension method calls the underlying agent's RunStreamingAsync with converted messages and parameters.
/// </summary>
[Fact]
public async Task RunStreamingAsync_ResponseItem_CallsUnderlyingAgentAsync()
{
// Arrange
var mockAgent = new Mock<AIAgent>();
var mockThread = new Mock<AgentThread>();
var options = new AgentRunOptions();
var cancellationToken = new CancellationToken(false);
const string TestMessageText = "Hello, assistant!";
const string ResponseText1 = "This is ";
const string ResponseText2 = "the assistant's response.";
IEnumerable<ResponseItem> responseItemMessages = [ResponseItem.CreateUserMessageItem(TestMessageText)];
var responseUpdates = new List<AgentResponseUpdate>
{
new(ChatRole.Assistant, ResponseText1),
new(ChatRole.Assistant, ResponseText2)
};
mockAgent
.Protected()
.Setup<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentThread?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.Returns(ToAsyncEnumerableAsync(responseUpdates));
// Act
var result = mockAgent.Object.RunStreamingAsync(responseItemMessages, mockThread.Object, options, cancellationToken);
var updateCount = 0;
await foreach (var update in result)
{
updateCount++;
}
// Assert
mockAgent.Protected()
.Verify("RunCoreStreamingAsync",
Times.Once(),
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
mockThread.Object,
options,
cancellationToken
);
}
#endregion
}
@@ -0,0 +1,142 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using OpenAI.Chat;
using ChatMessage = Microsoft.Extensions.AI.ChatMessage;
using ChatRole = Microsoft.Extensions.AI.ChatRole;
using TextContent = Microsoft.Extensions.AI.TextContent;
namespace Microsoft.Agents.AI.OpenAI.UnitTests.Extensions;
/// <summary>
/// Unit tests for the AgentResponseExtensions class that provides OpenAI extension methods.
/// </summary>
public sealed class AgentResponseExtensionsTests
{
/// <summary>
/// Verify that AsOpenAIChatCompletion throws ArgumentNullException when response is null.
/// </summary>
[Fact]
public void AsOpenAIChatCompletion_WithNullResponse_ThrowsArgumentNullException()
{
// Arrange
AgentResponse? response = null;
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(
() => response!.AsOpenAIChatCompletion());
Assert.Equal("response", exception.ParamName);
}
/// <summary>
/// Verify that AsOpenAIChatCompletion returns the RawRepresentation when it is a ChatCompletion.
/// </summary>
[Fact]
public void AsOpenAIChatCompletion_WithChatCompletionRawRepresentation_ReturnsChatCompletion()
{
// Arrange
ChatCompletion chatCompletion = ModelReaderWriterHelper.CreateChatCompletion("assistant_id", "Hello");
var responseMessage = new ChatMessage(ChatRole.Assistant, [new TextContent("Hello")]);
var agentResponse = new AgentResponse([responseMessage])
{
RawRepresentation = chatCompletion
};
// Act
ChatCompletion result = agentResponse.AsOpenAIChatCompletion();
// Assert
Assert.NotNull(result);
Assert.Same(chatCompletion, result);
}
/// <summary>
/// Verify that AsOpenAIChatCompletion converts a ChatResponse when RawRepresentation is not a ChatCompletion.
/// </summary>
[Fact]
public void AsOpenAIChatCompletion_WithNonChatCompletionRawRepresentation_ConvertsChatResponse()
{
// Arrange
const string ResponseText = "This is a test response.";
var responseMessage = new ChatMessage(ChatRole.Assistant, [new TextContent(ResponseText)]);
var agentResponse = new AgentResponse([responseMessage]);
// Act
ChatCompletion result = agentResponse.AsOpenAIChatCompletion();
// Assert
Assert.NotNull(result);
Assert.Single(result.Content);
Assert.Equal(ResponseText, result.Content[0].Text);
}
/// <summary>
/// Verify that AsOpenAIResponse throws ArgumentNullException when response is null.
/// </summary>
[Fact]
public void AsOpenAIResponse_WithNullResponse_ThrowsArgumentNullException()
{
// Arrange
AgentResponse? response = null;
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(
() => response!.AsOpenAIResponse());
Assert.Equal("response", exception.ParamName);
}
/// <summary>
/// Verify that AsOpenAIResponse converts a ChatResponse when RawRepresentation is not a ResponseResult.
/// </summary>
[Fact]
public void AsOpenAIResponse_WithNonResponseResultRawRepresentation_ConvertsChatResponse()
{
// Arrange
const string ResponseText = "This is a test response.";
var responseMessage = new ChatMessage(ChatRole.Assistant, [new TextContent(ResponseText)]);
var agentResponse = new AgentResponse([responseMessage]);
// Act
var result = agentResponse.AsOpenAIResponse();
// Assert
Assert.NotNull(result);
}
}
/// <summary>
/// Helper class for creating OpenAI model objects using ModelReaderWriter.
/// </summary>
internal static class ModelReaderWriterHelper
{
public static ChatCompletion CreateChatCompletion(string id, string contentText)
{
string json = $$"""
{
"id": "{{id}}",
"object": "chat.completion",
"created": 1700000000,
"model": "gpt-4",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{{contentText}}"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 10,
"total_tokens": 20
}
}
""";
return System.ClientModel.Primitives.ModelReaderWriter.Read<ChatCompletion>(BinaryData.FromString(json))!;
}
}
@@ -569,6 +569,387 @@ public sealed class OpenAIAssistantClientExtensionsTests
return property?.GetValue(client) as IServiceProvider;
}
/// <summary>
/// Verify that CreateAIAgentAsync with HostedCodeInterpreterTool properly adds CodeInterpreter tool definition.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithHostedCodeInterpreterTool_CreatesAgentWithToolAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
const string ModelId = "test-model";
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
ChatOptions = new ChatOptions
{
Instructions = "Test instructions",
Tools = [new HostedCodeInterpreterTool()]
}
};
// Act
var agent = await assistantClient.CreateAIAgentAsync(ModelId, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
}
/// <summary>
/// Verify that CreateAIAgentAsync with HostedCodeInterpreterTool with HostedFileContent input properly creates agent.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithHostedCodeInterpreterToolAndHostedFileContent_CreatesAgentWithToolResourcesAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
const string ModelId = "test-model";
var codeInterpreterTool = new HostedCodeInterpreterTool
{
Inputs = [new HostedFileContent("test-file-id")]
};
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
ChatOptions = new ChatOptions
{
Instructions = "Test instructions",
Tools = [codeInterpreterTool]
}
};
// Act
var agent = await assistantClient.CreateAIAgentAsync(ModelId, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
}
/// <summary>
/// Verify that CreateAIAgentAsync with HostedFileSearchTool properly adds FileSearch tool definition.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithHostedFileSearchTool_CreatesAgentWithToolAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
const string ModelId = "test-model";
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
ChatOptions = new ChatOptions
{
Instructions = "Test instructions",
Tools = [new HostedFileSearchTool()]
}
};
// Act
var agent = await assistantClient.CreateAIAgentAsync(ModelId, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
}
/// <summary>
/// Verify that CreateAIAgentAsync with HostedFileSearchTool with HostedVectorStoreContent input properly creates agent.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithHostedFileSearchToolAndHostedVectorStoreContent_CreatesAgentWithToolResourcesAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
const string ModelId = "test-model";
var fileSearchTool = new HostedFileSearchTool
{
MaximumResultCount = 10,
Inputs = [new HostedVectorStoreContent("test-vector-store-id")]
};
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
ChatOptions = new ChatOptions
{
Instructions = "Test instructions",
Tools = [fileSearchTool]
}
};
// Act
var agent = await assistantClient.CreateAIAgentAsync(ModelId, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
}
/// <summary>
/// Verify that CreateAIAgentAsync with multiple tools including functions properly creates agent.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithMixedTools_CreatesAgentWithAllToolsAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
const string ModelId = "test-model";
var testFunction = AIFunctionFactory.Create(() => "test", "TestFunction", "A test function");
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
ChatOptions = new ChatOptions
{
Instructions = "Test instructions",
Tools = [new HostedCodeInterpreterTool(), new HostedFileSearchTool(), testFunction]
}
};
// Act
var agent = await assistantClient.CreateAIAgentAsync(ModelId, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
}
/// <summary>
/// Verify that CreateAIAgentAsync with function tools properly categorizes them as other tools.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithFunctionTools_CategorizesAsOtherToolsAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
const string ModelId = "test-model";
var testFunction = AIFunctionFactory.Create(() => "test", "TestFunction", "A test function");
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
ChatOptions = new ChatOptions
{
Instructions = "Test instructions",
Tools = [testFunction]
}
};
// Act
var agent = await assistantClient.CreateAIAgentAsync(ModelId, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
}
/// <summary>
/// Verify that AsAIAgent with legacy overload works correctly when assistant instructions are set.
/// </summary>
[Fact]
public void AsAIAgent_LegacyOverload_WithAssistantInstructions_SetsInstructions()
{
// Arrange
var assistantClient = new TestAssistantClient();
var assistant = ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123", "name": "Test Agent", "instructions": "Original Instructions"}"""))!;
// Act
var agent = assistantClient.AsAIAgent(assistant);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
Assert.Equal("Original Instructions", agent.Instructions);
}
/// <summary>
/// Verify that AsAIAgent with legacy overload works correctly when chatOptions with instructions is provided.
/// </summary>
[Fact]
public void AsAIAgent_LegacyOverload_WithChatOptionsInstructions_UsesChatOptionsInstructions()
{
// Arrange
var assistantClient = new TestAssistantClient();
var assistant = ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123", "name": "Test Agent", "instructions": "Original Instructions"}"""))!;
var chatOptions = new ChatOptions { Instructions = "Override Instructions" };
// Act
var agent = assistantClient.AsAIAgent(assistant, chatOptions);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
Assert.Equal("Override Instructions", agent.Instructions);
}
/// <summary>
/// Verify that AsAIAgent with legacy overload and ClientResult works correctly.
/// </summary>
[Fact]
public void AsAIAgent_LegacyOverload_WithClientResult_WorksCorrectly()
{
// Arrange
var assistantClient = new TestAssistantClient();
var assistant = ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123", "name": "Test Agent", "instructions": "Original Instructions"}"""))!;
var clientResult = ClientResult.FromValue(assistant, new FakePipelineResponse());
// Act
var agent = assistantClient.AsAIAgent(clientResult);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
}
/// <summary>
/// Verify that AsAIAgent with legacy overload throws ArgumentNullException when assistant client is null.
/// </summary>
[Fact]
public void AsAIAgent_LegacyOverload_WithNullAssistantClient_ThrowsArgumentNullException()
{
// Arrange
AssistantClient? assistantClient = null;
var assistant = ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123"}"""))!;
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
assistantClient!.AsAIAgent(assistant));
Assert.Equal("assistantClient", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent with legacy overload throws ArgumentNullException when assistantMetadata is null.
/// </summary>
[Fact]
public void AsAIAgent_LegacyOverload_WithNullAssistantMetadata_ThrowsArgumentNullException()
{
// Arrange
var assistantClient = new TestAssistantClient();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
assistantClient.AsAIAgent((Assistant)null!));
Assert.Equal("assistantMetadata", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent with legacy overload throws ArgumentNullException when clientResult is null.
/// </summary>
[Fact]
public void AsAIAgent_LegacyOverload_WithNullClientResult_ThrowsArgumentNullException()
{
// Arrange
var assistantClient = new TestAssistantClient();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
assistantClient.AsAIAgent(null!, chatOptions: null));
Assert.Equal("assistantClientResult", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgentAsync with legacy overload works correctly.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_LegacyOverload_WorksCorrectlyAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
const string AgentId = "asst_abc123";
// Act
var agent = await assistantClient.GetAIAgentAsync(AgentId);
// Assert
Assert.NotNull(agent);
Assert.Equal("Original Name", agent.Name);
}
/// <summary>
/// Verify that GetAIAgentAsync with legacy overload throws ArgumentNullException when assistantClient is null.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_LegacyOverload_WithNullAssistantClient_ThrowsArgumentNullExceptionAsync()
{
// Arrange
AssistantClient? assistantClient = null;
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
assistantClient!.GetAIAgentAsync("asst_abc123"));
Assert.Equal("assistantClient", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgentAsync with legacy overload throws ArgumentException when agentId is empty.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_LegacyOverload_WithEmptyAgentId_ThrowsArgumentExceptionAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
assistantClient.GetAIAgentAsync(string.Empty));
Assert.Equal("agentId", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgentAsync with options throws ArgumentNullException when assistantClient is null.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithOptions_WithNullAssistantClient_ThrowsArgumentNullExceptionAsync()
{
// Arrange
AssistantClient? assistantClient = null;
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
assistantClient!.GetAIAgentAsync("asst_abc123", options));
Assert.Equal("assistantClient", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgentAsync with options throws ArgumentNullException when options is null.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithOptions_WithNullOptions_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
assistantClient.GetAIAgentAsync("asst_abc123", (ChatClientAgentOptions)null!));
Assert.Equal("options", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent with options throws ArgumentNullException when assistantClient is null.
/// </summary>
[Fact]
public void AsAIAgent_WithOptions_WithNullAssistantClient_ThrowsArgumentNullException()
{
// Arrange
AssistantClient? assistantClient = null;
var assistant = ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123"}"""))!;
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
assistantClient!.AsAIAgent(assistant, options));
Assert.Equal("assistantClient", exception.ParamName);
}
/// <summary>
/// Creates a test AssistantClient implementation for testing.
/// </summary>