diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs index eb2ea449e6..da65d53c30 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs @@ -1803,6 +1803,863 @@ public void GetService_WithAgentReference_ReturnsCorrectVersionInformation() #endregion + #region GetAIAgentAsync - Empty Name Tests + + /// + /// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when name is null. + /// + [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(() => + client.GetAIAgentAsync(options)); + + Assert.Equal("options", exception.ParamName); + Assert.Contains("Agent name must be provided", exception.Message); + } + + /// + /// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when name is empty. + /// + [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(() => + client.GetAIAgentAsync(options)); + + Assert.Equal("options", exception.ParamName); + Assert.Contains("Agent name must be provided", exception.Message); + } + + /// + /// Verify that GetAIAgentAsync with ChatClientAgentOptions throws ArgumentException when name is whitespace. + /// + [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(() => + client.GetAIAgentAsync(options)); + + Assert.Equal("options", exception.ParamName); + Assert.Contains("Agent name must be provided", exception.Message); + } + + #endregion + + #region CreateAIAgentAsync - Empty Name Tests + + /// + /// Verify that CreateAIAgentAsync with model and options throws ArgumentException when name is null. + /// + [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(() => + client.CreateAIAgentAsync("test-model", options)); + + Assert.Equal("options", exception.ParamName); + Assert.Contains("Agent name must be provided", exception.Message); + } + + /// + /// Verify that CreateAIAgentAsync with model and options throws ArgumentException when name is empty. + /// + [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(() => + client.CreateAIAgentAsync("test-model", options)); + + Assert.Equal("options", exception.ParamName); + Assert.Contains("Agent name must be provided", exception.Message); + } + + /// + /// Verify that CreateAIAgentAsync with model and options throws ArgumentException when name is whitespace. + /// + [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(() => + 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 + + /// + /// Verify that CreateAIAgentAsync with ChatResponseFormatText response format creates agent successfully. + /// + [Fact] + public async Task CreateAIAgentAsync_WithTextResponseFormat_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions + { + Instructions = "Test", + ResponseFormat = ChatResponseFormat.Text + } + }; + + // Act + ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + /// + /// Verify that CreateAIAgentAsync with ChatResponseFormatJson response format without schema creates agent successfully. + /// + [Fact] + public async Task CreateAIAgentAsync_WithJsonResponseFormatWithoutSchema_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions + { + Instructions = "Test", + ResponseFormat = ChatResponseFormat.Json + } + }; + + // Act + ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + /// + /// Verify that CreateAIAgentAsync with ChatResponseFormatJson with schema creates agent successfully. + /// + [Fact] + public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchema_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + 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 client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + /// + /// Verify that CreateAIAgentAsync with ChatResponseFormatJson with schema and strict mode creates agent successfully. + /// + [Fact] + public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchemaAndStrictMode_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + 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 client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + /// + /// Verify that CreateAIAgentAsync with ChatResponseFormatJson with schema and strict mode false creates agent successfully. + /// + [Fact] + public async Task CreateAIAgentAsync_WithJsonResponseFormatWithSchemaAndStrictModeFalse_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + 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 client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + #endregion + + #region CreateAIAgentAsync - RawRepresentationFactory Tests + + /// + /// Verify that CreateAIAgentAsync with RawRepresentationFactory that returns CreateResponseOptions creates agent successfully. + /// + [Fact] + public async Task CreateAIAgentAsync_WithRawRepresentationFactory_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions + { + Instructions = "Test", + RawRepresentationFactory = _ => new CreateResponseOptions() + } + }; + + // Act + ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + /// + /// Verify that CreateAIAgentAsync with RawRepresentationFactory that returns null does not fail. + /// + [Fact] + public async Task CreateAIAgentAsync_WithRawRepresentationFactoryReturningNull_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions + { + Instructions = "Test", + RawRepresentationFactory = _ => null + } + }; + + // Act + ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + /// + /// Verify that CreateAIAgentAsync with RawRepresentationFactory that returns non-CreateResponseOptions does not fail. + /// + [Fact] + public async Task CreateAIAgentAsync_WithRawRepresentationFactoryReturningNonCreateResponseOptions_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions + { + Instructions = "Test", + RawRepresentationFactory = _ => new object() + } + }; + + // Act + ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + #endregion + + #region CreateAIAgentAsync - Description Tests + + /// + /// Verify that CreateAIAgentAsync with description sets description on the agent. + /// + [Fact] + public async Task CreateAIAgentAsync_WithDescription_SetsDescriptionAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(description: "Test description"); + var options = new ChatClientAgentOptions + { + Name = "test-agent", + Description = "Test description", + ChatOptions = new ChatOptions { Instructions = "Test" } + }; + + // Act + ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + Assert.Equal("Test description", agent.Description); + } + + /// + /// Verify that CreateAIAgentAsync without description still creates agent successfully. + /// + [Fact] + public async Task CreateAIAgentAsync_WithoutDescription_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions { Instructions = "Test" } + }; + + // Act + ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + } + + #endregion + + #region CreateChatClientAgentOptions - Missing Tools Tests + + /// + /// Verify that when invocable tools are required but not provided, an exception is thrown. + /// + [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(() => + client.GetAIAgentAsync(options)); + + Assert.Contains("in-process tools must be provided", exception.Message); + } + + /// + /// Verify that when specific invocable tools are required but wrong ones are provided, InvalidOperationException is thrown. + /// + [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 + { + 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(() => + client.GetAIAgentAsync(options)); + + Assert.Contains("required_function", exception.Message); + Assert.Contains("were not provided", exception.Message); + } + + /// + /// Verify that when tools are provided that match the definition, agent is created successfully. + /// + [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 + { + 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(agent); + } + + #endregion + + #region CreateChatClientAgentOptions - Options Preservation Tests + + /// + /// Verify that CreateChatClientAgentOptions preserves AIContextProviderFactory. + /// + [Fact] + public async Task GetAIAgentAsync_WithAIContextProviderFactory_PreservesFactoryAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + bool factoryInvoked = false; + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions { Instructions = "Test" }, + AIContextProviderFactory = (_, _) => + { + factoryInvoked = true; + return new ValueTask(new TestAIContextProvider()); + } + }; + + // Act + ChatClientAgent agent = await client.GetAIAgentAsync(options); + + // Assert + Assert.NotNull(agent); + // Verify the factory was captured (though not necessarily invoked yet) + Assert.False(factoryInvoked); // Factory is not invoked during creation + } + + /// + /// Verify that CreateChatClientAgentOptions preserves ChatHistoryProviderFactory. + /// + [Fact] + public async Task GetAIAgentAsync_WithChatHistoryProviderFactory_PreservesFactoryAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions { Instructions = "Test" }, + ChatHistoryProviderFactory = (_, _) => new ValueTask(new TestChatHistoryProvider()) + }; + + // Act + ChatClientAgent agent = await client.GetAIAgentAsync(options); + + // Assert + Assert.NotNull(agent); + } + + /// + /// Verify that CreateChatClientAgentOptions preserves UseProvidedChatClientAsIs. + /// + [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); + } + + #endregion + + #region ApplyToolsToAgentDefinition Tests + + /// + /// Verify that CreateAIAgentAsync with non-PromptAgentDefinition and tools throws ArgumentException. + /// + [Fact] + public async Task CreateAIAgentAsync_WithNonPromptAgentDefinitionAndTools_ThrowsArgumentExceptionAsync() + { + // Arrange + var tools = new List + { + 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(() => + client.CreateAIAgentAsync( + name: "test-agent", + model: "test-model", + instructions: "Test", + tools: [declarativeFunction])); + + Assert.Contains("invokable AIFunctions", exception.Message); + } + + /// + /// Verify that CreateAIAgentAsync with AIFunctionDeclaration tools throws InvalidOperationException. + /// + [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(() => + client.CreateAIAgentAsync( + name: "test-agent", + model: "test-model", + instructions: "Test", + tools: [declarativeFunction])); + + Assert.Contains("invokable AIFunctions", exception.Message); + } + + /// + /// Verify that CreateAIAgentAsync with ResponseTool converted via AsAITool works. + /// + [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(agent); + } + + /// + /// Verify that CreateAIAgentAsync with hosted tool types works correctly. + /// + [Fact] + public async Task CreateAIAgentAsync_WithHostedToolTypes_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + var webSearchTool = new HostedWebSearchTool(); + + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions + { + Instructions = "Test", + Tools = [webSearchTool] + } + }; + + // Act + ChatClientAgent agent = await client.CreateAIAgentAsync("test-model", options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + } + + /// + /// Verify that when the server returns tools but matching tools are provided, the agent is created. + /// + [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 + { + 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(agent); + } + + /// + /// Verify that when the server returns mixed tools (function and hosted), the agent handles them correctly. + /// + [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() ?? new HostedWebSearchTool().AsOpenAIResponseTool()); + + AIProjectClient client = this.CreateTestAgentClient(agentDefinitionResponse: definition); + + var tools = new List + { + 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(agent); + } + + /// + /// Verify that when partial tools are provided (some missing), InvalidOperationException is thrown listing missing tools. + /// + [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 + { + // 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(() => + client.GetAIAgentAsync(options)); + + Assert.Contains("missing_tool", exception.Message); + Assert.DoesNotContain("provided_tool", exception.Message); + } + + /// + /// Verify that when AsAIAgent is called without requireInvocableTools, hosted tools are correctly added. + /// + [Fact] + public void AsAIAgent_WithServerHostedTools_AddsToolsToAgentOptions() + { + // Arrange + PromptAgentDefinition definition = new("test-model") { Instructions = "Test" }; + definition.Tools.Add(new HostedWebSearchTool().GetService() ?? new HostedWebSearchTool().AsOpenAIResponseTool()); + + AIProjectClient client = this.CreateTestAgentClient(); + AgentVersion agentVersion = ModelReaderWriter.Read(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(agent); + } + + #endregion + #region Helper Methods /// @@ -2056,6 +2913,49 @@ public override IEnumerator> GetEnumerator() return chatOptionsProperty?.GetValue(agent) 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 + + /// + /// Test AIContextProvider for options preservation tests. + /// + private sealed class TestAIContextProvider : AIContextProvider + { + public override ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) + { + return new ValueTask(new AIContext()); + } + } + + /// + /// Test ChatHistoryProvider for options preservation tests. + /// + private sealed class TestChatHistoryProvider : ChatHistoryProvider + { + public override ValueTask> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) + { + return new ValueTask>(Array.Empty()); + } + + public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) + { + return default; + } + + public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) + { + return default; + } + } } ///