.NET: Add dedicated Foundry.Hosting UnitTest project (#5592)

* Foundry.Hosting.UnitTests: extract project from Foundry.UnitTests

Move all Hosting/* tests, three toolbox TestData JSONs, and the FakeAuthenticationTokenProvider/HttpHandlerAssert/TestDataUtil helpers (trimmed to toolbox getters) into a new Microsoft.Agents.AI.Foundry.Hosting.UnitTests project. Add it to the slnx and grant the new assembly InternalsVisibleTo from Microsoft.Agents.AI.Foundry and Microsoft.Agents.AI.Foundry.Hosting.

* Foundry.Hosting.UnitTests: align namespaces to assembly name

Rename namespaces from Microsoft.Agents.AI.Foundry.UnitTests(.Hosting) to Microsoft.Agents.AI.Foundry.Hosting.UnitTests across all moved tests, the duplicated helpers, and the trimmed TestDataUtil. Also fixes the prior namespace inconsistency in FoundryToolboxTests.

* Foundry.Hosting.UnitTests: split WorkflowIntegrationTests by SUT

Replace the WorkflowIntegrationTests file (an IT-named file inside a UT project) with two SUT-focused files plus a shared test-doubles file:

- AgentFrameworkResponseHandlerWorkflowTests.cs - the 5 handler-driven tests that exercise AgentFrameworkResponseHandler with a real workflow agent.
- OutputConverterWorkflowTests.cs - the 5 OutputConverter tests driven by hand-crafted update sequences mirroring real workflow patterns.
- WorkflowTestAgents.cs - StreamingTextAgent and ThrowingStreamingAgent extracted as internal types used by both files.

* Foundry.UnitTests: trim Hosting-related conditionals and dead testdata

Now that Hosting tests live in their own project:
- drop the Compile Remove guard for the Hosting subfolder,
- drop the .NETCoreApp-only PackageReferences (Azure.AI.AgentServer.Responses, Microsoft.AspNetCore.TestHost, OpenTelemetry, OpenTelemetry.Exporter.InMemory),
- drop the conditional ProjectReference to Microsoft.Agents.AI.Foundry.Hosting,
- delete the three Toolbox JSON files and the matching Toolbox getters in TestDataUtil.

* Foundry.Hosting.UnitTests: drop redundant 'using Microsoft.Agents.AI.Foundry.Hosting'

The new project namespace is Microsoft.Agents.AI.Foundry.Hosting.UnitTests, which already brings the parent Microsoft.Agents.AI.Foundry.Hosting namespace into scope. The explicit using statement is therefore redundant (IDE0005). Caught by 'dotnet format --verify-no-changes' running on Linux against the .NET 10 SDK.

* Foundry.Hosting: drop InternalsVisibleTo to Foundry.UnitTests

The non-hosting Foundry.UnitTests project no longer holds any Hosting tests after the split, so it doesn't need access to internal types in Microsoft.Agents.AI.Foundry.Hosting. Only Microsoft.Agents.AI.Foundry.Hosting.UnitTests needs it.

* Foundry.Hosting: rename DelegatingResponsesClient to UserAgentResponsesClient

Address westey-m's review feedback on PR #5453: `Delegating*` is conventionally reserved for inheritable base classes (mirroring `DelegatingHandler`) where consumers override one or two members. This polyfill is sealed and only injects the User-Agent supplement, so the new name reflects its actual purpose.

Renamed via `git mv` to preserve history:
* `src/Microsoft.Agents.AI.Foundry.Hosting/DelegatingResponsesClient.cs` to `UserAgentResponsesClient.cs`
* `tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/DelegatingResponsesClientTests.cs` to `UserAgentResponsesClientTests.cs`

Class, constructor, and all references updated across:
* `src/.../UserAgentResponsesClient.cs` (class + constructor + internal log message)
* `src/.../ServiceCollectionExtensions.cs` (cref + type check + instantiation)
* `src/.../HostedAgentUserAgentPolicy.cs` (cref)
* `tests/Foundry.UnitTests/RequestOptionsExtensionsTests.cs` (comment)
* `tests/Foundry.Hosting.UnitTests/UserAgentResponsesClientTests.cs` (class + cref + instantiations)
This commit is contained in:
Roger Barreto
2026-04-30 22:09:25 +01:00
committed by GitHub
Unverified
parent 317ef4491e
commit fb97e93a01
31 changed files with 681 additions and 591 deletions
@@ -19,7 +19,7 @@ namespace Microsoft.Agents.AI.Foundry.Hosting;
/// </para>
/// <para>
/// This policy is added at request time (per-call <see cref="PipelinePosition"/>)
/// by <see cref="DelegatingResponsesClient"/> when invoking the wrapped
/// by <see cref="UserAgentResponsesClient"/> when invoking the wrapped
/// <see cref="OpenAI.Responses.ResponsesClient"/>. It is only registered when an agent is
/// resolved by the Foundry hosting layer.
/// </para>
@@ -44,7 +44,7 @@
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.AI.Foundry.UnitTests" />
<InternalsVisibleTo Include="Microsoft.Agents.AI.Foundry.Hosting.UnitTests" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>
@@ -210,7 +210,7 @@ public static class FoundryHostingExtensions
/// <summary>
/// Attempts to wrap the agent's underlying <see cref="ResponsesClient"/>
/// with a <see cref="DelegatingResponsesClient"/> so every outgoing Responses-API request
/// with a <see cref="UserAgentResponsesClient"/> so every outgoing Responses-API request
/// carries the hosted-agent <c>User-Agent</c> segment.
/// </summary>
/// <remarks>
@@ -219,7 +219,7 @@ public static class FoundryHostingExtensions
/// <list type="bullet">
/// <item><description><paramref name="agent"/> exposes no <see cref="IChatClient"/>;</description></item>
/// <item><description>the chat client is not backed by MEAI's internal <c>OpenAIResponsesChatClient</c> (e.g., a non-OpenAI provider or a custom impl);</description></item>
/// <item><description>the inner <see cref="ResponsesClient"/> is already a <see cref="DelegatingResponsesClient"/>.</description></item>
/// <item><description>the inner <see cref="ResponsesClient"/> is already a <see cref="UserAgentResponsesClient"/>.</description></item>
/// </list>
/// </para>
/// <para>
@@ -261,12 +261,12 @@ public static class FoundryHostingExtensions
}
var current = field.GetValue(meaiInstance) as ResponsesClient;
if (current is null or DelegatingResponsesClient)
if (current is null or UserAgentResponsesClient)
{
return agent;
}
field.SetValue(meaiInstance, new DelegatingResponsesClient(current));
field.SetValue(meaiInstance, new UserAgentResponsesClient(current));
return agent;
}
@@ -33,11 +33,11 @@ namespace Microsoft.Agents.AI.Foundry.Hosting;
/// never expected to run; the throwing transport surfaces any unexpected escape route loudly.
/// </para>
/// </remarks>
internal sealed class DelegatingResponsesClient : ResponsesClient
internal sealed class UserAgentResponsesClient : ResponsesClient
{
private readonly ResponsesClient _inner;
public DelegatingResponsesClient(ResponsesClient inner)
public UserAgentResponsesClient(ResponsesClient inner)
: base(BuildDummyPipeline(), new OpenAIClientOptions { Endpoint = inner?.Endpoint })
{
this._inner = inner ?? throw new ArgumentNullException(nameof(inner));
@@ -104,7 +104,7 @@ internal sealed class DelegatingResponsesClient : ResponsesClient
private sealed class ThrowingTransport : PipelineTransport
{
private const string Message =
"DelegatingResponsesClient transport invoked bypassed the override-and-delegate design. This exception should be unreachable and should never be thrown following the correct usage of DelegatingResponsesClient.";
"UserAgentResponsesClient transport invoked bypassed the override-and-delegate design. This exception should be unreachable and should never be thrown following the correct usage of UserAgentResponsesClient.";
protected override PipelineMessage CreateMessageCore() => throw new InvalidOperationException(Message);
protected override void ProcessCore(PipelineMessage message) => throw new InvalidOperationException(Message);
@@ -53,6 +53,7 @@
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.AI.Foundry.UnitTests" />
<InternalsVisibleTo Include="Microsoft.Agents.AI.Foundry.Hosting.UnitTests" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>