Files
agent-framework/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs
Copilot 88ea9d08c7 .NET: Update to OpenAI 2.9.1, Azure.AI.OpenAI 2.9.0-beta.1, Microsoft.Extensions.AI 10.4.0, and Azure.AI.Projects 2.0.0-beta.2 (#4613)
* Initial plan

* Update code for Microsoft.Extensions.AI.Abstractions 10.4.0 breaking changes

- Rename FunctionApprovalRequestContent → ToolApprovalRequestContent
- Rename FunctionApprovalResponseContent → ToolApprovalResponseContent
- Rename UserInputRequestContent → ToolApprovalRequestContent
- Rename UserInputResponseContent → ToolApprovalResponseContent
- Update .FunctionCall property → .ToolCall with FunctionCallContent casts where needed
- Update .Id property → .RequestId on the renamed types
- Rename FunctionApprovalRequestEventGenerator → ToolApprovalRequestEventGenerator
- Rename FunctionApprovalResponseEventGenerator → ToolApprovalResponseEventGenerator

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

* Update OpenAI 2.9.1, ME.AI 10.4.0, fix breaking API changes

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix remaining ME.AI 10.4.0 breaking changes: MCP approval types, .Output→.Outputs

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Use pattern matching with `when` for ToolApprovalRequestContent/FunctionCallContent

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Update Azure.AI.OpenAI to 2.9.0-beta.1

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Fix remaining GetResponsesClient(model) build failures for Azure.AI.OpenAI 2.9.0-beta.1

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Address review feedback: remove redundant type checks in TestRequestAgent.cs and fix error message in AIAgentHostExecutor.cs

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>

* Update Azure.AI.Projects to 2.0.0-beta.2 with namespace migration

- Azure.AI.Projects 2.0.0-beta.1 → 2.0.0-beta.2
- Azure.AI.Projects.OpenAI → Azure.AI.Extensions.OpenAI (transitive)
- Agent types moved to Azure.AI.Projects.Agents namespace
- AgentRecord.Versions.Latest → AgentRecord.GetLatestVersion()
- OpenAPIFunctionDefinition → OpenApiFunctionDefinition
- BingCustomSearchToolParameters → BingCustomSearchToolOptions
- MemorySearchPreviewTool.UpdateDelay → UpdateDelayInSecs
- Azure.Identity 1.17.1 → 1.19.0
- Microsoft.Identity.Client.Extensions.Msal 4.78.0 → 4.83.1

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

* Fix remaining type renames for Azure.AI.Projects 2.0.0-beta.2

- BrowserAutomationToolParameters → BrowserAutomationToolOptions
- MemoryUpdateOptions.UpdateDelay stays as UpdateDelay (not renamed)
- WaitForMemoriesUpdateAsync parameter order: pollingInterval before options
- AIProjectAgentsOperations → AgentsClient

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

* Fix format errors and OpenTelemetry test for ME.AI 10.4.0

- Remove unused 'using Azure.AI.Extensions.OpenAI' and fix import ordering
  in Agent_With_AzureAIProject/Program.cs
- Update OpenTelemetryAgentTests: gen_ai.tool.definitions is now always
  emitted regardless of EnableSensitiveData per ME.AI 10.4.0 change
  (dotnet/extensions#7346). Tool definitions are not considered sensitive.

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

* Fix GetRepoFolder() to work in git worktrees

Use 'workflow-samples' directory as repo root marker instead of '.git',
which fails in worktrees (.git is a file) and also matches too early
when a '.github' folder exists in subdirectories.

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

* Fix formatting: remove unused usings and fix import ordering

dotnet format applied across 59 impacted projects. Primarily removes
unnecessary 'using Azure.AI.Projects' where Azure.AI.Projects.Agents
provides all needed types, and fixes import ordering per editorconfig.

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

* Disable AzureAIAgentsPersistent integration tests (#4769)

Azure.AI.Agents.Persistent 1.2.0-beta.9 references McpServerToolApprovalResponseContent
which was removed in ME.AI 10.4.0 (renamed to ToolApprovalResponseContent), causing
TypeLoadException at runtime. Mark all 6 test classes with IntegrationDisabled trait
until Persistent ships a version targeting ME.AI 10.4.0+.

Upstream fix: https://github.com/Azure/azure-sdk-for-net/pull/56929

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

* Add README with compatibility note for AzureAI.Persistent (#4769)

Documents that Azure.AI.Agents.Persistent 1.2.0-beta.9 is only compatible
with ME.AI ≤10.3.0 and OpenAI ≤2.8.0 due to type renames in ME.AI 10.4.0.

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

* Fix file encoding: restore UTF-8 BOM on Persistent test files

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

* Mark AzureAI.Persistent as IsPackable=false (#4769)

Prevent shipping until Azure.AI.Agents.Persistent targets ME.AI 10.4.0+.

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

* Moving IsPackable after import

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com>
2026-03-20 14:29:29 +00:00

3310 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.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
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<AgentsClient>();
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 BingCustomSearchToolOptions([new BingCustomSearchConfiguration("connection-id", "instance-name")])));
definition.Tools.Add((ResponseTool)AgentTool.CreateBrowserAutomationTool(new BrowserAutomationToolOptions(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 FakeAgentsClient(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 AgentsClient Agents { get; }
private sealed class FakeAgentsClient : AgentsClient
{
private readonly string? _agentName;
private readonly string? _instructions;
private readonly string? _description;
private readonly AgentDefinition? _agentDefinition;
private readonly VersionMode _versionMode;
public FakeAgentsClient(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) };
}
}