.NET: Fix flaky declarative test (#5669)

* Fix flaky declarative test

* Addressed host gating and guid parsing concerns in test file.
This commit is contained in:
Peter Ibekwe
2026-05-06 08:07:23 -07:00
committed by GitHub
Unverified
parent b12109b7e4
commit 65455751a4
2 changed files with 85 additions and 27 deletions
@@ -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
/// <summary>
/// The Azure ARM scope used to acquire bearer tokens for the HttpRequestAction
/// integration test. Matches the URL configured in <c>HttpRequest.yaml</c>.
/// </summary>
private const string ArmScope = "https://management.azure.com/.default";
/// <summary>
/// The expected ARM endpoint. Only requests whose absolute URL exactly matches
/// this scheme and host receive the authenticated <see cref="HttpClient"/>; all
/// other URLs (including subdomain look-alikes such as
/// <c>https://management.azure.com.evil.com</c>) fall through to the handler
/// default and never see the bearer token.
/// </summary>
private static readonly Uri s_armEndpoint = new("https://management.azure.com/");
/// <summary>
/// Runs an HttpRequestAction workflow test with the specified configuration.
/// </summary>
/// <remarks>
/// 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 <see cref="HttpClient"/>, and route
/// matching requests through that client via <see cref="DefaultHttpRequestHandler"/>'s
/// <c>httpClientProvider</c> callback. The test owns the <see cref="HttpClient"/>'s
/// lifetime and disposes it explicitly — <see cref="DefaultHttpRequestHandler"/> does
/// not dispose provider-returned clients.
/// </remarks>
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<HttpClient?>(authenticatedClient);
#pragma warning restore CA2025
}
// Fall back to the handler's internal client for any non-ARM URLs.
return Task.FromResult<HttpClient?>(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<MessageActivityEvent>()
.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
@@ -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}"