From 65455751a4298411c1037c847475dad6bd185cfd Mon Sep 17 00:00:00 2001
From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com>
Date: Wed, 6 May 2026 08:07:23 -0700
Subject: [PATCH] .NET: Fix flaky declarative test (#5669)
* Fix flaky declarative test
* Addressed host gating and guid parsing concerns in test file.
---
.../InvokeToolWorkflowTest.cs | 81 ++++++++++++++++---
.../Workflows/HttpRequest.yaml | 31 ++++---
2 files changed, 85 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..00f00307c6 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;
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 sealed class InvokeToolWorkflowTest(ITestOutputHelper output) : Integrati
#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,65 @@ public sealed class InvokeToolWorkflowTest(ITestOutputHelper output) : Integrati
#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 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 static readonly Uri s_armEndpoint = new("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 (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);
+#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 +338,16 @@ public sealed class InvokeToolWorkflowTest(ITestOutputHelper output) : Integrati
// 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);
+ 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);
}
#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}"