Merge branch 'main' into feature-foundry-agents

This commit is contained in:
Chris
2025-11-07 09:14:33 -08:00
committed by GitHub
50 changed files with 3808 additions and 284 deletions
@@ -5,7 +5,7 @@ using A2A;
using Microsoft.Agents.AI.Hosting.A2A.Converters;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.A2A.Tests.Converters;
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests.Converters;
public class MessageConverterTests
{
@@ -0,0 +1,502 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using A2A;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests;
/// <summary>
/// Tests for MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions.MapA2A method.
/// </summary>
public sealed class EndpointRouteA2ABuilderExtensionsTests
{
/// <summary>
/// Verifies that MapA2A throws ArgumentNullException for null endpoints.
/// </summary>
[Fact]
public void MapA2A_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.MapA2A(agentBuilder, "/a2a"));
Assert.Equal("endpoints", exception.ParamName);
}
/// <summary>
/// Verifies that MapA2A throws ArgumentNullException for null agentBuilder.
/// </summary>
[Fact]
public void MapA2A_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.MapA2A(agentBuilder, "/a2a"));
Assert.Equal("agentBuilder", exception.ParamName);
}
/// <summary>
/// Verifies that MapA2A with IHostedAgentBuilder correctly maps the agent with default task manager configuration.
/// </summary>
[Fact]
public void MapA2A_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");
builder.Services.AddLogging();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
var result = app.MapA2A(agentBuilder, "/a2a");
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A with IHostedAgentBuilder and custom task manager configuration succeeds.
/// </summary>
[Fact]
public void MapA2A_WithAgentBuilder_CustomTaskManagerConfiguration_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");
builder.Services.AddLogging();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
var result = app.MapA2A(agentBuilder, "/a2a", taskManager => { });
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A with IHostedAgentBuilder and agent card succeeds.
/// </summary>
[Fact]
public void MapA2A_WithAgentBuilder_WithAgentCard_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");
builder.Services.AddLogging();
using WebApplication app = builder.Build();
var agentCard = new AgentCard
{
Name = "Test Agent",
Description = "A test agent for A2A communication"
};
// Act & Assert - Should not throw
var result = app.MapA2A(agentBuilder, "/a2a", agentCard);
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A with IHostedAgentBuilder, agent card, and custom task manager configuration succeeds.
/// </summary>
[Fact]
public void MapA2A_WithAgentBuilder_WithAgentCardAndCustomConfiguration_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");
builder.Services.AddLogging();
using WebApplication app = builder.Build();
var agentCard = new AgentCard
{
Name = "Test Agent",
Description = "A test agent for A2A communication"
};
// Act & Assert - Should not throw
var result = app.MapA2A(agentBuilder, "/a2a", agentCard, taskManager => { });
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A throws ArgumentNullException for null endpoints when using string agent name.
/// </summary>
[Fact]
public void MapA2A_WithAgentName_NullEndpoints_ThrowsArgumentNullException()
{
// Arrange
AspNetCore.Routing.IEndpointRouteBuilder endpoints = null!;
// Act & Assert
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
endpoints.MapA2A("agent", "/a2a"));
Assert.Equal("endpoints", exception.ParamName);
}
/// <summary>
/// Verifies that MapA2A with string agent name correctly maps the agent.
/// </summary>
[Fact]
public void MapA2A_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.AddLogging();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
var result = app.MapA2A("agent", "/a2a");
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A with string agent name and custom task manager configuration succeeds.
/// </summary>
[Fact]
public void MapA2A_WithAgentName_CustomTaskManagerConfiguration_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.AddLogging();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
var result = app.MapA2A("agent", "/a2a", taskManager => { });
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A with string agent name and agent card succeeds.
/// </summary>
[Fact]
public void MapA2A_WithAgentName_WithAgentCard_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.AddLogging();
using WebApplication app = builder.Build();
var agentCard = new AgentCard
{
Name = "Test Agent",
Description = "A test agent for A2A communication"
};
// Act & Assert - Should not throw
var result = app.MapA2A("agent", "/a2a", agentCard);
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A with string agent name, agent card, and custom task manager configuration succeeds.
/// </summary>
[Fact]
public void MapA2A_WithAgentName_WithAgentCardAndCustomConfiguration_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.AddLogging();
using WebApplication app = builder.Build();
var agentCard = new AgentCard
{
Name = "Test Agent",
Description = "A test agent for A2A communication"
};
// Act & Assert - Should not throw
var result = app.MapA2A("agent", "/a2a", agentCard, taskManager => { });
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A throws ArgumentNullException for null endpoints when using AIAgent.
/// </summary>
[Fact]
public void MapA2A_WithAIAgent_NullEndpoints_ThrowsArgumentNullException()
{
// Arrange
AspNetCore.Routing.IEndpointRouteBuilder endpoints = null!;
// Act & Assert
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
endpoints.MapA2A((AIAgent)null!, "/a2a"));
Assert.Equal("endpoints", exception.ParamName);
}
/// <summary>
/// Verifies that MapA2A with AIAgent correctly maps the agent.
/// </summary>
[Fact]
public void MapA2A_WithAIAgent_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.AddLogging();
using WebApplication app = builder.Build();
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
// Act & Assert - Should not throw
var result = app.MapA2A(agent, "/a2a");
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A with AIAgent and custom task manager configuration succeeds.
/// </summary>
[Fact]
public void MapA2A_WithAIAgent_CustomTaskManagerConfiguration_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.AddLogging();
using WebApplication app = builder.Build();
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
// Act & Assert - Should not throw
var result = app.MapA2A(agent, "/a2a", taskManager => { });
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A with AIAgent and agent card succeeds.
/// </summary>
[Fact]
public void MapA2A_WithAIAgent_WithAgentCard_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.AddLogging();
using WebApplication app = builder.Build();
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
var agentCard = new AgentCard
{
Name = "Test Agent",
Description = "A test agent for A2A communication"
};
// Act & Assert - Should not throw
var result = app.MapA2A(agent, "/a2a", agentCard);
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A with AIAgent, agent card, and custom task manager configuration succeeds.
/// </summary>
[Fact]
public void MapA2A_WithAIAgent_WithAgentCardAndCustomConfiguration_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.AddLogging();
using WebApplication app = builder.Build();
AIAgent agent = app.Services.GetRequiredKeyedService<AIAgent>("agent");
var agentCard = new AgentCard
{
Name = "Test Agent",
Description = "A test agent for A2A communication"
};
// Act & Assert - Should not throw
var result = app.MapA2A(agent, "/a2a", agentCard, taskManager => { });
Assert.NotNull(result);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapA2A throws ArgumentNullException for null endpoints when using ITaskManager.
/// </summary>
[Fact]
public void MapA2A_WithTaskManager_NullEndpoints_ThrowsArgumentNullException()
{
// Arrange
AspNetCore.Routing.IEndpointRouteBuilder endpoints = null!;
ITaskManager taskManager = null!;
// Act & Assert
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
endpoints.MapA2A(taskManager, "/a2a"));
Assert.Equal("endpoints", exception.ParamName);
}
/// <summary>
/// Verifies that multiple agents can be mapped to different paths.
/// </summary>
[Fact]
public void MapA2A_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");
builder.Services.AddLogging();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
app.MapA2A(agent1Builder, "/a2a/agent1");
app.MapA2A(agent2Builder, "/a2a/agent2");
Assert.NotNull(app);
}
/// <summary>
/// Verifies that custom paths can be specified for A2A endpoints.
/// </summary>
[Fact]
public void MapA2A_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");
builder.Services.AddLogging();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
app.MapA2A(agentBuilder, "/custom/a2a/path");
Assert.NotNull(app);
}
/// <summary>
/// Verifies that task manager configuration callback is invoked correctly.
/// </summary>
[Fact]
public void MapA2A_WithAgentBuilder_TaskManagerConfigurationCallbackInvoked()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new DummyChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
builder.Services.AddLogging();
using WebApplication app = builder.Build();
bool configureCallbackInvoked = false;
// Act
app.MapA2A(agentBuilder, "/a2a", taskManager =>
{
configureCallbackInvoked = true;
Assert.NotNull(taskManager);
});
// Assert
Assert.True(configureCallbackInvoked);
}
/// <summary>
/// Verifies that agent card with all properties is accepted.
/// </summary>
[Fact]
public void MapA2A_WithAgentBuilder_FullAgentCard_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");
builder.Services.AddLogging();
using WebApplication app = builder.Build();
var agentCard = new AgentCard
{
Name = "Test Agent",
Description = "A comprehensive test agent"
};
// Act & Assert - Should not throw
var result = app.MapA2A(agentBuilder, "/a2a", agentCard);
Assert.NotNull(result);
}
private 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();
}
}
}
@@ -11,6 +11,7 @@
</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>
@@ -223,4 +223,174 @@ public sealed class EndpointRouteBuilderExtensionsTests
app.MapOpenAIResponses(responsesPath: "/custom/path/responses");
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapOpenAIResponses throws ArgumentNullException for null endpoints when using IHostedAgentBuilder.
/// </summary>
[Fact]
public void MapOpenAIResponses_WithAgentBuilder_NullEndpoints_ThrowsArgumentNullException()
{
// Arrange
AspNetCore.Routing.IEndpointRouteBuilder endpoints = null!;
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
// Act & Assert
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
endpoints.MapOpenAIResponses(agentBuilder));
Assert.Equal("endpoints", exception.ParamName);
}
/// <summary>
/// Verifies that MapOpenAIResponses throws ArgumentNullException for null agentBuilder.
/// </summary>
[Fact]
public void MapOpenAIResponses_WithAgentBuilder_NullAgentBuilder_ThrowsArgumentNullException()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
builder.AddOpenAIResponses();
using WebApplication app = builder.Build();
IHostedAgentBuilder agentBuilder = null!;
// Act & Assert
ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() =>
app.MapOpenAIResponses(agentBuilder));
Assert.Equal("agentBuilder", exception.ParamName);
}
/// <summary>
/// Verifies that MapOpenAIResponses with IHostedAgentBuilder correctly resolves and maps the agent.
/// </summary>
[Fact]
public void MapOpenAIResponses_WithAgentBuilder_Succeeds()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
app.MapOpenAIResponses(agentBuilder);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that MapOpenAIResponses with IHostedAgentBuilder and custom path works correctly.
/// </summary>
[Fact]
public void MapOpenAIResponses_WithAgentBuilder_CustomPath_Succeeds()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
IHostedAgentBuilder agentBuilder = builder.AddAIAgent("my-agent", "Instructions", chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
app.MapOpenAIResponses(agentBuilder, path: "/agents/my-agent/responses");
Assert.NotNull(app);
}
/// <summary>
/// Verifies that multiple agents can be mapped using IHostedAgentBuilder.
/// </summary>
[Fact]
public void MapOpenAIResponses_WithAgentBuilder_MultipleAgents_Succeeds()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
IHostedAgentBuilder agent1Builder = builder.AddAIAgent("agent1", "Instructions1", chatClientServiceKey: "chat-client");
IHostedAgentBuilder agent2Builder = builder.AddAIAgent("agent2", "Instructions2", chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
app.MapOpenAIResponses(agent1Builder);
app.MapOpenAIResponses(agent2Builder);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that IHostedAgentBuilder overload validates agent name characters.
/// </summary>
[Theory]
[InlineData("agent with spaces")]
[InlineData("agent<script>")]
[InlineData("agent?query")]
[InlineData("agent#fragment")]
public void MapOpenAIResponses_WithAgentBuilder_InvalidAgentNameCharacters_ThrowsArgumentException(string invalidName)
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
IHostedAgentBuilder agentBuilder = builder.AddAIAgent(invalidName, "Instructions", chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
using WebApplication app = builder.Build();
// Act & Assert
ArgumentException exception = Assert.Throws<ArgumentException>(() =>
app.MapOpenAIResponses(agentBuilder));
Assert.Contains("invalid for URL routes", exception.Message);
}
/// <summary>
/// Verifies that IHostedAgentBuilder overload accepts valid agent names.
/// </summary>
[Theory]
[InlineData("agent-name")]
[InlineData("agent_name")]
[InlineData("agent.name")]
[InlineData("agent123")]
[InlineData("my-agent_v1.0")]
public void MapOpenAIResponses_WithAgentBuilder_ValidAgentNameCharacters_DoesNotThrow(string validName)
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
IHostedAgentBuilder agentBuilder = builder.AddAIAgent(validName, "Instructions", chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
app.MapOpenAIResponses(agentBuilder);
Assert.NotNull(app);
}
/// <summary>
/// Verifies that IHostedAgentBuilder overload with custom paths can be specified.
/// </summary>
[Fact]
public void MapOpenAIResponses_WithAgentBuilder_MultipleAgentsWithCustomPaths_Succeeds()
{
// Arrange
WebApplicationBuilder builder = WebApplication.CreateBuilder();
IChatClient mockChatClient = new TestHelpers.SimpleMockChatClient();
builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
IHostedAgentBuilder agent1Builder = builder.AddAIAgent("agent1", "Instructions1", chatClientServiceKey: "chat-client");
IHostedAgentBuilder agent2Builder = builder.AddAIAgent("agent2", "Instructions2", chatClientServiceKey: "chat-client");
builder.AddOpenAIResponses();
using WebApplication app = builder.Build();
// Act & Assert - Should not throw
app.MapOpenAIResponses(agent1Builder, path: "/api/v1/agent1/responses");
app.MapOpenAIResponses(agent2Builder, path: "/api/v1/agent2/responses");
Assert.NotNull(app);
}
}
@@ -137,102 +137,6 @@ public class HostApplicationBuilderWorkflowExtensionsTests
Assert.NotNull(descriptor);
}
/// <summary>
/// Verifies that providing a null builder to AddConcurrentWorkflow throws an ArgumentNullException.
/// </summary>
[Fact]
public void AddConcurrentWorkflow_NullBuilder_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
HostApplicationBuilderWorkflowExtensions.AddConcurrentWorkflow(null!, "workflow", [null!]));
}
/// <summary>
/// Verifies that AddConcurrentWorkflow throws ArgumentNullException for null name.
/// </summary>
[Fact]
public void AddConcurrentWorkflow_NullName_ThrowsArgumentNullException()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddConcurrentWorkflow(null!, [new HostedAgentBuilder("test", builder)]));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verifies that AddConcurrentWorkflow throws ArgumentNullException for null agent builders.
/// </summary>
[Fact]
public void AddConcurrentWorkflow_NullAgentBuilders_ThrowsArgumentNullException()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddConcurrentWorkflow("workflowName", null!));
Assert.Equal("agentBuilders", exception.ParamName);
}
/// <summary>
/// Verifies that AddConcurrentWorkflow returns IHostWorkflowBuilder instance.
/// </summary>
[Fact]
public void AddConcurrentWorkflow_ValidParameters_ReturnsBuilder()
{
var builder = new HostApplicationBuilder();
var result = builder.AddConcurrentWorkflow("concurrentWorkflow", [new HostedAgentBuilder("test", builder)]);
Assert.NotNull(result);
Assert.IsAssignableFrom<IHostedWorkflowBuilder>(result);
}
/// <summary>
/// Verifies that providing a null builder to AddSequentialWorkflow throws an ArgumentNullException.
/// </summary>
[Fact]
public void AddSequentialWorkflow_NullBuilder_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() =>
HostApplicationBuilderWorkflowExtensions.AddSequentialWorkflow(null!, "workflow", [null!]));
}
/// <summary>
/// Verifies that AddSequentialWorkflow throws ArgumentNullException for null name.
/// </summary>
[Fact]
public void AddSequentialWorkflow_NullName_ThrowsArgumentNullException()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddSequentialWorkflow(null!, [new HostedAgentBuilder("test", builder)]));
Assert.Equal("name", exception.ParamName);
}
/// <summary>
/// Verifies that AddSequentialWorkflow throws ArgumentNullException for null agent builders.
/// </summary>
[Fact]
public void AddSequentialWorkflow_NullAgentBuilders_ThrowsArgumentNullException()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws<ArgumentNullException>(() =>
builder.AddSequentialWorkflow("workflowName", null!));
Assert.Equal("agentBuilders", exception.ParamName);
}
[Fact]
public void AddSequentialWorkflow_EmptyAgentBuilders_Throws()
{
var builder = new HostApplicationBuilder();
var exception = Assert.Throws<ArgumentException>(() =>
builder.AddSequentialWorkflow("sequentialWorkflow", Array.Empty<IHostedAgentBuilder>()));
Assert.Equal("agentBuilders", exception.ParamName);
}
/// <summary>
/// Verifies that AddAsAIAgent without a name parameter uses the workflow name as the agent name.
/// </summary>
@@ -0,0 +1,150 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Agents.AI.Hosting.UnitTests;
/// <summary>
/// Unit tests for AI tool registration extensions on <see cref="IHostedAgentBuilder"/>.
/// </summary>
public sealed class HostedAgentBuilderToolsExtensionsTests
{
[Fact]
public void WithAITool_ThrowsWhenBuilderIsNull()
{
// Arrange
var tool = new DummyAITool();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => HostedAgentBuilderExtensions.WithAITool(null!, tool));
}
[Fact]
public void WithAITool_ThrowsWhenToolIsNull()
{
// Arrange
var services = new ServiceCollection();
var builder = services.AddAIAgent("test-agent", "Test instructions");
// Act & Assert
Assert.Throws<ArgumentNullException>(() => builder.WithAITool(null!));
}
[Fact]
public void WithAITools_ThrowsWhenBuilderIsNull()
{
// Arrange
var tools = new[] { new DummyAITool() };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => HostedAgentBuilderExtensions.WithAITools(null!, tools));
}
[Fact]
public void WithAITools_ThrowsWhenToolsArrayIsNull()
{
// Arrange
var services = new ServiceCollection();
var builder = services.AddAIAgent("test-agent", "Test instructions");
// Act & Assert
Assert.Throws<ArgumentNullException>(() => builder.WithAITools(null!));
}
[Fact]
public void RegisteredTools_ResolvesAllToolsForAgent()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IChatClient>(new MockChatClient());
var builder = services.AddAIAgent("test-agent", "Test instructions");
var tool1 = new DummyAITool();
var tool2 = new DummyAITool();
builder
.WithAITool(tool1)
.WithAITool(tool2);
var serviceProvider = services.BuildServiceProvider();
var agent1Tools = ResolveAgentTools(serviceProvider, "test-agent");
Assert.Contains(tool1, agent1Tools);
Assert.Contains(tool2, agent1Tools);
}
[Fact]
public void RegisteredTools_IsolatedPerAgent()
{
var services = new ServiceCollection();
services.AddSingleton<IChatClient>(new MockChatClient());
var builder1 = services.AddAIAgent("agent1", "Agent 1 instructions");
var builder2 = services.AddAIAgent("agent2", "Agent 2 instructions");
var tool1 = new DummyAITool();
var tool2 = new DummyAITool();
var tool3 = new DummyAITool();
builder1
.WithAITool(tool1)
.WithAITool(tool2);
builder2
.WithAITool(tool3);
var serviceProvider = services.BuildServiceProvider();
var agent1Tools = ResolveAgentTools(serviceProvider, "agent1");
var agent2Tools = ResolveAgentTools(serviceProvider, "agent2");
Assert.Contains(tool1, agent1Tools);
Assert.Contains(tool2, agent1Tools);
Assert.Contains(tool3, agent2Tools);
}
private static IList<AITool> ResolveAgentTools(IServiceProvider serviceProvider, string name)
{
var agent = serviceProvider.GetRequiredKeyedService<AIAgent>(name) as ChatClientAgent;
Assert.NotNull(agent?.ChatOptions?.Tools);
return agent.ChatOptions.Tools;
}
/// <summary>
/// Dummy AITool implementation for testing.
/// </summary>
private sealed class DummyAITool : AITool
{
}
/// <summary>
/// Mock chat client for testing.
/// </summary>
private sealed class MockChatClient : IChatClient
{
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public object? GetService(Type serviceType, object? serviceKey = null)
{
return null;
}
public void Dispose()
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,396 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.VectorData;
using Moq;
namespace Microsoft.Agents.AI.Memory.UnitTests;
/// <summary>
/// Contains unit tests for the <see cref="ChatHistoryMemoryProvider"/> class.
/// </summary>
public class ChatHistoryMemoryProviderTests
{
private readonly Mock<ILogger<ChatHistoryMemoryProvider>> _loggerMock;
private readonly Mock<ILoggerFactory> _loggerFactoryMock;
private readonly Mock<VectorStore> _vectorStoreMock;
private readonly Mock<VectorStoreCollection<object, Dictionary<string, object?>>> _vectorStoreCollectionMock;
private const string TestCollectionName = "testcollection";
public ChatHistoryMemoryProviderTests()
{
this._loggerMock = new();
this._loggerFactoryMock = new();
this._loggerFactoryMock
.Setup(f => f.CreateLogger(It.IsAny<string>()))
.Returns(this._loggerMock.Object);
this._loggerFactoryMock
.Setup(f => f.CreateLogger(typeof(ChatHistoryMemoryProvider).FullName!))
.Returns(this._loggerMock.Object);
this._vectorStoreCollectionMock = new(MockBehavior.Strict);
this._vectorStoreMock = new(MockBehavior.Strict);
this._vectorStoreCollectionMock
.Setup(c => c.EnsureCollectionExistsAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
this._vectorStoreMock
.Setup(vs => vs.GetDynamicCollection(
It.IsAny<string>(),
It.IsAny<VectorStoreCollectionDefinition>()))
.Returns(this._vectorStoreCollectionMock.Object);
}
[Fact]
public void Constructor_Throws_ForNullVectorStore()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryMemoryProvider(null!, "testcollection", 1, new ChatHistoryMemoryProviderScope() { UserId = "UID" }));
}
[Fact]
public void Constructor_Throws_ForNullCollectionName()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryMemoryProvider(this._vectorStoreMock.Object, null!, 1, new ChatHistoryMemoryProviderScope() { UserId = "UID" }));
}
[Fact]
public void Constructor_Throws_ForNullStorageScope()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryMemoryProvider(this._vectorStoreMock.Object, "testcollection", 1, null!));
}
[Fact]
public void Constructor_Throws_ForInvalidVectorDimensions()
{
// Act & Assert
Assert.Throws<ArgumentOutOfRangeException>(() => new ChatHistoryMemoryProvider(this._vectorStoreMock.Object, "testcollection", 0, new ChatHistoryMemoryProviderScope() { UserId = "UID" }));
Assert.Throws<ArgumentOutOfRangeException>(() => new ChatHistoryMemoryProvider(this._vectorStoreMock.Object, "testcollection", -5, new ChatHistoryMemoryProviderScope() { UserId = "UID" }));
}
#region InvokedAsync Tests
[Fact]
public async Task InvokedAsync_UpsertsMessages_ToCollectionAsync()
{
// Arrange
var stored = new List<Dictionary<string, object?>>();
this._vectorStoreCollectionMock
.Setup(c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()))
.Callback<IEnumerable<Dictionary<string, object?>>, CancellationToken>((items, ct) =>
{
if (items != null)
{
stored.AddRange(items);
}
})
.Returns(Task.CompletedTask);
var storeScope = new ChatHistoryMemoryProviderScope
{
ApplicationId = "app1",
AgentId = "agent1",
ThreadId = "thread1",
UserId = "user1"
};
var provider = new ChatHistoryMemoryProvider(this._vectorStoreMock.Object, TestCollectionName, 1, storeScope);
var requestMsgWithValues = new ChatMessage(ChatRole.User, "request text") { MessageId = "req-1", AuthorName = "user1", CreatedAt = new DateTimeOffset(new DateTime(2000, 1, 1), TimeSpan.Zero) };
var requestMsgWithNulls = new ChatMessage(ChatRole.User, "request text nulls");
var responseMsg = new ChatMessage(ChatRole.Assistant, "response text") { MessageId = "resp-1", AuthorName = "assistant" };
var invokedContext = new AIContextProvider.InvokedContext([requestMsgWithValues, requestMsgWithNulls], aiContextProviderMessages: null)
{
ResponseMessages = [responseMsg]
};
// Act
await provider.InvokedAsync(invokedContext, CancellationToken.None);
// Assert
this._vectorStoreCollectionMock.Verify(
m => m.EnsureCollectionExistsAsync(It.IsAny<CancellationToken>()),
Times.Once);
Assert.Equal(3, stored.Count);
Assert.Equal("req-1", stored[0]["MessageId"]);
Assert.Equal("request text", stored[0]["Content"]);
Assert.Equal("user1", stored[0]["AuthorName"]);
Assert.Equal(ChatRole.User.ToString(), stored[0]["Role"]);
Assert.Equal("2000-01-01T00:00:00.0000000+00:00", stored[0]["CreatedAt"]);
Assert.Equal("app1", stored[0]["ApplicationId"]);
Assert.Equal("agent1", stored[0]["AgentId"]);
Assert.Equal("thread1", stored[0]["ThreadId"]);
Assert.Equal("user1", stored[0]["UserId"]);
Assert.Null(stored[1]["MessageId"]);
Assert.Equal("request text nulls", stored[1]["Content"]);
Assert.Null(stored[1]["AuthorName"]);
Assert.Equal(ChatRole.User.ToString(), stored[1]["Role"]);
Assert.Equal("app1", stored[1]["ApplicationId"]);
Assert.Equal("agent1", stored[1]["AgentId"]);
Assert.Equal("thread1", stored[1]["ThreadId"]);
Assert.Equal("user1", stored[1]["UserId"]);
Assert.Equal("resp-1", stored[2]["MessageId"]);
Assert.Equal("response text", stored[2]["Content"]);
Assert.Equal("assistant", stored[2]["AuthorName"]);
Assert.Equal(ChatRole.Assistant.ToString(), stored[2]["Role"]);
Assert.Equal("app1", stored[2]["ApplicationId"]);
Assert.Equal("agent1", stored[2]["AgentId"]);
Assert.Equal("thread1", stored[2]["ThreadId"]);
Assert.Equal("user1", stored[2]["UserId"]);
}
[Fact]
public async Task InvokedAsync_DoesNotUpsertMessages_WhenInvokeFailedAsync()
{
// Arrange
this._vectorStoreCollectionMock
.Setup(c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
var provider = new ChatHistoryMemoryProvider(
this._vectorStoreMock.Object,
TestCollectionName,
1,
new ChatHistoryMemoryProviderScope() { UserId = "UID" });
var requestMsg = new ChatMessage(ChatRole.User, "request text") { MessageId = "req-1" };
var invokedContext = new AIContextProvider.InvokedContext([requestMsg], aiContextProviderMessages: null)
{
InvokeException = new InvalidOperationException("Invoke failed")
};
// Act
await provider.InvokedAsync(invokedContext, CancellationToken.None);
// Assert
this._vectorStoreCollectionMock.Verify(
c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task InvokedAsync_DoesNotThrow_WhenUpsertThrowsAsync()
{
// Arrange
this._vectorStoreCollectionMock
.Setup(c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("Upsert failed"));
var provider = new ChatHistoryMemoryProvider(
this._vectorStoreMock.Object,
TestCollectionName,
1,
new ChatHistoryMemoryProviderScope() { UserId = "UID" },
loggerFactory: this._loggerFactoryMock.Object);
var requestMsg = new ChatMessage(ChatRole.User, "request text") { MessageId = "req-1" };
var invokedContext = new AIContextProvider.InvokedContext([requestMsg], aiContextProviderMessages: null);
// Act
await provider.InvokedAsync(invokedContext, CancellationToken.None);
// Assert
this._loggerMock.Verify(
l => l.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("ChatHistoryMemoryProvider: Failed to add messages to chat history vector store due to error")),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
#endregion
#region InvokingAsync Tests
[Fact]
public async Task InvokedAsync_SearchesVectorStoreAsync()
{
// Arrange
var providerOptions = new ChatHistoryMemoryProviderOptions
{
SearchTime = ChatHistoryMemoryProviderOptions.SearchBehavior.BeforeAIInvoke,
MaxResults = 2,
ContextPrompt = "Here is the relevant chat history:\n"
};
var storedItems = new List<VectorSearchResult<Dictionary<string, object?>>>
{
new(
new Dictionary<string, object?>
{
["MessageId"] = "msg-1",
["Content"] = "First stored message",
["Role"] = ChatRole.User.ToString(),
["CreatedAt"] = "2023-01-01T00:00:00.0000000+00:00"
},
0.9f),
new(
new Dictionary<string, object?>
{
["MessageId"] = "msg-2",
["Content"] = "Second stored message",
["Role"] = ChatRole.User.ToString(),
["CreatedAt"] = "2023-01-02T00:00:00.0000000+00:00"
},
0.8f)
};
this._vectorStoreCollectionMock
.Setup(c => c.SearchAsync(
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<VectorSearchOptions<Dictionary<string, object?>>>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(storedItems));
var provider = new ChatHistoryMemoryProvider(
this._vectorStoreMock.Object,
TestCollectionName,
1,
new ChatHistoryMemoryProviderScope() { UserId = "UID" },
options: providerOptions);
var requestMsg = new ChatMessage(ChatRole.User, "requesting relevant history");
var invokingContext = new AIContextProvider.InvokingContext([requestMsg]);
// Act
await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert
this._vectorStoreCollectionMock.Verify(
c => c.SearchAsync(
It.Is<string>(s => s == "requesting relevant history"),
2,
It.IsAny<VectorSearchOptions<Dictionary<string, object?>>>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task InvokedAsync_CreatesFilter_WhenSearchScopeProvidedAsync()
{
// Arrange
var providerOptions = new ChatHistoryMemoryProviderOptions
{
SearchTime = ChatHistoryMemoryProviderOptions.SearchBehavior.BeforeAIInvoke,
MaxResults = 2,
ContextPrompt = "Here is the relevant chat history:\n"
};
var searchScope = new ChatHistoryMemoryProviderScope
{
ApplicationId = "app1",
AgentId = "agent1",
ThreadId = "thread1",
UserId = "user1"
};
this._vectorStoreCollectionMock
.Setup(c => c.SearchAsync(
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<VectorSearchOptions<Dictionary<string, object?>>>(),
It.IsAny<CancellationToken>()))
.Callback((string query, int maxResults, VectorSearchOptions<Dictionary<string, object?>> options, CancellationToken ct) =>
{
// Verify that the filter was created correctly
const string ExpectedFilter = "x => ((((x.ApplicationId == value(Microsoft.Agents.AI.VectorDataMemory.ChatHistoryMemoryProvider+<>c__DisplayClass20_0).applicationId) AndAlso (x.AgentId == value(Microsoft.Agents.AI.VectorDataMemory.ChatHistoryMemoryProvider+<>c__DisplayClass20_0).agentId)) AndAlso (x.UserId == value(Microsoft.Agents.AI.VectorDataMemory.ChatHistoryMemoryProvider+<>c__DisplayClass20_0).userId)) AndAlso (x.ThreadId == value(Microsoft.Agents.AI.VectorDataMemory.ChatHistoryMemoryProvider+<>c__DisplayClass20_0).threadId))";
Assert.Equal(ExpectedFilter, options.Filter!.ToString());
})
.Returns(ToAsyncEnumerableAsync(new List<VectorSearchResult<Dictionary<string, object?>>>()));
var provider = new ChatHistoryMemoryProvider(this._vectorStoreMock.Object, TestCollectionName, 1, options: providerOptions, storageScope: searchScope, searchScope: searchScope);
var requestMsg = new ChatMessage(ChatRole.User, "requesting relevant history");
var invokingContext = new AIContextProvider.InvokingContext([requestMsg]);
// Act
await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert
this._vectorStoreCollectionMock.Verify(
c => c.SearchAsync(
It.Is<string>(s => s == "requesting relevant history"),
2,
It.IsAny<VectorSearchOptions<Dictionary<string, object?>>>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
#endregion
#region Serialization Tests
[Fact]
public void Serialize_Deserialize_RoundtripsScopes()
{
// Arrange
var storageScope = new ChatHistoryMemoryProviderScope
{
ApplicationId = "app",
AgentId = "agent",
ThreadId = "thread",
UserId = "user"
};
var searchScope = new ChatHistoryMemoryProviderScope
{
ApplicationId = "app2",
AgentId = "agent2",
ThreadId = "thread2",
UserId = "user2"
};
var provider = new ChatHistoryMemoryProvider(this._vectorStoreMock.Object, TestCollectionName, 1, storageScope: storageScope, searchScope: searchScope);
// Act
var stateElement = provider.Serialize();
using JsonDocument doc = JsonDocument.Parse(stateElement.GetRawText());
var storage = doc.RootElement.GetProperty("storageScope");
Assert.Equal("app", storage.GetProperty("applicationId").GetString());
Assert.Equal("agent", storage.GetProperty("agentId").GetString());
Assert.Equal("thread", storage.GetProperty("threadId").GetString());
Assert.Equal("user", storage.GetProperty("userId").GetString());
var search = doc.RootElement.GetProperty("searchScope");
Assert.Equal("app2", search.GetProperty("applicationId").GetString());
Assert.Equal("agent2", search.GetProperty("agentId").GetString());
Assert.Equal("thread2", search.GetProperty("threadId").GetString());
Assert.Equal("user2", search.GetProperty("userId").GetString());
// Act - deserialize and serialize again
var provider2 = new ChatHistoryMemoryProvider(this._vectorStoreMock.Object, TestCollectionName, 1, serializedState: stateElement);
var stateElement2 = provider2.Serialize();
// Assert - roundtrip the state
Assert.Equal(stateElement.GetRawText(), stateElement2.GetRawText());
}
#endregion
private static async IAsyncEnumerable<T> ToAsyncEnumerableAsync<T>(IEnumerable<T> values)
{
await Task.Yield();
foreach (var update in values)
{
yield return update;
}
}
}