// 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.Foundry.UnitTests;
#pragma warning disable CS0618
///
/// Unit tests for the class.
///
public sealed class AIProjectClientExtensionsTests
{
#region AsAIAgent(AIProjectClient, model, instructions) Tests
///
/// Verify that the non-versioned AsAIAgent overload throws ArgumentNullException when AIProjectClient is null.
///
[Fact]
public void AsAIAgent_WithModelAndInstructions_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient? client = null;
// Act & Assert
ArgumentNullException exception = Assert.Throws(() =>
client!.AsAIAgent("gpt-4o-mini", "You are helpful."));
Assert.Equal("aiProjectClient", exception.ParamName);
}
///
/// Verify that the non-versioned AsAIAgent overload creates a valid ChatClientAgent.
///
[Fact]
public void AsAIAgent_Rapi_WithModelAndInstructions_CreatesChatClientAgent()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
List tools =
[
AIFunctionFactory.Create(() => "test", "test_function", "A test function")
];
// Act
ChatClientAgent agent = client.AsAIAgent(
"gpt-4o-mini",
"You are helpful.",
name: "test-agent",
description: "A test agent",
tools: tools);
// Assert
Assert.NotNull(agent);
Assert.Equal("test-agent", agent.Name);
Assert.Equal("A test agent", agent.Description);
Assert.NotNull(agent.GetService());
// After the FoundryChatClient consolidation the inner chat-client now exposes the
// AIProjectClient via GetService — Foundry callers can walk to the project client from
// the agent without holding their own reference. (Previously this path returned null
// because AsAIAgent(model, instructions) skipped the decorator entirely.)
Assert.NotNull(agent.GetService());
}
///
/// Verify that the non-versioned AsAIAgent overload applies the clientFactory.
///
[Fact]
public void AsAIAgent_WithModelAndInstructions_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
TestChatClient? testChatClient = null;
// Act
ChatClientAgent agent = client.AsAIAgent(
"gpt-4o-mini",
"You are helpful.",
clientFactory: innerClient => testChatClient = new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent);
TestChatClient? retrievedTestClient = agent.GetService();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
///
/// Verify that the options-based non-versioned AsAIAgent overload creates a valid ChatClientAgent.
///
[Fact]
public void AsAIAgent_Rapi_WithOptions_CreatesChatClientAgent()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ChatClientAgentOptions options = new()
{
Name = "options-agent",
Description = "Agent from options",
ChatOptions = new ChatOptions
{
ModelId = "gpt-4o-mini",
Instructions = "You are helpful.",
},
};
// Act
ChatClientAgent agent = client.AsAIAgent(options);
// Assert
Assert.NotNull(agent);
Assert.Equal("options-agent", agent.Name);
Assert.Equal("Agent from options", agent.Description);
// After the FoundryChatClient consolidation the inner chat-client now exposes the
// AIProjectClient via GetService — see twin assertion in
// AsAIAgent_Rapi_WithModelAndInstructions_CreatesChatClientAgent for the rationale.
Assert.NotNull(agent.GetService());
}
///
/// Verify that the non-versioned AsAIAgent overload adds the MEAI user-agent header to Responses API requests.
///
[Fact]
public async Task AsAIAgent_Rapi_WithModelAndInstructions_UserAgentHeaderAddedToResponsesRequestsAsync()
{
// Arrange
bool userAgentFound = false;
using HttpHandlerAssert httpHandler = new(request =>
{
if (request.Headers.TryGetValues("User-Agent", out IEnumerable? values))
{
foreach (string value in values)
{
if (value.Contains("MEAI"))
{
userAgentFound = true;
}
}
}
if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses"))
{
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(
TestDataUtil.GetOpenAIDefaultResponseJson(),
Encoding.UTF8,
"application/json")
};
}
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{}", Encoding.UTF8, "application/json")
};
});
#pragma warning disable CA5399
using HttpClient httpClient = new(httpHandler);
#pragma warning restore CA5399
AIProjectClient aiProjectClient = new(
new Uri("https://test.openai.azure.com/"),
new FakeAuthenticationTokenProvider(),
new() { Transport = new HttpClientPipelineTransport(httpClient) });
ChatClientAgent agent = aiProjectClient.AsAIAgent(
"gpt-4o-mini",
"You are helpful.");
// Act
AgentSession session = await agent.CreateSessionAsync();
await agent.RunAsync("Hello", session);
// Assert
Assert.True(userAgentFound, "MEAI user-agent header was not found in any request");
}
///
/// Verify that the non-versioned AsAIAgent overload now wraps with FoundryChatClient
/// (regression-prevention for the previously-untagged extension path).
///
[Fact]
public void AsAIAgent_Rapi_WithModelAndInstructions_ExposesFoundryChatClientAndProviderName()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
// Act
ChatClientAgent agent = client.AsAIAgent("gpt-4o-mini", "You are helpful.");
// Assert: FoundryChatClient is internal-sealed and reachable via GetService().
var chatClient = agent.GetService();
Assert.NotNull(chatClient);
// Provider tag is "microsoft.foundry" (previously this path had no Foundry tag at all).
var metadata = chatClient!.GetService();
Assert.NotNull(metadata);
Assert.Equal("microsoft.foundry", metadata!.ProviderName);
Assert.Equal("gpt-4o-mini", metadata.DefaultModelId);
// Reaching the FoundryChatClient by type (via InternalsVisibleTo).
Assert.NotNull(agent.GetService());
}
///
/// Verify that the options-based non-versioned AsAIAgent overload now wraps with FoundryChatClient.
///
[Fact]
public void AsAIAgent_Rapi_WithOptions_ExposesFoundryChatClientAndProviderName()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ChatClientAgentOptions options = new()
{
Name = "options-agent",
ChatOptions = new ChatOptions { ModelId = "gpt-4o-mini", Instructions = "x" },
};
// Act
ChatClientAgent agent = client.AsAIAgent(options);
// Assert
var chatClient = agent.GetService();
Assert.NotNull(chatClient);
var metadata = chatClient!.GetService();
Assert.NotNull(metadata);
Assert.Equal("microsoft.foundry", metadata!.ProviderName);
Assert.NotNull(agent.GetService());
}
///
/// Verify that the non-versioned AsAIAgent overload stamps the
/// agent-framework-dotnet/{version} segment on outbound requests via the new
/// AgentFrameworkUserAgentPolicy registered by FoundryChatClient.
///
[Fact]
public async Task AsAIAgent_Rapi_WithModelAndInstructions_StampsAgentFrameworkUserAgentSegmentAsync()
{
bool afSeen = false;
using HttpHandlerAssert httpHandler = new(request =>
{
if (request.Headers.TryGetValues("User-Agent", out IEnumerable? values))
{
foreach (string value in values)
{
if (value.Contains("agent-framework-dotnet/"))
{
afSeen = true;
}
}
}
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json")
};
});
#pragma warning disable CA5399
using HttpClient httpClient = new(httpHandler);
#pragma warning restore CA5399
AIProjectClient aiProjectClient = new(
new Uri("https://test.openai.azure.com/"),
new FakeAuthenticationTokenProvider(),
new() { Transport = new HttpClientPipelineTransport(httpClient) });
ChatClientAgent agent = aiProjectClient.AsAIAgent("gpt-4o-mini", "You are helpful.");
// Act
AgentSession session = await agent.CreateSessionAsync();
await agent.RunAsync("Hello", session);
// Assert
Assert.True(afSeen, "Expected agent-framework-dotnet/{version} segment on outbound requests from AsAIAgent(model, instructions).");
}
#endregion
#region AsAIAgent(AIProjectClient, ProjectsAgentRecord) Tests
///
/// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null.
///
[Fact]
public void AsAIAgent_WithAgentRecord_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient? client = null;
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act & Assert
var exception = Assert.Throws(() =>
client!.AsAIAgent(agentRecord));
Assert.Equal("aiProjectClient", exception.ParamName);
}
///
/// Verify that AsAIAgent throws ArgumentNullException when agentRecord is null.
///
[Fact]
public void AsAIAgent_WithAgentRecord_WithNullAgentRecord_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock();
// Act & Assert
var exception = Assert.Throws(() =>
mockClient.Object.AsAIAgent((ProjectsAgentRecord)null!));
Assert.Equal("agentRecord", exception.ParamName);
}
///
/// Verify that AsAIAgent with ProjectsAgentRecord creates a valid agent.
///
[Fact]
public void AsAIAgent_WithAgentRecord_CreatesValidAgent()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
Assert.Equal("agent_abc123", agent.Name);
Assert.Same(client, agent.GetService());
}
///
/// Verify that AsAIAgent with ProjectsAgentRecord and clientFactory applies the factory.
///
[Fact]
public void AsAIAgent_WithAgentRecord_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentRecord 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();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
#endregion
#region AsAIAgent(AIProjectClient, ProjectsAgentVersion) Tests
///
/// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null.
///
[Fact]
public void AsAIAgent_WithAgentVersion_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient? client = null;
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act & Assert
var exception = Assert.Throws(() =>
client!.AsAIAgent(agentVersion));
Assert.Equal("aiProjectClient", exception.ParamName);
}
///
/// Verify that AsAIAgent throws ArgumentNullException when agentVersion is null.
///
[Fact]
public void AsAIAgent_WithAgentVersion_WithNullAgentVersion_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock();
// Act & Assert
var exception = Assert.Throws(() =>
mockClient.Object.AsAIAgent((ProjectsAgentVersion)null!));
Assert.Equal("agentVersion", exception.ParamName);
}
///
/// Verify that AsAIAgent with ProjectsAgentVersion creates a valid agent.
///
[Fact]
public void AsAIAgent_WithAgentVersion_CreatesValidAgent()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
Assert.Equal("agent_abc123", agent.Name);
Assert.Same(client, agent.GetService());
}
///
/// Verify that AsAIAgent with ProjectsAgentVersion and clientFactory applies the factory.
///
[Fact]
public void AsAIAgent_WithAgentVersion_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentVersion 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();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
///
/// Verify that AsAIAgent with requireInvocableTools=true enforces invocable tools.
///
[Fact]
public void AsAIAgent_WithAgentVersion_WithRequireInvocableToolsTrue_EnforcesInvocableTools()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
var tools = new List
{
AIFunctionFactory.Create(() => "test", "test_function", "A test function")
};
// Act
var agent = client.AsAIAgent(agentVersion, tools: tools);
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
}
///
/// Verify that AsAIAgent with requireInvocableTools=false allows declarative functions.
///
[Fact]
public void AsAIAgent_WithAgentVersion_WithRequireInvocableToolsFalse_AllowsDeclarativeFunctions()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentVersion 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(agent);
}
#endregion
#region AsAIAgent(AIProjectClient, string) Tests
///
/// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null.
///
[Fact]
public void AsAIAgent_ByName_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient? client = null;
// Act & Assert
var exception = Assert.Throws(() =>
client!.AsAIAgent("test-agent"));
Assert.Equal("aiProjectClient", exception.ParamName);
}
///
/// Verify that AsAIAgent throws ArgumentNullException when name is null.
///
[Fact]
public void AsAIAgent_ByName_WithNullName_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock();
// Act & Assert
var exception = Assert.Throws(() =>
mockClient.Object.AsAIAgent((string)null!));
Assert.Equal("name", exception.ParamName);
}
///
/// Verify that AsAIAgent throws ArgumentException when name is empty.
///
[Fact]
public void AsAIAgent_ByName_WithEmptyName_ThrowsArgumentException()
{
// Arrange
var mockClient = new Mock();
// Act & Assert
var exception = Assert.Throws(() =>
mockClient.Object.AsAIAgent(string.Empty));
Assert.Equal("name", exception.ParamName);
}
#endregion
#region AsAIAgent(AIProjectClient, ProjectsAgentRecord) with tools Tests
///
/// Verify that AsAIAgent with additional tools when the definition has no tools does not throw and results in an agent with no tools.
///
[Fact]
public void AsAIAgent_WithAgentRecordAndAdditionalTools_WhenDefinitionHasNoTools_ShouldNotThrow()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
var tools = new List
{
AIFunctionFactory.Create(() => "test", "test_function", "A test function")
};
// Act
var agent = client.AsAIAgent(agentRecord, tools: tools);
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
var chatClient = agent.GetService();
Assert.NotNull(chatClient);
var agentVersion = chatClient.GetService();
Assert.NotNull(agentVersion);
var definition = Assert.IsType(agentVersion.Definition);
Assert.Empty(definition.Tools);
}
///
/// Verify that AsAIAgent with null tools works correctly.
///
[Fact]
public void AsAIAgent_WithAgentRecordAndNullTools_WorksCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord, tools: null);
// Assert
Assert.NotNull(agent);
Assert.Equal("agent_abc123", agent.Name);
}
#endregion
#region Tool Validation Tests
///
/// Verify that when providing AITools with AsAIAgent, any additional tool that doesn't match the tools in agent definition are ignored.
///
[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 DeclarativeAgentDefinition 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();
Assert.NotNull(version);
var definition = Assert.IsType(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
///
/// Verify that tools passed as parameters are accepted by AsAIAgent.
///
[Fact]
public void AsAIAgent_WithParameterTools_AcceptsTools()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
var tools = new List
{
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(agent);
var chatClient = agent.GetService();
Assert.NotNull(chatClient);
var agentVersion = chatClient.GetService();
Assert.NotNull(agentVersion);
}
#endregion
#region Declarative Function Handling Tests
///
/// Verifies that CreateAIAgent uses tools from definition when they are ResponseTool instances, resulting in successful agent creation.
///
[Fact]
public async Task CreateAIAgentAsync_WithResponseToolsInDefinition_CreatesAgentSuccessfullyAsync()
{
// Arrange
var definition = new DeclarativeAgentDefinition("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 { ["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)ProjectsAgentTool.CreateBingCustomSearchTool(new BingCustomSearchToolOptions([new BingCustomSearchConfiguration("connection-id", "instance-name")])));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateBrowserAutomationTool(new BrowserAutomationToolOptions(new BrowserAutomationToolConnectionParameters("id"))));
definition.Tools.Add(ProjectsAgentTool.CreateA2ATool(new Uri("https://test-uri.microsoft.com")));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateBingGroundingTool(new BingGroundingSearchToolOptions([new BingGroundingSearchConfiguration("connection-id")])));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateMicrosoftFabricTool(fabricToolOptions));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateOpenApiTool(new OpenApiFunctionDefinition("name", BinaryData.FromString(OpenAPISpec), new OpenAPIAnonymousAuthenticationDetails())));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateSharepointTool(sharepointOptions));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.CreateStructuredOutputsTool(structuredOutputs));
definition.Tools.Add((ResponseTool)ProjectsAgentTool.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 ProjectsAgentVersionCreationOptions(definition);
// Act
var agentVersion = (await testClient.Client.AgentAdministrationClient.CreateAgentVersionAsync("test-agent", options)).Value;
var agent = testClient.Client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
var agentVersion2 = agent.GetService()!;
Assert.NotNull(agentVersion);
if (agentVersion2.Definition is DeclarativeAgentDefinition promptDef)
{
Assert.NotEmpty(promptDef.Tools);
Assert.Equal(10, promptDef.Tools.Count);
}
}
///
/// Verify that AsAIAgentAsync accepts FunctionTools from definition.
///
[Fact]
public async Task AsAIAgent_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 DeclarativeAgentDefinition("test-model") { Instructions = "Test" };
definition.Tools.Add(functionTool);
// Generate response with the declarative function
var definitionResponse = new DeclarativeAgentDefinition("test-model") { Instructions = "Test" };
definitionResponse.Tools.Add(functionTool);
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new ProjectsAgentVersionCreationOptions(definition);
// Act
var agentVersion = (await testClient.Client.AgentAdministrationClient.CreateAgentVersionAsync("test-agent", options)).Value;
var agent = testClient.Client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
}
///
/// Verify that AsAIAgentAsync accepts declarative functions from definition.
///
[Fact]
public async Task AsAIAgent_WithDeclarativeFunctionFromDefinition_AcceptsDeclarativeFunctionAsync()
{
// Arrange
using var testClient = CreateTestAgentClientWithHandler();
var definition = new DeclarativeAgentDefinition("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 ProjectsAgentVersionCreationOptions(definition);
// Act
var agentVersion = (await testClient.Client.AgentAdministrationClient.CreateAgentVersionAsync("test-agent", options)).Value;
var agent = testClient.Client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
}
///
/// Verify that AsAIAgentAsync accepts declarative functions from definition.
///
[Fact]
public async Task AsAIAgent_WithDeclarativeFunctionInDefinition_AcceptsDeclarativeFunctionAsync()
{
// Arrange
var definition = new DeclarativeAgentDefinition("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 DeclarativeAgentDefinition("test-model") { Instructions = "Test" };
definitionResponse.Tools.Add(declarativeFunction.AsOpenAIResponseTool() ?? throw new InvalidOperationException());
using var testClient = CreateTestAgentClientWithHandler(agentName: "test-agent", agentDefinitionResponse: definitionResponse);
var options = new ProjectsAgentVersionCreationOptions(definition);
// Act
var agentVersion = (await testClient.Client.AgentAdministrationClient.CreateAgentVersionAsync("test-agent", options)).Value;
var agent = testClient.Client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
}
#endregion
#region AgentName Validation Tests
///
/// Verify that AsAIAgent throws ArgumentException when agent name is invalid.
///
[Theory]
[MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))]
public void AsAIAgent_ByName_WithInvalidAgentName_ThrowsArgumentException(string invalidName)
{
// Arrange
var mockClient = new Mock();
// Act & Assert
var exception = Assert.Throws(() =>
mockClient.Object.AsAIAgent(invalidName));
Assert.Equal("name", exception.ParamName);
Assert.Contains("Agent name must be 1-63 characters long", exception.Message);
}
///
/// Verify that AsAIAgent with AgentReference throws ArgumentException when agent name is invalid.
///
[Theory]
[MemberData(nameof(InvalidAgentNameTestData.GetInvalidAgentNames), MemberType = typeof(InvalidAgentNameTestData))]
public void AsAIAgent_WithAgentReference_WithInvalidAgentName_ThrowsArgumentException(string invalidName)
{
// Arrange
var mockClient = new Mock();
var agentReference = new AgentReference(invalidName, "1");
// Act & Assert
var exception = Assert.Throws(() =>
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
///
/// Verify that the underlying chat client created by extension methods can be wrapped with clientFactory.
///
[Fact]
public void AsAIAgent_WithClientFactory_WrapsUnderlyingChatClient()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentRecord 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();
Assert.NotNull(wrappedClient);
}
///
/// Verify that multiple clientFactory calls create independent wrapped clients.
///
[Fact]
public void AsAIAgent_MultipleCallsWithClientFactory_CreatesIndependentClients()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentRecord 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();
var client2 = agent2.GetService();
Assert.NotNull(client1);
Assert.NotNull(client2);
Assert.NotSame(client1, client2);
}
#endregion
#region User-Agent Header Tests
///
/// Verifies that the MEAI user-agent header is added to Responses API POST requests
/// via the protocol method's RequestOptions pipeline policy.
///
[Fact]
public async Task AsAIAgent_Rapi_UserAgentHeaderAddedToRequestsAsync()
{
bool userAgentFound = false;
using var httpHandler = new HttpHandlerAssert(request =>
{
if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses"))
{
// Verify MEAI user-agent header is present on Responses API POST request
if (request.Headers.TryGetValues("User-Agent", out var userAgentValues)
&& userAgentValues.Any(v => v.Contains("MEAI")))
{
userAgentFound = true;
}
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(
TestDataUtil.GetOpenAIDefaultResponseJson(),
Encoding.UTF8,
"application/json")
};
}
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}", 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",
ChatOptions = new ChatOptions { ModelId = "gpt-4o-mini" }
};
// Act
var agent = aiProjectClient.AsAIAgent(agentOptions);
var response = await agent.RunAsync("Hello");
// Assert
Assert.NotNull(agent);
Assert.NotNull(response);
Assert.True(userAgentFound, "MEAI user-agent header was not found in any Responses API request");
}
///
/// Verifies that the MEAI user-agent header is added to Responses API POST requests
/// when using a versioned agent created via CreateAgentVersionAsync.
///
[Fact]
public async Task AsAIAgent_Versioned_UserAgentHeaderAddedToRequestsAsync()
{
bool userAgentFound = false;
using var httpHandler = new HttpHandlerAssert(request =>
{
Assert.Equal("POST", request.Method.Method);
if (request.RequestUri!.PathAndQuery.Contains("/responses"))
{
// Verify MEAI user-agent header is present on Responses API POST request
Assert.True(request.Headers.TryGetValues("User-Agent", out var userAgentValues));
Assert.Contains(userAgentValues, v => v.Contains("MEAI"));
userAgentFound = true;
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(
TestDataUtil.GetOpenAIDefaultResponseJson(),
Encoding.UTF8,
"application/json")
};
}
// CreateAgentVersion POST — return agent version response
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 agentVersion = (await aiProjectClient.AgentAdministrationClient.CreateAgentVersionAsync("test-agent", new ProjectsAgentVersionCreationOptions(new DeclarativeAgentDefinition("test-model") { Instructions = "Test instructions" }))).Value;
// Act
var agent = aiProjectClient.AsAIAgent(agentVersion);
var response = await agent.RunAsync("Hello");
// Assert
Assert.NotNull(agent);
Assert.NotNull(response);
Assert.True(userAgentFound, "MEAI user-agent header was not found in any Responses API request");
}
#endregion
#region GetAIAgent(AIProjectClient, AgentReference) Tests
///
/// Verify that AsAIAgent throws ArgumentNullException when AIProjectClient is null.
///
[Fact]
public void AsAIAgent_WithAgentReference_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient? client = null;
var agentReference = new AgentReference("test-name", "1");
// Act & Assert
var exception = Assert.Throws(() =>
client!.AsAIAgent(agentReference));
Assert.Equal("aiProjectClient", exception.ParamName);
}
///
/// Verify that AsAIAgent throws ArgumentNullException when agentReference is null.
///
[Fact]
public void AsAIAgent_WithAgentReference_WithNullAgentReference_ThrowsArgumentNullException()
{
// Arrange
var mockClient = new Mock();
// Act & Assert
var exception = Assert.Throws(() =>
mockClient.Object.AsAIAgent((AgentReference)null!));
Assert.Equal("agentReference", exception.ParamName);
}
///
/// Verify that AsAIAgent with AgentReference creates a valid agent.
///
[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.IsType(agent);
Assert.Equal("test-name", agent.Name);
Assert.Equal("test-name:1", agent.Id);
Assert.Same(client, agent.GetService());
}
///
/// Verify that AsAIAgent with AgentReference and clientFactory applies the factory.
///
[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();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
///
/// Verify that AsAIAgent with AgentReference sets the agent ID correctly.
///
[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);
}
///
/// Verify that AsAIAgent with AgentReference and tools includes the tools in ChatOptions.
///
[Fact]
public void AsAIAgent_WithAgentReference_WithTools_IncludesToolsInChatOptions()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var agentReference = new AgentReference("test-name", "1");
var tools = new List
{
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 Tests
///
/// Verify that GetService returns ProjectsAgentRecord for agents created from ProjectsAgentRecord.
///
[Fact]
public void GetService_WithAgentRecord_ReturnsAgentRecord()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
var retrievedRecord = agent.GetService();
// Assert
Assert.NotNull(retrievedRecord);
Assert.Equal(agentRecord.Id, retrievedRecord.Id);
}
///
/// Verify that GetService returns null for ProjectsAgentRecord when agent is created from AgentReference.
///
[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();
// Assert
Assert.Null(retrievedRecord);
}
#endregion
#region GetService Tests
///
/// Verify that GetService returns ProjectsAgentVersion for agents created from ProjectsAgentVersion.
///
[Fact]
public void GetService_WithAgentVersion_ReturnsAgentVersion()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
var retrievedVersion = agent.GetService();
// Assert
Assert.NotNull(retrievedVersion);
Assert.Equal(agentVersion.Id, retrievedVersion.Id);
}
///
/// Verify that GetService returns null for ProjectsAgentVersion when agent is created from AgentReference.
///
[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();
// Assert
Assert.Null(retrievedVersion);
}
#endregion
#region ChatClientMetadata Tests
///
/// Verify that ChatClientMetadata is properly populated for agents created from ProjectsAgentRecord.
///
[Fact]
public void ChatClientMetadata_WithAgentRecord_IsPopulatedCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
var metadata = agent.GetService();
// Assert
Assert.NotNull(metadata);
Assert.NotNull(metadata.DefaultModelId);
}
///
/// Verify that ChatClientMetadata.DefaultModelId is set from DeclarativeAgentDefinition model property.
///
[Fact]
public void ChatClientMetadata_WithDeclarativeAgentDefinition_SetsDefaultModelIdFromModel()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var definition = new DeclarativeAgentDefinition("gpt-4-turbo")
{
Instructions = "Test instructions"
};
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord(definition);
// Act
var agent = client.AsAIAgent(agentRecord);
var metadata = agent.GetService();
// 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);
}
///
/// Verify that ChatClientMetadata is properly populated for agents created from ProjectsAgentVersion.
///
[Fact]
public void ChatClientMetadata_WithAgentVersion_IsPopulatedCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
var metadata = agent.GetService();
// Assert
Assert.NotNull(metadata);
Assert.NotNull(metadata.DefaultModelId);
Assert.Equal((agentVersion.Definition as DeclarativeAgentDefinition)!.Model, metadata.DefaultModelId);
}
#endregion
#region AgentReference Availability Tests
///
/// Verify that GetService returns AgentReference for agents created from AgentReference.
///
[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();
// Assert
Assert.NotNull(retrievedReference);
Assert.Equal("test-agent", retrievedReference.Name);
Assert.Equal("1.0", retrievedReference.Version);
}
///
/// Verify that GetService returns null for AgentReference when agent is created from ProjectsAgentRecord.
///
[Fact]
public void GetService_WithAgentRecord_ReturnsAlsoAgentReference()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentRecord agentRecord = this.CreateTestAgentRecord();
// Act
var agent = client.AsAIAgent(agentRecord);
var retrievedReference = agent.GetService();
// Assert
Assert.NotNull(retrievedReference);
Assert.Equal(agentRecord.Name, retrievedReference.Name);
}
///
/// Verify that GetService returns null for AgentReference when agent is created from ProjectsAgentVersion.
///
[Fact]
public void GetService_WithAgentVersion_ReturnsAlsoAgentReference()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentVersion agentVersion = this.CreateTestAgentVersion();
// Act
var agent = client.AsAIAgent(agentVersion);
var retrievedReference = agent.GetService();
// Assert
Assert.NotNull(retrievedReference);
Assert.Equal(agentVersion.Name, retrievedReference.Name);
}
///
/// Verify that GetService returns AgentReference with correct version information.
///
[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();
// Assert
Assert.NotNull(retrievedReference);
Assert.Equal("versioned-agent", retrievedReference.Name);
Assert.Equal("3.5", retrievedReference.Version);
}
#endregion
#region Empty Version and ID Handling Tests
///
/// Verify that AsAIAgent with ProjectsAgentRecord handles empty version by using "latest" as fallback.
///
[Fact]
public void AsAIAgent_WithAgentRecordEmptyVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
ProjectsAgentRecord 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);
}
///
/// Verify that AsAIAgent with ProjectsAgentVersion handles empty version by using "latest" as fallback.
///
[Fact]
public void AsAIAgent_WithAgentVersionEmptyVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion();
ProjectsAgentVersion 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);
}
///
/// Verify that AsAIAgent with ProjectsAgentRecord handles whitespace-only version by using "latest" as fallback.
///
[Fact]
public void AsAIAgent_WithAgentRecordWhitespaceVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
ProjectsAgentRecord 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);
}
///
/// Verify that AsAIAgent with ProjectsAgentVersion handles whitespace-only version by using "latest" as fallback.
///
[Fact]
public void AsAIAgent_WithAgentVersionWhitespaceVersion_CreatesAgentWithGeneratedId()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion();
ProjectsAgentVersion 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
///
/// Verify that when AsAIAgent is called without requireInvocableTools, hosted tools are correctly added.
///
[Fact]
public void AsAIAgent_WithServerHostedTools_AddsToolsToAgentOptions()
{
// Arrange
DeclarativeAgentDefinition definition = new("test-model") { Instructions = "Test" };
definition.Tools.Add(new HostedWebSearchTool().GetService() ?? new HostedWebSearchTool().AsOpenAIResponseTool());
AIProjectClient client = this.CreateTestAgentClient();
ProjectsAgentVersion agentVersion = ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson(agentDefinition: definition)))!;
// Act - no tools provided, but requireInvocableTools is false when no tools param is passed
FoundryAgent agent = client.AsAIAgent(agentVersion);
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
}
#endregion
#region AsAIAgent(AIProjectClient, Uri agentEndpoint) Tests
private const string TestAgentEndpointUrl = "https://test.services.ai.azure.com/api/projects/test-project/agents/it-happy-path/endpoint/protocols/openai";
///
/// Verify that AsAIAgent(Uri agentEndpoint) throws ArgumentNullException when AIProjectClient is null.
///
[Fact]
public void AsAIAgent_WithAgentEndpoint_WithNullClient_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient? client = null;
// Act & Assert
var exception = Assert.Throws(() =>
client!.AsAIAgent(new Uri(TestAgentEndpointUrl)));
Assert.Equal("aiProjectClient", exception.ParamName);
}
///
/// Verify that AsAIAgent(Uri agentEndpoint) throws ArgumentNullException when agentEndpoint is null.
///
[Fact]
public void AsAIAgent_WithAgentEndpoint_WithNullEndpoint_ThrowsArgumentNullException()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
// Act & Assert
var exception = Assert.Throws(() =>
client.AsAIAgent((Uri)null!));
Assert.Equal("agentEndpoint", exception.ParamName);
}
///
/// Verify that AsAIAgent(Uri agentEndpoint) populates Name/Id from the parsed endpoint slug
/// and exposes the supplied via .
///
[Fact]
public void AsAIAgent_WithAgentEndpoint_PopulatesNameAndIdFromSlugAndReusesProjectClient()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
// Act
var agent = client.AsAIAgent(new Uri(TestAgentEndpointUrl));
// Assert
Assert.NotNull(agent);
Assert.IsType(agent);
Assert.Equal("it-happy-path", agent.Name);
Assert.Equal("it-happy-path", agent.Id);
Assert.Same(client, agent.GetService());
}
///
/// Verify that AsAIAgent(Uri agentEndpoint) applies the supplied client factory exactly once.
///
[Fact]
public void AsAIAgent_WithAgentEndpoint_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
TestChatClient? testChatClient = null;
// Act
var agent = client.AsAIAgent(
new Uri(TestAgentEndpointUrl),
clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent);
var retrievedTestClient = agent.GetService();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
///
/// Verify that AsAIAgent(Uri agentEndpoint) forwards the supplied tools to the inner
/// 's .
///
[Fact]
public void AsAIAgent_WithAgentEndpoint_ForwardsToolsToInnerChatOptions()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
var tool1 = AIFunctionFactory.Create(() => "result-1", "tool_1", "First test tool.");
var tool2 = AIFunctionFactory.Create(() => "result-2", "tool_2", "Second test tool.");
List tools = [tool1, tool2];
// Act
var agent = client.AsAIAgent(new Uri(TestAgentEndpointUrl), tools: tools);
// Assert
Assert.NotNull(agent);
ChatOptions? chatOptions = GetAgentChatOptions(agent);
Assert.NotNull(chatOptions);
Assert.NotNull(chatOptions!.Tools);
Assert.Equal(2, chatOptions.Tools!.Count);
Assert.Same(tool1, chatOptions.Tools[0]);
Assert.Same(tool2, chatOptions.Tools[1]);
}
///
/// Verify that AsAIAgent(Uri agentEndpoint) accepts a null tools argument without throwing
/// and produces an agent whose inner is null.
///
[Fact]
public void AsAIAgent_WithAgentEndpoint_WithNullTools_DoesNotThrow()
{
// Arrange
AIProjectClient client = this.CreateTestAgentClient();
// Act
var agent = client.AsAIAgent(new Uri(TestAgentEndpointUrl), tools: null);
// Assert
Assert.NotNull(agent);
ChatOptions? chatOptions = GetAgentChatOptions(agent);
Assert.NotNull(chatOptions);
Assert.Null(chatOptions!.Tools);
}
#endregion
#region Helper Methods
///
/// Creates a test AIProjectClient with fake behavior.
///
private FakeAgentClient CreateTestAgentClient(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse);
}
///
/// 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.
///
private static DisposableTestClient CreateTestAgentClientWithHandler(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? 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);
}
///
/// Wraps an AIProjectClient and its disposable dependencies for deterministic cleanup.
///
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();
}
}
///
/// Creates a test ProjectsAgentRecord for testing.
///
private ProjectsAgentRecord CreateTestAgentRecord(ProjectsAgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJson(agentDefinition: agentDefinition)))!;
}
///
/// Creates a test AIProjectClient with empty version fields for testing hosted MCP agents.
///
private FakeAgentClient CreateTestAgentClientWithEmptyVersion(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion: true);
}
///
/// Creates a test ProjectsAgentRecord with empty version for testing hosted MCP agents.
///
private ProjectsAgentRecord CreateTestAgentRecordWithEmptyVersion(ProjectsAgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithEmptyVersion(agentDefinition: agentDefinition)))!;
}
///
/// Creates a test ProjectsAgentVersion with empty version for testing hosted MCP agents.
///
private ProjectsAgentVersion CreateTestAgentVersionWithEmptyVersion()
{
return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion()))!;
}
///
/// Creates a test AIProjectClient with whitespace-only version fields for testing hosted MCP agents.
///
private FakeAgentClient CreateTestAgentClientWithWhitespaceVersion(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? agentDefinitionResponse = null)
{
return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, versionMode: VersionMode.Whitespace);
}
///
/// Creates a test ProjectsAgentRecord with whitespace-only version for testing hosted MCP agents.
///
private ProjectsAgentRecord CreateTestAgentRecordWithWhitespaceVersion(ProjectsAgentDefinition? agentDefinition = null)
{
return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(agentDefinition: agentDefinition)))!;
}
///
/// Creates a test ProjectsAgentVersion with whitespace-only version for testing hosted MCP agents.
///
private ProjectsAgentVersion CreateTestAgentVersionWithWhitespaceVersion()
{
return ModelReaderWriter.Read(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" }
}
}
}
}
}
}
}
}
""";
///
/// Creates a test ProjectsAgentVersion for testing.
///
private ProjectsAgentVersion CreateTestAgentVersion()
{
return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!;
}
///
/// Specifies the version mode for test data generation.
///
private enum VersionMode
{
Normal,
Empty,
Whitespace
}
///
/// Fake AIProjectClient for testing.
///
private sealed class FakeAgentClient : AIProjectClient
{
public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false, VersionMode versionMode = VersionMode.Normal)
{
// Handle backward compatibility with bool parameter
var effectiveVersionMode = useEmptyVersion ? VersionMode.Empty : versionMode;
this.AgentAdministrationClient = 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 AgentAdministrationClient AgentAdministrationClient { get; }
private sealed class FakeAgentsClient : AgentAdministrationClient
{
private readonly string? _agentName;
private readonly string? _instructions;
private readonly string? _description;
private readonly ProjectsAgentDefinition? _agentDefinition;
private readonly VersionMode _versionMode;
public FakeAgentsClient(string? agentName = null, string? instructions = null, string? description = null, ProjectsAgentDefinition? 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(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)));
}
public override ClientResult GetAgent(string agentName, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task GetAgentAsync(string agentName, RequestOptions options)
{
var responseJson = this.GetAgentResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))));
}
public override Task> GetAgentAsync(string agentName, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
public override ClientResult CreateAgentVersion(string agentName, ProjectsAgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentVersionResponseJson();
return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200));
}
public override Task> CreateAgentVersionAsync(string agentName, ProjectsAgentVersionCreationOptions? options = null, string? foundryFeatures = null, CancellationToken cancellationToken = default)
{
var responseJson = this.GetAgentVersionResponseJson();
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)));
}
}
}
private static DeclarativeAgentDefinition GeneratePromptDefinitionResponse(DeclarativeAgentDefinition inputDefinition, List? tools)
{
var definitionResponse = new DeclarativeAgentDefinition(inputDefinition.Model) { Instructions = inputDefinition.Instructions };
if (tools is not null)
{
foreach (var tool in tools)
{
definitionResponse.Tools.Add(tool.GetService() ?? tool.AsOpenAIResponseTool());
}
}
return definitionResponse;
}
///
/// Test custom chat client that can be used to verify clientFactory functionality.
///
private sealed class TestChatClient : DelegatingChatClient
{
public TestChatClient(IChatClient innerClient) : base(innerClient)
{
}
}
///
/// Mock pipeline response for testing ClientResult wrapping.
///
private sealed class MockPipelineResponse : PipelineResponse
{
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 { get; }
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 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 _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? values)
{
if (this._headers.TryGetValue(name, out var value))
{
values = [value];
return true;
}
values = null;
return false;
}
public override IEnumerator> GetEnumerator()
{
return this._headers.GetEnumerator();
}
}
}
#endregion
///
/// Helper method to access internal ChatOptions property via reflection.
///
private static ChatOptions? GetAgentChatOptions(AIAgent agent)
{
ChatClientAgent? chatClientAgent = agent as ChatClientAgent ?? agent.GetService();
if (chatClientAgent 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(chatClientAgent) as ChatOptions;
}
///
/// Test schema for JSON response format tests.
///
#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
#pragma warning restore CS0618
}
///
/// Provides test data for invalid agent name validation tests.
///
internal static class InvalidAgentNameTestData
{
///
/// Gets a collection of invalid agent names for theory-based testing.
///
/// Collection of invalid agent name test cases.
public static IEnumerable