chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,183 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace Microsoft.Agents.AI.DevUI.UnitTests;
public class DevUIAccessControlTests
{
private static WebApplicationBuilder NewBuilder()
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, "Test", "agent-name");
builder.Services.AddKeyedSingleton<AIAgent>("agent-name", agent);
return builder;
}
private static void SimulateRemoteIp(WebApplication app, IPAddress remoteIp)
{
app.Use(async (HttpContext ctx, RequestDelegate next) =>
{
ctx.Connection.RemoteIpAddress = remoteIp;
await next(ctx);
});
}
[Fact]
public async Task NonLoopbackRequest_ReturnsForbiddenByDefaultAsync()
{
var builder = NewBuilder();
builder.Services.AddDevUI();
using var app = builder.Build();
SimulateRemoteIp(app, IPAddress.Parse("192.0.2.1"));
app.MapDevUI();
await app.StartAsync();
var response = await app.GetTestClient().GetAsync(new Uri("/v1/entities", UriKind.Relative));
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task NonLoopbackRequest_IsAllowedWhenAllowRemoteAccessAsync()
{
var builder = NewBuilder();
builder.Services.AddDevUI(o => o.AllowRemoteAccess = true);
using var app = builder.Build();
SimulateRemoteIp(app, IPAddress.Parse("192.0.2.1"));
app.MapDevUI();
await app.StartAsync();
var response = await app.GetTestClient().GetAsync(new Uri("/v1/entities", UriKind.Relative));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task LoopbackRequest_WithAuthTokenSet_RequiresBearerHeaderAsync()
{
var builder = NewBuilder();
builder.Services.AddDevUI(o => o.AuthToken = "secret-token");
using var app = builder.Build();
SimulateRemoteIp(app, IPAddress.Loopback);
app.MapDevUI();
await app.StartAsync();
var response = await app.GetTestClient().GetAsync(new Uri("/v1/entities", UriKind.Relative));
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task LoopbackRequest_WithCorrectBearerToken_SucceedsAsync()
{
var builder = NewBuilder();
builder.Services.AddDevUI(o => o.AuthToken = "secret-token");
using var app = builder.Build();
SimulateRemoteIp(app, IPAddress.Loopback);
app.MapDevUI();
await app.StartAsync();
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("/v1/entities", UriKind.Relative));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "secret-token");
var response = await app.GetTestClient().SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task EnvironmentVariableToken_IsEnforcedWhenAuthTokenNotConfiguredAsync()
{
const string EnvVar = "DEVUI_AUTH_TOKEN";
const string EnvToken = "env-token";
var previous = Environment.GetEnvironmentVariable(EnvVar);
Environment.SetEnvironmentVariable(EnvVar, EnvToken);
WebApplication? app = null;
try
{
var builder = NewBuilder();
builder.Services.AddDevUI();
app = builder.Build();
// Force singleton construction so the env var is captured before we
// restore it; otherwise tests running in parallel can pick up the
// leaked DEVUI_AUTH_TOKEN.
_ = app.Services.GetRequiredService<DevUIAuthFilter>();
}
finally
{
Environment.SetEnvironmentVariable(EnvVar, previous);
}
await using (app)
{
SimulateRemoteIp(app, IPAddress.Loopback);
app.MapDevUI();
await app.StartAsync();
var missing = await app.GetTestClient().GetAsync(new Uri("/v1/entities", UriKind.Relative));
Assert.Equal(HttpStatusCode.Unauthorized, missing.StatusCode);
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("/v1/entities", UriKind.Relative));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", EnvToken);
var accepted = await app.GetTestClient().SendAsync(request);
Assert.Equal(HttpStatusCode.OK, accepted.StatusCode);
}
}
[Fact]
public async Task MetaEndpoint_IsReachableWithoutAuthenticationAsync()
{
var builder = NewBuilder();
builder.Services.AddDevUI(o => o.AuthToken = "secret-token");
using var app = builder.Build();
SimulateRemoteIp(app, IPAddress.Loopback);
app.MapDevUI();
await app.StartAsync();
var response = await app.GetTestClient().GetAsync(new Uri("/meta", UriKind.Relative));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Contains("\"auth_required\":true", body);
}
[Fact]
public async Task LoopbackRequest_WithWrongBearerToken_ReturnsUnauthorizedAsync()
{
var builder = NewBuilder();
builder.Services.AddDevUI(o => o.AuthToken = "secret-token");
using var app = builder.Build();
SimulateRemoteIp(app, IPAddress.Loopback);
app.MapDevUI();
await app.StartAsync();
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("/v1/entities", UriKind.Relative));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "not-the-token");
var response = await app.GetTestClient().SendAsync(request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
@@ -0,0 +1,222 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace Microsoft.Agents.AI.DevUI.UnitTests;
/// <summary>
/// Unit tests for DevUI service collection extensions.
/// Tests verify that workflows and agents can be resolved even when registered non-conventionally.
/// </summary>
public class DevUIExtensionsTests
{
/// <summary>
/// Verifies that AddDevUI throws ArgumentNullException when services collection is null.
/// </summary>
[Fact]
public void AddDevUI_NullServices_ThrowsArgumentNullException()
{
IServiceCollection services = null!;
Assert.Throws<ArgumentNullException>(() => services.AddDevUI());
}
/// <summary>
/// Verifies that GetRequiredKeyedService throws for non-existent keys.
/// </summary>
[Fact]
public void AddDevUI_GetRequiredKeyedServiceNonExistent_ThrowsInvalidOperationException()
{
// Arrange
var services = new ServiceCollection();
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// Act & Assert
Assert.Throws<InvalidOperationException>(() => serviceProvider.GetRequiredKeyedService<AIAgent>("non-existent"));
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_WorkflowWithName_CanBeResolved_AsAIAgent()
{
// Arrange
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
services.AddKeyedSingleton("workflow", workflow);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// Act
var resolvedWorkflowAsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow");
// Assert
Assert.NotNull(resolvedWorkflowAsAgent);
Assert.Null(resolvedWorkflowAsAgent.Name);
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_MultipleWorkflowsWithName_CanBeResolved_AsAIAgent()
{
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow1 = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
var workflow2 = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
services.AddKeyedSingleton("workflow1", workflow1);
services.AddKeyedSingleton("workflow2", workflow2);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
var resolvedWorkflow1AsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow1");
Assert.NotNull(resolvedWorkflow1AsAgent);
Assert.Null(resolvedWorkflow1AsAgent.Name);
var resolvedWorkflow2AsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow2");
Assert.NotNull(resolvedWorkflow2AsAgent);
Assert.Null(resolvedWorkflow2AsAgent.Name);
Assert.False(resolvedWorkflow1AsAgent == resolvedWorkflow2AsAgent);
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_NonKeyedWorkflow_CanBeResolved_AsAIAgent()
{
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow = AgentWorkflowBuilder.BuildSequential(agent1, agent2);
services.AddKeyedSingleton("workflow", workflow);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
var resolvedWorkflowAsAgent = serviceProvider.GetKeyedService<AIAgent>("workflow");
Assert.NotNull(resolvedWorkflowAsAgent);
Assert.Null(resolvedWorkflowAsAgent.Name);
}
/// <summary>
/// Verifies that an agent with null name can be resolved by its workflow.
/// </summary>
[Fact]
public void AddDevUI_NonKeyedWorkflow_PlusKeyedWorkflow_CanBeResolved_AsAIAgent()
{
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null);
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null);
var workflow = AgentWorkflowBuilder.BuildSequential("standardname", agent1, agent2);
var keyedWorkflow = AgentWorkflowBuilder.BuildSequential("keyedname", agent1, agent2);
services.AddSingleton(workflow);
services.AddKeyedSingleton("keyed", keyedWorkflow);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// resolve a workflow with the same name as workflow's name (which is registered without a key)
var standardAgent = serviceProvider.GetKeyedService<AIAgent>("standardname");
Assert.NotNull(standardAgent);
Assert.Equal("standardname", standardAgent.Name);
var keyedAgent = serviceProvider.GetKeyedService<AIAgent>("keyed");
Assert.NotNull(keyedAgent);
Assert.Equal("keyedname", keyedAgent.Name);
var nonExisting = serviceProvider.GetKeyedService<AIAgent>("random-non-existing!!!");
Assert.Null(nonExisting);
}
/// <summary>
/// Verifies that an agent registered with a different key than its name can be resolved by key.
/// </summary>
[Fact]
public void AddDevUI_AgentRegisteredWithDifferentKey_CanBeResolvedByKey()
{
// Arrange
var services = new ServiceCollection();
const string AgentName = "actual-agent-name";
const string RegistrationKey = "different-key";
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, "Test", AgentName);
services.AddKeyedSingleton<AIAgent>(RegistrationKey, agent);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
// Act
var resolvedAgent = serviceProvider.GetKeyedService<AIAgent>(RegistrationKey);
// Assert
Assert.NotNull(resolvedAgent);
// The resolved agent should have the agent's name, not the registration key
Assert.Equal(AgentName, resolvedAgent.Name);
}
/// <summary>
/// Verifies that an agent registered with a different key than its name can be resolved by key.
/// </summary>
[Fact]
public void AddDevUI_Keyed_AndStandard_BothCanBeResolved()
{
// Arrange
var services = new ServiceCollection();
var mockChatClient = new Mock<IChatClient>();
var defaultAgent = new ChatClientAgent(mockChatClient.Object, "default", "default");
var keyedAgent = new ChatClientAgent(mockChatClient.Object, "keyed", "keyed");
services.AddSingleton<AIAgent>(defaultAgent);
services.AddKeyedSingleton<AIAgent>("keyed-registration", keyedAgent);
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
var resolvedKeyedAgent = serviceProvider.GetKeyedService<AIAgent>("keyed-registration");
Assert.NotNull(resolvedKeyedAgent);
Assert.Equal("keyed", resolvedKeyedAgent.Name);
// resolving default agent based on its name, not on the registration-key
var resolvedDefaultAgent = serviceProvider.GetKeyedService<AIAgent>("default");
Assert.NotNull(resolvedDefaultAgent);
Assert.Equal("default", resolvedDefaultAgent.Name);
}
/// <summary>
/// Verifies that the DevUI fallback handler error message includes helpful information.
/// </summary>
[Fact]
public void AddDevUI_InvalidResolution_ErrorMessageIsInformative()
{
// Arrange
var services = new ServiceCollection();
services.AddDevUI();
var serviceProvider = services.BuildServiceProvider();
const string InvalidKey = "invalid-key-name";
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => serviceProvider.GetRequiredKeyedService<AIAgent>(InvalidKey));
}
}
@@ -0,0 +1,285 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.Agents.AI.DevUI.Entities;
using Microsoft.Agents.AI.Workflows;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace Microsoft.Agents.AI.DevUI.UnitTests;
public class DevUIIntegrationTests
{
private sealed class NoOpExecutor(string id) : Executor(id)
{
protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder)
=> protocolBuilder.ConfigureRoutes(routeBuilder =>
routeBuilder.AddHandler<object>((msg, ctx) => ctx.SendMessageAsync(msg)));
}
[Fact]
public async Task TestServerWithDevUI_ResolvesRequestToWorkflow_ByKeyAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
var agent = new ChatClientAgent(mockChatClient.Object, "Test", "agent-name");
builder.Services.AddKeyedSingleton<AIAgent>("registration-key", agent);
builder.Services.AddDevUI(o => o.AllowRemoteAccess = true);
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var resolvedAgent = app.Services.GetKeyedService<AIAgent>("registration-key");
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
Assert.NotNull(discoveryResponse);
Assert.Single(discoveryResponse.Entities);
Assert.Equal("agent-name", discoveryResponse.Entities[0].Name);
}
[Fact]
public async Task TestServerWithDevUI_ResolvesMultipleAIAgents_ByKeyAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-one");
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-two");
var agent3 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-three");
builder.Services.AddKeyedSingleton<AIAgent>("key-1", agent1);
builder.Services.AddKeyedSingleton<AIAgent>("key-2", agent2);
builder.Services.AddKeyedSingleton<AIAgent>("key-3", agent3);
builder.Services.AddDevUI(o => o.AllowRemoteAccess = true);
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-one" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-two" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-three" && e.Type == "agent");
}
[Fact]
public async Task TestServerWithDevUI_ResolvesAIAgents_WithKeyedAndDefaultRegistrationAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
var agentKeyed1 = new ChatClientAgent(mockChatClient.Object, "Test", "keyed-agent-one");
var agentKeyed2 = new ChatClientAgent(mockChatClient.Object, "Test", "keyed-agent-two");
var agentDefault = new ChatClientAgent(mockChatClient.Object, "Test", "default-agent");
builder.Services.AddKeyedSingleton<AIAgent>("key-1", agentKeyed1);
builder.Services.AddKeyedSingleton<AIAgent>("key-2", agentKeyed2);
builder.Services.AddSingleton<AIAgent>(agentDefault);
builder.Services.AddDevUI(o => o.AllowRemoteAccess = true);
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-agent-one" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-agent-two" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-agent" && e.Type == "agent");
}
[Fact]
public async Task TestServerWithDevUI_ResolvesMultipleWorkflows_ByKeyAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var workflow1 = new WorkflowBuilder("executor-1")
.WithName("workflow-one")
.WithDescription("First workflow")
.BindExecutor(new NoOpExecutor("executor-1"))
.Build();
var workflow2 = new WorkflowBuilder("executor-2")
.WithName("workflow-two")
.WithDescription("Second workflow")
.BindExecutor(new NoOpExecutor("executor-2"))
.Build();
var workflow3 = new WorkflowBuilder("executor-3")
.WithName("workflow-three")
.WithDescription("Third workflow")
.BindExecutor(new NoOpExecutor("executor-3"))
.Build();
builder.Services.AddKeyedSingleton("key-1", workflow1);
builder.Services.AddKeyedSingleton("key-2", workflow2);
builder.Services.AddKeyedSingleton("key-3", workflow3);
builder.Services.AddDevUI(o => o.AllowRemoteAccess = true);
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-one" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-two" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-three" && e.Type == "workflow");
}
[Fact(Skip = "Flaky in merge_group; see https://github.com/microsoft/agent-framework/issues/5845")]
public async Task TestServerWithDevUI_ResolvesWorkflows_WithKeyedAndDefaultRegistrationAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var workflowKeyed1 = new WorkflowBuilder("executor-1")
.WithName("keyed-workflow-one")
.BindExecutor(new NoOpExecutor("executor-1"))
.Build();
var workflowKeyed2 = new WorkflowBuilder("executor-2")
.WithName("keyed-workflow-two")
.BindExecutor(new NoOpExecutor("executor-2"))
.Build();
var workflowDefault = new WorkflowBuilder("executor-default")
.WithName("default-workflow")
.BindExecutor(new NoOpExecutor("executor-default"))
.Build();
builder.Services.AddKeyedSingleton("key-1", workflowKeyed1);
builder.Services.AddKeyedSingleton("key-2", workflowKeyed2);
builder.Services.AddSingleton(workflowDefault);
builder.Services.AddDevUI(o => o.AllowRemoteAccess = true);
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(3, discoveryResponse.Entities.Count);
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-workflow-one" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-workflow-two" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-workflow" && e.Type == "workflow");
}
[Fact(Skip = "Flaky in merge_group; see https://github.com/microsoft/agent-framework/issues/5845")]
public async Task TestServerWithDevUI_ResolvesMixedAgentsAndWorkflows_AllRegistrationsAsync()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockChatClient = new Mock<IChatClient>();
// Create AIAgents
var agent1 = new ChatClientAgent(mockChatClient.Object, "Test", "mixed-agent-one");
var agent2 = new ChatClientAgent(mockChatClient.Object, "Test", "mixed-agent-two");
var agentDefault = new ChatClientAgent(mockChatClient.Object, "Test", "default-mixed-agent");
// Create Workflows
var workflow1 = new WorkflowBuilder("executor-1")
.WithName("mixed-workflow-one")
.BindExecutor(new NoOpExecutor("executor-1"))
.Build();
var workflow2 = new WorkflowBuilder("executor-2")
.WithName("mixed-workflow-two")
.BindExecutor(new NoOpExecutor("executor-2"))
.Build();
var workflowDefault = new WorkflowBuilder("executor-default")
.WithName("default-mixed-workflow")
.BindExecutor(new NoOpExecutor("executor-default"))
.Build();
// Register all
builder.Services.AddKeyedSingleton<AIAgent>("agent-key-1", agent1);
builder.Services.AddKeyedSingleton<AIAgent>("agent-key-2", agent2);
builder.Services.AddSingleton<AIAgent>(agentDefault);
builder.Services.AddKeyedSingleton("workflow-key-1", workflow1);
builder.Services.AddKeyedSingleton("workflow-key-2", workflow2);
builder.Services.AddSingleton(workflowDefault);
builder.Services.AddDevUI(o => o.AllowRemoteAccess = true);
using WebApplication app = builder.Build();
app.MapDevUI();
await app.StartAsync();
// Act
var client = app.GetTestClient();
var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative));
var discoveryResponse = await response.Content.ReadFromJsonAsync<DiscoveryResponse>();
// Assert
Assert.NotNull(discoveryResponse);
Assert.Equal(6, discoveryResponse.Entities.Count);
// Verify agents
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-agent-one" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-agent-two" && e.Type == "agent");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-mixed-agent" && e.Type == "agent");
// Verify workflows
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-workflow-one" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-workflow-two" && e.Type == "workflow");
Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-mixed-workflow" && e.Type == "workflow");
}
}
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CA1812</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.DevUI\Microsoft.Agents.AI.DevUI.csproj" />
</ItemGroup>
</Project>