From 1ed1afdbcbe30c6e9526c5ec0f0be80aebb139ed Mon Sep 17 00:00:00 2001 From: Peter Ibekwe Date: Tue, 5 May 2026 17:26:18 -0700 Subject: [PATCH 1/2] Fix flaky declarative test --- .../InvokeToolWorkflowTest.cs | 74 ++++++++++++++++--- .../Workflows/HttpRequest.yaml | 31 ++++---- 2 files changed, 78 insertions(+), 27 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs index ec09197376..c8b0df51a5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs @@ -4,14 +4,19 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; +using Azure.Core; using Microsoft.Agents.AI.Workflows.Declarative.Events; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.AI.Workflows.Declarative.Mcp; using Microsoft.Extensions.AI; +using Shared.IntegrationTests; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; @@ -48,9 +53,9 @@ public Task ValidateInvokeMcpToolWithApprovalAsync(string workflowFileName, stri #region InvokeHttpRequest Tests [RetryTheory(3, 5000)] - [InlineData("HttpRequest.yaml", "visibility: public")] - public Task ValidateHttpRequestAsync(string workflowFileName, string? expectedResultContains) => - this.RunHttpRequestTestAsync(workflowFileName, expectedResultContains); + [InlineData("HttpRequest.yaml")] + public Task ValidateHttpRequestAsync(string workflowFileName) => + this.RunHttpRequestTestAsync(workflowFileName); #endregion @@ -261,16 +266,60 @@ private List ProcessMcpToolRequests( #region InvokeHttpRequest Test Helpers + /// + /// The Azure ARM scope used to acquire bearer tokens for the HttpRequestAction + /// integration test. Matches the URL configured in HttpRequest.yaml. + /// + private const string ArmScope = "https://management.azure.com/.default"; + + /// + /// The URL prefix used to gate which requests receive the authenticated + /// . Other URLs fall through to the handler default. + /// + private const string ArmUrlPrefix = "https://management.azure.com"; + /// /// Runs an HttpRequestAction workflow test with the specified configuration. /// + /// + /// The workflow under test calls an authenticated Azure ARM endpoint. We acquire a + /// single bearer token via the same Azure CLI credential used elsewhere in the + /// integration test suite, attach it to a cached , and route + /// matching requests through that client via 's + /// httpClientProvider callback. The test owns the 's + /// lifetime and disposes it explicitly — does + /// not dispose provider-returned clients. + /// private async Task RunHttpRequestTestAsync( - string workflowFileName, - string? expectedResultContains = null) + string workflowFileName) { // Arrange string workflowPath = GetWorkflowPath(workflowFileName); - await using DefaultHttpRequestHandler httpRequestHandler = new(); + + AccessToken accessToken = + await TestAzureCliCredentials + .CreateAzureCliCredential() + .GetTokenAsync(new TokenRequestContext([ArmScope]), CancellationToken.None) + .ConfigureAwait(false); + + using HttpClient authenticatedClient = new(); + authenticatedClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", accessToken.Token); + + await using DefaultHttpRequestHandler httpRequestHandler = + new(httpClientProvider: (request, _) => + { + if (request.Url.StartsWith(ArmUrlPrefix, StringComparison.OrdinalIgnoreCase)) + { +#pragma warning disable CA2025 // authenticatedClient outlives the handler (LIFO using disposal) and the workflow awaits all dispatches. + return Task.FromResult(authenticatedClient); +#pragma warning restore CA2025 + } + + // Fall back to the handler's internal client for any non-ARM URLs. + return Task.FromResult(null); + }); + DeclarativeWorkflowOptions workflowOptions = await this.CreateOptionsAsync( externalConversation: false, httpRequestHandler: httpRequestHandler); @@ -284,11 +333,14 @@ private async Task RunHttpRequestTestAsync( // Assert - Verify executor and action events AssertWorkflowEventsEmitted(workflowEvents); - // Assert - Verify expected result if specified - if (expectedResultContains is not null) - { - AssertResultContains(workflowEvents, expectedResultContains); - } + MessageActivityEvent? messageEvent = workflowEvents.Events + .OfType() + .LastOrDefault(); + + Assert.NotNull(messageEvent); + Assert.NotNull(messageEvent.Message); + _ = Guid.TryParse(messageEvent.Message, out Guid retrievedTenantId); + Assert.NotEqual(Guid.Empty, retrievedTenantId); } #endregion diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/HttpRequest.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/HttpRequest.yaml index 24ee0546e4..97efb3ee5a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/HttpRequest.yaml +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/HttpRequest.yaml @@ -1,6 +1,10 @@ # # This workflow tests invoking HttpRequestAction end-to-end. -# Uses the public GitHub API (unauthenticated) to fetch repo metadata. +# Uses the Azure ARM tenants endpoint, which is authenticated, fully static, and +# reachable with the credentials the integration test pipeline already provides +# (via az login). The bearer token is supplied by the test through a custom +# HttpClient passed to DefaultHttpRequestHandler; the YAML deliberately does not +# carry an Authorization header. # kind: Workflow trigger: @@ -9,24 +13,19 @@ trigger: id: workflow_http_request_test actions: - # Set the repo owner used to form the request URL. - - kind: SetVariable - id: set_repo_owner - variable: Local.RepoOwner - value: dotnet - - # Invoke the GitHub repo API. + # Invoke the Azure ARM tenants list API. - kind: HttpRequestAction - id: fetch_repo_info + id: fetch_tenants conversationId: =System.ConversationId method: GET - url: =Concatenate("https://api.github.com/repos/", Local.RepoOwner, "/runtime") + url: https://management.azure.com/tenants?api-version=2022-09-01 headers: - Accept: application/vnd.github+json - User-Agent: agent-framework-integration-test - response: Local.RepoInfo + Accept: application/json + response: Local.TenantsResponse - # Surface the Repo visibility field from the parsed JSON response. + # Surface the first tenant id from the parsed JSON response. Every + # authenticated principal belongs to at least one tenant, so this path + # always resolves on a successful call. - kind: SendMessage - id: show_visibility - message: "visibility: {Local.RepoInfo.visibility}" + id: show_first_tenant + message: "{First(Local.TenantsResponse.value).tenantId}" From 5b17f4419981372619c4660cdea6bf449e3e8f3d Mon Sep 17 00:00:00 2001 From: Peter Ibekwe Date: Tue, 5 May 2026 22:01:46 -0700 Subject: [PATCH 2/2] Addressed host gating and guid parsing concerns in test file. --- .../InvokeToolWorkflowTest.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs index c8b0df51a5..00f00307c6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs @@ -273,10 +273,13 @@ private List ProcessMcpToolRequests( private const string ArmScope = "https://management.azure.com/.default"; /// - /// The URL prefix used to gate which requests receive the authenticated - /// . Other URLs fall through to the handler default. + /// The expected ARM endpoint. Only requests whose absolute URL exactly matches + /// this scheme and host receive the authenticated ; all + /// other URLs (including subdomain look-alikes such as + /// https://management.azure.com.evil.com) fall through to the handler + /// default and never see the bearer token. /// - private const string ArmUrlPrefix = "https://management.azure.com"; + private static readonly Uri s_armEndpoint = new("https://management.azure.com/"); /// /// Runs an HttpRequestAction workflow test with the specified configuration. @@ -309,7 +312,9 @@ await TestAzureCliCredentials await using DefaultHttpRequestHandler httpRequestHandler = new(httpClientProvider: (request, _) => { - if (request.Url.StartsWith(ArmUrlPrefix, StringComparison.OrdinalIgnoreCase)) + if (Uri.TryCreate(request.Url, UriKind.Absolute, out Uri? requestUri) && + string.Equals(requestUri.Scheme, s_armEndpoint.Scheme, StringComparison.OrdinalIgnoreCase) && + string.Equals(requestUri.Host, s_armEndpoint.Host, StringComparison.OrdinalIgnoreCase)) { #pragma warning disable CA2025 // authenticatedClient outlives the handler (LIFO using disposal) and the workflow awaits all dispatches. return Task.FromResult(authenticatedClient); @@ -339,7 +344,9 @@ await TestAzureCliCredentials Assert.NotNull(messageEvent); Assert.NotNull(messageEvent.Message); - _ = Guid.TryParse(messageEvent.Message, out Guid retrievedTenantId); + Assert.True( + Guid.TryParse(messageEvent.Message, out Guid retrievedTenantId), + $"Expected the SendMessage payload to be a tenant GUID, but got: '{messageEvent.Message}'"); Assert.NotEqual(Guid.Empty, retrievedTenantId); }