chore: import upstream snapshot with attribution
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
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
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
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+559
@@ -0,0 +1,559 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for A2AEndpointRouteBuilderExtensions and A2AServerServiceCollectionExtensions methods.
|
||||
/// </summary>
|
||||
public sealed class A2AEndpointRouteBuilderExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws ArgumentNullException for null endpoints.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithAgentBuilder_NullEndpoints_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AspNetCore.Routing.IEndpointRouteBuilder endpoints = null!;
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
endpoints.MapA2AHttpJson(agentBuilder, "/a2a"));
|
||||
|
||||
Assert.Equal("endpoints", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws ArgumentNullException for null agentBuilder.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithAgentBuilder_NullAgentBuilder_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
IHostedAgentBuilder agentBuilder = null!;
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
app.MapA2AHttpJson(agentBuilder, "/a2a"));
|
||||
|
||||
Assert.Equal("agentBuilder", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson with IHostedAgentBuilder correctly maps the agent with default configuration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithAgentBuilder_DefaultConfiguration_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
agentBuilder.AddA2AServer();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2AHttpJson(agentBuilder, "/a2a");
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson with string agent name correctly maps the agent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithAgentName_DefaultConfiguration_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddA2AServer("agent");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2AHttpJson("agent", "/a2a");
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AJsonRpc with IHostedAgentBuilder correctly maps the agent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AJsonRpc_WithAgentBuilder_DefaultConfiguration_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
agentBuilder.AddA2AServer();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2AJsonRpc(agentBuilder, "/a2a");
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AJsonRpc with string agent name correctly maps the agent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AJsonRpc_WithAgentName_DefaultConfiguration_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddA2AServer("agent");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2AJsonRpc("agent", "/a2a");
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that both MapA2AHttpJson and MapA2AJsonRpc can be called for the same agent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_And_MapA2AJsonRpc_SameAgent_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
agentBuilder.AddA2AServer();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var httpResult = app.MapA2AHttpJson(agentBuilder, "/a2a");
|
||||
var rpcResult = app.MapA2AJsonRpc(agentBuilder, "/a2a");
|
||||
Assert.NotNull(httpResult);
|
||||
Assert.NotNull(rpcResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that multiple agents can be mapped to different paths.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_MultipleAgents_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agent1Builder = builder.AddAIAgent("agent1", "Instructions1", chatClientServiceKey: "chat-client");
|
||||
IHostedAgentBuilder agent2Builder = builder.AddAIAgent("agent2", "Instructions2", chatClientServiceKey: "chat-client");
|
||||
agent1Builder.AddA2AServer();
|
||||
agent2Builder.AddA2AServer();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
app.MapA2AHttpJson(agent1Builder, "/a2a/agent1");
|
||||
app.MapA2AHttpJson(agent2Builder, "/a2a/agent2");
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that custom paths can be specified for A2A endpoints.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithCustomPath_AcceptsValidPath()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
agentBuilder.AddA2AServer();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
app.MapA2AHttpJson(agentBuilder, "/custom/a2a/path");
|
||||
Assert.NotNull(app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer with custom A2AServerRegistrationOptions succeeds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_WithCustomOptions_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
agentBuilder.AddA2AServer(options => options.AgentRunMode = AgentRunMode.AllowBackgroundIfSupported);
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
var result = app.MapA2AHttpJson(agentBuilder, "/a2a");
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws ArgumentNullException for null endpoints when using string agent name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithAgentName_NullEndpoints_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AspNetCore.Routing.IEndpointRouteBuilder endpoints = null!;
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
endpoints.MapA2AHttpJson("agent", "/a2a"));
|
||||
|
||||
Assert.Equal("endpoints", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AJsonRpc throws ArgumentNullException for null endpoints when using string agent name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AJsonRpc_WithAgentName_NullEndpoints_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AspNetCore.Routing.IEndpointRouteBuilder endpoints = null!;
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
endpoints.MapA2AJsonRpc("agent", "/a2a"));
|
||||
|
||||
Assert.Equal("endpoints", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws ArgumentNullException for null agentName.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithAgentName_NullAgentName_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
app.MapA2AHttpJson((string)null!, "/a2a"));
|
||||
|
||||
Assert.Equal("agentName", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws ArgumentException for empty agentName.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithAgentName_EmptyAgentName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert
|
||||
ArgumentException exception = Assert.Throws<ArgumentException>(() =>
|
||||
app.MapA2AHttpJson(string.Empty, "/a2a"));
|
||||
|
||||
Assert.Equal("agentName", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws ArgumentNullException for null path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_NullPath_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
agentBuilder.AddA2AServer();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
app.MapA2AHttpJson(agentBuilder, null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws ArgumentException for whitespace-only path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WhitespacePath_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
agentBuilder.AddA2AServer();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
app.MapA2AHttpJson(agentBuilder, " "));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer throws ArgumentNullException for null services.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_NullServices_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
IServiceCollection services = null!;
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
services.AddA2AServer("agent"));
|
||||
|
||||
Assert.Equal("services", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer throws ArgumentNullException for null agentName.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_NullAgentName_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
IServiceCollection services = new ServiceCollection();
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
services.AddA2AServer((string)null!));
|
||||
|
||||
Assert.Equal("agentName", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer throws ArgumentException for empty agentName.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_EmptyAgentName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
IServiceCollection services = new ServiceCollection();
|
||||
|
||||
// Act & Assert
|
||||
ArgumentException exception = Assert.Throws<ArgumentException>(() =>
|
||||
services.AddA2AServer(string.Empty));
|
||||
|
||||
Assert.Equal("agentName", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer on IHostedAgentBuilder throws ArgumentNullException for null builder.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_NullAgentBuilder_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
IHostedAgentBuilder agentBuilder = null!;
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
agentBuilder.AddA2AServer());
|
||||
|
||||
Assert.Equal("agentBuilder", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws ArgumentNullException for null AIAgent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithAIAgent_NullAgent_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
AIAgent agent = null!;
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
app.MapA2AHttpJson(agent, "/a2a"));
|
||||
|
||||
Assert.Equal("agent", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws ArgumentNullException for AIAgent with null Name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithAIAgent_NullName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
var agentMock = new Mock<AIAgent>();
|
||||
agentMock.Setup(a => a.Name).Returns((string?)null);
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
app.MapA2AHttpJson(agentMock.Object, "/a2a"));
|
||||
|
||||
Assert.Equal("agent.Name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws ArgumentException for AIAgent with whitespace Name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithAIAgent_WhitespaceName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
var agentMock = new Mock<AIAgent>();
|
||||
agentMock.Setup(a => a.Name).Returns(" ");
|
||||
|
||||
// Act & Assert
|
||||
ArgumentException exception = Assert.Throws<ArgumentException>(() =>
|
||||
app.MapA2AHttpJson(agentMock.Object, "/a2a"));
|
||||
|
||||
Assert.Equal("agent.Name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AJsonRpc throws ArgumentNullException for null AIAgent.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AJsonRpc_WithAIAgent_NullAgent_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
AIAgent agent = null!;
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
app.MapA2AJsonRpc(agent, "/a2a"));
|
||||
|
||||
Assert.Equal("agent", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AJsonRpc throws ArgumentNullException for AIAgent with null Name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AJsonRpc_WithAIAgent_NullName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
var agentMock = new Mock<AIAgent>();
|
||||
agentMock.Setup(a => a.Name).Returns((string?)null);
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
app.MapA2AJsonRpc(agentMock.Object, "/a2a"));
|
||||
|
||||
Assert.Equal("agent.Name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AJsonRpc throws ArgumentException for AIAgent with whitespace Name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AJsonRpc_WithAIAgent_WhitespaceName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
var agentMock = new Mock<AIAgent>();
|
||||
agentMock.Setup(a => a.Name).Returns(" ");
|
||||
|
||||
// Act & Assert
|
||||
ArgumentException exception = Assert.Throws<ArgumentException>(() =>
|
||||
app.MapA2AJsonRpc(agentMock.Object, "/a2a"));
|
||||
|
||||
Assert.Equal("agent.Name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AHttpJson throws InvalidOperationException when no A2AServer has been
|
||||
/// registered for the specified agent via AddA2AServer.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AHttpJson_WithoutAddA2AServer_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert
|
||||
InvalidOperationException exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
app.MapA2AHttpJson("agent", "/a2a"));
|
||||
|
||||
Assert.Contains("agent", exception.Message);
|
||||
Assert.Contains("AddA2AServer", exception.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that MapA2AJsonRpc throws InvalidOperationException when no A2AServer has been
|
||||
/// registered for the specified agent via AddA2AServer.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapA2AJsonRpc_WithoutAddA2AServer_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
IChatClient mockChatClient = new DummyChatClient();
|
||||
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
|
||||
builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
|
||||
builder.Services.AddLogging();
|
||||
using WebApplication app = builder.Build();
|
||||
|
||||
// Act & Assert
|
||||
InvalidOperationException exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
app.MapA2AJsonRpc("agent", "/a2a"));
|
||||
|
||||
Assert.Contains("agent", exception.Message);
|
||||
Assert.Contains("AddA2AServer", exception.Message);
|
||||
}
|
||||
}
|
||||
+459
@@ -0,0 +1,459 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="A2AServerServiceCollectionExtensions"/> class.
|
||||
/// </summary>
|
||||
public sealed class A2AServerServiceCollectionExtensionsTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer with an agent name registers a keyed A2AServer
|
||||
/// that can be resolved from the service provider.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithAgentName_ResolvesKeyedA2AServerAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "test-agent";
|
||||
var services = new ServiceCollection();
|
||||
services.AddKeyedSingleton(AgentName, (_, _) => CreateAgentMock(AgentName).Object);
|
||||
|
||||
// Act
|
||||
services.AddA2AServer(AgentName);
|
||||
|
||||
// Assert
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetKeyedService<A2AServer>(AgentName);
|
||||
Assert.NotNull(server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer with an agent instance registers a keyed A2AServer
|
||||
/// that can be resolved from the service provider using the agent's name.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithAgentInstance_ResolvesKeyedA2AServerAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "instance-agent";
|
||||
var agentMock = CreateAgentMock(AgentName);
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Act
|
||||
services.AddA2AServer(agentMock.Object);
|
||||
|
||||
// Assert
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetKeyedService<A2AServer>(AgentName);
|
||||
Assert.NotNull(server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when no ITaskStore or AgentSessionStore are registered,
|
||||
/// AddA2AServer falls back to noop session store default and resolves successfully.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithNoCustomStores_FallsBackToNoopSessionStoreDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "default-stores-agent";
|
||||
var services = new ServiceCollection();
|
||||
services.AddKeyedSingleton(AgentName, (_, _) => CreateAgentMock(AgentName).Object);
|
||||
|
||||
// Act
|
||||
services.AddA2AServer(AgentName);
|
||||
|
||||
// Assert - resolution succeeds without any stores registered
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetKeyedService<A2AServer>(AgentName);
|
||||
Assert.NotNull(server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when a custom ITaskStore is registered, AddA2AServer uses it
|
||||
/// instead of the default InMemoryTaskStore.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithCustomTaskStore_ResolvesSuccessfullyAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "custom-taskstore-agent";
|
||||
var services = new ServiceCollection();
|
||||
services.AddKeyedSingleton(AgentName, (_, _) => CreateAgentMock(AgentName).Object);
|
||||
|
||||
var mockTaskStore = new Mock<ITaskStore>();
|
||||
services.AddKeyedSingleton(AgentName, mockTaskStore.Object);
|
||||
|
||||
// Act
|
||||
services.AddA2AServer(AgentName);
|
||||
|
||||
// Assert
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetKeyedService<A2AServer>(AgentName);
|
||||
Assert.NotNull(server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when a custom AgentSessionStore is registered, AddA2AServer uses it
|
||||
/// instead of the default InMemoryAgentSessionStore.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithCustomAgentSessionStore_ResolvesSuccessfullyAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "custom-sessionstore-agent";
|
||||
var services = new ServiceCollection();
|
||||
services.AddKeyedSingleton(AgentName, (_, _) => CreateAgentMock(AgentName).Object);
|
||||
|
||||
var mockSessionStore = new Mock<AgentSessionStore>();
|
||||
services.AddKeyedSingleton(AgentName, mockSessionStore.Object);
|
||||
|
||||
// Act
|
||||
services.AddA2AServer(AgentName);
|
||||
|
||||
// Assert
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetKeyedService<A2AServer>(AgentName);
|
||||
Assert.NotNull(server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when a custom IAgentHandler is registered, AddA2AServer uses it
|
||||
/// instead of creating a default A2AAgentHandler.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithCustomAgentHandler_ResolvesSuccessfullyAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "custom-handler-agent";
|
||||
var services = new ServiceCollection();
|
||||
services.AddKeyedSingleton(AgentName, (_, _) => CreateAgentMock(AgentName).Object);
|
||||
|
||||
var mockHandler = new Mock<IAgentHandler>();
|
||||
services.AddKeyedSingleton(AgentName, mockHandler.Object);
|
||||
|
||||
// Act
|
||||
services.AddA2AServer(AgentName);
|
||||
|
||||
// Assert
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetKeyedService<A2AServer>(AgentName);
|
||||
Assert.NotNull(server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the configureOptions callback is invoked when provided.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithConfigureOptions_InvokesCallbackAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "options-agent";
|
||||
var services = new ServiceCollection();
|
||||
services.AddKeyedSingleton(AgentName, (_, _) => CreateAgentMock(AgentName).Object);
|
||||
|
||||
bool callbackInvoked = false;
|
||||
|
||||
// Act
|
||||
services.AddA2AServer(AgentName, options =>
|
||||
{
|
||||
callbackInvoked = true;
|
||||
options.AgentRunMode = AgentRunMode.AllowBackgroundIfSupported;
|
||||
});
|
||||
|
||||
// Assert - callback is invoked during resolution
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetKeyedService<A2AServer>(AgentName);
|
||||
Assert.NotNull(server);
|
||||
Assert.True(callbackInvoked);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer with a null configureOptions does not throw.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithNullConfigureOptions_ResolvesSuccessfullyAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "null-options-agent";
|
||||
var services = new ServiceCollection();
|
||||
services.AddKeyedSingleton(AgentName, (_, _) => CreateAgentMock(AgentName).Object);
|
||||
|
||||
// Act
|
||||
services.AddA2AServer(AgentName, configureOptions: null);
|
||||
|
||||
// Assert
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetKeyedService<A2AServer>(AgentName);
|
||||
Assert.NotNull(server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer throws when the agent name is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_WithNullAgentName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Act & Assert
|
||||
Assert.ThrowsAny<ArgumentException>(() => services.AddA2AServer(agentName: null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer throws when the agent name is whitespace.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_WithWhitespaceAgentName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Act & Assert
|
||||
Assert.ThrowsAny<ArgumentException>(() => services.AddA2AServer(agentName: " "));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer throws when the services parameter is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_WithNullServices_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
IServiceCollection services = null!;
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => services.AddA2AServer("agent"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer with an agent instance throws when the agent is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_WithNullAgent_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => services.AddA2AServer(agent: null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer with an agent instance throws when the agent's Name is null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_WithAgent_NullName_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var agentMock = new Mock<AIAgent>();
|
||||
agentMock.Setup(a => a.Name).Returns((string?)null);
|
||||
|
||||
// Act & Assert
|
||||
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
services.AddA2AServer(agentMock.Object));
|
||||
|
||||
Assert.Equal("agent.Name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AddA2AServer with an agent instance throws when the agent's Name is whitespace.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AddA2AServer_WithAgent_WhitespaceName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
var services = new ServiceCollection();
|
||||
var agentMock = new Mock<AIAgent>();
|
||||
agentMock.Setup(a => a.Name).Returns(" ");
|
||||
|
||||
// Act & Assert
|
||||
ArgumentException exception = Assert.Throws<ArgumentException>(() =>
|
||||
services.AddA2AServer(agentMock.Object));
|
||||
|
||||
Assert.Equal("agent.Name", exception.ParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when a custom <see cref="IAgentHandler"/> is registered as a keyed service,
|
||||
/// the <see cref="A2AServer"/> uses it to process requests instead of the default handler.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithCustomHandler_CustomHandlerIsInvokedOnRequestAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "custom-handler-wiring";
|
||||
var services = new ServiceCollection();
|
||||
services.AddKeyedSingleton(AgentName, (_, _) => CreateAgentMock(AgentName).Object);
|
||||
|
||||
var mockHandler = new Mock<IAgentHandler>();
|
||||
mockHandler
|
||||
.Setup(h => h.ExecuteAsync(
|
||||
It.IsAny<RequestContext>(),
|
||||
It.IsAny<AgentEventQueue>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns((RequestContext _, AgentEventQueue eq, CancellationToken ct) =>
|
||||
eq.EnqueueMessageAsync(
|
||||
new Message { MessageId = "resp", Role = Role.Agent, Parts = [new Part { Text = "Reply" }] }, ct).AsTask());
|
||||
|
||||
services.AddKeyedSingleton(AgentName, mockHandler.Object);
|
||||
|
||||
services.AddA2AServer(AgentName);
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetRequiredKeyedService<A2AServer>(AgentName);
|
||||
|
||||
// Act
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var response = await server.SendMessageAsync(CreateTestSendMessageRequest(), cts.Token);
|
||||
|
||||
// Assert - the custom handler was invoked, not the default A2AAgentHandler
|
||||
mockHandler.Verify(
|
||||
h => h.ExecuteAsync(
|
||||
It.IsAny<RequestContext>(),
|
||||
It.IsAny<AgentEventQueue>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
Assert.Equal(SendMessageResponseCase.Message, response.PayloadCase);
|
||||
Assert.NotNull(response.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when a custom <see cref="AgentSessionStore"/> is registered as a keyed service
|
||||
/// and no custom <see cref="IAgentHandler"/> is registered, the default handler uses the custom
|
||||
/// session store for session management during request processing.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithCustomSessionStore_NoHandler_SessionStoreIsUsedOnRequestAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "custom-sessionstore-wiring";
|
||||
var services = new ServiceCollection();
|
||||
services.AddKeyedSingleton(AgentName, (_, _) => CreateAgentMock(AgentName).Object);
|
||||
|
||||
var mockSessionStore = new Mock<AgentSessionStore>();
|
||||
mockSessionStore
|
||||
.Setup(x => x.GetSessionAsync(
|
||||
It.IsAny<AIAgent>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new TestAgentSession());
|
||||
mockSessionStore
|
||||
.Setup(x => x.SaveSessionAsync(
|
||||
It.IsAny<AIAgent>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<AgentSession>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(ValueTask.CompletedTask);
|
||||
|
||||
services.AddKeyedSingleton(AgentName, mockSessionStore.Object);
|
||||
|
||||
services.AddA2AServer(AgentName);
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetRequiredKeyedService<A2AServer>(AgentName);
|
||||
|
||||
// Act
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var response = await server.SendMessageAsync(CreateTestSendMessageRequest(), cts.Token);
|
||||
|
||||
// Assert - the custom session store was used, not InMemoryAgentSessionStore
|
||||
mockSessionStore.Verify(
|
||||
x => x.GetSessionAsync(
|
||||
It.IsAny<AIAgent>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
Assert.Equal(SendMessageResponseCase.Message, response.PayloadCase);
|
||||
Assert.NotNull(response.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when no custom stores or handlers are registered, the server uses
|
||||
/// the default noop session store and processes requests successfully end-to-end.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddA2AServer_WithNoCustomStores_DefaultStoresProcessRequestSuccessfullyAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "default-stores-request";
|
||||
var services = new ServiceCollection();
|
||||
services.AddKeyedSingleton(AgentName, (_, _) => CreateAgentMockForRequests(AgentName).Object);
|
||||
|
||||
services.AddA2AServer(AgentName);
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var server = provider.GetRequiredKeyedService<A2AServer>(AgentName);
|
||||
|
||||
// Act
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var response = await server.SendMessageAsync(CreateTestSendMessageRequest(), cts.Token);
|
||||
|
||||
// Assert - request was processed successfully with default noop session store
|
||||
Assert.NotNull(response);
|
||||
Assert.Equal(SendMessageResponseCase.Message, response.PayloadCase);
|
||||
Assert.NotNull(response.Message);
|
||||
}
|
||||
|
||||
private static SendMessageRequest CreateTestSendMessageRequest() =>
|
||||
new()
|
||||
{
|
||||
Message = new Message
|
||||
{
|
||||
MessageId = "test-id",
|
||||
Role = Role.User,
|
||||
Parts = [new Part { Text = "Hello" }]
|
||||
}
|
||||
};
|
||||
|
||||
private static Mock<AIAgent> CreateAgentMock(string name)
|
||||
{
|
||||
Mock<AIAgent> agentMock = new() { CallBase = true };
|
||||
agentMock.SetupGet(x => x.Name).Returns(name);
|
||||
agentMock
|
||||
.Protected()
|
||||
.Setup<ValueTask<AgentSession>>("CreateSessionCoreAsync", ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new TestAgentSession());
|
||||
agentMock
|
||||
.Protected()
|
||||
.Setup<Task<AgentResponse>>("RunCoreAsync",
|
||||
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
|
||||
ItExpr.IsAny<AgentSession?>(),
|
||||
ItExpr.IsAny<AgentRunOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(new AgentResponse([new ChatMessage(ChatRole.Assistant, "Test response")]));
|
||||
|
||||
return agentMock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock <see cref="AIAgent"/> with session serialization support, suitable for
|
||||
/// tests that exercise the full request processing path with <see cref="InMemoryAgentSessionStore"/>.
|
||||
/// </summary>
|
||||
private static Mock<AIAgent> CreateAgentMockForRequests(string name)
|
||||
{
|
||||
Mock<AIAgent> agentMock = CreateAgentMock(name);
|
||||
agentMock
|
||||
.Protected()
|
||||
.Setup<ValueTask<JsonElement>>("SerializeSessionCoreAsync",
|
||||
ItExpr.IsAny<AgentSession>(),
|
||||
ItExpr.IsAny<JsonSerializerOptions?>(),
|
||||
ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(JsonDocument.Parse("{}").RootElement);
|
||||
|
||||
return agentMock;
|
||||
}
|
||||
|
||||
private sealed class TestAgentSession : AgentSession;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AgentRunMode"/> class.
|
||||
/// </summary>
|
||||
public sealed class AgentRunModeTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that AllowBackgroundWhen throws ArgumentNullException for null delegate.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AllowBackgroundWhen_NullDelegate_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
AgentRunMode.AllowBackgroundWhen(null!));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that DisallowBackground equals another DisallowBackground instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_DisallowBackground_AreEqual()
|
||||
{
|
||||
// Arrange
|
||||
var mode1 = AgentRunMode.DisallowBackground;
|
||||
var mode2 = AgentRunMode.DisallowBackground;
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(mode1.Equals(mode2));
|
||||
Assert.True(mode1 == mode2);
|
||||
Assert.False(mode1 != mode2);
|
||||
Assert.Equal(mode1.GetHashCode(), mode2.GetHashCode());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that AllowBackgroundIfSupported equals another AllowBackgroundIfSupported instance.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_AllowBackgroundIfSupported_AreEqual()
|
||||
{
|
||||
// Arrange
|
||||
var mode1 = AgentRunMode.AllowBackgroundIfSupported;
|
||||
var mode2 = AgentRunMode.AllowBackgroundIfSupported;
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(mode1.Equals(mode2));
|
||||
Assert.True(mode1 == mode2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that DisallowBackground and AllowBackgroundIfSupported are not equal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_DifferentModes_AreNotEqual()
|
||||
{
|
||||
// Arrange
|
||||
var disallow = AgentRunMode.DisallowBackground;
|
||||
var allow = AgentRunMode.AllowBackgroundIfSupported;
|
||||
|
||||
// Act & Assert
|
||||
Assert.False(disallow.Equals(allow));
|
||||
Assert.False(disallow == allow);
|
||||
Assert.True(disallow != allow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Equals returns false for null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_Null_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var mode = AgentRunMode.DisallowBackground;
|
||||
|
||||
// Act & Assert
|
||||
Assert.False(mode.Equals(null));
|
||||
Assert.False(mode.Equals((object?)null));
|
||||
Assert.False(mode == null);
|
||||
Assert.True(mode != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that two null AgentRunMode values are equal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_BothNull_AreEqual()
|
||||
{
|
||||
// Arrange
|
||||
AgentRunMode? mode1 = null;
|
||||
AgentRunMode? mode2 = null;
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(mode1 == mode2);
|
||||
Assert.False(mode1 != mode2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that ToString returns expected values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ToString_ReturnsExpectedValues()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Equal("message", AgentRunMode.DisallowBackground.ToString());
|
||||
Assert.Equal("task", AgentRunMode.AllowBackgroundIfSupported.ToString());
|
||||
Assert.Equal("dynamic", AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(true)).ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that Equals works correctly with object parameter.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_WithObjectParameter_WorksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var mode = AgentRunMode.DisallowBackground;
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(mode.Equals((object)AgentRunMode.DisallowBackground));
|
||||
Assert.False(mode.Equals((object)AgentRunMode.AllowBackgroundIfSupported));
|
||||
Assert.False(mode.Equals("not a run mode"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that two AllowBackgroundWhen instances with different delegates are not considered equal,
|
||||
/// because equality includes delegate identity for dynamic modes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_AllowBackgroundWhen_DifferentDelegates_AreNotEqual()
|
||||
{
|
||||
// Arrange
|
||||
var mode1 = AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(true));
|
||||
var mode2 = AgentRunMode.AllowBackgroundWhen((_, _) => ValueTask.FromResult(false));
|
||||
|
||||
// Act & Assert
|
||||
Assert.False(mode1.Equals(mode2));
|
||||
Assert.True(mode1 != mode2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that two AllowBackgroundWhen instances with the same delegate are considered equal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Equals_AllowBackgroundWhen_SameDelegate_AreEqual()
|
||||
{
|
||||
// Arrange
|
||||
static ValueTask<bool> CallbackAsync(A2ARunDecisionContext _, CancellationToken __) => ValueTask.FromResult(true);
|
||||
var mode1 = AgentRunMode.AllowBackgroundWhen(CallbackAsync);
|
||||
var mode2 = AgentRunMode.AllowBackgroundWhen(CallbackAsync);
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(mode1.Equals(mode2));
|
||||
Assert.True(mode1 == mode2);
|
||||
Assert.Equal(mode1.GetHashCode(), mode2.GetHashCode());
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using A2A;
|
||||
using Microsoft.Agents.AI.Hosting.A2A.Converters;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests.Converters;
|
||||
|
||||
public class MessageConverterTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToChatMessages_SendMessageRequest_Null_ReturnsEmptyCollection()
|
||||
{
|
||||
SendMessageRequest? sendMessageRequest = null;
|
||||
|
||||
var result = sendMessageRequest!.ToChatMessages();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToChatMessages_SendMessageRequest_WithNullMessage_ReturnsEmptyCollection()
|
||||
{
|
||||
var sendMessageRequest = new SendMessageRequest
|
||||
{
|
||||
Message = null!
|
||||
};
|
||||
|
||||
var result = sendMessageRequest.ToChatMessages();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToChatMessages_SendMessageRequest_WithMessageWithoutParts_ReturnsEmptyCollection()
|
||||
{
|
||||
var sendMessageRequest = new SendMessageRequest
|
||||
{
|
||||
Message = new Message
|
||||
{
|
||||
MessageId = "test-id",
|
||||
Role = Role.User,
|
||||
Parts = null!
|
||||
}
|
||||
};
|
||||
|
||||
var result = sendMessageRequest.ToChatMessages();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToChatMessages_SendMessageRequest_WithValidTextMessage_ReturnsCorrectChatMessage()
|
||||
{
|
||||
var sendMessageRequest = new SendMessageRequest
|
||||
{
|
||||
Message = new Message
|
||||
{
|
||||
MessageId = "test-id",
|
||||
Role = Role.User,
|
||||
Parts =
|
||||
[
|
||||
new Part { Text = "Hello, world!" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var result = sendMessageRequest.ToChatMessages();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
|
||||
var chatMessage = result.First();
|
||||
Assert.Equal("test-id", chatMessage.MessageId);
|
||||
Assert.Equal(ChatRole.User, chatMessage.Role);
|
||||
Assert.Single(chatMessage.Contents);
|
||||
|
||||
var textContent = Assert.IsType<TextContent>(chatMessage.Contents.First());
|
||||
Assert.Equal("Hello, world!", textContent.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToParts_NullList_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
IList<ChatMessage>? messages = null;
|
||||
|
||||
// Act
|
||||
var result = messages!.ToParts();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToParts_EmptyList_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
IList<ChatMessage> messages = [];
|
||||
|
||||
// Act
|
||||
var result = messages.ToParts();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToParts_WithTextContent_ReturnsTextPart()
|
||||
{
|
||||
// Arrange
|
||||
IList<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, "Hello from the agent!")
|
||||
];
|
||||
|
||||
// Act
|
||||
var result = messages.ToParts();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Hello from the agent!", result[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToParts_WithMultipleMessages_ReturnsAllParts()
|
||||
{
|
||||
// Arrange
|
||||
IList<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First message"),
|
||||
new ChatMessage(ChatRole.Assistant, "Second message")
|
||||
];
|
||||
|
||||
// Act
|
||||
var result = messages.ToParts();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("First message", result[0].Text);
|
||||
Assert.Equal("Second message", result[1].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToParts_AgentResponseUpdate_WithNoContents_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var update = new AgentResponseUpdate();
|
||||
|
||||
// Act
|
||||
var result = update.ToParts();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToParts_AgentResponseUpdate_WithTextContent_ReturnsTextPart()
|
||||
{
|
||||
// Arrange
|
||||
var update = new AgentResponseUpdate(ChatRole.Assistant, "Hello from streaming!");
|
||||
|
||||
// Act
|
||||
var result = update.ToParts();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Hello from streaming!", result[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToParts_AgentResponseUpdate_WithMultipleContents_ReturnsAllParts()
|
||||
{
|
||||
// Arrange
|
||||
var update = new AgentResponseUpdate(ChatRole.Assistant, [
|
||||
new TextContent("First chunk"),
|
||||
new TextContent("Second chunk")
|
||||
]);
|
||||
|
||||
// Act
|
||||
var result = update.ToParts();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("First chunk", result[0].Text);
|
||||
Assert.Equal("Second chunk", result[1].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToParts_AgentResponseUpdate_WithUnsupportedContent_FiltersOutNulls()
|
||||
{
|
||||
// Arrange - FunctionCallContent maps to null Part since it's not a supported A2A content type
|
||||
var update = new AgentResponseUpdate(ChatRole.Assistant, [
|
||||
new TextContent("Supported text"),
|
||||
new FunctionCallContent("call-1", "myFunction")
|
||||
]);
|
||||
|
||||
// Act
|
||||
var result = update.ToParts();
|
||||
|
||||
// Assert - only the text part should be returned
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Supported text", result[0].Text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal;
|
||||
|
||||
internal sealed class DummyChatClient : IChatClient
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public object? GetService(Type serviceType, object? serviceKey = null) =>
|
||||
serviceType.IsInstanceOfType(this) ? this : null;
|
||||
|
||||
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.A2A.AspNetCore\Microsoft.Agents.AI.Hosting.A2A.AspNetCore.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.A2A\Microsoft.Agents.AI.Hosting.A2A.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user