From 2514ca987ec13bea1146302d53331fce9d40a85c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:36:10 +0000 Subject: [PATCH 1/4] Initial plan From f37a399eab3e19228fd3c1bead1b2aa5dd113d29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:54:16 +0000 Subject: [PATCH 2/4] Fix issue #3195: Handle empty Version and ID in Azure AI agent responses This fix addresses the issue where hosted MCP agents (like AgentWithHostedMCP) fail with "ID cannot be null or empty (Parameter 'id')" error when deployed to Azure AI Foundry. Changes: - Add CreateAgentReference helper method in AzureAIProjectChatClient that defaults empty version to "latest" - Update CreateChatClientAgentOptions to generate a fallback ID from name and version when AgentVersion.Id is null or empty - Add GetAgentVersionResponseJsonWithEmptyVersion and GetAgentResponseJsonWithEmptyVersion test data methods - Add unit tests for empty version handling scenarios Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> --- .../AzureAIProjectChatClient.cs | 16 ++- .../AzureAIProjectChatClientExtensions.cs | 9 +- ...AzureAIProjectChatClientExtensionsTests.cs | 128 ++++++++++++++++-- .../TestDataUtil.cs | 32 +++++ 4 files changed, 172 insertions(+), 13 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs index f31c570508..1d1c09a61f 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs @@ -64,13 +64,27 @@ internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentRecord a internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentVersion agentVersion, ChatOptions? chatOptions) : this( aiProjectClient, - new AgentReference(Throw.IfNull(agentVersion).Name, agentVersion.Version), + CreateAgentReference(Throw.IfNull(agentVersion)), (agentVersion.Definition as PromptAgentDefinition)?.Model, chatOptions) { this._agentVersion = agentVersion; } + /// + /// Creates an from an . + /// Uses the agent version's version if available, otherwise defaults to "latest". + /// + /// The agent version to create a reference from. + /// An for the specified agent version. + private static AgentReference CreateAgentReference(AgentVersion agentVersion) + { + // If the version is null or empty, use "latest" as the default. + // This handles cases where hosted agents (like MCP agents) may not have a version assigned. + var version = string.IsNullOrEmpty(agentVersion.Version) ? "latest" : agentVersion.Version; + return new AgentReference(agentVersion.Name, version); + } + /// public override object? GetService(Type serviceType, object? serviceKey = null) { diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs index 37ee7fa82c..40caf59ade 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs @@ -543,9 +543,16 @@ private static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion } } + // Use the agent version's ID if available, otherwise generate one from name and version. + // This handles cases where hosted agents (like MCP agents) may not have an ID assigned. + var version = string.IsNullOrEmpty(agentVersion.Version) ? "latest" : agentVersion.Version; + var agentId = string.IsNullOrEmpty(agentVersion.Id) + ? $"{agentVersion.Name}:{version}" + : agentVersion.Id; + var agentOptions = new ChatClientAgentOptions() { - Id = agentVersion.Id, + Id = agentId, Name = agentVersion.Name, Description = agentVersion.Description, }; diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs index da65d53c30..b5e2d57436 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs @@ -2384,6 +2384,72 @@ public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_PreservesSetting #endregion + #region Empty Version and ID Handling Tests + + /// + /// Verify that GetAIAgentAsync handles an agent with empty version by using "latest" as fallback. + /// + [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(agent); + // Verify the agent ID is generated from name and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentRecord handles empty version by using "latest" as fallback. + /// + [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 name and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentVersion handles empty version by using "latest" as fallback. + /// + [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 name and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + #endregion + #region ApplyToolsToAgentDefinition Tests /// @@ -2678,6 +2744,30 @@ private AgentRecord CreateTestAgentRecord(AgentDefinition? agentDefinition = nul 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, AgentDefinition? agentDefinitionResponse = null) + { + return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion: true); + } + + /// + /// Creates a test AgentRecord with empty version for testing hosted MCP agents. + /// + private AgentRecord CreateTestAgentRecordWithEmptyVersion(AgentDefinition? agentDefinition = null) + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithEmptyVersion(agentDefinition: agentDefinition)))!; + } + + /// + /// Creates a test AgentVersion with empty version for testing hosted MCP agents. + /// + private AgentVersion CreateTestAgentVersionWithEmptyVersion() + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion()))!; + } + private const string OpenAPISpec = """ { "openapi": "3.0.3", @@ -2721,9 +2811,9 @@ private AgentVersion CreateTestAgentVersion() /// private sealed class FakeAgentClient : AIProjectClient { - public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) + public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false) { - this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse); + this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion); } public override ClientConnection GetConnection(string connectionId) @@ -2739,60 +2829,76 @@ private sealed class FakeAIProjectAgentsOperations : AIProjectAgentsOperations private readonly string? _instructions; private readonly string? _description; private readonly AgentDefinition? _agentDefinition; + private readonly bool _useEmptyVersion; - public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) + public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false) { this._agentName = agentName; this._instructions = instructions; this._description = description; this._agentDefinition = agentDefinitionResponse; + this._useEmptyVersion = useEmptyVersion; + } + + private string GetAgentResponseJson() + { + return this._useEmptyVersion + ? TestDataUtil.GetAgentResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description) + : TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + } + + private string GetAgentVersionResponseJson() + { + return this._useEmptyVersion + ? TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion(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 = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + 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 = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + 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 = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + 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 = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentResponseJson(); return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200))); } public override ClientResult CreateAgentVersion(string agentName, BinaryContent content, RequestOptions? options = null) { - var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentVersionResponseJson(); return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))); } public override ClientResult CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default) { - var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentVersionResponseJson(); return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)); } public override Task CreateAgentVersionAsync(string agentName, BinaryContent content, RequestOptions? options = null) { - var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentVersionResponseJson(); return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)))); } public override Task> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default) { - var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentVersionResponseJson(); return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200))); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs index c65d10de43..02b55a7e39 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs @@ -52,6 +52,38 @@ public static string GetAgentVersionResponseJson(string? agentName = null, Agent return json; } + /// + /// Gets the agent version response JSON with optional placeholder replacements applied, including support for empty version and ID. + /// + public static string GetAgentVersionResponseJsonWithEmptyVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null) + { + var json = s_agentVersionResponseJson; + json = ApplyAgentName(json, agentName); + json = ApplyAgentDefinition(json, agentDefinition); + json = ApplyInstructions(json, instructions); + json = ApplyDescription(json, description); + // Remove the version and id fields to simulate hosted agents without version + json = json.Replace("\"version\": \"1\",", "\"version\": \"\","); + json = json.Replace("\"id\": \"agent_abc123:1\",", "\"id\": \"\","); + return json; + } + + /// + /// Gets the agent response JSON with optional placeholder replacements applied, including support for empty version and ID in the latest version. + /// + public static string GetAgentResponseJsonWithEmptyVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null) + { + var json = s_agentResponseJson; + json = ApplyAgentName(json, agentName); + json = ApplyAgentDefinition(json, agentDefinition); + json = ApplyInstructions(json, instructions); + json = ApplyDescription(json, description); + // Remove the version and id fields to simulate hosted agents without version + json = json.Replace("\"version\": \"1\",", "\"version\": \"\","); + json = json.Replace("\"id\": \"agent_abc123:1\",", "\"id\": \"\","); + return json; + } + /// /// Gets the OpenAI default response JSON with optional placeholder replacements applied. /// From a6b4acaa2a5c0cb55941646982c72713c67c9828 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:59:24 +0000 Subject: [PATCH 3/4] Address code review feedback: improve documentation and test comments Co-authored-by: rogerbarreto <19890735+rogerbarreto@users.noreply.github.com> --- .../AzureAIProjectChatClientExtensionsTests.cs | 6 +++--- .../Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs index b5e2d57436..160422a1c5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs @@ -2406,7 +2406,7 @@ public async Task GetAIAgentAsync_WithEmptyVersion_CreatesAgentSuccessfullyAsync // Assert Assert.NotNull(agent); Assert.IsType(agent); - // Verify the agent ID is generated from name and "latest" + // Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest" Assert.Equal("agent_abc123:latest", agent.Id); } @@ -2425,7 +2425,7 @@ public void AsAIAgent_WithAgentRecordEmptyVersion_CreatesAgentWithGeneratedId() // Assert Assert.NotNull(agent); - // Verify the agent ID is generated from name and "latest" + // Verify the agent ID is generated from agent record name ("agent_abc123") and "latest" Assert.Equal("agent_abc123:latest", agent.Id); } @@ -2444,7 +2444,7 @@ public void AsAIAgent_WithAgentVersionEmptyVersion_CreatesAgentWithGeneratedId() // Assert Assert.NotNull(agent); - // Verify the agent ID is generated from name and "latest" + // Verify the agent ID is generated from agent version name ("agent_abc123") and "latest" Assert.Equal("agent_abc123:latest", agent.Id); } diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs index 02b55a7e39..071d03a911 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs @@ -53,7 +53,7 @@ public static string GetAgentVersionResponseJson(string? agentName = null, Agent } /// - /// Gets the agent version response JSON with optional placeholder replacements applied, including support for empty version and ID. + /// Gets the agent version response JSON with empty version and ID fields for testing hosted agents like MCP agents. /// public static string GetAgentVersionResponseJsonWithEmptyVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null) { @@ -69,7 +69,7 @@ public static string GetAgentVersionResponseJsonWithEmptyVersion(string? agentNa } /// - /// Gets the agent response JSON with optional placeholder replacements applied, including support for empty version and ID in the latest version. + /// Gets the agent response JSON with empty version and ID fields in the latest version for testing hosted agents like MCP agents. /// public static string GetAgentResponseJsonWithEmptyVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null) { From de5062ab6ff43872118216334f36b097f61d135c Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:56:59 +0000 Subject: [PATCH 4/4] Address PR review: Use IsNullOrWhiteSpace and add whitespace unit tests --- .../AzureAIProjectChatClient.cs | 4 +- .../AzureAIProjectChatClientExtensions.cs | 4 +- ...AzureAIProjectChatClientExtensionsTests.cs | 126 ++++++++++++++++-- .../TestDataUtil.cs | 32 +++++ 4 files changed, 151 insertions(+), 15 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs index 1d1c09a61f..0d3639b614 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs @@ -79,9 +79,9 @@ internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentVersion /// An for the specified agent version. private static AgentReference CreateAgentReference(AgentVersion agentVersion) { - // If the version is null or empty, use "latest" as the default. + // If the version is null, empty, or whitespace, use "latest" as the default. // This handles cases where hosted agents (like MCP agents) may not have a version assigned. - var version = string.IsNullOrEmpty(agentVersion.Version) ? "latest" : agentVersion.Version; + var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version; return new AgentReference(agentVersion.Name, version); } diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs index 40caf59ade..18a5ba3b72 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs @@ -545,8 +545,8 @@ private static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion // Use the agent version's ID if available, otherwise generate one from name and version. // This handles cases where hosted agents (like MCP agents) may not have an ID assigned. - var version = string.IsNullOrEmpty(agentVersion.Version) ? "latest" : agentVersion.Version; - var agentId = string.IsNullOrEmpty(agentVersion.Id) + var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version; + var agentId = string.IsNullOrWhiteSpace(agentVersion.Id) ? $"{agentVersion.Name}:{version}" : agentVersion.Id; diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs index 160422a1c5..447c195c83 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs @@ -2448,6 +2448,68 @@ public void AsAIAgent_WithAgentVersionEmptyVersion_CreatesAgentWithGeneratedId() Assert.Equal("agent_abc123:latest", agent.Id); } + /// + /// Verify that GetAIAgentAsync handles an agent with whitespace-only version by using "latest" as fallback. + /// + [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(agent); + // Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentRecord handles whitespace-only version by using "latest" as fallback. + /// + [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); + } + + /// + /// Verify that AsAIAgent with AgentVersion handles whitespace-only version by using "latest" as fallback. + /// + [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 @@ -2768,6 +2830,30 @@ private AgentVersion 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, AgentDefinition? agentDefinitionResponse = null) + { + return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, versionMode: VersionMode.Whitespace); + } + + /// + /// Creates a test AgentRecord with whitespace-only version for testing hosted MCP agents. + /// + private AgentRecord CreateTestAgentRecordWithWhitespaceVersion(AgentDefinition? agentDefinition = null) + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(agentDefinition: agentDefinition)))!; + } + + /// + /// Creates a test AgentVersion with whitespace-only version for testing hosted MCP agents. + /// + private AgentVersion CreateTestAgentVersionWithWhitespaceVersion() + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion()))!; + } + private const string OpenAPISpec = """ { "openapi": "3.0.3", @@ -2806,14 +2892,26 @@ private AgentVersion 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, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false) + public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false, VersionMode versionMode = VersionMode.Normal) { - this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion); + // Handle backward compatibility with bool parameter + var effectiveVersionMode = useEmptyVersion ? VersionMode.Empty : versionMode; + this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse, effectiveVersionMode); } public override ClientConnection GetConnection(string connectionId) @@ -2829,29 +2927,35 @@ private sealed class FakeAIProjectAgentsOperations : AIProjectAgentsOperations private readonly string? _instructions; private readonly string? _description; private readonly AgentDefinition? _agentDefinition; - private readonly bool _useEmptyVersion; + private readonly VersionMode _versionMode; - public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false) + public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, VersionMode versionMode = VersionMode.Normal) { this._agentName = agentName; this._instructions = instructions; this._description = description; this._agentDefinition = agentDefinitionResponse; - this._useEmptyVersion = useEmptyVersion; + this._versionMode = versionMode; } private string GetAgentResponseJson() { - return this._useEmptyVersion - ? TestDataUtil.GetAgentResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description) - : TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + 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._useEmptyVersion - ? TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description) - : TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + 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) diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs index 071d03a911..8471ddbcf1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs @@ -84,6 +84,38 @@ public static string GetAgentResponseJsonWithEmptyVersion(string? agentName = nu return json; } + /// + /// Gets the agent version response JSON with whitespace-only version and ID fields for testing hosted agents like MCP agents. + /// + public static string GetAgentVersionResponseJsonWithWhitespaceVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null) + { + var json = s_agentVersionResponseJson; + json = ApplyAgentName(json, agentName); + json = ApplyAgentDefinition(json, agentDefinition); + json = ApplyInstructions(json, instructions); + json = ApplyDescription(json, description); + // Use whitespace-only version and id fields to simulate hosted agents without version + return json + .Replace("\"version\": \"1\",", "\"version\": \" \",") + .Replace("\"id\": \"agent_abc123:1\",", "\"id\": \" \","); + } + + /// + /// Gets the agent response JSON with whitespace-only version and ID fields in the latest version for testing hosted agents like MCP agents. + /// + public static string GetAgentResponseJsonWithWhitespaceVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null) + { + var json = s_agentResponseJson; + json = ApplyAgentName(json, agentName); + json = ApplyAgentDefinition(json, agentDefinition); + json = ApplyInstructions(json, instructions); + json = ApplyDescription(json, description); + // Use whitespace-only version and id fields to simulate hosted agents without version + return json + .Replace("\"version\": \"1\",", "\"version\": \" \",") + .Replace("\"id\": \"agent_abc123:1\",", "\"id\": \" \","); + } + /// /// Gets the OpenAI default response JSON with optional placeholder replacements applied. ///