Files
agent-framework/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
Roger Barreto e7961571a8 .NET: Update Azure.AI.Projects 2.0.0-beta.1 (#4270)
* Update Microsoft.Agents.AI.AzureAI for Azure.AI.Projects SDK 2.0.0

- Bump Azure.AI.Projects to 2.0.0-alpha.20260213.1
- Bump Azure.AI.Projects.OpenAI to 2.0.0-alpha.20260213.1
- Bump System.ClientModel to 1.9.0 (transitive dependency)
- Switch both GetAgent and CreateAgentVersion to protocol methods
  with MEAI user-agent policy injection via RequestOptions
- Migrate 29 CREATE-path tests from FakeAgentClient to HttpHandlerAssert
  pattern for real HTTP pipeline testing
- Fix StructuredOutputDefinition constructor (BinaryData -> IDictionary)
- Fix responses endpoint path (openai/responses -> /responses)
- Add local-packages NuGet source for pre-release nupkgs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update Azure.AI.Projects to 2.0.0-beta.1 from NuGet.org

- Update Azure.AI.Projects and Azure.AI.Projects.OpenAI to 2.0.0-beta.1
- Remove local-packages NuGet source (packages now on nuget.org)
- Fix MemorySearchTool -> MemorySearchPreviewTool rename
- Fix RedTeams.CreateAsync ambiguous call
- Fix CreateAgentVersion/Async signature change (BinaryData -> string)
- Suppress AAIP001 experimental warning for WorkflowAgentDefinition

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Move s_modelWriterOptionsWire field before methods that use it

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix flaky test: prevent spurious workflow_invoke Activity on timeout wake-up

The StreamingRunEventStream run loop uses a 1-second timeout on
WaitForInputAsync. When the timeout fires before the consumer calls
StopAsync, the loop would create a spurious workflow_invoke Activity
even though no actual input was provided. This caused the
WorkflowRunActivity_IsStopped_Streaming_OffThread_MultiTurnAsync test
to intermittently fail (expecting 2 activities but finding 3).

Fix: guard the loop body with a HasUnprocessedMessages check. On
timeout wake-ups with no work, the loop waits again without creating
an activity or changing the run status.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix epoch race condition causing unit tests to hang on net10.0 and net472

The HasUnprocessedMessages guard (previous commit) correctly prevents
spurious workflow_invoke Activity creation on timeout wake-ups, but
exposed a latent race in the epoch-based signal filtering.

The race: when the run loop processes messages quickly and calls
Interlocked.Increment(ref _completionEpoch) before the consumer calls
TakeEventStreamAsync, the consumer reads the already-incremented epoch
and sets myEpoch = epoch + 1. This causes the consumer to skip the
valid InternalHaltSignal (its epoch < myEpoch) and block forever
waiting for a signal that will never arrive (since the guard prevents
spurious signal generation).

Fix: read _completionEpoch without +1. The +1 was originally needed to
filter stale signals from timeout-driven spurious loop iterations, but
those no longer exist thanks to the HasUnprocessedMessages guard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert "Fix epoch race condition causing unit tests to hang on net10.0 and net472"

This reverts commit 6ce7f01be8.

* Revert "Fix flaky test: prevent spurious workflow_invoke Activity on timeout wake-up"

This reverts commit 98963e17f2.

* Skip hanging multi-turn declarative integration tests

The ValidateMultiTurnAsync tests (ConfirmInput.yaml, RequestExternalInput.yaml)
hang indefinitely in CI, blocking the merge queue. The hang is SDK-independent
(reproduces with both Azure.AI.Projects 1.2.0-beta.5 and 2.0.0-beta.1) and
is a pre-existing issue in the declarative workflow multi-turn test logic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove unused using directive in IntegrationTest.cs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Restore Azure.AI.Projects 2.0.0-beta.1 version bump

The merge from main accidentally reverted the package versions back to
1.2.0-beta.5. This is the primary change of this PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address merge conflict

* Skip flaky WorkflowRunActivity_IsStopped_Streaming_OffThread_MultiTurnAsync test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Skip CheckSystem test cases temporarily

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-04 11:36:39 +00:00

3309 lines
122 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Microsoft.Extensions.AI;
using Moq;
using OpenAI.Responses;
namespace Microsoft.Agents.AI.AzureAI.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AzureAIProjectChatClientExtensions"/> class.
/// </summary>
public sealed class AzureAIProjectChatClientExtensionsTests
{
#region AsAIAgent(AIProjectClient, AgentRecord) Tests
/// <summary>
/// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecord_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient? client = null;
AgentRecord agentRecord = this.CreateTestAgentRecord();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
client!.AsAIAgent(agentRecord));
Assert.Equal("aiProjectClient", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent throws ArgumentNullException when agentRecord is null.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecord_WithNullAgentRecord_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
mockClient.Object.AsAIAgent((AgentRecord)null!));
Assert.Equal("agentRecord", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent with AgentRecord creates a valid agent.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecord_CreatesValidAgent()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
// Assert
Assert.NotNull(agent);
Assert.Equal("agent_abc123", agent.Name);
}
/// <summary>
/// Verify that AsAIAgent with AgentRecord and clientFactory applies the factory.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecord_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
TestChatClient? testChatClient = null;
// Act
var agent = client.AsAIAgent(
agentRecord,
clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent);
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
#endregion
#region AsAIAgent(AIProjectClient, AgentVersion) Tests
/// <summary>
/// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersion_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient? client = null;
AgentVersion agentVersion = this.CreateTestAgentVersion();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
client!.AsAIAgent(agentVersion));
Assert.Equal("aiProjectClient", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent throws ArgumentNullException when agentVersion is null.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersion_WithNullAgentVersion_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
mockClient.Object.AsAIAgent((AgentVersion)null!));
Assert.Equal("agentVersion", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent with AgentVersion creates a valid agent.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersion_CreatesValidAgent()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
Assert.Equal("agent_abc123", agent.Name);
}
/// <summary>
/// Verify that AsAIAgent with AgentVersion and clientFactory applies the factory.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersion_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
TestChatClient? testChatClient = null;
// Act
var agent = client.AsAIAgent(
agentVersion,
clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent);
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that AsAIAgent with requireInvocableTools=true enforces invocable tools.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersion_WithRequireInvocableToolsTrue_EnforcesInvocableTools()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "test", "test_function", "A test function")
};
// Act
var agent = client.AsAIAgent(agentVersion, tools: tools);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that AsAIAgent with requireInvocableTools=false allows declarative functions.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersion_WithRequireInvocableToolsFalse_AllowsDeclarativeFunctions()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
// Act - should not throw even without tools when requireInvocableTools is false
var agent = client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
#endregion
#region GetAIAgentAsync(AIProjectClient, ChatClientAgentOptions) Tests
/// <summary>
/// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentNullException when client is null.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithOptions_WithNullClient_ThrowsArgumentNullExceptionAsync()
{
// Arrange
AIProjectClient? client = null;
var options = new ChatClientAgentOptions { Name = "test-agent" };
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
client!.GetAIAgentAsync(options));
Assert.Equal("aiProjectClient", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentNullException when options is null.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithOptions_WithNullOptions_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
mockClient.Object.GetAIAgentAsync((ChatClientAgentOptions)null!));
Assert.Equal("options", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgentAsync with ChatClientAgentOptions creates a valid agent.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithOptions_CreatesValidAgentAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent");
var options = new ChatClientAgentOptions { Name = "test-agent" };
// Act
var agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
Assert.Equal("test-agent", agent.Name);
}
#endregion
#region AsAIAgent(AIProjectClient, string) Tests
/// <summary>
/// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null.
/// </summary>
[Fact]
public void AsAIAgent_ByName_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient? client = null;
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
client!.AsAIAgent("test-agent"));
Assert.Equal("aiProjectClient", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent throws ArgumentNullException when name is null.
/// </summary>
[Fact]
public void AsAIAgent_ByName_WithNullName_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
mockClient.Object.AsAIAgent((string)null!));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent throws ArgumentException when name is empty.
/// </summary>
[Fact]
public void AsAIAgent_ByName_WithEmptyName_ThrowsArgumentException()
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
mockClient.Object.AsAIAgent(string.Empty));
Assert.Equal("name", exception.ParamName);
}
#endregion
#region GetAIAgentAsync(AIProjectClient, string) Tests
/// <summary>
/// Verify that GetAIAgentAsync throws ArgumentNullException when AIProjectClient is null.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_ByName_WithNullClient_ThrowsArgumentNullExceptionAsync()
{
// Arrange
AIProjectClient? client = null;
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
client!.GetAIAgentAsync("test-agent"));
Assert.Equal("aiProjectClient", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgentAsync throws ArgumentNullException when name is null.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_ByName_WithNullName_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
mockClient.Object.GetAIAgentAsync(name: null!));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgentAsync throws InvalidOperationException when agent is not found.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_ByName_WithNonExistentAgent_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
var mockAgentOperations = new Mock<AIProjectAgentsOperations>();
mockAgentOperations
.Setup(c => c.GetAgentAsync(It.IsAny<string>(), It.IsAny<RequestOptions>()))
.ReturnsAsync(ClientResult.FromOptionalValue((AgentRecord)null!, new MockPipelineResponse(200, BinaryData.FromString("null"))));
var mockClient = new Mock<AIProjectClient>();
mockClient.SetupGet(c => c.Agents).Returns(mockAgentOperations.Object);
mockClient.Setup(x => x.GetConnection(It.IsAny<string>())).Returns(new ClientConnection("fake-connection-id", "http://localhost", ClientPipeline.Create(), CredentialKind.None));
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
mockClient.Object.GetAIAgentAsync("non-existent-agent"));
Assert.Contains("not found", exception.Message);
}
#endregion
#region AsAIAgent(AIProjectClient, AgentRecord) with tools Tests
/// <summary>
/// Verify that AsAIAgent with additional tools when the definition has no tools does not throw and results in an agent with no tools.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecordAndAdditionalTools_WhenDefinitionHasNoTools_ShouldNotThrow()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "test", "test_function", "A test function")
};
// Act
var agent = client.AsAIAgent(agentRecord, tools: tools);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
var chatClient = agent.GetService<IChatClient>();
Assert.NotNull(chatClient);
var agentVersion = chatClient.GetService<AgentVersion>();
Assert.NotNull(agentVersion);
var definition = Assert.IsType<PromptAgentDefinition>(agentVersion.Definition);
Assert.Empty(definition.Tools);
}
/// <summary>
/// Verify that AsAIAgent with null tools works correctly.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecordAndNullTools_WorksCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord, tools: null);
// Assert
Assert.NotNull(agent);
Assert.Equal("agent_abc123", agent.Name);
}
#endregion
#region GetAIAgentAsync(AIProjectClient, string) with tools Tests
/// <summary>
/// Verify that GetAIAgentAsync with tools parameter creates an agent.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithNameAndTools_CreatesAgentAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "test", "test_function", "A test function")
};
// Act
var agent = await client.GetAIAgentAsync("test-agent", tools: tools);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgentAsync with model and options creates a valid agent.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithModelAndOptions_CreatesValidAgentAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", instructions: "Test instructions");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new() { Instructions = "Test instructions" }
};
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.Equal("test-agent", agent.Name);
Assert.Equal("Test instructions", agent.Instructions);
}
/// <summary>
/// Verify that CreateAIAgentAsync with model and options and clientFactory applies the factory.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithModelAndOptions_WithClientFactory_AppliesFactoryCorrectlyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", instructions: "Test instructions");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new() { Instructions = "Test instructions" }
};
TestChatClient? testChatClient = null;
// Act
var agent = await testClient.Client.CreateAIAgentAsync(
"test-model",
options,
clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent);
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
#endregion
#region CreateAIAgentAsync(AIProjectClient, string, AgentDefinition) Tests
/// <summary>
/// Verify that CreateAIAgentAsync throws ArgumentNullException when AIProjectClient is null.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithAgentDefinition_WithNullClient_ThrowsArgumentNullExceptionAsync()
{
// Arrange
AIProjectClient? client = null;
var definition = new PromptAgentDefinition("test-model");
var options = new AgentVersionCreationOptions(definition);
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
client!.CreateAIAgentAsync("agent-name", options));
Assert.Equal("aiProjectClient", exception.ParamName);
}
/// <summary>
/// Verify that CreateAIAgentAsync throws ArgumentNullException when creationOptions is null.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithAgentDefinition_WithNullDefinition_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
mockClient.Object.CreateAIAgentAsync(name: "agent-name", null!));
Assert.Equal("creationOptions", exception.ParamName);
}
#endregion
#region Tool Validation Tests
/// <summary>
/// Verify that CreateAIAgent creates an agent successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithDefinition_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgent without tools parameter creates an agent successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithoutToolsParameter_CreatesAgentSuccessfullyAsync()
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var definitionResponse = GeneratePromptDefinitionResponse(definition, null);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgent without tools in definition creates an agent successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithoutToolsInDefinition_CreatesAgentSuccessfullyAsync()
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definition);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgent uses tools from the definition when no separate tools parameter is provided.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithDefinitionTools_UsesDefinitionToolsAsync()
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
// Add a function tool to the definition
definition.Tools.Add(ResponseTool.CreateFunctionTool("required_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
// Create a response definition with the same tool
var definitionResponse = GeneratePromptDefinitionResponse(definition, definition.Tools.Select(t => t.AsAITool()).ToList());
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
var agentVersion = agent.GetService<AgentVersion>();
Assert.NotNull(agentVersion);
if (agentVersion.Definition is PromptAgentDefinition promptDef)
{
Assert.NotEmpty(promptDef.Tools);
Assert.Single(promptDef.Tools);
Assert.Equal("required_tool", (promptDef.Tools.First() as FunctionTool)?.FunctionName);
}
}
/// <summary>
/// Verify that CreateAIAgent creates an agent successfully when definition has a mix of custom and hosted tools.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithMixedToolsInDefinition_CreatesAgentSuccessfullyAsync()
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("create_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
definition.Tools.Add(new HostedWebSearchTool().GetService<ResponseTool>() ?? new HostedWebSearchTool().AsOpenAIResponseTool());
definition.Tools.Add(new HostedFileSearchTool().GetService<ResponseTool>() ?? new HostedFileSearchTool().AsOpenAIResponseTool());
// Simulate agent definition response with the tools
var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" };
foreach (var tool in definition.Tools)
{
definitionResponse.Tools.Add(tool);
}
using var testClient = CreateTestAgentClientWithHandler(agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
var agentVersion = agent.GetService<AgentVersion>();
Assert.NotNull(agentVersion);
if (agentVersion.Definition is PromptAgentDefinition promptDef)
{
Assert.NotEmpty(promptDef.Tools);
Assert.Equal(3, promptDef.Tools.Count);
}
}
/// <summary>
/// Verify that CreateAIAgentAsync when AI Tools are provided, uses them for the definition via http request.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithNameAndAITools_SendsToolDefinitionViaHttpAsync()
{
// Arrange
using var httpHandler = new HttpHandlerAssert(async (request) =>
{
if (request.Content is not null)
{
var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
Assert.Contains("required_tool", requestBody);
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var client = new AIProjectClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
// Act
var agent = await client.CreateAIAgentAsync(
name: "test-agent",
model: "test-model",
instructions: "Test",
tools: [AIFunctionFactory.Create(() => true, "required_tool")]);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
var agentVersion = agent.GetService<AgentVersion>();
Assert.NotNull(agentVersion);
Assert.IsType<PromptAgentDefinition>(agentVersion.Definition);
}
/// <summary>
/// Verify that when providing AITools with AsAIAgent, any additional tool that doesn't match the tools in agent definition are ignored.
/// </summary>
[Fact]
public void AsAIAgent_AdditionalAITools_WhenNotInTheDefinitionAreIgnored()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var agentVersion = this.CreateTestAgentVersion();
// Manually add tools to the definition to simulate inline tools
if (agentVersion.Definition is PromptAgentDefinition promptDef)
{
promptDef.Tools.Add(ResponseTool.CreateFunctionTool("inline_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
}
var invocableInlineAITool = AIFunctionFactory.Create(() => "test", "inline_tool", "An invocable AIFunction for the inline function");
var shouldBeIgnoredTool = AIFunctionFactory.Create(() => "test", "additional_tool", "An additional test function that should be ignored");
// Act & Assert
var agent = client.AsAIAgent(agentVersion, tools: [invocableInlineAITool, shouldBeIgnoredTool]);
Assert.NotNull(agent);
var version = agent.GetService<AgentVersion>();
Assert.NotNull(version);
var definition = Assert.IsType<PromptAgentDefinition>(version.Definition);
Assert.NotEmpty(definition.Tools);
Assert.NotNull(GetAgentChatOptions(agent));
Assert.NotNull(GetAgentChatOptions(agent)!.Tools);
Assert.Single(GetAgentChatOptions(agent)!.Tools!);
Assert.Equal("inline_tool", (definition.Tools.First() as FunctionTool)?.FunctionName);
}
#endregion
#region Inline Tools vs Parameter Tools Tests
/// <summary>
/// Verify that tools passed as parameters are accepted by AsAIAgent.
/// </summary>
[Fact]
public void AsAIAgent_WithParameterTools_AcceptsTools()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "tool1", "param_tool_1", "First parameter tool"),
AIFunctionFactory.Create(() => "tool2", "param_tool_2", "Second parameter tool")
};
// Act
var agent = client.AsAIAgent(agentRecord, tools: tools);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
var chatClient = agent.GetService<IChatClient>();
Assert.NotNull(chatClient);
var agentVersion = chatClient.GetService<AgentVersion>();
Assert.NotNull(agentVersion);
}
/// <summary>
/// Verify that CreateAIAgent with string parameters and tools creates an agent.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithStringParamsAndTools_CreatesAgentAsync()
{
// Arrange
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "weather", "string_param_tool", "Tool from string params")
};
var definitionResponse = GeneratePromptDefinitionResponse(new PromptAgentDefinition("test-model") { Instructions = "Test instructions" }, tools);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
// Act
var agent = await testClient.Client.CreateAIAgentAsync(
"test-agent",
"test-model",
"Test instructions",
tools: tools);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
var agentVersion = agent.GetService<AgentVersion>();
Assert.NotNull(agentVersion);
if (agentVersion.Definition is PromptAgentDefinition promptDef)
{
Assert.NotEmpty(promptDef.Tools);
Assert.Single(promptDef.Tools);
}
}
/// <summary>
/// Verify that CreateAIAgentAsync with tools in definition creates an agent.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithDefinitionTools_CreatesAgentAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("async_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that GetAIAgentAsync with tools parameter creates an agent.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithToolsParameter_CreatesAgentAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "async_get_result", "async_get_tool", "An async get tool")
};
// Act
var agent = await client.GetAIAgentAsync("test-agent", tools: tools);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
#endregion
#region Declarative Function Handling Tests
/// <summary>
/// Verifies that CreateAIAgent uses tools from definition when they are ResponseTool instances, resulting in successful agent creation.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithResponseToolsInDefinition_CreatesAgentSuccessfullyAsync()
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" };
var fabricToolOptions = new FabricDataAgentToolOptions();
fabricToolOptions.ProjectConnections.Add(new ToolProjectConnection("connection-id"));
var sharepointOptions = new SharePointGroundingToolOptions();
sharepointOptions.ProjectConnections.Add(new ToolProjectConnection("connection-id"));
var structuredOutputs = new StructuredOutputDefinition("name", "description", new Dictionary<string, BinaryData> { ["schema"] = BinaryData.FromString(AIJsonUtilities.CreateJsonSchema(new { id = "test" }.GetType()).ToString()) }, false);
// Add tools to the definition
definition.Tools.Add(ResponseTool.CreateFunctionTool("create_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
definition.Tools.Add((ResponseTool)AgentTool.CreateBingCustomSearchTool(new BingCustomSearchToolParameters([new BingCustomSearchConfiguration("connection-id", "instance-name")])));
definition.Tools.Add((ResponseTool)AgentTool.CreateBrowserAutomationTool(new BrowserAutomationToolParameters(new BrowserAutomationToolConnectionParameters("id"))));
definition.Tools.Add(AgentTool.CreateA2ATool(new Uri("https://test-uri.microsoft.com")));
definition.Tools.Add((ResponseTool)AgentTool.CreateBingGroundingTool(new BingGroundingSearchToolOptions([new BingGroundingSearchConfiguration("connection-id")])));
definition.Tools.Add((ResponseTool)AgentTool.CreateMicrosoftFabricTool(fabricToolOptions));
definition.Tools.Add((ResponseTool)AgentTool.CreateOpenApiTool(new OpenAPIFunctionDefinition("name", BinaryData.FromString(OpenAPISpec), new OpenAPIAnonymousAuthenticationDetails())));
definition.Tools.Add((ResponseTool)AgentTool.CreateSharepointTool(sharepointOptions));
definition.Tools.Add((ResponseTool)AgentTool.CreateStructuredOutputsTool(structuredOutputs));
definition.Tools.Add((ResponseTool)AgentTool.CreateAzureAISearchTool(new AzureAISearchToolOptions([new AzureAISearchToolIndex() { IndexName = "name" }])));
// Generate agent definition response with the tools
var definitionResponse = GeneratePromptDefinitionResponse(definition, definition.Tools.Select(t => t.AsAITool()).ToList());
using var testClient = CreateTestAgentClientWithHandler(agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
var agentVersion = agent.GetService<AgentVersion>();
Assert.NotNull(agentVersion);
if (agentVersion.Definition is PromptAgentDefinition promptDef)
{
Assert.NotEmpty(promptDef.Tools);
Assert.Equal(10, promptDef.Tools.Count);
}
}
/// <summary>
/// Verify that CreateAIAgentAsync accepts FunctionTools from definition.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithFunctionToolsInDefinition_AcceptsDeclarativeFunctionAsync()
{
// Arrange
var functionTool = ResponseTool.CreateFunctionTool(
functionName: "get_user_name",
functionParameters: BinaryData.FromString("{}"),
strictModeEnabled: false,
functionDescription: "Gets the user's name, as used for friendly address."
);
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
definition.Tools.Add(functionTool);
// Generate response with the declarative function
var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" };
definitionResponse.Tools.Add(functionTool);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgentAsync accepts declarative functions from definition.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithDeclarativeFunctionFromDefinition_AcceptsDeclarativeFunctionAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
// Create a declarative function (not invocable) using AIFunctionFactory.CreateDeclaration
using var doc = JsonDocument.Parse("{}");
var declarativeFunction = AIFunctionFactory.CreateDeclaration("test_function", "A test function", doc.RootElement);
// Add to definition
definition.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException());
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgentAsync accepts declarative functions from definition.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithDeclarativeFunctionInDefinition_AcceptsDeclarativeFunctionAsync()
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
// Create a declarative function (not invocable) using AIFunctionFactory.CreateDeclaration
using var doc = JsonDocument.Parse("{}");
var declarativeFunction = AIFunctionFactory.CreateDeclaration("test_function", "A test function", doc.RootElement);
// Add to definition
definition.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException());
// Generate response with the declarative function
var definitionResponse = new PromptAgentDefinition("test-model") { Instructions = "Test" };
definitionResponse.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException());
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
#endregion
#region Options Generation Validation Tests
/// <summary>
/// Verify that ChatClientAgentOptions are generated correctly without tools.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_GeneratesCorrectChatClientAgentOptionsAsync()
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test instructions" };
var definitionResponse = GeneratePromptDefinitionResponse(definition, null);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-agent", options);
// Assert
Assert.NotNull(agent);
var agentVersion = agent.GetService<AgentVersion>();
Assert.NotNull(agentVersion);
Assert.Equal("test-agent", agentVersion.Name);
Assert.Equal("Test instructions", (agentVersion.Definition as PromptAgentDefinition)?.Instructions);
}
/// <summary>
/// Verify that GetAIAgentAsync with options preserves custom properties from input options.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithOptions_PreservesCustomPropertiesAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient(agentName: "test-agent", instructions: "Custom instructions", description: "Custom description");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
Description = "Custom description",
ChatOptions = new ChatOptions { Instructions = "Custom instructions" }
};
// Act
var agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
Assert.Equal("test-agent", agent.Name);
Assert.Equal("Custom instructions", agent.Instructions);
Assert.Equal("Custom description", agent.Description);
}
/// <summary>
/// Verify that CreateAIAgentAsync with options and tools generates correct ChatClientAgentOptions.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithOptionsAndTools_GeneratesCorrectOptionsAsync()
{
// Arrange
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "result", "option_tool", "A tool from options")
};
var definitionResponse = GeneratePromptDefinitionResponse(
new PromptAgentDefinition("test-model") { Instructions = "Test" },
tools);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test", Tools = tools }
};
// Act
var agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
var agentVersion = agent.GetService<AgentVersion>();
Assert.NotNull(agentVersion);
if (agentVersion.Definition is PromptAgentDefinition promptDef)
{
Assert.NotEmpty(promptDef.Tools);
Assert.Single(promptDef.Tools);
}
}
#endregion
#region AgentName Validation Tests
/// <summary>
/// Verify that AsAIAgent throws ArgumentException when agent name is invalid.
/// </summary>
[Theory]
[MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))]
public void AsAIAgent_ByName_WithInvalidAgentName_ThrowsArgumentException(string invalidName)
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
mockClient.Object.AsAIAgent(invalidName));
Assert.Equal("name", exception.ParamName);
Assert.Contains("Agent name must be 1-63 characters long", exception.Message);
}
/// <summary>
/// Verify that GetAIAgentAsync throws ArgumentException when agent name is invalid.
/// </summary>
[Theory]
[MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))]
public async Task GetAIAgentAsync_ByName_WithInvalidAgentName_ThrowsArgumentExceptionAsync(string invalidName)
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
mockClient.Object.GetAIAgentAsync(invalidName));
Assert.Equal("name", exception.ParamName);
Assert.Contains("Agent name must be 1-63 characters long", exception.Message);
}
/// <summary>
/// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when agent name is invalid.
/// </summary>
[Theory]
[MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))]
public async Task GetAIAgentAsync_WithOptions_WithInvalidAgentName_ThrowsArgumentExceptionAsync(string invalidName)
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions { Name = invalidName };
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.GetAIAgentAsync(options));
Assert.Equal("name", exception.ParamName);
Assert.Contains("Agent name must be 1-63 characters long", exception.Message);
}
/// <summary>
/// Verify that CreateAIAgentAsync throws ArgumentException when agent name is invalid.
/// </summary>
[Theory]
[MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))]
public async Task CreateAIAgentAsync_WithBasicParams_WithInvalidAgentName_ThrowsArgumentExceptionAsync(string invalidName)
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
mockClient.Object.CreateAIAgentAsync(invalidName, "model", "instructions"));
Assert.Equal("name", exception.ParamName);
Assert.Contains("Agent name must be 1-63 characters long", exception.Message);
}
/// <summary>
/// Verify that CreateAIAgentAsync with AgentVersionCreationOptions throws ArgumentException when agent name is invalid.
/// </summary>
[Theory]
[MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))]
public async Task CreateAIAgentAsync_WithAgentDefinition_WithInvalidAgentName_ThrowsArgumentExceptionAsync(string invalidName)
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
var definition = new PromptAgentDefinition("test-model");
var options = new AgentVersionCreationOptions(definition);
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
mockClient.Object.CreateAIAgentAsync(invalidName, options));
Assert.Equal("name", exception.ParamName);
Assert.Contains("Agent name must be 1-63 characters long", exception.Message);
}
/// <summary>
/// Verify that CreateAIAgentAsync with ChatClientAgentOptions throws ArgumentException when agent name is invalid.
/// </summary>
[Theory]
[MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))]
public async Task CreateAIAgentAsync_WithOptions_WithInvalidAgentName_ThrowsArgumentExceptionAsync(string invalidName)
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions { Name = invalidName };
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.CreateAIAgentAsync("test-model", options));
Assert.Equal("name", exception.ParamName);
Assert.Contains("Agent name must be 1-63 characters long", exception.Message);
}
/// <summary>
/// Verify that AsAIAgent with AgentReference throws ArgumentException when agent name is invalid.
/// </summary>
[Theory]
[MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))]
public void AsAIAgent_WithAgentReference_WithInvalidAgentName_ThrowsArgumentException(string invalidName)
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
var agentReference = new AgentReference(invalidName, "1");
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
mockClient.Object.AsAIAgent(agentReference));
Assert.Equal("name", exception.ParamName);
Assert.Contains("Agent name must be 1-63 characters long", exception.Message);
}
#endregion
#region AzureAIChatClient Behavior Tests
/// <summary>
/// Verify that the underlying chat client created by extension methods can be wrapped with clientFactory.
/// </summary>
[Fact]
public void AsAIAgent_WithClientFactory_WrapsUnderlyingChatClient()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
int factoryCallCount = 0;
// Act
var agent = client.AsAIAgent(
agentRecord,
clientFactory: (innerClient) =>
{
factoryCallCount++;
return new TestChatClient(innerClient);
});
// Assert
Assert.NotNull(agent);
Assert.Equal(1, factoryCallCount);
var wrappedClient = agent.GetService<TestChatClient>();
Assert.NotNull(wrappedClient);
}
/// <summary>
/// Verify that clientFactory is called with the correct underlying chat client.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithClientFactory_ReceivesCorrectUnderlyingClientAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
IChatClient? receivedClient = null;
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync(
"test-agent",
options,
clientFactory: (innerClient) =>
{
receivedClient = innerClient;
return new TestChatClient(innerClient);
});
// Assert
Assert.NotNull(agent);
Assert.NotNull(receivedClient);
var wrappedClient = agent.GetService<TestChatClient>();
Assert.NotNull(wrappedClient);
}
/// <summary>
/// Verify that multiple clientFactory calls create independent wrapped clients.
/// </summary>
[Fact]
public void AsAIAgent_MultipleCallsWithClientFactory_CreatesIndependentClients()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent1 = client.AsAIAgent(
agentRecord,
clientFactory: (innerClient) => new TestChatClient(innerClient));
var agent2 = client.AsAIAgent(
agentRecord,
clientFactory: (innerClient) => new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent1);
Assert.NotNull(agent2);
var client1 = agent1.GetService<TestChatClient>();
var client2 = agent2.GetService<TestChatClient>();
Assert.NotNull(client1);
Assert.NotNull(client2);
Assert.NotSame(client1, client2);
}
/// <summary>
/// Verify that agent created with clientFactory maintains agent properties.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithClientFactory_PreservesAgentPropertiesAsync()
{
// Arrange
const string AgentName = "test-agent";
const string Model = "test-model";
const string Instructions = "Test instructions";
using var testClient = CreateTestAgentClientWithHandler(AgentName, Instructions);
// Act
var agent = await testClient.Client.CreateAIAgentAsync(
AgentName,
Model,
Instructions,
clientFactory: (innerClient) => new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent);
Assert.Equal(AgentName, agent.Name);
Assert.Equal(Instructions, agent.Instructions);
var wrappedClient = agent.GetService<TestChatClient>();
Assert.NotNull(wrappedClient);
}
/// <summary>
/// Verify that agent created with clientFactory is created successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithClientFactory_CreatesAgentSuccessfullyAsync()
{
// Arrange
var definition = new PromptAgentDefinition("test-model") { Instructions = "Test" };
var agentDefinitionResponse = GeneratePromptDefinitionResponse(definition, null);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: agentDefinitionResponse);
var options = new AgentVersionCreationOptions(definition);
// Act
var agent = await testClient.Client.CreateAIAgentAsync(
"test-agent",
options,
clientFactory: (innerClient) => new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent);
var wrappedClient = agent.GetService<TestChatClient>();
Assert.NotNull(wrappedClient);
var agentVersion = agent.GetService<AgentVersion>();
Assert.NotNull(agentVersion);
}
#endregion
#region User-Agent Header Tests
/// <summary>
/// Verifies that the MEAI user-agent header is added to CreateAIAgentAsync POST requests
/// via the protocol method's RequestOptions pipeline policy.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_UserAgentHeaderAddedToRequestsAsync()
{
using var httpHandler = new HttpHandlerAssert(request =>
{
Assert.Equal("POST", request.Method.Method);
// Verify MEAI user-agent header is present on CreateAgentVersion POST request
Assert.True(request.Headers.TryGetValues("User-Agent", out var userAgentValues));
Assert.Contains(userAgentValues, v => v.Contains("MEAI"));
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
// Arrange
var aiProjectClient = new AIProjectClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
var agentOptions = new ChatClientAgentOptions { Name = "test-agent" };
// Act
var agent = await aiProjectClient.CreateAIAgentAsync("test", agentOptions);
// Assert
Assert.NotNull(agent);
}
/// <summary>
/// Verifies that the user-agent header is added to asynchronous GetAIAgentAsync requests.
/// </summary>
[Fact]
public async Task GetAIAgent_UserAgentHeaderAddedToRequestsAsync()
{
using var httpHandler = new HttpHandlerAssert(request =>
{
Assert.Equal("GET", request.Method.Method);
Assert.Contains("MEAI", request.Headers.UserAgent.ToString());
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") };
});
#pragma warning disable CA5399
using var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
// Arrange
var aiProjectClient = new AIProjectClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
// Act
var agent = await aiProjectClient.GetAIAgentAsync("test");
// Assert
Assert.NotNull(agent);
}
#endregion
#region GetAIAgent(AIProjectClient, AgentReference) Tests
/// <summary>
/// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentReference_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient? client = null;
var agentReference = new AgentReference("test-name", "1");
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
client!.AsAIAgent(agentReference));
Assert.Equal("aiProjectClient", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent throws ArgumentNullException when agentReference is null.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentReference_WithNullAgentReference_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock<AIProjectClient>();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
mockClient.Object.AsAIAgent((AgentReference)null!));
Assert.Equal("agentReference", exception.ParamName);
}
/// <summary>
/// Verify that AsAIAgent with AgentReference creates a valid agent.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentReference_CreatesValidAgent()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var agentReference = new AgentReference("test-name", "1");
// Act
var agent = client.AsAIAgent(agentReference);
// Assert
Assert.NotNull(agent);
Assert.Equal("test-name", agent.Name);
Assert.Equal("test-name:1", agent.Id);
}
/// <summary>
/// Verify that AsAIAgent with AgentReference and clientFactory applies the factory.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentReference_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var agentReference = new AgentReference("test-name", "1");
TestChatClient? testChatClient = null;
// Act
var agent = client.AsAIAgent(
agentReference,
clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent);
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that AsAIAgent with AgentReference sets the agent ID correctly.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentReference_SetsAgentIdCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var agentReference = new AgentReference("test-name", "2");
// Act
var agent = client.AsAIAgent(agentReference);
// Assert
Assert.NotNull(agent);
Assert.Equal("test-name:2", agent.Id);
}
/// <summary>
/// Verify that AsAIAgent with AgentReference and tools includes the tools in ChatOptions.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentReference_WithTools_IncludesToolsInChatOptions()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var agentReference = new AgentReference("test-name", "1");
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "test", "test_function", "A test function")
};
// Act
var agent = client.AsAIAgent(agentReference, tools: tools);
// Assert
Assert.NotNull(agent);
var chatOptions = GetAgentChatOptions(agent);
Assert.NotNull(chatOptions);
Assert.NotNull(chatOptions.Tools);
Assert.Single(chatOptions.Tools);
}
#endregion
#region GetService<AgentRecord> Tests
/// <summary>
/// Verify that GetService returns AgentRecord for agents created from AgentRecord.
/// </summary>
[Fact]
public void GetService_WithAgentRecord_ReturnsAgentRecord()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
var retrievedRecord = agent.GetService<AgentRecord>();
// Assert
Assert.NotNull(retrievedRecord);
Assert.Equal(agentRecord.Id, retrievedRecord.Id);
}
/// <summary>
/// Verify that GetService returns null for AgentRecord when agent is created from AgentReference.
/// </summary>
[Fact]
public void GetService_WithAgentReference_ReturnsNullForAgentRecord()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var agentReference = new AgentReference("test-name", "1");
// Act
var agent = client.AsAIAgent(agentReference);
var retrievedRecord = agent.GetService<AgentRecord>();
// Assert
Assert.Null(retrievedRecord);
}
#endregion
#region GetService<AgentVersion> Tests
/// <summary>
/// Verify that GetService returns AgentVersion for agents created from AgentVersion.
/// </summary>
[Fact]
public void GetService_WithAgentVersion_ReturnsAgentVersion()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
var retrievedVersion = agent.GetService<AgentVersion>();
// Assert
Assert.NotNull(retrievedVersion);
Assert.Equal(agentVersion.Id, retrievedVersion.Id);
}
/// <summary>
/// Verify that GetService returns null for AgentVersion when agent is created from AgentReference.
/// </summary>
[Fact]
public void GetService_WithAgentReference_ReturnsNullForAgentVersion()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var agentReference = new AgentReference("test-name", "1");
// Act
var agent = client.AsAIAgent(agentReference);
var retrievedVersion = agent.GetService<AgentVersion>();
// Assert
Assert.Null(retrievedVersion);
}
#endregion
#region ChatClientMetadata Tests
/// <summary>
/// Verify that ChatClientMetadata is properly populated for agents created from AgentRecord.
/// </summary>
[Fact]
public void ChatClientMetadata_WithAgentRecord_IsPopulatedCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
var metadata = agent.GetService<ChatClientMetadata>();
// Assert
Assert.NotNull(metadata);
Assert.NotNull(metadata.DefaultModelId);
}
/// <summary>
/// Verify that ChatClientMetadata.DefaultModelId is set from PromptAgentDefinition model property.
/// </summary>
[Fact]
public void ChatClientMetadata_WithPromptAgentDefinition_SetsDefaultModelIdFromModel()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var definition = new PromptAgentDefinition("gpt-4-turbo")
{
Instructions = "Test instructions"
};
AgentRecord agentRecord = this.CreateTestAgentRecord(definition);
// Act
var agent = client.AsAIAgent(agentRecord);
var metadata = agent.GetService<ChatClientMetadata>();
// Assert
Assert.NotNull(metadata);
// The metadata should contain the model information from the agent definition
Assert.NotNull(metadata.DefaultModelId);
Assert.Equal("gpt-4-turbo", metadata.DefaultModelId);
}
/// <summary>
/// Verify that ChatClientMetadata is properly populated for agents created from AgentVersion.
/// </summary>
[Fact]
public void ChatClientMetadata_WithAgentVersion_IsPopulatedCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
var metadata = agent.GetService<ChatClientMetadata>();
// Assert
Assert.NotNull(metadata);
Assert.NotNull(metadata.DefaultModelId);
Assert.Equal((agentVersion.Definition as PromptAgentDefinition)!.Model, metadata.DefaultModelId);
}
#endregion
#region AgentReference Availability Tests
/// <summary>
/// Verify that GetService returns AgentReference for agents created from AgentReference.
/// </summary>
[Fact]
public void GetService_WithAgentReference_ReturnsAgentReference()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var agentReference = new AgentReference("test-agent", "1.0");
// Act
var agent = client.AsAIAgent(agentReference);
var retrievedReference = agent.GetService<AgentReference>();
// Assert
Assert.NotNull(retrievedReference);
Assert.Equal("test-agent", retrievedReference.Name);
Assert.Equal("1.0", retrievedReference.Version);
}
/// <summary>
/// Verify that GetService returns null for AgentReference when agent is created from AgentRecord.
/// </summary>
[Fact]
public void GetService_WithAgentRecord_ReturnsAlsoAgentReference()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
var retrievedReference = agent.GetService<AgentReference>();
// Assert
Assert.NotNull(retrievedReference);
Assert.Equal(agentRecord.Name, retrievedReference.Name);
}
/// <summary>
/// Verify that GetService returns null for AgentReference when agent is created from AgentVersion.
/// </summary>
[Fact]
public void GetService_WithAgentVersion_ReturnsAlsoAgentReference()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
var retrievedReference = agent.GetService<AgentReference>();
// Assert
Assert.NotNull(retrievedReference);
Assert.Equal(agentVersion.Name, retrievedReference.Name);
}
/// <summary>
/// Verify that GetService returns AgentReference with correct version information.
/// </summary>
[Fact]
public void GetService_WithAgentReference_ReturnsCorrectVersionInformation()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var agentReference = new AgentReference("versioned-agent", "3.5");
// Act
var agent = client.AsAIAgent(agentReference);
var retrievedReference = agent.GetService<AgentReference>();
// Assert
Assert.NotNull(retrievedReference);
Assert.Equal("versioned-agent", retrievedReference.Name);
Assert.Equal("3.5", retrievedReference.Version);
}
#endregion
#region GetAIAgentAsync - Empty Name Tests
/// <summary>
/// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when name is null.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithOptions_WithNullName_ThrowsArgumentExceptionAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions { Name = null };
// Act & Assert
ArgumentException exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.GetAIAgentAsync(options));
Assert.Equal("options", exception.ParamName);
Assert.Contains("Agent name must be provided", exception.Message);
}
/// <summary>
/// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when name is empty.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithOptions_WithEmptyName_ThrowsArgumentExceptionAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions { Name = string.Empty };
// Act & Assert
ArgumentException exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.GetAIAgentAsync(options));
Assert.Equal("options", exception.ParamName);
Assert.Contains("Agent name must be provided", exception.Message);
}
/// <summary>
/// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when name is whitespace.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithOptions_WithWhitespaceName_ThrowsArgumentExceptionAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions { Name = " " };
// Act & Assert
ArgumentException exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.GetAIAgentAsync(options));
Assert.Equal("options", exception.ParamName);
Assert.Contains("Agent name must be provided", exception.Message);
}
#endregion
#region CreateAIAgentAsync - Empty Name Tests
/// <summary>
/// Verify that CreateAIAgentAsync with model and options throws ArgumentException when name is null.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithModelAndOptions_WithNullName_ThrowsArgumentExceptionAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = null,
ChatOptions = new ChatOptions { Instructions = "Test" }
};
// Act & Assert
ArgumentException exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.CreateAIAgentAsync("test-model", options));
Assert.Equal("options", exception.ParamName);
Assert.Contains("Agent name must be provided", exception.Message);
}
/// <summary>
/// Verify that CreateAIAgentAsync with model and options throws ArgumentException when name is empty.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithModelAndOptions_WithEmptyName_ThrowsArgumentExceptionAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = string.Empty,
ChatOptions = new ChatOptions { Instructions = "Test" }
};
// Act & Assert
ArgumentException exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.CreateAIAgentAsync("test-model", options));
Assert.Equal("options", exception.ParamName);
Assert.Contains("Agent name must be provided", exception.Message);
}
/// <summary>
/// Verify that CreateAIAgentAsync with model and options throws ArgumentException when name is whitespace.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithModelAndOptions_WithWhitespaceName_ThrowsArgumentExceptionAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = " ",
ChatOptions = new ChatOptions { Instructions = "Test" }
};
// Act & Assert
ArgumentException exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.CreateAIAgentAsync("test-model", options));
Assert.Equal("options", exception.ParamName);
Assert.Contains("Agent name must be provided", exception.Message);
}
#endregion
#region CreateAIAgentAsync - Response Format Tests
/// <summary>
/// Verify that CreateAIAgentAsync with ChatResponseFormatText response format creates agent successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithTextResponseFormat_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
ResponseFormat = ChatResponseFormat.Text
}
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgentAsync with ChatResponseFormatJson response format without schema creates agent successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithoutSchema_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
ResponseFormat = ChatResponseFormat.Json
}
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgentAsync with ChatResponseFormatJson with schema creates agent successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchema_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema));
var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
ResponseFormat = jsonFormat
}
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgentAsync with ChatResponseFormatJson with schema and strict mode creates agent successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchemaAndStrictMode_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema));
var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema");
var additionalProps = new AdditionalPropertiesDictionary
{
["strictJsonSchema"] = true
};
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
ResponseFormat = jsonFormat,
AdditionalProperties = additionalProps
}
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgentAsync with ChatResponseFormatJson with schema and strict mode false creates agent successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchemaAndStrictModeFalse_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
JsonElement schemaElement = AIJsonUtilities.CreateJsonSchema(typeof(TestSchema));
var jsonFormat = ChatResponseFormat.ForJsonSchema(schemaElement, "test_schema", "A test schema");
var additionalProps = new AdditionalPropertiesDictionary
{
["strictJsonSchema"] = false
};
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
ResponseFormat = jsonFormat,
AdditionalProperties = additionalProps
}
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
#endregion
#region CreateAIAgentAsync - RawRepresentationFactory Tests
/// <summary>
/// Verify that CreateAIAgentAsync with RawRepresentationFactory that returns CreateResponseOptions creates agent successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithRawRepresentationFactory_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
RawRepresentationFactory = _ => new CreateResponseOptions()
}
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgentAsync with RawRepresentationFactory that returns null does not fail.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithRawRepresentationFactoryReturningNull_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
RawRepresentationFactory = _ => null
}
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgentAsync with RawRepresentationFactory that returns non-CreateResponseOptions does not fail.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithRawRepresentationFactoryReturningNonCreateResponseOptions_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
RawRepresentationFactory = _ => new object()
}
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
#endregion
#region CreateAIAgentAsync - Description Tests
/// <summary>
/// Verify that CreateAIAgentAsync with description sets description on the agent.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithDescription_SetsDescriptionAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler(description: "Test description");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
Description = "Test description",
ChatOptions = new ChatOptions { Instructions = "Test" }
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test description", agent.Description);
}
/// <summary>
/// Verify that CreateAIAgentAsync without description still creates agent successfully.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithoutDescription_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" }
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
}
#endregion
#region CreateChatClientAgentOptions - Missing Tools Tests
/// <summary>
/// Verify that when invocable tools are required but not provided, an exception is thrown.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithToolsRequiredButNotProvided_ThrowsArgumentExceptionAsync()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false));
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" }
};
// Act & Assert
ArgumentException exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.GetAIAgentAsync(options));
Assert.Contains("in-process tools must be provided", exception.Message);
}
/// <summary>
/// Verify that when specific invocable tools are required but wrong ones are provided, InvalidOperationException is thrown.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithWrongToolsProvided_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false));
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "test", "wrong_function", "Wrong function")
};
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
Tools = tools
}
};
// Act & Assert
InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAIAgentAsync(options));
Assert.Contains("required_function", exception.Message);
Assert.Contains("were not provided", exception.Message);
}
/// <summary>
/// Verify that when tools are provided that match the definition, agent is created successfully.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithMatchingToolsProvided_CreatesAgentSuccessfullyAsync()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false));
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "test", "required_function", "Required function")
};
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
Tools = tools
}
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
#endregion
#region CreateChatClientAgentOptions - Options Preservation Tests
/// <summary>
/// Verify that CreateChatClientAgentOptions preserves AIContextProviders.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithAIContextProviders_PreservesProviderAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" },
AIContextProviders = [new TestAIContextProvider()]
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
}
/// <summary>
/// Verify that CreateChatClientAgentOptions preserves ChatHistoryProvider.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithChatHistoryProvider_PreservesProviderAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" },
ChatHistoryProvider = new TestChatHistoryProvider()
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
}
/// <summary>
/// Verify that CreateChatClientAgentOptions preserves UseProvidedChatClientAsIs.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_PreservesSettingAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" },
UseProvidedChatClientAsIs = true
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
}
/// <summary>
/// Verify that GetAIAgentAsync with UseProvidedChatClientAsIs=true skips tool validation
/// and does not throw even when server-side function tools exist without matching invocable tools.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_SkipsToolValidationAsync()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("required_function", BinaryData.FromString("{}"), strictModeEnabled: false));
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" },
UseProvidedChatClientAsIs = true
};
// Act - should not throw even without tools when UseProvidedChatClientAsIs is true
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
}
/// <summary>
/// Verify that GetAIAgentAsync with UseProvidedChatClientAsIs=true still matches provided AIFunction tools
/// to server-side function definitions, instead of falling back to the ResponseToolAITool wrapper.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_PreservesProvidedToolsAsync()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("my_function", BinaryData.FromString("{}"), strictModeEnabled: false));
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
var providedTool = AIFunctionFactory.Create(() => "test", "my_function", "A test function");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
UseProvidedChatClientAsIs = true,
ChatOptions = new ChatOptions
{
Instructions = "Test",
Tools = [providedTool]
},
};
// Act - UseProvidedChatClientAsIs is true, but provided AIFunctions should still be matched and preserved
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
// Verify the provided AIFunction was matched and preserved in ChatOptions.Tools (not replaced by AsAITool wrapper)
var chatOptions = agent.GetService<ChatOptions>();
Assert.NotNull(chatOptions);
Assert.NotNull(chatOptions!.Tools);
Assert.Contains(chatOptions.Tools, t => t is AIFunction af && af.Name == "my_function");
}
#endregion
#region Empty Version and ID Handling Tests
/// <summary>
/// Verify that GetAIAgentAsync handles an agent with empty version by using "latest" as fallback.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithEmptyVersion_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" }
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
// Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
/// <summary>
/// Verify that AsAIAgent with AgentRecord handles empty version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecordEmptyVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
AgentRecord agentRecord = this.CreateTestAgentRecordWithEmptyVersion();
// Act
var agent = client.AsAIAgent(agentRecord);
// Assert
Assert.NotNull(agent);
// Verify the agent ID is generated from agent record name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
/// <summary>
/// Verify that AsAIAgent with AgentVersion handles empty version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersionEmptyVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
AgentVersion agentVersion = this.CreateTestAgentVersionWithEmptyVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
// Verify the agent ID is generated from agent version name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
/// <summary>
/// Verify that GetAIAgentAsync handles an agent with whitespace-only version by using "latest" as fallback.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithWhitespaceVersion_CreatesAgentSuccessfullyAsync()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions { Instructions = "Test" }
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
// Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
/// <summary>
/// Verify that AsAIAgent with AgentRecord handles whitespace-only version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentRecordWhitespaceVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
AgentRecord agentRecord = this.CreateTestAgentRecordWithWhitespaceVersion();
// Act
var agent = client.AsAIAgent(agentRecord);
// Assert
Assert.NotNull(agent);
// Verify the agent ID is generated from agent record name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
/// <summary>
/// Verify that AsAIAgent with AgentVersion handles whitespace-only version by using "latest" as fallback.
/// </summary>
[Fact]
public void AsAIAgent_WithAgentVersionWhitespaceVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
AgentVersion agentVersion = this.CreateTestAgentVersionWithWhitespaceVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
// Verify the agent ID is generated from agent version name ("agent_abc123") and "latest"
Assert.Equal("agent_abc123:latest", agent.Id);
}
#endregion
#region ApplyToolsToAgentDefinition Tests
/// <summary>
/// Verify that CreateAIAgentAsync with non-PromptAgentDefinition and tools throws ArgumentException.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithNonPromptAgentDefinitionAndTools_ThrowsArgumentExceptionAsync()
{
// Arrange
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "test", "test_function", "A test function")
};
using HttpHandlerAssert httpHandler = new(_ => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json")
});
#pragma warning disable CA5399
using HttpClient httpClient = new(httpHandler);
#pragma warning restore CA5399
AIProjectClient client = new(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
// Create a mock AgentDefinition that is not PromptAgentDefinition
// Since we can't easily create a non-PromptAgentDefinition in the public API, we test this path via the CreateAIAgentAsync that builds a PromptAgentDefinition
// The ApplyToolsToAgentDefinition is only called when tools.Count > 0, and we provide tools
// But PromptAgentDefinition is always created by CreateAIAgentAsync(name, model, instructions, tools)
// So this path is hard to hit without mocking. Let's test the declarative function rejection instead.
var declarativeFunction = AIFunctionFactory.CreateDeclaration("test_function", "A test function", JsonDocument.Parse("{}").RootElement);
// Act & Assert
InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.CreateAIAgentAsync(
name: "test-agent",
model: "test-model",
instructions: "Test",
tools: [declarativeFunction]));
Assert.Contains("invokable AIFunctions", exception.Message);
}
/// <summary>
/// Verify that CreateAIAgentAsync with AIFunctionDeclaration tools throws InvalidOperationException.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithAIFunctionDeclarationTool_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
using var doc = JsonDocument.Parse("{}");
var declarativeFunction = AIFunctionFactory.CreateDeclaration("test_function", "A test function", doc.RootElement);
using HttpHandlerAssert httpHandler = new(_ => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(), Encoding.UTF8, "application/json")
});
#pragma warning disable CA5399
using HttpClient httpClient = new(httpHandler);
#pragma warning restore CA5399
AIProjectClient client = new(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
// Act & Assert
InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.CreateAIAgentAsync(
name: "test-agent",
model: "test-model",
instructions: "Test",
tools: [declarativeFunction]));
Assert.Contains("invokable AIFunctions", exception.Message);
}
/// <summary>
/// Verify that CreateAIAgentAsync with ResponseTool converted via AsAITool works.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithResponseToolAsAITool_CreatesAgentSuccessfullyAsync()
{
// Arrange
ResponseTool responseTool = ResponseTool.CreateFunctionTool("response_tool", BinaryData.FromString("{}"), strictModeEnabled: false);
AITool convertedTool = responseTool.AsAITool();
// Create a definition with the function tool already in it
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(responseTool);
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
// Matching invokable tool must be provided
var invokableTool = AIFunctionFactory.Create(() => "test", "response_tool", "Invokable version of the tool");
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
Tools = [invokableTool]
}
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that CreateAIAgentAsync with hosted tool types works correctly.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithHostedToolTypes_CreatesAgentSuccessfullyAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var webSearchTool = new HostedWebSearchTool();
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
Tools = [webSearchTool]
}
};
// Act
ChatClientAgent agent = await testClient.Client.CreateAIAgentAsync("test-model", options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that when the server returns tools but matching tools are provided, the agent is created.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithServerDefinedToolsAndMatchingProvidedTools_CreatesAgentAsync()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
// Add multiple function tools
definition.Tools.Add(ResponseTool.CreateFunctionTool("tool_one", BinaryData.FromString("{}"), strictModeEnabled: false));
definition.Tools.Add(ResponseTool.CreateFunctionTool("tool_two", BinaryData.FromString("{}"), strictModeEnabled: false));
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "one", "tool_one", "Tool one"),
AIFunctionFactory.Create(() => "two", "tool_two", "Tool two")
};
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
Tools = tools
}
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that when the server returns mixed tools (function and hosted), the agent handles them correctly.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithMixedServerTools_MatchesFunctionToolsOnlyAsync()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
// Add a function tool
definition.Tools.Add(ResponseTool.CreateFunctionTool("function_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
// Add a hosted tool
definition.Tools.Add(new HostedWebSearchTool().GetService<ResponseTool>() ?? new HostedWebSearchTool().AsOpenAIResponseTool());
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
var tools = new List<AITool>
{
AIFunctionFactory.Create(() => "result", "function_tool", "The function tool")
};
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
Tools = tools
}
};
// Act
ChatClientAgent agent = await client.GetAIAgentAsync(options);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
/// <summary>
/// Verify that when partial tools are provided (some missing), InvalidOperationException is thrown listing missing tools.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithPartialToolsProvided_ThrowsInvalidOperationWithMissingToolNamesAsync()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(ResponseTool.CreateFunctionTool("provided_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
definition.Tools.Add(ResponseTool.CreateFunctionTool("missing_tool", BinaryData.FromString("{}"), strictModeEnabled: false));
AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition);
var tools = new List<AITool>
{
// Only providing one of two required tools
AIFunctionFactory.Create(() => "result", "provided_tool", "The provided tool")
};
var options = new ChatClientAgentOptions
{
Name = "test-agent",
ChatOptions = new ChatOptions
{
Instructions = "Test",
Tools = tools
}
};
// Act & Assert
InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAIAgentAsync(options));
Assert.Contains("missing_tool", exception.Message);
Assert.DoesNotContain("provided_tool", exception.Message);
}
/// <summary>
/// Verify that when AsAIAgent is called without requireInvocableTools, hosted tools are correctly added.
/// </summary>
[Fact]
public void AsAIAgent_WithServerHostedTools_AddsToolsToAgentOptions()
{
// Arrange
PromptAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(new HostedWebSearchTool().GetService<ResponseTool>() ?? new HostedWebSearchTool().AsOpenAIResponseTool());
AIProjectClient client = this.CreateTestAgentClient();
AgentVersion agentVersion = ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson(agentDefinition: definition)))!;
// Act - no tools provided, but requireInvocableTools is false when no tools param is passed
ChatClientAgent agent = client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
Assert.IsType<ChatClientAgent>(agent);
}
#endregion
#region Helper Methods
/// <summary>
/// Creates a test AIProjectClient with fake behavior.
/// </summary>
private FakeAgentClient CreateTestAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse);
}
/// <summary>
/// Creates a test AIProjectClient backed by an HTTP handler that returns canned responses.
/// Used for tests that exercise the protocol-method code path (CreateAgentVersion).
/// The returned client must be disposed to clean up the underlying HttpClient/handler.
/// </summary>
private static DisposableTestClient CreateTestAgentClientWithHandler(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
{
var responseJson = TestDataUtil.GetAgentVersionResponseJson(agentName, agentDefinitionResponse, instructions, description);
var httpHandler = new HttpHandlerAssert(_ =>
new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(responseJson, Encoding.UTF8, "application/json") });
#pragma warning disable CA5399
var httpClient = new HttpClient(httpHandler);
#pragma warning restore CA5399
var client = new AIProjectClient(
new Uri("https://test.openai.azure.com/"),
new FakeAuthenticationTokenProvider(),
new() { Transport = new HttpClientPipelineTransport(httpClient) });
return new DisposableTestClient(client, httpClient, httpHandler);
}
/// <summary>
/// Wraps an AIProjectClient and its disposable dependencies for deterministic cleanup.
/// </summary>
private sealed class DisposableTestClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly HttpHandlerAssert _httpHandler;
public DisposableTestClient(AIProjectClient client, HttpClient httpClient, HttpHandlerAssert httpHandler)
{
this.Client = client;
this._httpClient = httpClient;
this._httpHandler = httpHandler;
}
public AIProjectClient Client { get; }
public void Dispose()
{
this._httpClient.Dispose();
this._httpHandler.Dispose();
}
}
/// <summary>
/// Creates a test AgentRecord for testing.
/// </summary>
private AgentRecord CreateTestAgentRecord(AgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJson(agentDefinition: agentDefinition)))!;
}
/// <summary>
/// Creates a test AIProjectClient with empty version fields for testing hosted MCP agents.
/// </summary>
private FakeAgentClient CreateTestAgentClientWithEmptyVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion: true);
}
/// <summary>
/// Creates a test AgentRecord with empty version for testing hosted MCP agents.
/// </summary>
private AgentRecord CreateTestAgentRecordWithEmptyVersion(AgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithEmptyVersion(agentDefinition: agentDefinition)))!;
}
/// <summary>
/// Creates a test AgentVersion with empty version for testing hosted MCP agents.
/// </summary>
private AgentVersion CreateTestAgentVersionWithEmptyVersion()
{
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion()))!;
}
/// <summary>
/// Creates a test AIProjectClient with whitespace-only version fields for testing hosted MCP agents.
/// </summary>
private FakeAgentClient CreateTestAgentClientWithWhitespaceVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, versionMode: VersionMode.Whitespace);
}
/// <summary>
/// Creates a test AgentRecord with whitespace-only version for testing hosted MCP agents.
/// </summary>
private AgentRecord CreateTestAgentRecordWithWhitespaceVersion(AgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(agentDefinition: agentDefinition)))!;
}
/// <summary>
/// Creates a test AgentVersion with whitespace-only version for testing hosted MCP agents.
/// </summary>
private AgentVersion CreateTestAgentVersionWithWhitespaceVersion()
{
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion()))!;
}
private const string OpenAPISpec = """
{
"openapi": "3.0.3",
"info": { "title": "Tiny Test API", "version": "1.0.0" },
"paths": {
"/ping": {
"get": {
"summary": "Health check",
"operationId": "getPing",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": { "message": { "type": "string" } },
"required": ["message"]
},
"example": { "message": "pong" }
}
}
}
}
}
}
}
}
""";
/// <summary>
/// Creates a test AgentVersion for testing.
/// </summary>
private AgentVersion CreateTestAgentVersion()
{
return ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!;
}
/// <summary>
/// Specifies the version mode for test data generation.
/// </summary>
private enum VersionMode
{
Normal,
Empty,
Whitespace
}
/// <summary>
/// Fake AIProjectClient for testing.
/// </summary>
private sealed class FakeAgentClient : AIProjectClient
{
public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false, VersionMode versionMode = VersionMode.Normal)
{
// Handle backward compatibility with bool parameter
var effectiveVersionMode = useEmptyVersion ? VersionMode.Empty : versionMode;
this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse, effectiveVersionMode);
}
public override ClientConnection GetConnection(string connectionId)
{
return new ClientConnection("fake-connection-id", "http://localhost", ClientPipeline.Create(), CredentialKind.None);
}
public override AIProjectAgentsOperations Agents { get; }
private sealed class FakeAIProjectAgentsOperations : AIProjectAgentsOperations
{
private readonly string? _agentName;
private readonly string? _instructions;
private readonly string? _description;
private readonly AgentDefinition? _agentDefinition;
private readonly VersionMode _versionMode;
public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, VersionMode versionMode = VersionMode.Normal)
{
this._agentName = agentName;
this._instructions = instructions;
this._description = description;
this._agentDefinition = agentDefinitionResponse;
this._versionMode = versionMode;
}
private string GetAgentResponseJson()
{
return this._versionMode switch
{
VersionMode.Empty => TestDataUtil.GetAgentResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
VersionMode.Whitespace => TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
_ => TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description)
};
}
private string GetAgentVersionResponseJson()
{
return this._versionMode switch
{
VersionMode.Empty => TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
VersionMode.Whitespace => TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description),
_ => TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description)
};
}
public override ClientResult GetAgent(string agentName, RequestOptions options)
{
var responseJson = this.GetAgentResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
}
public override ClientResult<AgentRecord> GetAgent(string agentName, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task<ClientResult> GetAgentAsync(string agentName, RequestOptions options)
{
var responseJson = this.GetAgentResponseJson();
return Task.FromResult<ClientResult>(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
}
public override Task<ClientResult<AgentRecord>> GetAgentAsync(string agentName, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentRecord>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
public override ClientResult<AgentVersion> CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentVersionResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task<ClientResult<AgentVersion>> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentVersionResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read<AgentVersion>(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
}
}
private static PromptAgentDefinition GeneratePromptDefinitionResponse(PromptAgentDefinition inputDefinition, List<AITool>? tools)
{
var definitionResponse = new PromptAgentDefinition(inputDefinition.Model) { Instructions = inputDefinition.Instructions };
if (tools is not null)
{
foreach (var tool in tools)
{
definitionResponse.Tools.Add(tool.GetService<ResponseTool>() ?? tool.AsOpenAIResponseTool());
}
}
return definitionResponse;
}
/// <summary>
/// Test custom chat client that can be used to verify clientFactory functionality.
/// </summary>
private sealed class TestChatClient : DelegatingChatClient
{
public TestChatClient(IChatClient innerClient) : base(innerClient)
{
}
}
/// <summary>
/// Mock pipeline response for testing ClientResult wrapping.
/// </summary>
private sealed class MockPipelineResponse : PipelineResponse
{
private readonly int _status;
private readonly MockPipelineResponseHeaders _headers;
public MockPipelineResponse(int status, BinaryData? content = null)
{
this._status = status;
this.Content = content ?? BinaryData.Empty;
this._headers = new MockPipelineResponseHeaders();
}
public override int Status => this._status;
public override string ReasonPhrase => "OK";
public override Stream? ContentStream
{
get => null;
set { }
}
public override BinaryData Content { get; }
protected override PipelineResponseHeaders HeadersCore => this._headers;
public override BinaryData BufferContent(CancellationToken cancellationToken = default) =>
throw new NotSupportedException("Buffering content is not supported for mock responses.");
public override ValueTask<BinaryData> BufferContentAsync(CancellationToken cancellationToken = default) =>
throw new NotSupportedException("Buffering content asynchronously is not supported for mock responses.");
public override void Dispose()
{
}
private sealed class MockPipelineResponseHeaders : PipelineResponseHeaders
{
private readonly Dictionary<string, string> _headers = new(StringComparer.OrdinalIgnoreCase)
{
{ "Content-Type", "application/json" },
{ "x-ms-request-id", "test-request-id" }
};
public override bool TryGetValue(string name, out string? value)
{
return this._headers.TryGetValue(name, out value);
}
public override bool TryGetValues(string name, out IEnumerable<string>? values)
{
if (this._headers.TryGetValue(name, out var value))
{
values = [value];
return true;
}
values = null;
return false;
}
public override IEnumerator<KeyValuePair<string, string>> GetEnumerator()
{
return this._headers.GetEnumerator();
}
}
}
#endregion
/// <summary>
/// Helper method to access internal ChatOptions property via reflection.
/// </summary>
private static ChatOptions? GetAgentChatOptions(ChatClientAgent agent)
{
if (agent is null)
{
return null;
}
var chatOptionsProperty = typeof(ChatClientAgent).GetProperty(
"ChatOptions",
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
return chatOptionsProperty?.GetValue(agent) as ChatOptions;
}
/// <summary>
/// Test schema for JSON response format tests.
/// </summary>
#pragma warning disable CA1812 // Avoid uninstantiated internal classes - used via reflection by AIJsonUtilities
private sealed class TestSchema
{
public string? Name { get; set; }
public int Value { get; set; }
}
#pragma warning restore CA1812
/// <summary>
/// Test AIContextProvider for options preservation tests.
/// </summary>
private sealed class TestAIContextProvider : AIContextProvider
{
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
return new ValueTask<AIContext>(context.AIContext);
}
}
/// <summary>
/// Test ChatHistoryProvider for options preservation tests.
/// </summary>
private sealed class TestChatHistoryProvider : ChatHistoryProvider
{
protected override ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
return new ValueTask<IEnumerable<ChatMessage>>(context.RequestMessages);
}
protected override ValueTask InvokedCoreAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
return default;
}
}
}
/// <summary>
/// Provides test data for invalid agent name validation tests.
/// </summary>
internal static class InvalidAgentNameTestData
{
/// <summary>
/// Gets a collection of invalid agent names for theory-based testing.
/// </summary>
/// <returns>Collection of invalid agent name test cases.</returns>
public static IEnumerable<object[]> GetInvalidAgentNames()
{
yield return new object[] { "-agent" };
yield return new object[] { "agent-" };
yield return new object[] { "agent_name" };
yield return new object[] { "agent name" };
yield return new object[] { "agent@name" };
yield return new object[] { "agent#name" };
yield return new object[] { "agent$name" };
yield return new object[] { "agent%name" };
yield return new object[] { "agent&name" };
yield return new object[] { "agent*name" };
yield return new object[] { "agent.name" };
yield return new object[] { "agent/name" };
yield return new object[] { "agent\\name" };
yield return new object[] { "agent:name" };
yield return new object[] { "agent;name" };
yield return new object[] { "agent,name" };
yield return new object[] { "agent<name" };
yield return new object[] { "agent>name" };
yield return new object[] { "agent?name" };
yield return new object[] { "agent!name" };
yield return new object[] { "agent~name" };
yield return new object[] { "agent`name" };
yield return new object[] { "agent^name" };
yield return new object[] { "agent|name" };
yield return new object[] { "agent[name" };
yield return new object[] { "agent]name" };
yield return new object[] { "agent{name" };
yield return new object[] { "agent}name" };
yield return new object[] { "agent(name" };
yield return new object[] { "agent)name" };
yield return new object[] { "agent+name" };
yield return new object[] { "agent=name" };
yield return new object[] { "a" + new string('b', 63) };
}
}