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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AIAgentMetadata"/> class.
/// </summary>
public class AIAgentMetadataTests
{
[Fact]
public void Constructor_WithNoArguments_SetsProviderNameToNull()
{
// Arrange & Act
AIAgentMetadata metadata = new();
// Assert
Assert.Null(metadata.ProviderName);
}
[Fact]
public void Constructor_WithProviderName_SetsProperty()
{
// Arrange
const string ProviderName = "TestProvider";
// Act
AIAgentMetadata metadata = new(ProviderName);
// Assert
Assert.Equal(ProviderName, metadata.ProviderName);
}
[Fact]
public void Constructor_WithNullProviderName_SetsProviderNameToNull()
{
// Arrange & Act
AIAgentMetadata metadata = new(null);
// Assert
Assert.Null(metadata.ProviderName);
}
}
@@ -0,0 +1,391 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Abstractions.UnitTests.Models;
using Microsoft.Extensions.AI;
using Moq;
using Moq.Protected;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Unit tests for the structured output functionality in <see cref="AIAgent"/>.
/// </summary>
public class AIAgentStructuredOutputTests
{
private readonly Mock<AIAgent> _agentMock;
public AIAgentStructuredOutputTests()
{
this._agentMock = new Mock<AIAgent> { CallBase = true };
}
#region Schema Wrapping Tests
/// <summary>
/// Verifies that when requesting an object type, the schema is NOT wrapped.
/// </summary>
[Fact]
public async Task RunAsyncGeneric_WithObjectType_DoesNotWrapSchemaAsync()
{
// Arrange
Animal expectedAnimal = new() { Id = 1, FullName = "Test", Species = Species.Tiger };
string responseJson = JsonSerializer.Serialize(expectedAnimal, TestJsonSerializerContext.Default.Animal);
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, responseJson));
this._agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
// Act
AgentResponse<Animal> result = await this._agentMock.Object.RunAsync<Animal>(
"Get me an animal",
serializerOptions: TestJsonSerializerContext.Default.Options);
// Assert - Verify the result is NOT marked as wrapped
Assert.False(result.IsWrappedInObject);
}
/// <summary>
/// Verifies that when requesting a primitive type (int), the schema IS wrapped.
/// </summary>
[Fact]
public async Task RunAsyncGeneric_WithPrimitiveType_WrapsSchemaAsync()
{
// Arrange
const string ResponseJson = "{\"data\":42}";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
this._agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
// Act
AgentResponse<int> result = await this._agentMock.Object.RunAsync<int>(
"Give me a number",
serializerOptions: TestJsonSerializerContext.Default.Options);
// Assert - Verify the result is marked as wrapped
Assert.True(result.IsWrappedInObject);
}
/// <summary>
/// Verifies that when requesting an array type, the schema IS wrapped.
/// </summary>
[Fact]
public async Task RunAsyncGeneric_WithArrayType_WrapsSchemaAsync()
{
// Arrange
const string ResponseJson = "{\"data\":[\"a\",\"b\",\"c\"]}";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
this._agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
// Act
AgentResponse<string[]> result = await this._agentMock.Object.RunAsync<string[]>(
"Give me an array of strings",
serializerOptions: TestJsonSerializerContext.Default.Options);
// Assert - Verify the result is marked as wrapped
Assert.True(result.IsWrappedInObject);
}
/// <summary>
/// Verifies that when requesting an enum type, the schema IS wrapped.
/// </summary>
[Fact]
public async Task RunAsyncGeneric_WithEnumType_WrapsSchemaAsync()
{
// Arrange
const string ResponseJson = "{\"data\":\"Tiger\"}";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
this._agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
// Act
AgentResponse<Species> result = await this._agentMock.Object.RunAsync<Species>(
"Give me a species",
serializerOptions: TestJsonSerializerContext.Default.Options);
// Assert - Verify the result is marked as wrapped
Assert.True(result.IsWrappedInObject);
}
#endregion
#region AgentResponse<T>.Result Unwrapping Tests
/// <summary>
/// Verifies that AgentResponse{T}.Result correctly deserializes an object without unwrapping.
/// </summary>
[Fact]
public void AgentResponseGeneric_Result_DeserializesObjectWithoutUnwrapping()
{
// Arrange
Animal expectedAnimal = new() { Id = 1, FullName = "Tigger", Species = Species.Tiger };
string responseJson = JsonSerializer.Serialize(expectedAnimal, TestJsonSerializerContext.Default.Animal);
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, responseJson));
AgentResponse<Animal> typedResponse = new(response, TestJsonSerializerContext.Default.Options);
// Act
Animal result = typedResponse.Result;
// Assert
Assert.Equal(expectedAnimal.Id, result.Id);
Assert.Equal(expectedAnimal.FullName, result.FullName);
Assert.Equal(expectedAnimal.Species, result.Species);
}
/// <summary>
/// Verifies that AgentResponse{T}.Result correctly unwraps and deserializes a primitive value.
/// </summary>
[Fact]
public void AgentResponseGeneric_Result_UnwrapsPrimitiveFromDataProperty()
{
// Arrange
const string ResponseJson = "{\"data\":42}";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
AgentResponse<int> typedResponse = new(response, TestJsonSerializerContext.Default.Options) { IsWrappedInObject = true };
// Act
int result = typedResponse.Result;
// Assert
Assert.Equal(42, result);
}
/// <summary>
/// Verifies that AgentResponse{T}.Result correctly unwraps and deserializes an array.
/// </summary>
[Fact]
public void AgentResponseGeneric_Result_UnwrapsArrayFromDataProperty()
{
// Arrange
const string ResponseJson = "{\"data\":[\"apple\",\"banana\",\"cherry\"]}";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
AgentResponse<string[]> typedResponse = new(response, TestJsonSerializerContext.Default.Options) { IsWrappedInObject = true };
// Act
string[] result = typedResponse.Result;
// Assert
Assert.Equal(["apple", "banana", "cherry"], result);
}
/// <summary>
/// Verifies that AgentResponse{T}.Result correctly unwraps and deserializes an enum.
/// </summary>
[Fact]
public void AgentResponseGeneric_Result_UnwrapsEnumFromDataProperty()
{
// Arrange
const string ResponseJson = "{\"data\":\"Walrus\"}";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
AgentResponse<Species> typedResponse = new(response, TestJsonSerializerContext.Default.Options) { IsWrappedInObject = true };
// Act
Species result = typedResponse.Result;
// Assert
Assert.Equal(Species.Walrus, result);
}
/// <summary>
/// Verifies that AgentResponse{T}.Result falls back to original JSON when data property is missing.
/// </summary>
[Fact]
public void AgentResponseGeneric_Result_FallsBackWhenDataPropertyMissing()
{
// Arrange - simulate a case where wrapping was expected but response does not have data
const string ResponseJson = "42";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
AgentResponse<int> typedResponse = new(response, TestJsonSerializerContext.Default.Options) { IsWrappedInObject = true };
// Act
int result = typedResponse.Result;
// Assert - should still work by falling back to original JSON
Assert.Equal(42, result);
}
/// <summary>
/// Verifies that AgentResponse{T}.Result throws when response text is empty.
/// </summary>
[Fact]
public void AgentResponseGeneric_Result_ThrowsWhenTextIsEmpty()
{
// Arrange
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, string.Empty));
AgentResponse<int> typedResponse = new(response, TestJsonSerializerContext.Default.Options);
// Act and Assert
Assert.Throws<System.InvalidOperationException>(() => typedResponse.Result);
}
/// <summary>
/// Verifies that AgentResponse{T}.Result throws when deserialized value is null.
/// </summary>
[Fact]
public void AgentResponseGeneric_Result_ThrowsWhenDeserializedValueIsNull()
{
// Arrange
const string ResponseJson = "null";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
AgentResponse<Animal> typedResponse = new(response, TestJsonSerializerContext.Default.Options);
// Act and Assert
Assert.Throws<System.InvalidOperationException>(() => typedResponse.Result);
}
#endregion
#region End-to-End Tests
/// <summary>
/// End-to-end test: Request a primitive type, verify wrapping, and verify correct deserialization.
/// </summary>
[Fact]
public async Task RunAsyncGeneric_PrimitiveEndToEnd_WrapsAndDeserializesCorrectlyAsync()
{
// Arrange
const string ResponseJson = "{\"data\":123}";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
this._agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
// Act
AgentResponse<int> result = await this._agentMock.Object.RunAsync<int>(
"Give me a number",
serializerOptions: TestJsonSerializerContext.Default.Options);
// Assert
Assert.True(result.IsWrappedInObject);
Assert.Equal(123, result.Result);
}
/// <summary>
/// End-to-end test: Request an array type, verify wrapping, and verify correct deserialization.
/// </summary>
[Fact]
public async Task RunAsyncGeneric_ArrayEndToEnd_WrapsAndDeserializesCorrectlyAsync()
{
// Arrange
const string ResponseJson = "{\"data\":[\"one\",\"two\",\"three\"]}";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
this._agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
// Act
AgentResponse<string[]> result = await this._agentMock.Object.RunAsync<string[]>(
"Give me an array of strings",
serializerOptions: TestJsonSerializerContext.Default.Options);
// Assert
Assert.True(result.IsWrappedInObject);
Assert.Equal(["one", "two", "three"], result.Result);
}
/// <summary>
/// End-to-end test: Request an object type, verify no wrapping, and verify correct deserialization.
/// </summary>
[Fact]
public async Task RunAsyncGeneric_ObjectEndToEnd_NoWrappingAndDeserializesCorrectlyAsync()
{
// Arrange
Animal expectedAnimal = new() { Id = 99, FullName = "Leo", Species = Species.Bear };
string responseJson = JsonSerializer.Serialize(expectedAnimal, TestJsonSerializerContext.Default.Animal);
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, responseJson));
this._agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
// Act
AgentResponse<Animal> result = await this._agentMock.Object.RunAsync<Animal>(
"Give me an animal",
serializerOptions: TestJsonSerializerContext.Default.Options);
// Assert
Assert.False(result.IsWrappedInObject);
Assert.Equal(expectedAnimal.Id, result.Result.Id);
Assert.Equal(expectedAnimal.FullName, result.Result.FullName);
Assert.Equal(expectedAnimal.Species, result.Result.Species);
}
/// <summary>
/// End-to-end test: Request an enum type, verify wrapping, and verify correct deserialization.
/// </summary>
[Fact]
public async Task RunAsyncGeneric_EnumEndToEnd_WrapsAndDeserializesCorrectlyAsync()
{
// Arrange
const string ResponseJson = "{\"data\":\"Bear\"}";
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, ResponseJson));
this._agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(response);
// Act
AgentResponse<Species> result = await this._agentMock.Object.RunAsync<Species>(
"Give me a species",
serializerOptions: TestJsonSerializerContext.Default.Options);
// Assert
Assert.True(result.IsWrappedInObject);
Assert.Equal(Species.Bear, result.Result);
}
#endregion
}
@@ -0,0 +1,646 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
using Moq.Protected;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AIAgent"/> class.
/// </summary>
public class AIAgentTests
{
private readonly Mock<AIAgent> _agentMock;
private readonly Mock<AgentSession> _agentSessionMock;
private readonly AgentResponse _invokeResponse;
private readonly List<AgentResponseUpdate> _invokeStreamingResponses = [];
/// <summary>
/// Initializes a new instance of the <see cref="AIAgentTests"/> class.
/// </summary>
public AIAgentTests()
{
this._agentSessionMock = new Mock<AgentSession>(MockBehavior.Strict);
this._invokeResponse = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Hi"));
this._invokeStreamingResponses.Add(new AgentResponseUpdate(ChatRole.Assistant, "Hi"));
this._agentMock = new Mock<AIAgent> { CallBase = true };
this._agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.Is<AgentSession?>(t => t == this._agentSessionMock.Object),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(this._invokeResponse);
this._agentMock
.Protected()
.Setup<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.Is<AgentSession?>(t => t == this._agentSessionMock.Object),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.Returns(ToAsyncEnumerableAsync(this._invokeStreamingResponses));
}
/// <summary>
/// Tests that invoking without a message calls the mocked invoke method with an empty array.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task InvokeWithoutMessageCallsMockedInvokeWithEmptyArrayAsync()
{
// Arrange
var options = new AgentRunOptions();
var cancellationToken = default(CancellationToken);
// Act
var response = await this._agentMock.Object.RunAsync(this._agentSessionMock.Object, options, cancellationToken);
Assert.Equal(this._invokeResponse, response);
// Verify that the mocked method was called with the expected parameters
this._agentMock
.Protected()
.Verify<Task<AgentResponse>>("RunCoreAsync",
Times.Once(),
ItExpr.Is<IEnumerable<ChatMessage>>(messages => !messages.Any()),
ItExpr.Is<AgentSession?>(t => t == this._agentSessionMock.Object),
ItExpr.Is<AgentRunOptions?>(o => o == options),
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
}
/// <summary>
/// Tests that invoking with a string message calls the mocked invoke method with the message in the ICollection of messages.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task InvokeWithStringMessageCallsMockedInvokeWithMessageInCollectionAsync()
{
// Arrange
const string Message = "Hello, Agent!";
var options = new AgentRunOptions();
var cancellationToken = default(CancellationToken);
// Act
var response = await this._agentMock.Object.RunAsync(Message, this._agentSessionMock.Object, options, cancellationToken);
Assert.Equal(this._invokeResponse, response);
// Verify that the mocked method was called with the expected parameters
this._agentMock
.Protected()
.Verify<Task<AgentResponse>>("RunCoreAsync",
Times.Once(),
ItExpr.Is<IEnumerable<ChatMessage>>(messages => messages.Count() == 1 && messages.First().Text == Message),
ItExpr.Is<AgentSession?>(t => t == this._agentSessionMock.Object),
ItExpr.Is<AgentRunOptions?>(o => o == options),
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
}
/// <summary>
/// Tests that invoking with a single message calls the mocked invoke method with the message in the ICollection of messages.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task InvokeWithSingleMessageCallsMockedInvokeWithMessageInCollectionAsync()
{
// Arrange
var message = new ChatMessage(ChatRole.User, "Hello, Agent!");
var options = new AgentRunOptions();
var cancellationToken = default(CancellationToken);
// Act
var response = await this._agentMock.Object.RunAsync(message, this._agentSessionMock.Object, options, cancellationToken);
Assert.Equal(this._invokeResponse, response);
// Verify that the mocked method was called with the expected parameters
this._agentMock
.Protected()
.Verify<Task<AgentResponse>>("RunCoreAsync",
Times.Once(),
ItExpr.Is<IEnumerable<ChatMessage>>(messages => messages.Count() == 1 && messages.First() == message),
ItExpr.Is<AgentSession?>(t => t == this._agentSessionMock.Object),
ItExpr.Is<AgentRunOptions?>(o => o == options),
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
}
/// <summary>
/// Tests that invoking streaming without a message calls the mocked invoke method with an empty array.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task InvokeStreamingWithoutMessageCallsMockedInvokeWithEmptyArrayAsync()
{
// Arrange
var options = new AgentRunOptions();
var cancellationToken = default(CancellationToken);
// Act
await foreach (var response in this._agentMock.Object.RunStreamingAsync(this._agentSessionMock.Object, options, cancellationToken))
{
// Assert
Assert.Contains(response, this._invokeStreamingResponses);
}
// Verify that the mocked method was called with the expected parameters
this._agentMock
.Protected()
.Verify<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
Times.Once(),
ItExpr.Is<IEnumerable<ChatMessage>>(messages => !messages.Any()),
ItExpr.Is<AgentSession?>(t => t == this._agentSessionMock.Object),
ItExpr.Is<AgentRunOptions?>(o => o == options),
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
}
/// <summary>
/// Tests that invoking streaming with a string message calls the mocked invoke method with the message in the ICollection of messages.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task InvokeStreamingWithStringMessageCallsMockedInvokeWithMessageInCollectionAsync()
{
// Arrange
const string Message = "Hello, Agent!";
var options = new AgentRunOptions();
var cancellationToken = default(CancellationToken);
// Act
await foreach (var response in this._agentMock.Object.RunStreamingAsync(Message, this._agentSessionMock.Object, options, cancellationToken))
{
// Assert
Assert.Contains(response, this._invokeStreamingResponses);
}
// Verify that the mocked method was called with the expected parameters
this._agentMock
.Protected()
.Verify<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
Times.Once(),
ItExpr.Is<IEnumerable<ChatMessage>>(messages => messages.Count() == 1 && messages.First().Text == Message),
ItExpr.Is<AgentSession?>(t => t == this._agentSessionMock.Object),
ItExpr.Is<AgentRunOptions?>(o => o == options),
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
}
/// <summary>
/// Tests that invoking streaming with a single message calls the mocked invoke method with the message in the ICollection of messages.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task InvokeStreamingWithSingleMessageCallsMockedInvokeWithMessageInCollectionAsync()
{
// Arrange
var message = new ChatMessage(ChatRole.User, "Hello, Agent!");
var options = new AgentRunOptions();
var cancellationToken = default(CancellationToken);
// Act
await foreach (var response in this._agentMock.Object.RunStreamingAsync(message, this._agentSessionMock.Object, options, cancellationToken))
{
// Assert
Assert.Contains(response, this._invokeStreamingResponses);
}
// Verify that the mocked method was called with the expected parameters
this._agentMock
.Protected()
.Verify<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
Times.Once(),
ItExpr.Is<IEnumerable<ChatMessage>>(messages => messages.Count() == 1 && messages.First() == message),
ItExpr.Is<AgentSession?>(t => t == this._agentSessionMock.Object),
ItExpr.Is<AgentRunOptions?>(o => o == options),
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
}
/// <summary>
/// Theory data for RunAsync overloads.
/// </summary>
public static TheoryData<string> RunAsyncOverloads => new()
{
"NoMessage",
"StringMessage",
"ChatMessage",
"MessagesCollection"
};
/// <summary>
/// Verifies that CurrentRunContext is properly set and accessible from RunCoreAsync for all RunAsync overloads.
/// </summary>
[Theory]
[MemberData(nameof(RunAsyncOverloads))]
public async Task RunAsync_SetsCurrentRunContext_AccessibleFromRunCoreAsync(string overload)
{
// Arrange
AgentRunContext? capturedContext = null;
var session = new TestAgentSession();
var options = new AgentRunOptions();
var agentMock = new Mock<AIAgent> { CallBase = true };
agentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.Returns((IEnumerable<ChatMessage> _, AgentSession? _, AgentRunOptions? _, CancellationToken _) =>
{
capturedContext = AIAgent.CurrentRunContext;
return Task.FromResult(new AgentResponse(new ChatMessage(ChatRole.Assistant, "Response")));
});
// Act
switch (overload)
{
case "NoMessage":
await agentMock.Object.RunAsync(session, options);
break;
case "StringMessage":
await agentMock.Object.RunAsync("Hello", session, options);
break;
case "ChatMessage":
await agentMock.Object.RunAsync(new ChatMessage(ChatRole.User, "Hello"), session, options);
break;
case "MessagesCollection":
await agentMock.Object.RunAsync([new ChatMessage(ChatRole.User, "Hello")], session, options);
break;
}
// Assert
Assert.NotNull(capturedContext);
Assert.Same(agentMock.Object, capturedContext!.Agent);
Assert.Same(session, capturedContext.Session);
Assert.Same(options, capturedContext.RunOptions);
if (overload == "NoMessage")
{
Assert.Empty(capturedContext.RequestMessages);
}
else
{
Assert.Single(capturedContext.RequestMessages);
}
}
/// <summary>
/// Verifies that CurrentRunContext is properly set and accessible from RunCoreStreamingAsync for all RunStreamingAsync overloads.
/// </summary>
[Theory]
[MemberData(nameof(RunAsyncOverloads))]
public async Task RunStreamingAsync_SetsCurrentRunContext_AccessibleFromRunCoreStreamingAsync(string overload)
{
// Arrange
AgentRunContext? capturedContext = null;
var session = new TestAgentSession();
var options = new AgentRunOptions();
var agentMock = new Mock<AIAgent> { CallBase = true };
agentMock
.Protected()
.Setup<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.Returns((IEnumerable<ChatMessage> _, AgentSession? _, AgentRunOptions? _, CancellationToken _) =>
{
capturedContext = AIAgent.CurrentRunContext;
return ToAsyncEnumerableAsync([new AgentResponseUpdate(ChatRole.Assistant, "Response")]);
});
// Act
IAsyncEnumerable<AgentResponseUpdate> stream = overload switch
{
"NoMessage" => agentMock.Object.RunStreamingAsync(session, options),
"StringMessage" => agentMock.Object.RunStreamingAsync("Hello", session, options),
"ChatMessage" => agentMock.Object.RunStreamingAsync(new ChatMessage(ChatRole.User, "Hello"), session, options),
"MessagesCollection" => agentMock.Object.RunStreamingAsync(new[] { new ChatMessage(ChatRole.User, "Hello") }, session, options),
_ => throw new InvalidOperationException($"Unknown overload: {overload}")
};
await foreach (AgentResponseUpdate _ in stream)
{
// Consume the stream
}
// Assert
Assert.NotNull(capturedContext);
Assert.Same(agentMock.Object, capturedContext!.Agent);
Assert.Same(session, capturedContext.Session);
Assert.Same(options, capturedContext.RunOptions);
if (overload == "NoMessage")
{
Assert.Empty(capturedContext.RequestMessages);
}
else
{
Assert.Single(capturedContext.RequestMessages);
}
}
[Fact]
public void ValidateAgentIDIsIdempotent()
{
// Arrange
var agent = new MockAgent();
// Act
string id = agent.Id;
// Assert
Assert.NotNull(id);
Assert.Equal(id, agent.Id);
}
[Fact]
public void ValidateAgentIDCanBeProvidedByDerivedAgentClass()
{
// Arrange
var agent = new MockAgent(id: "test-agent-id");
// Act
string id = agent.Id;
// Assert
Assert.NotNull(id);
Assert.Equal("test-agent-id", id);
}
#region GetService Method Tests
/// <summary>
/// Verify that GetService returns the agent itself when requesting the exact agent type.
/// </summary>
[Fact]
public void GetService_RequestingExactAgentType_ReturnsAgent()
{
// Arrange
var agent = new MockAgent();
// Act
var result = agent.GetService(typeof(MockAgent));
// Assert
Assert.NotNull(result);
Assert.Same(agent, result);
}
/// <summary>
/// Verify that GetService returns the agent itself when requesting the base AIAgent type.
/// </summary>
[Fact]
public void GetService_RequestingAIAgentType_ReturnsAgent()
{
// Arrange
var agent = new MockAgent();
// Act
var result = agent.GetService(typeof(AIAgent));
// Assert
Assert.NotNull(result);
Assert.Same(agent, result);
}
/// <summary>
/// Verify that GetService returns null when requesting an unrelated type.
/// </summary>
[Fact]
public void GetService_RequestingUnrelatedType_ReturnsNull()
{
// Arrange
var agent = new MockAgent();
// Act
var result = agent.GetService(typeof(string));
// Assert
Assert.Null(result);
}
/// <summary>
/// Verify that GetService returns null when a service key is provided, even for matching types.
/// </summary>
[Fact]
public void GetService_WithServiceKey_ReturnsNull()
{
// Arrange
var agent = new MockAgent();
// Act
var result = agent.GetService(typeof(MockAgent), "some-key");
// Assert
Assert.Null(result);
}
/// <summary>
/// Verify that GetService throws ArgumentNullException when serviceType is null.
/// </summary>
[Fact]
public void GetService_WithNullServiceType_ThrowsArgumentNullException()
{
// Arrange
var agent = new MockAgent();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => agent.GetService(null!));
}
/// <summary>
/// Verify that GetService generic method works correctly.
/// </summary>
[Fact]
public void GetService_Generic_ReturnsCorrectType()
{
// Arrange
var agent = new MockAgent();
// Act
var result = agent.GetService<MockAgent>();
// Assert
Assert.NotNull(result);
Assert.Same(agent, result);
}
/// <summary>
/// Verify that GetService generic method returns null for unrelated types.
/// </summary>
[Fact]
public void GetService_Generic_ReturnsNullForUnrelatedType()
{
// Arrange
var agent = new MockAgent();
// Act
var result = agent.GetService<string>();
// Assert
Assert.Null(result);
}
#endregion
#region Name and Description Property Tests
/// <summary>
/// Verify that Name property returns the value from the derived class.
/// </summary>
[Fact]
public void Name_ReturnsValueFromDerivedClass()
{
// Arrange
var agent = new MockAgentWithName("TestAgentName", "TestAgentDescription");
// Act
string? name = agent.Name;
// Assert
Assert.Equal("TestAgentName", name);
}
/// <summary>
/// Verify that Description property returns the value from the derived class.
/// </summary>
[Fact]
public void Description_ReturnsValueFromDerivedClass()
{
// Arrange
var agent = new MockAgentWithName("TestAgentName", "TestAgentDescription");
// Act
string? description = agent.Description;
// Assert
Assert.Equal("TestAgentDescription", description);
}
/// <summary>
/// Verify that Name property returns null when not overridden.
/// </summary>
[Fact]
public void Name_ReturnsNullByDefault()
{
// Arrange
var agent = new MockAgent();
// Act
string? name = agent.Name;
// Assert
Assert.Null(name);
}
/// <summary>
/// Verify that Description property returns null when not overridden.
/// </summary>
[Fact]
public void Description_ReturnsNullByDefault()
{
// Arrange
var agent = new MockAgent();
// Act
string? description = agent.Description;
// Assert
Assert.Null(description);
}
#endregion
/// <summary>
/// Typed mock session for testing purposes.
/// </summary>
private sealed class TestAgentSession : AgentSession;
private sealed class MockAgent : AIAgent
{
public MockAgent(string? id = null)
{
this.IdCore = id;
}
protected override string? IdCore { get; }
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
}
private sealed class MockAgentWithName : AIAgent
{
private readonly string? _name;
private readonly string? _description;
public MockAgentWithName(string? name, string? description)
{
this._name = name;
this._description = description;
}
public override string? Name => this._name;
public override string? Description => this._description;
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
}
private static async IAsyncEnumerable<T> ToAsyncEnumerableAsync<T>(IEnumerable<T> values)
{
await Task.Yield();
foreach (var update in values)
{
yield return update;
}
}
}
@@ -0,0 +1,739 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
public class AIContextProviderTests
{
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
#region Basic Tests
[Fact]
public async Task InvokedAsync_ReturnsCompletedTaskAsync()
{
// Arrange
var provider = new TestAIContextProvider();
var messages = new ReadOnlyCollection<ChatMessage>([]);
// Act
ValueTask task = provider.InvokedAsync(new(s_mockAgent, s_mockSession, messages, []));
// Assert
Assert.Equal(default, task);
}
[Fact]
public void InvokingContext_Constructor_ThrowsForNullMessages()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, null!));
}
[Fact]
public void InvokedContext_Constructor_ThrowsForNullMessages()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, null!, []));
}
#endregion
#region GetService Method Tests
/// <summary>
/// Verify that GetService returns the context provider itself when requesting the exact context provider type.
/// </summary>
[Fact]
public void GetService_RequestingExactContextProviderType_ReturnsContextProvider()
{
// Arrange
var contextProvider = new TestAIContextProvider();
// Act
var result = contextProvider.GetService(typeof(TestAIContextProvider));
// Assert
Assert.NotNull(result);
Assert.Same(contextProvider, result);
}
/// <summary>
/// Verify that GetService returns the context provider itself when requesting the base AIContextProvider type.
/// </summary>
[Fact]
public void GetService_RequestingAIContextProviderType_ReturnsContextProvider()
{
// Arrange
var contextProvider = new TestAIContextProvider();
// Act
var result = contextProvider.GetService(typeof(AIContextProvider));
// Assert
Assert.NotNull(result);
Assert.Same(contextProvider, result);
}
/// <summary>
/// Verify that GetService returns null when requesting an unrelated type.
/// </summary>
[Fact]
public void GetService_RequestingUnrelatedType_ReturnsNull()
{
// Arrange
var contextProvider = new TestAIContextProvider();
// Act
var result = contextProvider.GetService(typeof(string));
// Assert
Assert.Null(result);
}
/// <summary>
/// Verify that GetService returns null when a service key is provided, even for matching types.
/// </summary>
[Fact]
public void GetService_WithServiceKey_ReturnsNull()
{
// Arrange
var contextProvider = new TestAIContextProvider();
// Act
var result = contextProvider.GetService(typeof(TestAIContextProvider), "some-key");
// Assert
Assert.Null(result);
}
/// <summary>
/// Verify that GetService throws ArgumentNullException when serviceType is null.
/// </summary>
[Fact]
public void GetService_WithNullServiceType_ThrowsArgumentNullException()
{
// Arrange
var contextProvider = new TestAIContextProvider();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => contextProvider.GetService(null!));
}
/// <summary>
/// Verify that GetService generic method works correctly.
/// </summary>
[Fact]
public void GetService_Generic_ReturnsCorrectType()
{
// Arrange
var contextProvider = new TestAIContextProvider();
// Act
var result = contextProvider.GetService<TestAIContextProvider>();
// Assert
Assert.NotNull(result);
Assert.Same(contextProvider, result);
}
/// <summary>
/// Verify that GetService generic method returns null for unrelated types.
/// </summary>
[Fact]
public void GetService_Generic_ReturnsNullForUnrelatedType()
{
// Arrange
var contextProvider = new TestAIContextProvider();
// Act
var result = contextProvider.GetService<string>();
// Assert
Assert.Null(result);
}
#endregion
#region InvokingContext Tests
[Fact]
public void InvokingContext_Constructor_ThrowsForNullAIContext()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, null!));
}
[Fact]
public void InvokingContext_AIContext_ConstructorValueRoundtrips()
{
// Arrange
var aiContext = new AIContext { Messages = [new ChatMessage(ChatRole.User, "Hello")] };
// Act
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, aiContext);
// Assert
Assert.Same(aiContext, context.AIContext);
}
[Fact]
public void InvokingContext_Agent_ReturnsConstructorValue()
{
// Arrange
var aiContext = new AIContext { Messages = [new ChatMessage(ChatRole.User, "Hello")] };
// Act
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, aiContext);
// Assert
Assert.Same(s_mockAgent, context.Agent);
}
[Fact]
public void InvokingContext_Session_ReturnsConstructorValue()
{
// Arrange
var aiContext = new AIContext { Messages = [new ChatMessage(ChatRole.User, "Hello")] };
// Act
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, aiContext);
// Assert
Assert.Same(s_mockSession, context.Session);
}
[Fact]
public void InvokingContext_Session_CanBeNull()
{
// Arrange
var aiContext = new AIContext { Messages = [new ChatMessage(ChatRole.User, "Hello")] };
// Act
var context = new AIContextProvider.InvokingContext(s_mockAgent, null, aiContext);
// Assert
Assert.Null(context.Session);
}
[Fact]
public void InvokingContext_Constructor_ThrowsForNullAgent()
{
// Arrange
var aiContext = new AIContext { Messages = [new ChatMessage(ChatRole.User, "Hello")] };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokingContext(null!, s_mockSession, aiContext));
}
#endregion
#region InvokedContext Tests
[Fact]
public void InvokedContext_ResponseMessages_Roundtrips()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response message") };
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, responseMessages);
// Assert
Assert.Same(responseMessages, context.ResponseMessages);
}
[Fact]
public void InvokedContext_InvokeException_Roundtrips()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
var exception = new InvalidOperationException("Test exception");
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, exception);
// Assert
Assert.Same(exception, context.InvokeException);
}
[Fact]
public void InvokedContext_Agent_ReturnsConstructorValue()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Assert
Assert.Same(s_mockAgent, context.Agent);
}
[Fact]
public void InvokedContext_Session_ReturnsConstructorValue()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Assert
Assert.Same(s_mockSession, context.Session);
}
[Fact]
public void InvokedContext_Session_CanBeNull()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act
var context = new AIContextProvider.InvokedContext(s_mockAgent, null, requestMessages, []);
// Assert
Assert.Null(context.Session);
}
[Fact]
public void InvokedContext_Constructor_ThrowsForNullAgent()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(null!, s_mockSession, requestMessages, []));
}
[Fact]
public void InvokedContext_SuccessConstructor_ThrowsForNullResponseMessages()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, (IEnumerable<ChatMessage>)null!));
}
[Fact]
public void InvokedContext_FailureConstructor_ThrowsForNullException()
{
// Arrange
var requestMessages = new ReadOnlyCollection<ChatMessage>([new(ChatRole.User, "Hello")]);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, (Exception)null!));
}
#endregion
#region InvokingAsync / InvokedAsync Null Check Tests
[Fact]
public async Task InvokingAsync_NullContext_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var provider = new TestAIContextProvider();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => provider.InvokingAsync(null!).AsTask());
}
[Fact]
public async Task InvokedAsync_NullContext_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var provider = new TestAIContextProvider();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => provider.InvokedAsync(null!).AsTask());
}
#endregion
#region InvokingCoreAsync Tests
[Fact]
public async Task InvokingCoreAsync_CallsProvideAIContextAndReturnsMergedContextAsync()
{
// Arrange
var providedMessages = new[] { new ChatMessage(ChatRole.System, "Context message") };
var provider = new TestAIContextProvider(provideContext: new AIContext { Messages = providedMessages });
var inputContext = new AIContext { Messages = [new ChatMessage(ChatRole.User, "User input")] };
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, inputContext);
// Act
var result = await provider.InvokingAsync(context);
// Assert - input messages + provided messages merged
var messages = result.Messages!.ToList();
Assert.Equal(2, messages.Count);
Assert.Equal("User input", messages[0].Text);
Assert.Equal("Context message", messages[1].Text);
}
[Fact]
public async Task InvokingCoreAsync_FiltersInputToExternalOnlyByDefaultAsync()
{
// Arrange
var provider = new TestAIContextProvider(captureFilteredContext: true);
var externalMsg = new ChatMessage(ChatRole.User, "External");
var chatHistoryMsg = new ChatMessage(ChatRole.User, "History")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
var contextProviderMsg = new ChatMessage(ChatRole.User, "ContextProvider")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, "src");
var inputContext = new AIContext { Messages = [externalMsg, chatHistoryMsg, contextProviderMsg] };
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, inputContext);
// Act
await provider.InvokingAsync(context);
// Assert - ProvideAIContextAsync received only External messages
Assert.NotNull(provider.LastProvidedContext);
var filteredMessages = provider.LastProvidedContext!.AIContext.Messages!.ToList();
Assert.Single(filteredMessages);
Assert.Equal("External", filteredMessages[0].Text);
}
[Fact]
public async Task InvokingCoreAsync_StampsProvidedMessagesWithAIContextProviderSourceAsync()
{
// Arrange
var providedMessages = new[] { new ChatMessage(ChatRole.System, "Provided") };
var provider = new TestAIContextProvider(provideContext: new AIContext { Messages = providedMessages });
var inputContext = new AIContext { Messages = [] };
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, inputContext);
// Act
var result = await provider.InvokingAsync(context);
// Assert
var messages = result.Messages!.ToList();
Assert.Single(messages);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, messages[0].GetAgentRequestMessageSourceType());
}
[Fact]
public async Task InvokingCoreAsync_MergesInstructionsAsync()
{
// Arrange
var provider = new TestAIContextProvider(provideContext: new AIContext { Instructions = "Provided instructions" });
var inputContext = new AIContext { Instructions = "Input instructions" };
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, inputContext);
// Act
var result = await provider.InvokingAsync(context);
// Assert - instructions are joined with newline
Assert.Equal("Input instructions\nProvided instructions", result.Instructions);
}
[Fact]
public async Task InvokingCoreAsync_MergesToolsAsync()
{
// Arrange
var inputTool = AIFunctionFactory.Create(() => "a", "inputTool");
var providedTool = AIFunctionFactory.Create(() => "b", "providedTool");
var provider = new TestAIContextProvider(provideContext: new AIContext { Tools = [providedTool] });
var inputContext = new AIContext { Tools = [inputTool] };
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, inputContext);
// Act
var result = await provider.InvokingAsync(context);
// Assert - both tools present
var tools = result.Tools!.ToList();
Assert.Equal(2, tools.Count);
}
[Fact]
public async Task InvokingCoreAsync_UsesCustomProvideInputFilterAsync()
{
// Arrange - filter that keeps all messages (not just External)
var provider = new TestAIContextProvider(
captureFilteredContext: true,
provideInputMessageFilter: msgs => msgs);
var externalMsg = new ChatMessage(ChatRole.User, "External");
var chatHistoryMsg = new ChatMessage(ChatRole.User, "History")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
var inputContext = new AIContext { Messages = [externalMsg, chatHistoryMsg] };
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, inputContext);
// Act
await provider.InvokingAsync(context);
// Assert - ProvideAIContextAsync received ALL messages (custom filter keeps everything)
Assert.NotNull(provider.LastProvidedContext);
var filteredMessages = provider.LastProvidedContext!.AIContext.Messages!.ToList();
Assert.Equal(2, filteredMessages.Count);
}
[Fact]
public async Task InvokingCoreAsync_ReturnsEmptyContextByDefaultAsync()
{
// Arrange - provider that doesn't override ProvideAIContextAsync
var provider = new DefaultAIContextProvider();
var inputContext = new AIContext { Messages = [new ChatMessage(ChatRole.User, "Hello")] };
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, inputContext);
// Act
var result = await provider.InvokingAsync(context);
// Assert - only the input messages (no additional provided)
var messages = result.Messages!.ToList();
Assert.Single(messages);
Assert.Equal("Hello", messages[0].Text);
}
[Fact]
public async Task InvokingCoreAsync_MergesWithOriginalUnfilteredMessagesAsync()
{
// Arrange - default filter is External-only, but the MERGED result should include
// the original unfiltered input messages plus the provided messages
var providedMessages = new[] { new ChatMessage(ChatRole.System, "Provided") };
var provider = new TestAIContextProvider(provideContext: new AIContext { Messages = providedMessages });
var externalMsg = new ChatMessage(ChatRole.User, "External");
var chatHistoryMsg = new ChatMessage(ChatRole.User, "History")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
var inputContext = new AIContext { Messages = [externalMsg, chatHistoryMsg] };
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, inputContext);
// Act
var result = await provider.InvokingAsync(context);
// Assert - original 2 input messages + 1 provided message
var messages = result.Messages!.ToList();
Assert.Equal(3, messages.Count);
Assert.Equal("External", messages[0].Text);
Assert.Equal("History", messages[1].Text);
Assert.Equal("Provided", messages[2].Text);
}
#endregion
#region InvokedCoreAsync Tests
[Fact]
public async Task InvokedCoreAsync_CallsStoreAIContextWithFilteredMessagesAsync()
{
// Arrange
var provider = new TestAIContextProvider();
var externalMessage = new ChatMessage(ChatRole.User, "External");
var chatHistoryMessage = new ChatMessage(ChatRole.User, "History")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
var responseMessages = new[] { new ChatMessage(ChatRole.Assistant, "Response") };
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, new[] { externalMessage, chatHistoryMessage }, responseMessages);
// Act
await provider.InvokedAsync(context);
// Assert - default filter keeps only External messages
Assert.NotNull(provider.LastStoredContext);
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Single(storedRequest);
Assert.Equal("External", storedRequest[0].Text);
var storedResponse = provider.LastStoredContext.ResponseMessages!.ToList();
Assert.Single(storedResponse);
Assert.Equal("Response", storedResponse[0].Text);
}
[Fact]
public async Task InvokedCoreAsync_SkipsStorageWhenInvokeExceptionIsNotNullAsync()
{
// Arrange
var provider = new TestAIContextProvider();
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "msg")], new InvalidOperationException("Failed"));
// Act
await provider.InvokedAsync(context);
// Assert - StoreAIContextAsync was NOT called
Assert.Null(provider.LastStoredContext);
}
[Fact]
public async Task InvokedCoreAsync_UsesCustomStoreInputFilterAsync()
{
// Arrange - filter that only keeps System messages
var provider = new TestAIContextProvider(
storeInputRequestMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.System),
storeInputResponseMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.Assistant));
var messages = new[]
{
new ChatMessage(ChatRole.User, "User msg"),
new ChatMessage(ChatRole.System, "System msg")
};
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, messages, [new ChatMessage(ChatRole.Assistant, "Response"), new ChatMessage(ChatRole.Tool, "Response")]);
// Act
await provider.InvokedAsync(context);
// Assert - only System messages were passed to store
Assert.NotNull(provider.LastStoredContext);
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Single(storedRequest);
Assert.Equal("System msg", storedRequest[0].Text);
var storedResponse = provider.LastStoredContext.ResponseMessages!.ToList();
Assert.Single(storedResponse);
Assert.Equal("Response", storedResponse[0].Text);
}
[Fact]
public async Task InvokedCoreAsync_DefaultFilterExcludesNonExternalMessagesAsync()
{
// Arrange
var provider = new TestAIContextProvider();
var external = new ChatMessage(ChatRole.User, "External");
var fromHistory = new ChatMessage(ChatRole.User, "History")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
var fromContext = new ChatMessage(ChatRole.User, "Context")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, "src");
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, [external, fromHistory, fromContext], []);
// Act
await provider.InvokedAsync(context);
// Assert - only External messages kept
Assert.NotNull(provider.LastStoredContext);
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Single(storedRequest);
Assert.Equal("External", storedRequest[0].Text);
}
[Fact]
public async Task InvokedCoreAsync_DefaultResponseFilterPassesAllResponseMessagesAsync()
{
// Arrange
var provider = new TestAIContextProvider();
var requestMessages = new[] { new ChatMessage(ChatRole.User, "Request") };
var externalResponse = new ChatMessage(ChatRole.Assistant, "ExternalResp");
var historyResponse = new ChatMessage(ChatRole.Assistant, "HistoryResp")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
var contextResponse = new ChatMessage(ChatRole.Assistant, "ContextResp")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, "src");
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, [externalResponse, historyResponse, contextResponse]);
// Act
await provider.InvokedAsync(context);
// Assert - default response filter is a noop, so all response messages are kept
Assert.NotNull(provider.LastStoredContext);
var storedResponse = provider.LastStoredContext!.ResponseMessages!.ToList();
Assert.Equal(3, storedResponse.Count);
Assert.Equal("ExternalResp", storedResponse[0].Text);
Assert.Equal("HistoryResp", storedResponse[1].Text);
Assert.Equal("ContextResp", storedResponse[2].Text);
}
[Fact]
public async Task InvokedCoreAsync_UsesCustomResponseFilterAsync()
{
// Arrange - response filter that only keeps Assistant messages with specific text
var provider = new TestAIContextProvider(
storeInputResponseMessageFilter: msgs => msgs.Where(m => m.Text == "Keep"));
var requestMessages = new[] { new ChatMessage(ChatRole.User, "Request") };
var responseMessages = new[]
{
new ChatMessage(ChatRole.Assistant, "Keep"),
new ChatMessage(ChatRole.Assistant, "Drop")
};
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, responseMessages);
// Act
await provider.InvokedAsync(context);
// Assert
Assert.NotNull(provider.LastStoredContext);
var storedResponse = provider.LastStoredContext!.ResponseMessages!.ToList();
Assert.Single(storedResponse);
Assert.Equal("Keep", storedResponse[0].Text);
}
[Fact]
public async Task InvokedCoreAsync_RequestAndResponseFiltersOperateIndependentlyAsync()
{
// Arrange - different filters for request and response
var provider = new TestAIContextProvider(
storeInputRequestMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.System),
storeInputResponseMessageFilter: msgs => msgs.Where(m => m.Text == "Resp1"));
var requestMessages = new[]
{
new ChatMessage(ChatRole.User, "User"),
new ChatMessage(ChatRole.System, "System")
};
var responseMessages = new[]
{
new ChatMessage(ChatRole.Assistant, "Resp1"),
new ChatMessage(ChatRole.Assistant, "Resp2")
};
var context = new AIContextProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, responseMessages);
// Act
await provider.InvokedAsync(context);
// Assert - request filter kept only System, response filter kept only Resp1
Assert.NotNull(provider.LastStoredContext);
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Single(storedRequest);
Assert.Equal("System", storedRequest[0].Text);
var storedResponse = provider.LastStoredContext!.ResponseMessages!.ToList();
Assert.Single(storedResponse);
Assert.Equal("Resp1", storedResponse[0].Text);
}
#endregion
private sealed class TestAIContextProvider : AIContextProvider
{
private readonly AIContext? _provideContext;
private readonly bool _captureFilteredContext;
public InvokedContext? LastStoredContext { get; private set; }
public InvokingContext? LastProvidedContext { get; private set; }
public TestAIContextProvider(
AIContext? provideContext = null,
bool captureFilteredContext = false,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideInputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
: base(provideInputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
this._provideContext = provideContext;
this._captureFilteredContext = captureFilteredContext;
}
protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
if (this._captureFilteredContext)
{
this.LastProvidedContext = context;
}
return new(this._provideContext ?? new AIContext());
}
protected override ValueTask StoreAIContextAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
this.LastStoredContext = context;
return default;
}
}
/// <summary>
/// A provider that uses only base class defaults (no overrides of ProvideAIContextAsync/StoreAIContextAsync).
/// </summary>
private sealed class DefaultAIContextProvider : AIContextProvider;
}
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Unit tests for <see cref="AIContext"/>.
/// </summary>
public class AIContextTests
{
[Fact]
public void SetInstructionsRoundtrips()
{
var context = new AIContext
{
Instructions = "Test Instructions"
};
Assert.Equal("Test Instructions", context.Instructions);
}
[Fact]
public void SetMessagesRoundtrips()
{
var context = new AIContext
{
Messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!")
]
};
Assert.NotNull(context.Messages);
var messages = context.Messages.ToList();
Assert.Equal(2, messages.Count);
Assert.Equal("Hello", messages[0].Text);
Assert.Equal("Hi there!", messages[1].Text);
}
[Fact]
public void SetAIFunctionsRoundtrips()
{
var context = new AIContext
{
Tools =
[
AIFunctionFactory.Create(() => "Function1", "Function1", "Description1"),
AIFunctionFactory.Create(() => "Function2", "Function2", "Description2"),
]
};
Assert.NotNull(context.Tools);
var tools = context.Tools.ToList();
Assert.Equal(2, tools.Count);
Assert.Equal("Function1", tools[0].Name);
Assert.Equal("Function2", tools[1].Name);
}
}
@@ -0,0 +1,490 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="AdditionalPropertiesExtensions"/> class.
/// </summary>
public sealed class AdditionalPropertiesExtensionsTests
{
#region Add Method Tests
[Fact]
public void Add_WithValidValue_StoresValueUsingTypeName()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass value = new() { Name = "Test" };
// Act
additionalProperties.Add(value);
// Assert
Assert.True(additionalProperties.ContainsKey(typeof(TestClass).FullName!));
Assert.Same(value, additionalProperties[typeof(TestClass).FullName!]);
}
[Fact]
public void Add_WithNullDictionary_ThrowsArgumentNullException()
{
// Arrange
AdditionalPropertiesDictionary? additionalProperties = null;
TestClass value = new() { Name = "Test" };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => additionalProperties!.Add(value));
}
[Fact]
public void Add_WithStringValue_StoresValueCorrectly()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
const string Value = "test string";
// Act
additionalProperties.Add(Value);
// Assert
Assert.True(additionalProperties.ContainsKey(typeof(string).FullName!));
Assert.Equal(Value, additionalProperties[typeof(string).FullName!]);
}
[Fact]
public void Add_WithIntValue_StoresValueCorrectly()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
const int Value = 42;
// Act
additionalProperties.Add(Value);
// Assert
Assert.True(additionalProperties.ContainsKey(typeof(int).FullName!));
Assert.Equal(Value, additionalProperties[typeof(int).FullName!]);
}
[Fact]
public void Add_ThrowsArgumentException_WhenSameTypeAddedTwice()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass firstValue = new() { Name = "First" };
TestClass secondValue = new() { Name = "Second" };
additionalProperties.Add(firstValue);
// Act & Assert
Assert.Throws<ArgumentException>(() => additionalProperties.Add(secondValue));
}
[Fact]
public void Add_WithMultipleDifferentTypes_StoresAllValues()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass testClassValue = new() { Name = "Test" };
AnotherTestClass anotherValue = new() { Id = 123 };
const string StringValue = "test";
// Act
additionalProperties.Add(testClassValue);
additionalProperties.Add(anotherValue);
additionalProperties.Add(StringValue);
// Assert
Assert.Equal(3, additionalProperties.Count);
Assert.Same(testClassValue, additionalProperties[typeof(TestClass).FullName!]);
Assert.Same(anotherValue, additionalProperties[typeof(AnotherTestClass).FullName!]);
Assert.Equal(StringValue, additionalProperties[typeof(string).FullName!]);
}
#endregion
#region TryAdd Method Tests
[Fact]
public void TryAdd_WithValidValue_ReturnsTrueAndStoresValue()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass value = new() { Name = "Test" };
// Act
bool result = additionalProperties.TryAdd(value);
// Assert
Assert.True(result);
Assert.True(additionalProperties.ContainsKey(typeof(TestClass).FullName!));
Assert.Same(value, additionalProperties[typeof(TestClass).FullName!]);
}
[Fact]
public void TryAdd_WithNullDictionary_ThrowsArgumentNullException()
{
// Arrange
AdditionalPropertiesDictionary? additionalProperties = null;
TestClass value = new() { Name = "Test" };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => additionalProperties!.TryAdd(value));
}
[Fact]
public void TryAdd_WithExistingType_ReturnsFalseAndKeepsOriginalValue()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass firstValue = new() { Name = "First" };
TestClass secondValue = new() { Name = "Second" };
additionalProperties.Add(firstValue);
// Act
bool result = additionalProperties.TryAdd(secondValue);
// Assert
Assert.False(result);
Assert.Single(additionalProperties);
Assert.Same(firstValue, additionalProperties[typeof(TestClass).FullName!]);
}
[Fact]
public void TryAdd_WithStringValue_ReturnsTrueAndStoresValue()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
const string Value = "test string";
// Act
bool result = additionalProperties.TryAdd(Value);
// Assert
Assert.True(result);
Assert.True(additionalProperties.ContainsKey(typeof(string).FullName!));
Assert.Equal(Value, additionalProperties[typeof(string).FullName!]);
}
[Fact]
public void TryAdd_WithIntValue_ReturnsTrueAndStoresValue()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
const int Value = 42;
// Act
bool result = additionalProperties.TryAdd(Value);
// Assert
Assert.True(result);
Assert.True(additionalProperties.ContainsKey(typeof(int).FullName!));
Assert.Equal(Value, additionalProperties[typeof(int).FullName!]);
}
[Fact]
public void TryAdd_WithMultipleDifferentTypes_StoresAllValues()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass testClassValue = new() { Name = "Test" };
AnotherTestClass anotherValue = new() { Id = 123 };
const string StringValue = "test";
// Act
bool result1 = additionalProperties.TryAdd(testClassValue);
bool result2 = additionalProperties.TryAdd(anotherValue);
bool result3 = additionalProperties.TryAdd(StringValue);
// Assert
Assert.True(result1);
Assert.True(result2);
Assert.True(result3);
Assert.Equal(3, additionalProperties.Count);
Assert.Same(testClassValue, additionalProperties[typeof(TestClass).FullName!]);
Assert.Same(anotherValue, additionalProperties[typeof(AnotherTestClass).FullName!]);
Assert.Equal(StringValue, additionalProperties[typeof(string).FullName!]);
}
#endregion
#region TryGetValue Method Tests
[Fact]
public void TryGetValue_WithExistingValue_ReturnsTrueAndValue()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass expectedValue = new() { Name = "Test" };
additionalProperties.Add(expectedValue);
// Act
bool result = additionalProperties.TryGetValue(out TestClass? actualValue);
// Assert
Assert.True(result);
Assert.NotNull(actualValue);
Assert.Same(expectedValue, actualValue);
}
[Fact]
public void TryGetValue_WithNonExistingValue_ReturnsFalseAndNull()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
// Act
bool result = additionalProperties.TryGetValue(out TestClass? actualValue);
// Assert
Assert.False(result);
Assert.Null(actualValue);
}
[Fact]
public void TryGetValue_WithNullDictionary_ThrowsArgumentNullException()
{
// Arrange
AdditionalPropertiesDictionary? additionalProperties = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => additionalProperties!.TryGetValue<TestClass>(out _));
}
[Fact]
public void TryGetValue_WithStringValue_ReturnsCorrectValue()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
const string ExpectedValue = "test string";
additionalProperties.Add(ExpectedValue);
// Act
bool result = additionalProperties.TryGetValue(out string? actualValue);
// Assert
Assert.True(result);
Assert.Equal(ExpectedValue, actualValue);
}
[Fact]
public void TryGetValue_WithIntValue_ReturnsCorrectValue()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
const int ExpectedValue = 42;
additionalProperties.Add(ExpectedValue);
// Act
bool result = additionalProperties.TryGetValue(out int actualValue);
// Assert
Assert.True(result);
Assert.Equal(ExpectedValue, actualValue);
}
[Fact]
public void TryGetValue_WithWrongType_ReturnsFalse()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass testValue = new() { Name = "Test" };
additionalProperties.Add(testValue);
// Act
bool result = additionalProperties.TryGetValue(out AnotherTestClass? actualValue);
// Assert
Assert.False(result);
Assert.Null(actualValue);
}
[Fact]
public void TryGetValue_AfterTryAddFails_ReturnsOriginalValue()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass firstValue = new() { Name = "First" };
TestClass secondValue = new() { Name = "Second" };
additionalProperties.Add(firstValue);
additionalProperties.TryAdd(secondValue);
// Act
bool result = additionalProperties.TryGetValue(out TestClass? actualValue);
// Assert
Assert.Single(additionalProperties);
Assert.True(result);
Assert.Same(firstValue, actualValue);
}
#endregion
#region Contains Method Tests
[Fact]
public void Contains_WithExistingType_ReturnsTrue()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass value = new() { Name = "Test" };
additionalProperties.Add(value);
// Act
bool result = additionalProperties.Contains<TestClass>();
// Assert
Assert.True(result);
}
[Fact]
public void Contains_WithNonExistingType_ReturnsFalse()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
// Act
bool result = additionalProperties.Contains<TestClass>();
// Assert
Assert.False(result);
}
[Fact]
public void Contains_WithNullDictionary_ThrowsArgumentNullException()
{
// Arrange
AdditionalPropertiesDictionary? additionalProperties = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => additionalProperties!.Contains<TestClass>());
}
[Fact]
public void Contains_WithDifferentType_ReturnsFalse()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass value = new() { Name = "Test" };
additionalProperties.Add(value);
// Act
bool result = additionalProperties.Contains<AnotherTestClass>();
// Assert
Assert.False(result);
}
[Fact]
public void Contains_AfterRemove_ReturnsFalse()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass value = new() { Name = "Test" };
additionalProperties.Add(value);
additionalProperties.Remove<TestClass>();
// Act
bool result = additionalProperties.Contains<TestClass>();
// Assert
Assert.False(result);
}
#endregion
#region Remove Method Tests
[Fact]
public void Remove_WithExistingType_ReturnsTrueAndRemovesValue()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass value = new() { Name = "Test" };
additionalProperties.Add(value);
// Act
bool result = additionalProperties.Remove<TestClass>();
// Assert
Assert.True(result);
Assert.Empty(additionalProperties);
}
[Fact]
public void Remove_WithNonExistingType_ReturnsFalse()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
// Act
bool result = additionalProperties.Remove<TestClass>();
// Assert
Assert.False(result);
}
[Fact]
public void Remove_WithNullDictionary_ThrowsArgumentNullException()
{
// Arrange
AdditionalPropertiesDictionary? additionalProperties = null;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => additionalProperties!.Remove<TestClass>());
}
[Fact]
public void Remove_OnlyRemovesSpecifiedType()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass testValue = new() { Name = "Test" };
AnotherTestClass anotherValue = new() { Id = 123 };
additionalProperties.Add(testValue);
additionalProperties.Add(anotherValue);
// Act
bool result = additionalProperties.Remove<TestClass>();
// Assert
Assert.True(result);
Assert.Single(additionalProperties);
Assert.False(additionalProperties.Contains<TestClass>());
Assert.True(additionalProperties.Contains<AnotherTestClass>());
}
[Fact]
public void Remove_CalledTwice_ReturnsFalseOnSecondCall()
{
// Arrange
AdditionalPropertiesDictionary additionalProperties = new();
TestClass value = new() { Name = "Test" };
additionalProperties.Add(value);
// Act
bool firstResult = additionalProperties.Remove<TestClass>();
bool secondResult = additionalProperties.Remove<TestClass>();
// Assert
Assert.True(firstResult);
Assert.False(secondResult);
}
#endregion
#region Test Helper Classes
private sealed class TestClass
{
public string Name { get; set; } = string.Empty;
}
private sealed class AnotherTestClass
{
public int Id { get; set; }
}
#endregion
}
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Tests for <see cref="AgentAbstractionsJsonUtilities"/>
/// </summary>
public class AgentAbstractionsJsonUtilitiesTests
{
[Fact]
public void DefaultOptions_HasExpectedConfiguration()
{
var options = AgentAbstractionsJsonUtilities.DefaultOptions;
// Must be read-only singleton.
Assert.NotNull(options);
Assert.Same(options, AgentAbstractionsJsonUtilities.DefaultOptions);
Assert.True(options.IsReadOnly);
// Must conform to JsonSerializerDefaults.Web
Assert.Equal(JsonNamingPolicy.CamelCase, options.PropertyNamingPolicy);
Assert.True(options.PropertyNameCaseInsensitive);
Assert.Equal(JsonNumberHandling.AllowReadingFromString, options.NumberHandling);
// Additional settings
Assert.Equal(JsonIgnoreCondition.WhenWritingNull, options.DefaultIgnoreCondition);
Assert.Same(JavaScriptEncoder.UnsafeRelaxedJsonEscaping, options.Encoder);
}
[Theory]
[InlineData("<script>alert('XSS')</script>", "<script>alert('XSS')</script>")]
[InlineData("""{"forecast":"sunny", "temperature":"75"}""", """{\"forecast\":\"sunny\", \"temperature\":\"75\"}""")]
[InlineData("""{"message":"Πάντα ῥεῖ."}""", """{\"message\":\"Πάντα ῥεῖ.\"}""")]
[InlineData("""{"message":"七転び八起き"}""", """{\"message\":\"七転び八起き\"}""")]
[InlineData("""☺️🤖🌍𝄞""", """☺️\uD83E\uDD16\uD83C\uDF0D\uD834\uDD1E""")]
public void DefaultOptions_UsesExpectedEscaping(string input, string expectedJsonString)
{
var options = AgentAbstractionsJsonUtilities.DefaultOptions;
string json = JsonSerializer.Serialize(input, options);
Assert.Equal($@"""{expectedJsonString}""", json);
}
[Fact]
public void DefaultOptions_UsesReflectionWhenDefault()
{
Type anonType = new { Name = 42 }.GetType();
Assert.Equal(JsonSerializer.IsReflectionEnabledByDefault, AgentAbstractionsJsonUtilities.DefaultOptions.TryGetTypeInfo(anonType, out _));
}
// The following two tests validate behaviors of reflection-based serialization
// which is only available in .NET Framework builds.
#if NETFRAMEWORK
[Fact]
public void DefaultOptions_AllowsReadingNumbersFromStrings_AndOmitsNulls()
{
var obj = JsonSerializer.Deserialize<NumberContainer>(
"{\"value\":\"42\",\"optional\":null}", // value as string, optional null
AgentAbstractionsJsonUtilities.DefaultOptions);
Assert.NotNull(obj);
Assert.Equal(42, obj!.Value);
Assert.Null(obj.Optional);
Assert.Equal("{\"value\":42}",
JsonSerializer.Serialize(obj, AgentAbstractionsJsonUtilities.DefaultOptions)); // null omitted
}
[Fact]
public void DefaultOptions_SerializesEnumsAsStrings()
{
Assert.Equal("\"Monday\"", JsonSerializer.Serialize(DayOfWeek.Monday, AgentAbstractionsJsonUtilities.DefaultOptions));
}
#endif
[Fact]
public void DefaultOptions_UsesCamelCasePropertyNames_ForAgentResponse()
{
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Hello"));
string json = JsonSerializer.Serialize(response, AgentAbstractionsJsonUtilities.DefaultOptions);
Assert.Contains("\"messages\"", json);
Assert.DoesNotContain("\"Messages\"", json);
}
private sealed class NumberContainer
{
public int Value { get; set; }
public string? Optional { get; set; }
}
}
@@ -0,0 +1,509 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="AgentRequestMessageSourceAttribution"/> struct.
/// </summary>
public sealed class AgentRequestMessageSourceAttributionTests
{
#region Constructor Tests
[Fact]
public void Constructor_SetsSourceTypeAndSourceId()
{
// Arrange
AgentRequestMessageSourceType expectedType = AgentRequestMessageSourceType.AIContextProvider;
const string ExpectedId = "MyProvider";
// Act
AgentRequestMessageSourceAttribution attribution = new(expectedType, ExpectedId);
// Assert
Assert.Equal(expectedType, attribution.SourceType);
Assert.Equal(ExpectedId, attribution.SourceId);
}
[Fact]
public void Constructor_WithNullSourceId_SetsNullSourceId()
{
// Arrange
AgentRequestMessageSourceType sourceType = AgentRequestMessageSourceType.ChatHistory;
// Act
AgentRequestMessageSourceAttribution attribution = new(sourceType, null);
// Assert
Assert.Equal(sourceType, attribution.SourceType);
Assert.Null(attribution.SourceId);
}
#endregion
#region AdditionalPropertiesKey Tests
[Fact]
public void AdditionalPropertiesKey_IsAttribution()
{
// Assert
Assert.Equal("_attribution", AgentRequestMessageSourceAttribution.AdditionalPropertiesKey);
}
#endregion
#region Default Value Tests
[Fact]
public void Default_HasDefaultSourceTypeAndNullSourceId()
{
// Arrange & Act
AgentRequestMessageSourceAttribution attribution = default;
// Assert
Assert.Equal(default, attribution.SourceType);
Assert.Null(attribution.SourceId);
}
#endregion
#region Equals (IEquatable) Tests
[Fact]
public void Equals_WithSameSourceTypeAndSourceId_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_WithDifferentSourceType_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.ChatHistory, "Provider1");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_WithDifferentSourceId_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider2");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_WithDifferentSourceTypeAndSourceId_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.ChatHistory, "Provider2");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_WithDifferentCaseSourceId_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "provider");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_BothDefaultValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = default;
AgentRequestMessageSourceAttribution attribution2 = default;
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_WithBothNullSourceIds_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.External, null!);
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.External, null!);
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_WithOneNullSourceId_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.External, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.External, null!);
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
#endregion
#region Object.Equals Tests
[Fact]
public void ObjectEquals_WithEqualAttribution_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.ChatHistory, "Provider");
object attribution2 = new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "Provider");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.True(result);
}
[Fact]
public void ObjectEquals_WithDifferentType_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution = new(AgentRequestMessageSourceType.ChatHistory, "Provider");
object other = "NotAnAttribution";
// Act
bool result = attribution.Equals(other);
// Assert
Assert.False(result);
}
[Fact]
public void ObjectEquals_WithNullObject_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution = new(AgentRequestMessageSourceType.ChatHistory, "Provider");
object? other = null;
// Act
bool result = attribution.Equals(other);
// Assert
Assert.False(result);
}
[Fact]
public void ObjectEquals_WithBoxedDifferentAttribution_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.ChatHistory, "Provider1");
object attribution2 = new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "Provider2");
// Act
bool result = attribution1.Equals(attribution2);
// Assert
Assert.False(result);
}
#endregion
#region GetHashCode Tests
[Fact]
public void GetHashCode_WithSameValues_ReturnsSameHashCode()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
// Act
int hashCode1 = attribution1.GetHashCode();
int hashCode2 = attribution2.GetHashCode();
// Assert
Assert.Equal(hashCode1, hashCode2);
}
[Fact]
public void GetHashCode_WithDifferentSourceType_ReturnsDifferentHashCode()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.ChatHistory, "Provider");
// Act
int hashCode1 = attribution1.GetHashCode();
int hashCode2 = attribution2.GetHashCode();
// Assert
Assert.NotEqual(hashCode1, hashCode2);
}
[Fact]
public void GetHashCode_WithDifferentSourceId_ReturnsDifferentHashCode()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider2");
// Act
int hashCode1 = attribution1.GetHashCode();
int hashCode2 = attribution2.GetHashCode();
// Assert
Assert.NotEqual(hashCode1, hashCode2);
}
[Fact]
public void GetHashCode_ConsistentWithEquals()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.External, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.External, "Provider");
// Act & Assert
Assert.True(attribution1.Equals(attribution2));
Assert.Equal(attribution1.GetHashCode(), attribution2.GetHashCode());
}
[Fact]
public void GetHashCode_WithNullSourceId_DoesNotThrow()
{
// Arrange
AgentRequestMessageSourceAttribution attribution = new(AgentRequestMessageSourceType.External, null!);
// Act
int hashCode = attribution.GetHashCode();
// Assert
Assert.IsType<int>(hashCode);
}
#endregion
#region Equality Operator Tests
[Fact]
public void EqualityOperator_WithEqualValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
// Act
bool result = attribution1 == attribution2;
// Assert
Assert.True(result);
}
[Fact]
public void EqualityOperator_WithDifferentValues_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.ChatHistory, "Provider2");
// Act
bool result = attribution1 == attribution2;
// Assert
Assert.False(result);
}
[Fact]
public void EqualityOperator_WithBothDefault_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = default;
AgentRequestMessageSourceAttribution attribution2 = default;
// Act
bool result = attribution1 == attribution2;
// Assert
Assert.True(result);
}
[Fact]
public void EqualityOperator_WithDifferentSourceTypeOnly_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.External, "Provider");
// Act
bool result = attribution1 == attribution2;
// Assert
Assert.False(result);
}
[Fact]
public void EqualityOperator_WithDifferentSourceIdOnly_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider2");
// Act
bool result = attribution1 == attribution2;
// Assert
Assert.False(result);
}
#endregion
#region ToString Tests
[Fact]
public void ToString_WithSourceId_ReturnsTypeColonId()
{
// Arrange
AgentRequestMessageSourceAttribution attribution = new(AgentRequestMessageSourceType.AIContextProvider, "MyProvider");
// Act
string result = attribution.ToString();
// Assert
Assert.Equal("AIContextProvider:MyProvider", result);
}
[Fact]
public void ToString_WithNullSourceId_ReturnsTypeOnly()
{
// Arrange
AgentRequestMessageSourceAttribution attribution = new(AgentRequestMessageSourceType.ChatHistory, null);
// Act
string result = attribution.ToString();
// Assert
Assert.Equal("ChatHistory", result);
}
[Fact]
public void ToString_Default_ReturnsExternalOnly()
{
// Arrange
AgentRequestMessageSourceAttribution attribution = default;
// Act
string result = attribution.ToString();
// Assert
Assert.Equal("External", result);
}
#endregion
#region Inequality Operator Tests
[Fact]
public void InequalityOperator_WithEqualValues_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
// Act
bool result = attribution1 != attribution2;
// Assert
Assert.False(result);
}
[Fact]
public void InequalityOperator_WithDifferentValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.ChatHistory, "Provider2");
// Act
bool result = attribution1 != attribution2;
// Assert
Assert.True(result);
}
[Fact]
public void InequalityOperator_WithBothDefault_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = default;
AgentRequestMessageSourceAttribution attribution2 = default;
// Act
bool result = attribution1 != attribution2;
// Assert
Assert.False(result);
}
[Fact]
public void InequalityOperator_WithDifferentSourceTypeOnly_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.External, "Provider");
// Act
bool result = attribution1 != attribution2;
// Assert
Assert.True(result);
}
[Fact]
public void InequalityOperator_WithDifferentSourceIdOnly_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceAttribution attribution1 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider1");
AgentRequestMessageSourceAttribution attribution2 = new(AgentRequestMessageSourceType.AIContextProvider, "Provider2");
// Act
bool result = attribution1 != attribution2;
// Assert
Assert.True(result);
}
#endregion
}
@@ -0,0 +1,470 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="AgentRequestMessageSourceType"/> struct.
/// </summary>
public sealed class AgentRequestMessageSourceTypeTests
{
#region Constructor Tests
[Fact]
public void Constructor_WithValue_SetsValueProperty()
{
// Arrange
const string ExpectedValue = "CustomSource";
// Act
AgentRequestMessageSourceType source = new(ExpectedValue);
// Assert
Assert.Equal(ExpectedValue, source.Value);
}
[Fact]
public void Constructor_WithNullValue_Throws()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AgentRequestMessageSourceType(null!));
}
[Fact]
public void Constructor_WithEmptyValue_Throws()
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new AgentRequestMessageSourceType(string.Empty));
}
[Fact]
public void Default_DefaultsToExternal()
{
// Act
AgentRequestMessageSourceType defaultSource = default;
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, defaultSource);
}
#endregion
#region Static Properties Tests
[Fact]
public void External_ReturnsInstanceWithExternalValue()
{
// Arrange & Act
AgentRequestMessageSourceType source = AgentRequestMessageSourceType.External;
// Assert
Assert.Equal("External", source.Value);
}
[Fact]
public void AIContextProvider_ReturnsInstanceWithAIContextProviderValue()
{
// Arrange & Act
AgentRequestMessageSourceType source = AgentRequestMessageSourceType.AIContextProvider;
// Assert
Assert.Equal("AIContextProvider", source.Value);
}
[Fact]
public void ChatHistory_ReturnsInstanceWithChatHistoryValue()
{
// Arrange & Act
AgentRequestMessageSourceType source = AgentRequestMessageSourceType.ChatHistory;
// Assert
Assert.Equal("ChatHistory", source.Value);
}
[Fact]
public void StaticProperties_ReturnEqualValuesOnMultipleCalls()
{
// Arrange & Act
AgentRequestMessageSourceType external1 = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType external2 = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType aiContextProvider1 = AgentRequestMessageSourceType.AIContextProvider;
AgentRequestMessageSourceType aiContextProvider2 = AgentRequestMessageSourceType.AIContextProvider;
AgentRequestMessageSourceType chatHistory1 = AgentRequestMessageSourceType.ChatHistory;
AgentRequestMessageSourceType chatHistory2 = AgentRequestMessageSourceType.ChatHistory;
// Assert
Assert.Equal(external1, external2);
Assert.Equal(aiContextProvider1, aiContextProvider2);
Assert.Equal(chatHistory1, chatHistory2);
}
#endregion
#region Equals Tests
[Fact]
public void Equals_WithSameInstance_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
// Act
bool result = source.Equals(source);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_WithEqualValue_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("Test");
// Act
bool result = source1.Equals(source2);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_WithDifferentValue_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test1");
AgentRequestMessageSourceType source2 = new("Test2");
// Act
bool result = source1.Equals(source2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_WithNullObject_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
// Act
bool result = source.Equals(null);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_WithDifferentCase_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("test");
// Act
bool result = source1.Equals(source2);
// Assert
Assert.False(result);
}
[Fact]
public void Equals_StaticExternalWithNewInstanceHavingSameValue_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType external = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType newExternal = new("External");
// Act
bool result = external.Equals(newExternal);
// Assert
Assert.True(result);
}
#endregion
#region Object.Equals Tests
[Fact]
public void ObjectEquals_WithEqualAgentRequestMessageSource_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
object source2 = new AgentRequestMessageSourceType("Test");
// Act
bool result = source1.Equals(source2);
// Assert
Assert.True(result);
}
[Fact]
public void ObjectEquals_WithDifferentType_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
object other = "Test";
// Act
bool result = source.Equals(other);
// Assert
Assert.False(result);
}
[Fact]
public void ObjectEquals_WithNullObject_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
object? other = null;
// Act
bool result = source.Equals(other);
// Assert
Assert.False(result);
}
#endregion
#region GetHashCode Tests
[Fact]
public void GetHashCode_WithSameValue_ReturnsSameHashCode()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("Test");
// Act
int hashCode1 = source1.GetHashCode();
int hashCode2 = source2.GetHashCode();
// Assert
Assert.Equal(hashCode1, hashCode2);
}
[Fact]
public void GetHashCode_WithDifferentValue_ReturnsDifferentHashCode()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test1");
AgentRequestMessageSourceType source2 = new("Test2");
// Act
int hashCode1 = source1.GetHashCode();
int hashCode2 = source2.GetHashCode();
// Assert
Assert.NotEqual(hashCode1, hashCode2);
}
[Fact]
public void GetHashCode_ConsistentWithEquals()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("Test");
// Act & Assert
// If two objects are equal, they must have the same hash code
Assert.True(source1.Equals(source2));
Assert.Equal(source1.GetHashCode(), source2.GetHashCode());
}
#endregion
#region Equality Operator Tests
[Fact]
public void EqualityOperator_WithEqualValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("Test");
// Act
bool result = source1 == source2;
// Assert
Assert.True(result);
}
[Fact]
public void EqualityOperator_WithDifferentValues_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test1");
AgentRequestMessageSourceType source2 = new("Test2");
// Act
bool result = source1 == source2;
// Assert
Assert.False(result);
}
[Fact]
public void EqualityOperator_WithDefaultValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = default;
AgentRequestMessageSourceType source2 = default;
// Act
bool result = source1 == source2;
// Assert
Assert.True(result);
}
[Fact]
public void EqualityOperator_WithStaticInstances_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType external1 = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType external2 = AgentRequestMessageSourceType.External;
// Act
bool result = external1 == external2;
// Assert
Assert.True(result);
}
[Fact]
public void EqualityOperator_StaticWithNewInstanceHavingSameValue_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType external = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType newExternal = new("External");
// Act
bool result = external == newExternal;
// Assert
Assert.True(result);
}
#endregion
#region Inequality Operator Tests
[Fact]
public void InequalityOperator_WithEqualValues_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test");
AgentRequestMessageSourceType source2 = new("Test");
// Act
bool result = source1 != source2;
// Assert
Assert.False(result);
}
[Fact]
public void InequalityOperator_WithDifferentValues_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType source1 = new("Test1");
AgentRequestMessageSourceType source2 = new("Test2");
// Act
bool result = source1 != source2;
// Assert
Assert.True(result);
}
[Fact]
public void InequalityOperator_WithBothDefault_ReturnsFalse()
{
// Arrange
AgentRequestMessageSourceType source1 = default;
AgentRequestMessageSourceType source2 = default;
// Act
bool result = source1 != source2;
// Assert
Assert.False(result);
}
[Fact]
public void InequalityOperator_DifferentStaticInstances_ReturnsTrue()
{
// Arrange
AgentRequestMessageSourceType external = AgentRequestMessageSourceType.External;
AgentRequestMessageSourceType chatHistory = AgentRequestMessageSourceType.ChatHistory;
// Act
bool result = external != chatHistory;
// Assert
Assert.True(result);
}
#endregion
#region ToString Tests
[Fact]
public void ToString_ReturnsValue()
{
// Arrange
AgentRequestMessageSourceType source = new("CustomSource");
// Act
string result = source.ToString();
// Assert
Assert.Equal("CustomSource", result);
}
[Fact]
public void ToString_StaticExternal_ReturnsExternal()
{
// Arrange & Act
string result = AgentRequestMessageSourceType.External.ToString();
// Assert
Assert.Equal("External", result);
}
[Fact]
public void ToString_Default_ReturnsExternal()
{
// Arrange
AgentRequestMessageSourceType source = default;
// Act
string result = source.ToString();
// Assert
Assert.Equal("External", result);
}
#endregion
#region IEquatable Tests
[Fact]
public void IEquatable_ImplementedCorrectly()
{
// Arrange
AgentRequestMessageSourceType source = new("Test");
// Act & Assert
Assert.IsAssignableFrom<IEquatable<AgentRequestMessageSourceType>>(source);
}
#endregion
}
@@ -0,0 +1,326 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Microsoft.Agents.AI.Abstractions.UnitTests.Models;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
public class AgentResponseTests
{
[Fact]
public void ConstructorWithNullEmptyArgsIsValid()
{
AgentResponse response;
response = new();
Assert.Empty(response.Messages);
Assert.Empty(response.Text);
Assert.Null(response.ContinuationToken);
response = new((IList<ChatMessage>?)null);
Assert.Empty(response.Messages);
Assert.Empty(response.Text);
Assert.Null(response.ContinuationToken);
Assert.Throws<ArgumentNullException>("message", () => new AgentResponse((ChatMessage)null!));
}
[Fact]
public void ConstructorWithMessagesRoundtrips()
{
AgentResponse response = new();
Assert.NotNull(response.Messages);
Assert.Same(response.Messages, response.Messages);
List<ChatMessage> messages = [];
response = new(messages);
Assert.Same(messages, response.Messages);
messages = [];
Assert.NotSame(messages, response.Messages);
response.Messages = messages;
Assert.Same(messages, response.Messages);
}
[Fact]
public void ConstructorWithChatResponseRoundtrips()
{
ChatResponse chatResponse = new()
{
AdditionalProperties = [],
CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
FinishReason = ChatFinishReason.ContentFilter,
Messages = [new(ChatRole.Assistant, "This is a test message.")],
RawRepresentation = new object(),
ResponseId = "responseId",
Usage = new UsageDetails(),
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })
};
AgentResponse response = new(chatResponse);
Assert.Same(chatResponse.AdditionalProperties, response.AdditionalProperties);
Assert.Equal(chatResponse.CreatedAt, response.CreatedAt);
Assert.Equal(chatResponse.FinishReason, response.FinishReason);
Assert.Same(chatResponse.Messages, response.Messages);
Assert.Equal(chatResponse.ResponseId, response.ResponseId);
Assert.Same(chatResponse, response.RawRepresentation as ChatResponse);
Assert.Same(chatResponse.Usage, response.Usage);
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), response.ContinuationToken);
}
[Fact]
public void PropertiesRoundtrip()
{
AgentResponse response = new();
Assert.Null(response.AgentId);
response.AgentId = "agentId";
Assert.Equal("agentId", response.AgentId);
Assert.Null(response.ResponseId);
response.ResponseId = "id";
Assert.Equal("id", response.ResponseId);
Assert.Null(response.CreatedAt);
response.CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero);
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), response.CreatedAt);
Assert.Null(response.Usage);
UsageDetails usage = new();
response.Usage = usage;
Assert.Same(usage, response.Usage);
Assert.Null(response.RawRepresentation);
object raw = new();
response.RawRepresentation = raw;
Assert.Same(raw, response.RawRepresentation);
Assert.Null(response.AdditionalProperties);
AdditionalPropertiesDictionary additionalProps = [];
response.AdditionalProperties = additionalProps;
Assert.Same(additionalProps, response.AdditionalProperties);
Assert.Null(response.ContinuationToken);
response.ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), response.ContinuationToken);
Assert.Null(response.FinishReason);
response.FinishReason = ChatFinishReason.Length;
Assert.Equal(ChatFinishReason.Length, response.FinishReason);
}
[Fact]
public void JsonSerializationRoundtrips()
{
AgentResponse original = new(new ChatMessage(ChatRole.Assistant, "the message"))
{
AgentId = "agentId",
ResponseId = "id",
CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
Usage = new UsageDetails(),
RawRepresentation = new(),
AdditionalProperties = new() { ["key"] = "value" },
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
};
string json = JsonSerializer.Serialize(original, AgentAbstractionsJsonUtilities.DefaultOptions);
AgentResponse? result = JsonSerializer.Deserialize<AgentResponse>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Messages.Single().Role);
Assert.Equal("the message", result.Messages.Single().Text);
Assert.Equal("agentId", result.AgentId);
Assert.Equal("id", result.ResponseId);
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), result.CreatedAt);
Assert.NotNull(result.Usage);
Assert.NotNull(result.AdditionalProperties);
Assert.Single(result.AdditionalProperties);
Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value));
Assert.IsType<JsonElement>(value);
Assert.Equal("value", ((JsonElement)value!).GetString());
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), result.ContinuationToken);
}
[Fact]
public void ToStringOutputsText()
{
AgentResponse response = new(new ChatMessage(ChatRole.Assistant, $"This is a test.{Environment.NewLine}It's multiple lines."));
Assert.Equal(response.Text, response.ToString());
}
[Fact]
public void TextGetConcatenatesAllTextContent()
{
AgentResponse response = new(
[
new ChatMessage(
ChatRole.Assistant,
[
new DataContent("data:image/audio;base64,aGVsbG8="),
new DataContent("data:image/image;base64,aGVsbG8="),
new FunctionCallContent("callId1", "fc1"),
new TextContent("message1-text-1"),
new TextContent("message1-text-2"),
new FunctionResultContent("callId1", "result"),
]),
new ChatMessage(ChatRole.Assistant, "message2")
]);
Assert.Equal($"message1-text-1message1-text-2{Environment.NewLine}message2", response.Text);
}
[Fact]
public void TextGetReturnsEmptyStringWithNoMessages()
{
AgentResponse response = new();
Assert.Equal(string.Empty, response.Text);
}
[Fact]
public void ToAgentResponseUpdatesProducesUpdates()
{
AgentResponse response = new(new ChatMessage(new ChatRole("customRole"), "Text") { MessageId = "someMessage" })
{
AgentId = "agentId",
ResponseId = "12345",
CreatedAt = new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero),
AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 },
FinishReason = ChatFinishReason.ContentFilter,
Usage = new UsageDetails
{
TotalTokenCount = 100
},
};
AgentResponseUpdate[] updates = response.ToAgentResponseUpdates();
Assert.NotNull(updates);
Assert.Equal(2, updates.Length);
AgentResponseUpdate update0 = updates[0];
Assert.Equal("agentId", update0.AgentId);
Assert.Equal("12345", update0.ResponseId);
Assert.Equal("someMessage", update0.MessageId);
Assert.Equal(new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero), update0.CreatedAt);
Assert.Equal("customRole", update0.Role?.Value);
Assert.Equal("Text", update0.Text);
Assert.Equal(ChatFinishReason.ContentFilter, update0.FinishReason);
AgentResponseUpdate update1 = updates[1];
Assert.Equal("value1", update1.AdditionalProperties?["key1"]);
Assert.Equal(42, update1.AdditionalProperties?["key2"]);
Assert.IsType<UsageContent>(update1.Contents[0]);
UsageContent usageContent = (UsageContent)update1.Contents[0];
Assert.Equal(100, usageContent.Details.TotalTokenCount);
}
[Fact]
public void ToAgentResponseUpdatesPropagatesCreatedAt()
{
// Sets different CreatedAt values on the AgentResponse and the ChatMessage to verify that the ChatMessage.CreatedAt is the one that gets propagated to the AgentResponseUpdate
AgentResponse response = new(new ChatMessage(new ChatRole("customRole"), "Text") { MessageId = "someMessage", CreatedAt = new DateTimeOffset(2024, 11, 11, 9, 20, 0, TimeSpan.Zero) })
{
AgentId = "agentId",
ResponseId = "12345",
CreatedAt = new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero),
AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 },
Usage = new UsageDetails
{
TotalTokenCount = 100
},
};
AgentResponseUpdate[] updates = response.ToAgentResponseUpdates();
Assert.NotNull(updates);
Assert.Equal(2, updates.Length);
AgentResponseUpdate update0 = updates[0];
Assert.Equal("agentId", update0.AgentId);
Assert.Equal("12345", update0.ResponseId);
Assert.Equal("someMessage", update0.MessageId);
Assert.Equal(new DateTimeOffset(2024, 11, 11, 9, 20, 0, TimeSpan.Zero), update0.CreatedAt);
Assert.Equal("customRole", update0.Role?.Value);
Assert.Equal("Text", update0.Text);
AgentResponseUpdate update1 = updates[1];
Assert.Equal("value1", update1.AdditionalProperties?["key1"]);
Assert.Equal(42, update1.AdditionalProperties?["key2"]);
Assert.IsType<UsageContent>(update1.Contents[0]);
UsageContent usageContent = (UsageContent)update1.Contents[0];
Assert.Equal(100, usageContent.Details.TotalTokenCount);
}
[Fact]
public void ParseAsStructuredOutputWithJSOSuccess()
{
// Arrange.
var expectedResult = new Animal { Id = 1, FullName = "Tigger", Species = Species.Tiger };
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedResult, TestJsonSerializerContext.Default.Animal)));
// Act.
var animal = JsonSerializer.Deserialize<Animal>(response.Text, TestJsonSerializerContext.Default.Options);
// Assert.
Assert.NotNull(animal);
Assert.Equal(expectedResult.Id, animal.Id);
Assert.Equal(expectedResult.FullName, animal.FullName);
Assert.Equal(expectedResult.Species, animal.Species);
}
[Fact]
public void ToAgentResponseUpdatesWithNoMessagesProducesEmptyArray()
{
// Arrange
AgentResponse response = new();
// Act
AgentResponseUpdate[] updates = response.ToAgentResponseUpdates();
// Assert
Assert.Empty(updates);
}
[Fact]
public void ToAgentResponseUpdatesWithUsageOnlyProducesSingleUpdate()
{
// Arrange
AgentResponse response = new()
{
Usage = new UsageDetails { TotalTokenCount = 100 }
};
// Act
AgentResponseUpdate[] updates = response.ToAgentResponseUpdates();
// Assert
AgentResponseUpdate update = Assert.Single(updates);
UsageContent usageContent = Assert.IsType<UsageContent>(update.Contents[0]);
Assert.Equal(100, usageContent.Details.TotalTokenCount);
}
[Fact]
public void ToAgentResponseUpdatesWithAdditionalPropertiesOnlyProducesSingleUpdate()
{
// Arrange
AgentResponse response = new()
{
AdditionalProperties = new() { ["key"] = "value" }
};
// Act
AgentResponseUpdate[] updates = response.ToAgentResponseUpdates();
// Assert
AgentResponseUpdate update = Assert.Single(updates);
Assert.NotNull(update.AdditionalProperties);
Assert.Equal("value", update.AdditionalProperties!["key"]);
}
}
@@ -0,0 +1,508 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
public class AgentResponseUpdateExtensionsTests
{
public static IEnumerable<object[]> ToAgentResponseCoalescesVariousSequenceAndGapLengthsMemberData()
{
foreach (bool useAsync in new[] { false, true })
{
for (int numSequences = 1; numSequences <= 3; numSequences++)
{
for (int sequenceLength = 1; sequenceLength <= 3; sequenceLength++)
{
for (int gapLength = 1; gapLength <= 3; gapLength++)
{
foreach (bool gapBeginningEnd in new[] { false, true })
{
yield return new object[] { useAsync, numSequences, sequenceLength, gapLength, false };
}
}
}
}
}
}
[Fact]
public void ToAgentResponseWithInvalidArgsThrows() =>
Assert.Throws<ArgumentNullException>("updates", () => ((List<AgentResponseUpdate>)null!).ToAgentResponse());
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task ToAgentResponseSuccessfullyCreatesResponseAsync(bool useAsync)
{
AgentResponseUpdate[] updates =
[
new(ChatRole.Assistant, "Hello") { ResponseId = "someResponse", MessageId = "12345", CreatedAt = new DateTimeOffset(2024, 2, 3, 4, 5, 6, TimeSpan.Zero), AgentId = "agentId" },
new(new("human"), ", ") { AuthorName = "Someone", AdditionalProperties = new() { ["a"] = "b" } },
new(null, "world!") { CreatedAt = new DateTimeOffset(2025, 2, 3, 4, 5, 6, TimeSpan.Zero), AdditionalProperties = new() { ["c"] = "d" } },
new() { Contents = [new UsageContent(new() { InputTokenCount = 1, OutputTokenCount = 2 })] },
new() { Contents = [new UsageContent(new() { InputTokenCount = 4, OutputTokenCount = 5 })] },
];
AgentResponse response = useAsync ?
updates.ToAgentResponse() :
await YieldAsync(updates).ToAgentResponseAsync();
Assert.NotNull(response);
Assert.Equal("agentId", response.AgentId);
Assert.NotNull(response.Usage);
Assert.Equal(5, response.Usage.InputTokenCount);
Assert.Equal(7, response.Usage.OutputTokenCount);
Assert.Equal("someResponse", response.ResponseId);
Assert.Equal(new DateTimeOffset(2024, 2, 3, 4, 5, 6, TimeSpan.Zero), response.CreatedAt);
Assert.Equal(2, response.Messages.Count);
ChatMessage message = response.Messages[0];
Assert.Equal("12345", message.MessageId);
Assert.Equal(ChatRole.Assistant, message.Role);
Assert.Null(message.AuthorName);
Assert.Null(message.AdditionalProperties);
Assert.Single(message.Contents);
Assert.Equal("Hello", Assert.IsType<TextContent>(message.Contents[0]).Text);
message = response.Messages[1];
Assert.Null(message.MessageId);
Assert.Equal(new("human"), message.Role);
Assert.Equal("Someone", message.AuthorName);
Assert.Single(message.Contents);
Assert.Equal(", world!", Assert.IsType<TextContent>(message.Contents[0]).Text);
Assert.NotNull(response.AdditionalProperties);
Assert.Equal(2, response.AdditionalProperties.Count);
Assert.Equal("b", response.AdditionalProperties["a"]);
Assert.Equal("d", response.AdditionalProperties["c"]);
Assert.Equal("Hello" + Environment.NewLine + ", world!", response.Text);
}
[Theory]
[MemberData(nameof(ToAgentResponseCoalescesVariousSequenceAndGapLengthsMemberData))]
public async Task ToAgentResponseCoalescesVariousSequenceAndGapLengthsAsync(bool useAsync, int numSequences, int sequenceLength, int gapLength, bool gapBeginningEnd)
{
List<AgentResponseUpdate> updates = [];
List<string> expected = [];
if (gapBeginningEnd)
{
AddGap();
}
for (int sequenceNum = 0; sequenceNum < numSequences; sequenceNum++)
{
StringBuilder sb = new();
for (int i = 0; i < sequenceLength; i++)
{
string text = $"{(char)('A' + sequenceNum)}{i}";
updates.Add(new(null, text));
sb.Append(text);
}
expected.Add(sb.ToString());
if (sequenceNum < numSequences - 1)
{
AddGap();
}
}
if (gapBeginningEnd)
{
AddGap();
}
void AddGap()
{
for (int i = 0; i < gapLength; i++)
{
updates.Add(new() { Contents = [new DataContent("data:image/png;base64,aGVsbG8=")] });
}
}
AgentResponse response = useAsync ? await YieldAsync(updates).ToAgentResponseAsync() : updates.ToAgentResponse();
Assert.NotNull(response);
ChatMessage message = response.Messages.Single();
Assert.NotNull(message);
Assert.Equal(expected.Count + (gapLength * (numSequences - 1 + (gapBeginningEnd ? 2 : 0))), message.Contents.Count);
TextContent[] contents = message.Contents.OfType<TextContent>().ToArray();
Assert.Equal(expected.Count, contents.Length);
for (int i = 0; i < expected.Count; i++)
{
Assert.Equal(expected[i], contents[i].Text);
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task ToAgentResponseCoalescesTextContentAndTextReasoningContentSeparatelyAsync(bool useAsync)
{
AgentResponseUpdate[] updates =
[
new(null, "A"),
new(null, "B"),
new(null, "C"),
new() { Contents = [new TextReasoningContent("D")] },
new() { Contents = [new TextReasoningContent("E")] },
new() { Contents = [new TextReasoningContent("F")] },
new(null, "G"),
new(null, "H"),
new() { Contents = [new TextReasoningContent("I")] },
new() { Contents = [new TextReasoningContent("J")] },
new(null, "K"),
new() { Contents = [new TextReasoningContent("L")] },
new(null, "M"),
new(null, "N"),
new() { Contents = [new TextReasoningContent("O")] },
new() { Contents = [new TextReasoningContent("P")] },
];
AgentResponse response = useAsync ? await YieldAsync(updates).ToAgentResponseAsync() : updates.ToAgentResponse();
ChatMessage message = Assert.Single(response.Messages);
Assert.Equal(8, message.Contents.Count);
Assert.Equal("ABC", Assert.IsType<TextContent>(message.Contents[0]).Text);
Assert.Equal("DEF", Assert.IsType<TextReasoningContent>(message.Contents[1]).Text);
Assert.Equal("GH", Assert.IsType<TextContent>(message.Contents[2]).Text);
Assert.Equal("IJ", Assert.IsType<TextReasoningContent>(message.Contents[3]).Text);
Assert.Equal("K", Assert.IsType<TextContent>(message.Contents[4]).Text);
Assert.Equal("L", Assert.IsType<TextReasoningContent>(message.Contents[5]).Text);
Assert.Equal("MN", Assert.IsType<TextContent>(message.Contents[6]).Text);
Assert.Equal("OP", Assert.IsType<TextReasoningContent>(message.Contents[7]).Text);
}
[Fact]
public async Task ToAgentResponseUsesContentExtractedFromContentsAsync()
{
AgentResponseUpdate[] updates =
[
new(null, "Hello, "),
new(null, "world!"),
new() { Contents = [new UsageContent(new() { TotalTokenCount = 42 })] },
];
AgentResponse response = await YieldAsync(updates).ToAgentResponseAsync();
Assert.NotNull(response);
Assert.NotNull(response.Usage);
Assert.Equal(42, response.Usage.TotalTokenCount);
Assert.Equal("Hello, world!", Assert.IsType<TextContent>(Assert.Single(Assert.Single(response.Messages).Contents)).Text);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task ToAgentResponse_AlternativeTimestampsAsync(bool useAsync)
{
DateTimeOffset early = new(2024, 1, 1, 10, 0, 0, TimeSpan.Zero);
DateTimeOffset middle = new(2024, 1, 1, 11, 0, 0, TimeSpan.Zero);
DateTimeOffset late = new(2024, 1, 1, 12, 0, 0, TimeSpan.Zero);
DateTimeOffset unixEpoch = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
AgentResponseUpdate[] updates =
[
// Start with an early timestamp
new(ChatRole.Tool, "a") { MessageId = "4", CreatedAt = early },
// Unix epoch (as "null") should not overwrite
new(null, "b") { CreatedAt = unixEpoch },
// Newer timestamp should not overwrite (first timestamp wins)
new(null, "c") { CreatedAt = middle },
// Older timestamp should not overwrite
new(null, "d") { CreatedAt = early },
// Even newer timestamp should not overwrite (first timestamp wins)
new(null, "e") { CreatedAt = late },
// Unix epoch should not overwrite again
new(null, "f") { CreatedAt = unixEpoch },
// null should not overwrite
new(null, "g") { CreatedAt = null },
];
AgentResponse response = useAsync ?
updates.ToAgentResponse() :
await YieldAsync(updates).ToAgentResponseAsync();
Assert.Single(response.Messages);
Assert.Equal("abcdefg", response.Messages[0].Text);
Assert.Equal(ChatRole.Tool, response.Messages[0].Role);
Assert.Equal(early, response.Messages[0].CreatedAt);
Assert.Equal(early, response.CreatedAt);
}
public static IEnumerable<object?[]> ToAgentResponse_TimestampFolding_MemberData()
{
// Base test cases - first non-null valid timestamp wins
var testCases = new (string? timestamp1, string? timestamp2, string? expectedTimestamp)[]
{
(null, null, null),
("2024-01-01T10:00:00Z", null, "2024-01-01T10:00:00Z"),
(null, "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z"),
("2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z"), // First timestamp wins
("2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z"), // First timestamp wins
("2024-01-01T10:00:00Z", "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z"),
("1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z"),
};
// Yield each test case twice, once for useAsync = false and once for useAsync = true
foreach (var (timestamp1, timestamp2, expectedTimestamp) in testCases)
{
yield return new object?[] { false, timestamp1, timestamp2, expectedTimestamp };
yield return new object?[] { true, timestamp1, timestamp2, expectedTimestamp };
}
}
[Theory]
[MemberData(nameof(ToAgentResponse_TimestampFolding_MemberData))]
public async Task ToAgentResponse_TimestampFoldingAsync(bool useAsync, string? timestamp1, string? timestamp2, string? expectedTimestamp)
{
DateTimeOffset? first = timestamp1 is not null ? DateTimeOffset.Parse(timestamp1) : null;
DateTimeOffset? second = timestamp2 is not null ? DateTimeOffset.Parse(timestamp2) : null;
DateTimeOffset? expected = expectedTimestamp is not null ? DateTimeOffset.Parse(expectedTimestamp) : null;
AgentResponseUpdate[] updates =
[
new(ChatRole.Assistant, "a") { CreatedAt = first },
new(null, "b") { CreatedAt = second },
];
AgentResponse response = useAsync ?
updates.ToAgentResponse() :
await YieldAsync(updates).ToAgentResponseAsync();
Assert.Single(response.Messages);
Assert.Equal("ab", response.Messages[0].Text);
Assert.Equal(expected, response.Messages[0].CreatedAt);
Assert.Equal(expected, response.CreatedAt);
}
#region AsChatResponse Tests
[Fact]
public void AsChatResponse_WithNullArgument_ThrowsArgumentNullException()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>("response", () => ((AgentResponse)null!).AsChatResponse());
}
[Fact]
public void AsChatResponse_WithRawRepresentationAsChatResponse_ReturnsSameInstance()
{
// Arrange
ChatResponse originalChatResponse = new()
{
ResponseId = "original-response",
Messages = [new ChatMessage(ChatRole.Assistant, "Hello")]
};
AgentResponse agentResponse = new(originalChatResponse);
// Act
ChatResponse result = agentResponse.AsChatResponse();
// Assert
Assert.Same(originalChatResponse, result);
}
[Fact]
public void AsChatResponse_WithoutRawRepresentation_CreatesNewChatResponse()
{
// Arrange
AgentResponse agentResponse = new(new ChatMessage(ChatRole.Assistant, "Test message"))
{
ResponseId = "test-response-id",
CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero),
FinishReason = ChatFinishReason.ContentFilter,
Usage = new UsageDetails { TotalTokenCount = 50 },
AdditionalProperties = new() { ["key"] = "value" },
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
};
// Act
ChatResponse result = agentResponse.AsChatResponse();
// Assert
Assert.NotNull(result);
Assert.Equal("test-response-id", result.ResponseId);
Assert.Equal(new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero), result.CreatedAt);
Assert.Equal(ChatFinishReason.ContentFilter, result.FinishReason);
Assert.Same(agentResponse.Messages, result.Messages);
Assert.Same(agentResponse, result.RawRepresentation);
Assert.Same(agentResponse.Usage, result.Usage);
Assert.Same(agentResponse.AdditionalProperties, result.AdditionalProperties);
Assert.Equal(agentResponse.ContinuationToken, result.ContinuationToken);
}
#endregion
#region AsChatResponseUpdate Tests
[Fact]
public void AsChatResponseUpdate_WithNullArgument_ThrowsArgumentNullException()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>("responseUpdate", () => ((AgentResponseUpdate)null!).AsChatResponseUpdate());
}
[Fact]
public void AsChatResponseUpdate_WithRawRepresentationAsChatResponseUpdate_ReturnsSameInstance()
{
// Arrange
ChatResponseUpdate originalChatResponseUpdate = new()
{
ResponseId = "original-update",
Contents = [new TextContent("Hello")]
};
AgentResponseUpdate agentResponseUpdate = new(originalChatResponseUpdate);
// Act
ChatResponseUpdate result = agentResponseUpdate.AsChatResponseUpdate();
// Assert
Assert.Same(originalChatResponseUpdate, result);
}
[Fact]
public void AsChatResponseUpdate_WithRawRepresentationNullMessageId_ReturnsRawDirectly()
{
// Arrange - RawRepresentation has null MessageId
ChatResponseUpdate originalChatResponseUpdate = new()
{
ResponseId = "original-update",
Contents = [new TextContent("Hello")]
};
AgentResponseUpdate agentResponseUpdate = new(originalChatResponseUpdate);
// Act
ChatResponseUpdate result = agentResponseUpdate.AsChatResponseUpdate();
// Assert - Returns the raw representation directly without mutation
Assert.Same(originalChatResponseUpdate, result);
Assert.Null(result.MessageId);
}
[Fact]
public void AsChatResponseUpdate_WithRawRepresentationExistingMessageId_PreservesOriginal()
{
// Arrange - RawRepresentation already has MessageId set by provider
ChatResponseUpdate originalChatResponseUpdate = new()
{
ResponseId = "original-update",
MessageId = "provider-message-id",
Contents = [new TextContent("Hello")]
};
AgentResponseUpdate agentResponseUpdate = new(originalChatResponseUpdate);
// Act
ChatResponseUpdate result = agentResponseUpdate.AsChatResponseUpdate();
// Assert - Provider's original MessageId should be preserved
Assert.Same(originalChatResponseUpdate, result);
Assert.Equal("provider-message-id", result.MessageId);
}
[Fact]
public void AsChatResponseUpdate_WithoutRawRepresentation_CreatesNewChatResponseUpdate()
{
// Arrange
AgentResponseUpdate agentResponseUpdate = new(ChatRole.Assistant, "Test")
{
AuthorName = "TestAuthor",
ResponseId = "update-id",
MessageId = "message-id",
CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero),
FinishReason = ChatFinishReason.ToolCalls,
AdditionalProperties = new() { ["key"] = "value" },
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
};
// Act
ChatResponseUpdate result = agentResponseUpdate.AsChatResponseUpdate();
// Assert
Assert.NotNull(result);
Assert.Equal("TestAuthor", result.AuthorName);
Assert.Equal("update-id", result.ResponseId);
Assert.Equal("message-id", result.MessageId);
Assert.Equal(new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero), result.CreatedAt);
Assert.Equal(ChatFinishReason.ToolCalls, result.FinishReason);
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.Same(agentResponseUpdate.Contents, result.Contents);
Assert.Same(agentResponseUpdate, result.RawRepresentation);
Assert.Same(agentResponseUpdate.AdditionalProperties, result.AdditionalProperties);
Assert.Equal(agentResponseUpdate.ContinuationToken, result.ContinuationToken);
}
#endregion
#region AsChatResponseUpdatesAsync Tests
[Fact]
public async Task AsChatResponseUpdatesAsync_WithNullArgument_ThrowsArgumentNullExceptionAsync()
{
// Arrange & Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>("responseUpdates", async () =>
{
await foreach (ChatResponseUpdate _ in ((IAsyncEnumerable<AgentResponseUpdate>)null!).AsChatResponseUpdatesAsync())
{
// Do nothing
}
});
}
[Fact]
public async Task AsChatResponseUpdatesAsync_ConvertsUpdatesAsync()
{
// Arrange
AgentResponseUpdate[] updates =
[
new(ChatRole.Assistant, "First"),
new(ChatRole.Assistant, "Second"),
];
// Act
List<ChatResponseUpdate> results = [];
await foreach (ChatResponseUpdate update in YieldAsync(updates).AsChatResponseUpdatesAsync())
{
results.Add(update);
}
// Assert
Assert.Equal(2, results.Count);
Assert.Equal("First", Assert.IsType<TextContent>(results[0].Contents[0]).Text);
Assert.Equal("Second", Assert.IsType<TextContent>(results[1].Contents[0]).Text);
}
#endregion
private static async IAsyncEnumerable<AgentResponseUpdate> YieldAsync(IEnumerable<AgentResponseUpdate> updates)
{
foreach (AgentResponseUpdate update in updates)
{
await Task.Yield();
yield return update;
}
}
}
@@ -0,0 +1,208 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
public class AgentResponseUpdateTests
{
[Fact]
public void ConstructorPropsDefaulted()
{
AgentResponseUpdate update = new();
Assert.Null(update.AuthorName);
Assert.Null(update.Role);
Assert.Empty(update.Text);
Assert.Empty(update.Contents);
Assert.Null(update.RawRepresentation);
Assert.Null(update.AdditionalProperties);
Assert.Null(update.ResponseId);
Assert.Null(update.MessageId);
Assert.Null(update.CreatedAt);
Assert.Equal(string.Empty, update.ToString());
Assert.Null(update.ContinuationToken);
Assert.Null(update.FinishReason);
}
[Fact]
public void ConstructorWithChatResponseUpdateRoundtrips()
{
ChatResponseUpdate chatResponseUpdate = new()
{
AdditionalProperties = [],
AuthorName = "author",
Contents = [new TextContent("hello")],
ConversationId = "conversationId",
CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
FinishReason = ChatFinishReason.Length,
MessageId = "messageId",
ModelId = "modelId",
RawRepresentation = new object(),
ResponseId = "responseId",
Role = ChatRole.Assistant,
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
};
AgentResponseUpdate response = new(chatResponseUpdate);
Assert.Same(chatResponseUpdate.AdditionalProperties, response.AdditionalProperties);
Assert.Equal(chatResponseUpdate.AuthorName, response.AuthorName);
Assert.Same(chatResponseUpdate.Contents, response.Contents);
Assert.Equal(chatResponseUpdate.CreatedAt, response.CreatedAt);
Assert.Equal(chatResponseUpdate.FinishReason, response.FinishReason);
Assert.Equal(chatResponseUpdate.MessageId, response.MessageId);
Assert.Same(chatResponseUpdate, response.RawRepresentation as ChatResponseUpdate);
Assert.Equal(chatResponseUpdate.ResponseId, response.ResponseId);
Assert.Equal(chatResponseUpdate.Role, response.Role);
Assert.Same(chatResponseUpdate.ContinuationToken, response.ContinuationToken);
}
[Fact]
public void PropertiesRoundtrip()
{
AgentResponseUpdate update = new();
Assert.Null(update.AuthorName);
update.AuthorName = "author";
Assert.Equal("author", update.AuthorName);
Assert.Null(update.Role);
update.Role = ChatRole.Assistant;
Assert.Equal(ChatRole.Assistant, update.Role);
Assert.Empty(update.Contents);
update.Contents.Add(new TextContent("text"));
Assert.Single(update.Contents);
Assert.Equal("text", update.Text);
Assert.Same(update.Contents, update.Contents);
IList<AIContent> newList = [new TextContent("text")];
update.Contents = newList;
Assert.Same(newList, update.Contents);
update.Contents = null;
Assert.NotNull(update.Contents);
Assert.Empty(update.Contents);
Assert.Empty(update.Text);
Assert.Null(update.RawRepresentation);
object raw = new();
update.RawRepresentation = raw;
Assert.Same(raw, update.RawRepresentation);
Assert.Null(update.AdditionalProperties);
AdditionalPropertiesDictionary props = new() { ["key"] = "value" };
update.AdditionalProperties = props;
Assert.Same(props, update.AdditionalProperties);
Assert.Null(update.ResponseId);
update.ResponseId = "id";
Assert.Equal("id", update.ResponseId);
Assert.Null(update.MessageId);
update.MessageId = "messageid";
Assert.Equal("messageid", update.MessageId);
Assert.Null(update.CreatedAt);
update.CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero);
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), update.CreatedAt);
Assert.Null(update.ContinuationToken);
update.ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), update.ContinuationToken);
Assert.Null(update.FinishReason);
update.FinishReason = ChatFinishReason.ToolCalls;
Assert.Equal(ChatFinishReason.ToolCalls, update.FinishReason);
}
[Fact]
public void TextGetUsesAllTextContent()
{
AgentResponseUpdate update = new()
{
Role = ChatRole.User,
Contents =
[
new DataContent("data:image/audio;base64,aGVsbG8="),
new DataContent("data:image/image;base64,aGVsbG8="),
new FunctionCallContent("callId1", "fc1"),
new TextContent("text-1"),
new TextContent("text-2"),
new FunctionResultContent("callId1", "result"),
],
};
TextContent textContent = Assert.IsType<TextContent>(update.Contents[3]);
Assert.Equal("text-1", textContent.Text);
Assert.Equal("text-1text-2", update.Text);
Assert.Equal("text-1text-2", update.ToString());
((TextContent)update.Contents[3]).Text = "text-3";
Assert.Equal("text-3text-2", update.Text);
Assert.Same(textContent, update.Contents[3]);
Assert.Equal("text-3text-2", update.ToString());
}
[Fact]
public void JsonSerializationRoundtrips()
{
AgentResponseUpdate original = new()
{
AuthorName = "author",
Role = ChatRole.Assistant,
Contents =
[
new TextContent("text-1"),
new DataContent("data:image/png;base64,aGVsbG8="),
new FunctionCallContent("callId1", "fc1"),
new DataContent("data"u8.ToArray(), "text/plain"),
new TextContent("text-2"),
],
RawRepresentation = new object(),
ResponseId = "id",
MessageId = "messageid",
CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
AdditionalProperties = new() { ["key"] = "value" },
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })
};
string json = JsonSerializer.Serialize(original, AgentAbstractionsJsonUtilities.DefaultOptions);
AgentResponseUpdate? result = JsonSerializer.Deserialize<AgentResponseUpdate>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal(5, result.Contents.Count);
Assert.IsType<TextContent>(result.Contents[0]);
Assert.Equal("text-1", ((TextContent)result.Contents[0]).Text);
Assert.IsType<DataContent>(result.Contents[1]);
Assert.Equal("data:image/png;base64,aGVsbG8=", ((DataContent)result.Contents[1]).Uri);
Assert.IsType<FunctionCallContent>(result.Contents[2]);
Assert.Equal("fc1", ((FunctionCallContent)result.Contents[2]).Name);
Assert.IsType<DataContent>(result.Contents[3]);
Assert.Equal("data"u8.ToArray(), ((DataContent)result.Contents[3]).Data.ToArray());
Assert.IsType<TextContent>(result.Contents[4]);
Assert.Equal("text-2", ((TextContent)result.Contents[4]).Text);
Assert.Equal("author", result.AuthorName);
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.Equal("id", result.ResponseId);
Assert.Equal("messageid", result.MessageId);
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), result.CreatedAt);
Assert.NotNull(result.AdditionalProperties);
Assert.Single(result.AdditionalProperties);
Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value));
Assert.IsType<JsonElement>(value);
Assert.Equal("value", ((JsonElement)value!).GetString());
Assert.NotNull(result.ContinuationToken);
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), result.ContinuationToken);
}
}
@@ -0,0 +1,233 @@
// 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;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AgentRunContext"/> class.
/// </summary>
public sealed class AgentRunContextTests
{
#region Constructor Validation Tests
/// <summary>
/// Verifies that passing null for agent throws ArgumentNullException.
/// </summary>
[Fact]
public void Constructor_NullAgent_ThrowsArgumentNullException()
{
// Arrange
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AgentRunContext(null!, session, messages, options));
}
/// <summary>
/// Verifies that passing null for session does not throw
/// </summary>
[Fact]
public void Constructor_NullSession_DoesNotThrow()
{
// Arrange
AIAgent agent = new TestAgent();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new();
// Act
AgentRunContext context = new(agent, null, messages, options);
// Assert
Assert.NotNull(context);
Assert.Null(context.Session);
}
/// <summary>
/// Verifies that passing null for requestMessages throws ArgumentNullException.
/// </summary>
[Fact]
public void Constructor_NullRequestMessages_ThrowsArgumentNullException()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
AgentRunOptions options = new();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AgentRunContext(agent, session, null!, options));
}
/// <summary>
/// Verifies that passing null for agentRunOptions does not throw.
/// </summary>
[Fact]
public void Constructor_NullAgentRunOptions_DoesNotThrow()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
// Act
AgentRunContext context = new(agent, session, messages, null);
// Assert
Assert.NotNull(context);
Assert.Null(context.RunOptions);
}
#endregion
#region Property Roundtrip Tests
/// <summary>
/// Verifies that the Agent property returns the value passed to the constructor.
/// </summary>
[Fact]
public void Agent_ReturnsValueFromConstructor()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new();
// Act
AgentRunContext context = new(agent, session, messages, options);
// Assert
Assert.Same(agent, context.Agent);
}
/// <summary>
/// Verifies that the Session property returns the value passed to the constructor.
/// </summary>
[Fact]
public void Session_ReturnsValueFromConstructor()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new();
// Act
AgentRunContext context = new(agent, session, messages, options);
// Assert
Assert.Same(session, context.Session);
}
/// <summary>
/// Verifies that the RequestMessages property returns the value passed to the constructor.
/// </summary>
[Fact]
public void RequestMessages_ReturnsValueFromConstructor()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!")
};
AgentRunOptions options = new();
// Act
AgentRunContext context = new(agent, session, messages, options);
// Assert
Assert.Same(messages, context.RequestMessages);
Assert.Equal(2, context.RequestMessages.Count);
}
/// <summary>
/// Verifies that the RunOptions property returns the value passed to the constructor.
/// </summary>
[Fact]
public void RunOptions_ReturnsValueFromConstructor()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new()
{
AllowBackgroundResponses = true,
AdditionalProperties = new AdditionalPropertiesDictionary
{
["key1"] = "value1"
}
};
// Act
AgentRunContext context = new(agent, session, messages, options);
// Assert
Assert.Same(options, context.RunOptions);
Assert.True(context.RunOptions!.AllowBackgroundResponses);
}
/// <summary>
/// Verifies that an empty messages collection is handled correctly.
/// </summary>
[Fact]
public void RequestMessages_EmptyCollection_ReturnsEmptyCollection()
{
// Arrange
AIAgent agent = new TestAgent();
AgentSession session = new TestAgentSession();
IReadOnlyCollection<ChatMessage> messages = new List<ChatMessage>();
AgentRunOptions options = new();
// Act
AgentRunContext context = new(agent, session, messages, options);
// Assert
Assert.NotNull(context.RequestMessages);
Assert.Empty(context.RequestMessages);
}
#endregion
#region Test Helpers
private sealed class TestAgentSession : AgentSession;
private sealed class TestAgent : AIAgent
{
protected override ValueTask<AgentSession> CreateSessionCoreAsync(CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override ValueTask<AgentSession> DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override ValueTask<JsonElement> SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentSession? session = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
}
#endregion
}
@@ -0,0 +1,127 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Unit tests for the <see cref="AgentRunOptions"/> class.
/// </summary>
public class AgentRunOptionsTests
{
[Fact]
public void CloningConstructorCopiesProperties()
{
// Arrange
var options = new AgentRunOptions
{
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
AllowBackgroundResponses = true,
AdditionalProperties = new AdditionalPropertiesDictionary
{
["key1"] = "value1",
["key2"] = 42
}
};
// Act
var clone = options.Clone();
// Assert
Assert.NotNull(clone);
Assert.Same(options.ContinuationToken, clone.ContinuationToken);
Assert.Equal(options.AllowBackgroundResponses, clone.AllowBackgroundResponses);
Assert.NotNull(clone.AdditionalProperties);
Assert.NotSame(options.AdditionalProperties, clone.AdditionalProperties);
Assert.Equal("value1", clone.AdditionalProperties["key1"]);
Assert.Equal(42, clone.AdditionalProperties["key2"]);
}
[Fact]
public void JsonSerializationRoundtrips()
{
// Arrange
var options = new AgentRunOptions
{
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
AllowBackgroundResponses = true,
AdditionalProperties = new AdditionalPropertiesDictionary
{
["key1"] = "value1",
["key2"] = 42
}
};
// Act
string json = JsonSerializer.Serialize(options, AgentAbstractionsJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<AgentRunOptions>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), deserialized!.ContinuationToken);
Assert.Equal(options.AllowBackgroundResponses, deserialized.AllowBackgroundResponses);
Assert.NotNull(deserialized.AdditionalProperties);
Assert.Equal(2, deserialized.AdditionalProperties.Count);
Assert.True(deserialized.AdditionalProperties.TryGetValue("key1", out object? value1));
Assert.IsType<JsonElement>(value1);
Assert.Equal("value1", ((JsonElement)value1!).GetString());
Assert.True(deserialized.AdditionalProperties.TryGetValue("key2", out object? value2));
Assert.IsType<JsonElement>(value2);
Assert.Equal(42, ((JsonElement)value2!).GetInt32());
}
[Fact]
public void CloneReturnsNewInstanceWithSameValues()
{
// Arrange
var options = new AgentRunOptions
{
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
AllowBackgroundResponses = true,
AdditionalProperties = new AdditionalPropertiesDictionary
{
["key1"] = "value1",
["key2"] = 42
},
ResponseFormat = ChatResponseFormat.Json
};
// Act
AgentRunOptions clone = options.Clone();
// Assert
Assert.NotNull(clone);
Assert.IsType<AgentRunOptions>(clone);
Assert.NotSame(options, clone);
Assert.Same(options.ContinuationToken, clone.ContinuationToken);
Assert.Equal(options.AllowBackgroundResponses, clone.AllowBackgroundResponses);
Assert.NotNull(clone.AdditionalProperties);
Assert.NotSame(options.AdditionalProperties, clone.AdditionalProperties);
Assert.Equal("value1", clone.AdditionalProperties["key1"]);
Assert.Equal(42, clone.AdditionalProperties["key2"]);
Assert.Same(options.ResponseFormat, clone.ResponseFormat);
}
[Fact]
public void CloneCreatesIndependentAdditionalPropertiesDictionary()
{
// Arrange
var options = new AgentRunOptions
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
["key1"] = "value1"
}
};
// Act
AgentRunOptions clone = options.Clone();
clone.AdditionalProperties!["key2"] = "value2";
// Assert
Assert.True(clone.AdditionalProperties.ContainsKey("key2"));
Assert.False(options.AdditionalProperties.ContainsKey("key2"));
}
}
@@ -0,0 +1,231 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Tests for <see cref="AgentSessionExtensions"/>.
/// </summary>
public class AgentSessionExtensionsTests
{
#region TryGetInMemoryChatHistory Tests
[Fact]
public void TryGetInMemoryChatHistory_WithNullSession_ThrowsArgumentNullException()
{
// Arrange
AgentSession session = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => session.TryGetInMemoryChatHistory(out _));
}
[Fact]
public void TryGetInMemoryChatHistory_WhenStateExists_ReturnsTrueAndMessages()
{
// Arrange
var session = new Mock<AgentSession>().Object;
var expectedMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!")
};
session.StateBag.SetValue(
nameof(InMemoryChatHistoryProvider),
new InMemoryChatHistoryProvider.State { Messages = expectedMessages });
// Act
var result = session.TryGetInMemoryChatHistory(out var messages);
// Assert
Assert.True(result);
Assert.NotNull(messages);
Assert.Same(expectedMessages, messages);
}
[Fact]
public void TryGetInMemoryChatHistory_WhenStateDoesNotExist_ReturnsFalse()
{
// Arrange
var session = new Mock<AgentSession>().Object;
// Act
var result = session.TryGetInMemoryChatHistory(out var messages);
// Assert
Assert.False(result);
Assert.Null(messages);
}
[Fact]
public void TryGetInMemoryChatHistory_WithCustomStateKey_UsesCustomKey()
{
// Arrange
var session = new Mock<AgentSession>().Object;
const string CustomKey = "custom-history-key";
var expectedMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
session.StateBag.SetValue(
CustomKey,
new InMemoryChatHistoryProvider.State { Messages = expectedMessages });
// Act
var result = session.TryGetInMemoryChatHistory(out var messages, stateKey: CustomKey);
// Assert
Assert.True(result);
Assert.NotNull(messages);
Assert.Same(expectedMessages, messages);
}
[Fact]
public void TryGetInMemoryChatHistory_WithCustomStateKey_DoesNotFindDefaultKey()
{
// Arrange
var session = new Mock<AgentSession>().Object;
var expectedMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
session.StateBag.SetValue(
nameof(InMemoryChatHistoryProvider),
new InMemoryChatHistoryProvider.State { Messages = expectedMessages });
// Act
var result = session.TryGetInMemoryChatHistory(out var messages, stateKey: "other-key");
// Assert
Assert.False(result);
Assert.Null(messages);
}
[Fact]
public void TryGetInMemoryChatHistory_WhenStateExistsWithNullMessages_ReturnsFalse()
{
// Arrange
var session = new Mock<AgentSession>().Object;
session.StateBag.SetValue(
nameof(InMemoryChatHistoryProvider),
new InMemoryChatHistoryProvider.State { Messages = null! });
// Act
var result = session.TryGetInMemoryChatHistory(out var messages);
// Assert
Assert.False(result);
Assert.Null(messages);
}
#endregion
#region SetInMemoryChatHistory Tests
[Fact]
public void SetInMemoryChatHistory_WithNullSession_ThrowsArgumentNullException()
{
// Arrange
AgentSession session = null!;
var messages = new List<ChatMessage>();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => session.SetInMemoryChatHistory(messages));
}
[Fact]
public void SetInMemoryChatHistory_WhenNoExistingState_CreatesNewState()
{
// Arrange
var session = new Mock<AgentSession>().Object;
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi!")
};
// Act
session.SetInMemoryChatHistory(messages);
// Assert
var result = session.TryGetInMemoryChatHistory(out var retrievedMessages);
Assert.True(result);
Assert.Same(messages, retrievedMessages);
}
[Fact]
public void SetInMemoryChatHistory_WhenExistingState_ReplacesMessages()
{
// Arrange
var session = new Mock<AgentSession>().Object;
var originalMessages = new List<ChatMessage>
{
new(ChatRole.User, "Original")
};
var newMessages = new List<ChatMessage>
{
new(ChatRole.User, "New message"),
new(ChatRole.Assistant, "New response")
};
session.SetInMemoryChatHistory(originalMessages);
// Act
session.SetInMemoryChatHistory(newMessages);
// Assert
var result = session.TryGetInMemoryChatHistory(out var retrievedMessages);
Assert.True(result);
Assert.Same(newMessages, retrievedMessages);
}
[Fact]
public void SetInMemoryChatHistory_WithCustomStateKey_UsesCustomKey()
{
// Arrange
var session = new Mock<AgentSession>().Object;
const string CustomKey = "custom-history-key";
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Test")
};
// Act
session.SetInMemoryChatHistory(messages, stateKey: CustomKey);
// Assert
var result = session.TryGetInMemoryChatHistory(out var retrievedMessages, stateKey: CustomKey);
Assert.True(result);
Assert.Same(messages, retrievedMessages);
// Verify default key is not set
var defaultResult = session.TryGetInMemoryChatHistory(out _);
Assert.False(defaultResult);
}
[Fact]
public void SetInMemoryChatHistory_WithEmptyList_SetsEmptyList()
{
// Arrange
var session = new Mock<AgentSession>().Object;
var messages = new List<ChatMessage>();
// Act
session.SetInMemoryChatHistory(messages);
// Assert
var result = session.TryGetInMemoryChatHistory(out var retrievedMessages);
Assert.True(result);
Assert.NotNull(retrievedMessages);
Assert.Empty(retrievedMessages);
}
#endregion
}
@@ -0,0 +1,840 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using Microsoft.Agents.AI.Abstractions.UnitTests.Models;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="AgentSessionStateBag"/> class.
/// </summary>
public sealed class AgentSessionStateBagTests
{
#region Constructor Tests
[Fact]
public void Constructor_Default_CreatesEmptyStateBag()
{
// Act
var stateBag = new AgentSessionStateBag();
// Assert
Assert.False(stateBag.TryGetValue<string>("nonexistent", out _));
}
#endregion
#region SetValue Tests
[Fact]
public void SetValue_WithValidKeyAndValue_StoresValue()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act
stateBag.SetValue("key1", "value1");
// Assert
Assert.True(stateBag.TryGetValue<string>("key1", out var result));
Assert.Equal("value1", result);
}
[Fact]
public void SetValue_WithNullKey_ThrowsArgumentException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => stateBag.SetValue(null!, "value"));
}
[Fact]
public void SetValue_WithEmptyKey_ThrowsArgumentException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act & Assert
Assert.Throws<ArgumentException>(() => stateBag.SetValue("", "value"));
}
[Fact]
public void SetValue_WithWhitespaceKey_ThrowsArgumentException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act & Assert
Assert.Throws<ArgumentException>(() => stateBag.SetValue(" ", "value"));
}
[Fact]
public void SetValue_OverwritesExistingValue()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "originalValue");
// Act
stateBag.SetValue("key1", "newValue");
// Assert
Assert.Equal("newValue", stateBag.GetValue<string>("key1"));
}
#endregion
#region GetValue Tests
[Fact]
public void GetValue_WithExistingKey_ReturnsValue()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "value1");
// Act
var result = stateBag.GetValue<string>("key1");
// Assert
Assert.Equal("value1", result);
}
[Fact]
public void GetValue_WithNonexistentKey_ReturnsNull()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act
var result = stateBag.GetValue<string>("nonexistent");
// Assert
Assert.Null(result);
}
[Fact]
public void GetValue_WithNullKey_ThrowsArgumentException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => stateBag.GetValue<string>(null!));
}
[Fact]
public void GetValue_WithEmptyKey_ThrowsArgumentException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act & Assert
Assert.Throws<ArgumentException>(() => stateBag.GetValue<string>(""));
}
[Fact]
public void GetValue_CachesDeserializedValue()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "value1");
// Act
var result1 = stateBag.GetValue<string>("key1");
var result2 = stateBag.GetValue<string>("key1");
// Assert
Assert.Same(result1, result2);
}
#endregion
#region TryGetValue Tests
[Fact]
public void TryGetValue_WithExistingKey_ReturnsTrueAndValue()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "value1");
// Act
var found = stateBag.TryGetValue<string>("key1", out var result);
// Assert
Assert.True(found);
Assert.Equal("value1", result);
}
[Fact]
public void TryGetValue_WithNonexistentKey_ReturnsFalseAndNull()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act
var found = stateBag.TryGetValue<string>("nonexistent", out var result);
// Assert
Assert.False(found);
Assert.Null(result);
}
[Fact]
public void TryGetValue_WithNullKey_ThrowsArgumentException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => stateBag.TryGetValue<string>(null!, out _));
}
[Fact]
public void TryGetValue_WithEmptyKey_ThrowsArgumentException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act & Assert
Assert.Throws<ArgumentException>(() => stateBag.TryGetValue<string>("", out _));
}
#endregion
#region Null Value Tests
[Fact]
public void SetValue_WithNullValue_StoresNull()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act
stateBag.SetValue<string>("key1", null);
// Assert
Assert.Equal(1, stateBag.Count);
}
[Fact]
public void TryGetValue_WithNullValue_ReturnsTrueAndNull()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue<string>("key1", null);
// Act
var found = stateBag.TryGetValue<string>("key1", out var result);
// Assert
Assert.True(found);
Assert.Null(result);
}
[Fact]
public void GetValue_WithNullValue_ReturnsNull()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue<string>("key1", null);
// Act
var result = stateBag.GetValue<string>("key1");
// Assert
Assert.Null(result);
}
[Fact]
public void SetValue_OverwriteWithNull_ReturnsNull()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "value1");
// Act
stateBag.SetValue<string>("key1", null);
// Assert
Assert.True(stateBag.TryGetValue<string>("key1", out var result));
Assert.Null(result);
}
[Fact]
public void SetValue_OverwriteNullWithValue_ReturnsValue()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue<string>("key1", null);
// Act
stateBag.SetValue("key1", "newValue");
// Assert
Assert.True(stateBag.TryGetValue<string>("key1", out var result));
Assert.Equal("newValue", result);
}
[Fact]
public void SerializeDeserialize_WithNullValue_SerializesAsNull()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue<string>("nullKey", null);
// Act
var json = stateBag.Serialize();
// Assert - null values are serialized as JSON null
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.True(json.TryGetProperty("nullKey", out var nullElement));
Assert.Equal(JsonValueKind.Null, nullElement.ValueKind);
}
#endregion
#region TryRemoveValue Tests
[Fact]
public void TryRemoveValue_ExistingKey_ReturnsTrueAndRemoves()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "value1");
// Act
var removed = stateBag.TryRemoveValue("key1");
// Assert
Assert.True(removed);
Assert.Equal(0, stateBag.Count);
Assert.False(stateBag.TryGetValue<string>("key1", out _));
}
[Fact]
public void TryRemoveValue_NonexistentKey_ReturnsFalse()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act
var removed = stateBag.TryRemoveValue("nonexistent");
// Assert
Assert.False(removed);
}
[Fact]
public void TryRemoveValue_WithNullKey_ThrowsArgumentException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => stateBag.TryRemoveValue(null!));
}
[Fact]
public void TryRemoveValue_WithEmptyKey_ThrowsArgumentException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act & Assert
Assert.Throws<ArgumentException>(() => stateBag.TryRemoveValue(""));
}
[Fact]
public void TryRemoveValue_WithWhitespaceKey_ThrowsArgumentException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act & Assert
Assert.Throws<ArgumentException>(() => stateBag.TryRemoveValue(" "));
}
[Fact]
public void TryRemoveValue_DoesNotAffectOtherKeys()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "value1");
stateBag.SetValue("key2", "value2");
// Act
stateBag.TryRemoveValue("key1");
// Assert
Assert.Equal(1, stateBag.Count);
Assert.False(stateBag.TryGetValue<string>("key1", out _));
Assert.True(stateBag.TryGetValue<string>("key2", out var value));
Assert.Equal("value2", value);
}
[Fact]
public void TryRemoveValue_ThenSetValue_Works()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "original");
// Act
stateBag.TryRemoveValue("key1");
stateBag.SetValue("key1", "replacement");
// Assert
Assert.True(stateBag.TryGetValue<string>("key1", out var result));
Assert.Equal("replacement", result);
}
#endregion
#region Serialize/Deserialize Tests
[Fact]
public void Serialize_EmptyStateBag_ReturnsEmptyObject()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act
var json = stateBag.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
}
[Fact]
public void Serialize_WithStringValue_ReturnsJsonWithValue()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("stringKey", "stringValue");
// Act
var json = stateBag.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.True(json.TryGetProperty("stringKey", out _));
}
[Fact]
public void Deserialize_FromJsonDocument_ReturnsEmptyStateBag()
{
// Arrange
var emptyJson = JsonDocument.Parse("{}").RootElement;
// Act
var stateBag = AgentSessionStateBag.Deserialize(emptyJson);
// Assert
Assert.False(stateBag.TryGetValue<string>("nonexistent", out _));
}
[Fact]
public void Deserialize_NullElement_ReturnsEmptyStateBag()
{
// Arrange
var nullJson = default(JsonElement);
// Act
var stateBag = AgentSessionStateBag.Deserialize(nullJson);
// Assert
Assert.False(stateBag.TryGetValue<string>("nonexistent", out _));
}
[Fact]
public void SerializeDeserialize_WithStringValue_Roundtrips()
{
// Arrange
var originalStateBag = new AgentSessionStateBag();
originalStateBag.SetValue("stringKey", "stringValue");
// Act
var json = originalStateBag.Serialize();
var restoredStateBag = AgentSessionStateBag.Deserialize(json);
// Assert
Assert.Equal("stringValue", restoredStateBag.GetValue<string>("stringKey"));
}
#endregion
#region Thread Safety Tests
[Fact]
public async System.Threading.Tasks.Task SetValue_MultipleConcurrentWrites_DoesNotThrowAsync()
{
// Arrange
var stateBag = new AgentSessionStateBag();
var tasks = new System.Threading.Tasks.Task[100];
// Act
for (int i = 0; i < 100; i++)
{
int index = i;
tasks[i] = System.Threading.Tasks.Task.Run(() => stateBag.SetValue($"key{index}", $"value{index}"));
}
await System.Threading.Tasks.Task.WhenAll(tasks);
// Assert
for (int i = 0; i < 100; i++)
{
Assert.True(stateBag.TryGetValue<string>($"key{i}", out var value));
Assert.Equal($"value{i}", value);
}
}
[Fact]
public async System.Threading.Tasks.Task ConcurrentWritesAndSerialize_DoesNotThrowAsync()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("shared", "initial");
var tasks = new System.Threading.Tasks.Task[100];
// Act - concurrently write and serialize the same key
for (int i = 0; i < 100; i++)
{
int index = i;
tasks[i] = System.Threading.Tasks.Task.Run(() =>
{
stateBag.SetValue("shared", $"value{index}");
_ = stateBag.Serialize();
});
}
await System.Threading.Tasks.Task.WhenAll(tasks);
// Assert - should have some value and serialize without error
Assert.True(stateBag.TryGetValue<string>("shared", out var result));
Assert.NotNull(result);
var json = stateBag.Serialize();
Assert.Equal(JsonValueKind.Object, json.ValueKind);
}
[Fact]
public async System.Threading.Tasks.Task ConcurrentReadsAndWrites_DoesNotThrowAsync()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key", "initial");
var tasks = new System.Threading.Tasks.Task[200];
// Act - half readers, half writers on the same key
for (int i = 0; i < 200; i++)
{
int index = i;
tasks[i] = (index % 2 == 0)
? System.Threading.Tasks.Task.Run(() => stateBag.GetValue<string>("key"))
: System.Threading.Tasks.Task.Run(() => stateBag.SetValue("key", $"value{index}"));
}
await System.Threading.Tasks.Task.WhenAll(tasks);
// Assert - should have a consistent value
Assert.True(stateBag.TryGetValue<string>("key", out var result));
Assert.NotNull(result);
}
#endregion
#region Complex Object Tests
[Fact]
public void SetValue_WithComplexObject_StoresValue()
{
// Arrange
var stateBag = new AgentSessionStateBag();
var animal = new Animal { Id = 1, FullName = "Buddy", Species = Species.Bear };
// Act
stateBag.SetValue("animal", animal, TestJsonSerializerContext.Default.Options);
// Assert
Animal? result = stateBag.GetValue<Animal>("animal", TestJsonSerializerContext.Default.Options);
Assert.NotNull(result);
Assert.Equal(1, result.Id);
Assert.Equal("Buddy", result.FullName);
Assert.Equal(Species.Bear, result.Species);
}
[Fact]
public void GetValue_WithComplexObject_CachesDeserializedValue()
{
// Arrange
var stateBag = new AgentSessionStateBag();
var animal = new Animal { Id = 2, FullName = "Whiskers", Species = Species.Tiger };
stateBag.SetValue("animal", animal, TestJsonSerializerContext.Default.Options);
// Act
Animal? result1 = stateBag.GetValue<Animal>("animal", TestJsonSerializerContext.Default.Options);
Animal? result2 = stateBag.GetValue<Animal>("animal", TestJsonSerializerContext.Default.Options);
// Assert
Assert.Same(result1, result2);
}
[Fact]
public void TryGetValue_WithComplexObject_ReturnsTrueAndValue()
{
// Arrange
var stateBag = new AgentSessionStateBag();
var animal = new Animal { Id = 3, FullName = "Goldie", Species = Species.Walrus };
stateBag.SetValue("animal", animal, TestJsonSerializerContext.Default.Options);
// Act
bool found = stateBag.TryGetValue("animal", out Animal? result, TestJsonSerializerContext.Default.Options);
// Assert
Assert.True(found);
Assert.NotNull(result);
Assert.Equal(3, result.Id);
Assert.Equal("Goldie", result.FullName);
Assert.Equal(Species.Walrus, result.Species);
}
[Fact]
public void SerializeDeserialize_WithComplexObject_Roundtrips()
{
// Arrange
var originalStateBag = new AgentSessionStateBag();
var animal = new Animal { Id = 4, FullName = "Polly", Species = Species.Bear };
originalStateBag.SetValue("animal", animal, TestJsonSerializerContext.Default.Options);
// Act
JsonElement json = originalStateBag.Serialize();
AgentSessionStateBag restoredStateBag = AgentSessionStateBag.Deserialize(json);
// Assert
Animal? restoredAnimal = restoredStateBag.GetValue<Animal>("animal", TestJsonSerializerContext.Default.Options);
Assert.NotNull(restoredAnimal);
Assert.Equal(4, restoredAnimal.Id);
Assert.Equal("Polly", restoredAnimal.FullName);
Assert.Equal(Species.Bear, restoredAnimal.Species);
}
[Fact]
public void Serialize_WithComplexObject_ReturnsJsonWithProperties()
{
// Arrange
var stateBag = new AgentSessionStateBag();
var animal = new Animal { Id = 7, FullName = "Spot", Species = Species.Walrus };
stateBag.SetValue("animal", animal, TestJsonSerializerContext.Default.Options);
// Act
JsonElement json = stateBag.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.True(json.TryGetProperty("animal", out JsonElement animalElement));
Assert.Equal(JsonValueKind.Object, animalElement.ValueKind);
Assert.Equal(7, animalElement.GetProperty("id").GetInt32());
Assert.Equal("Spot", animalElement.GetProperty("fullName").GetString());
Assert.Equal("Walrus", animalElement.GetProperty("species").GetString());
}
#endregion
#region Type Mismatch Tests
[Fact]
public void TryGetValue_WithDifferentTypeAfterSet_ReturnsFalse()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "hello");
// Act
var found = stateBag.TryGetValue<Animal>("key1", out var result, TestJsonSerializerContext.Default.Options);
// Assert
Assert.False(found);
Assert.Null(result);
}
[Fact]
public void GetValue_WithDifferentTypeAfterSet_ThrowsInvalidOperationException()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "hello");
// Act & Assert
Assert.Throws<InvalidOperationException>(() => stateBag.GetValue<Animal>("key1", TestJsonSerializerContext.Default.Options));
}
[Fact]
public void TryGetValue_WithDifferentTypeAfterDeserializedRead_ReturnsFalse()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "hello");
// First read caches the value as string
var cachedValue = stateBag.GetValue<string>("key1");
Assert.Equal("hello", cachedValue);
// Act - request as a different type
var found = stateBag.TryGetValue<Animal>("key1", out var result, TestJsonSerializerContext.Default.Options);
// Assert
Assert.False(found);
Assert.Null(result);
}
[Fact]
public void GetValue_WithDifferentTypeAfterDeserializedRoundtrip_ThrowsInvalidOperationException()
{
// Arrange
var originalStateBag = new AgentSessionStateBag();
originalStateBag.SetValue("key1", "hello");
// Round-trip through serialization
var json = originalStateBag.Serialize();
var restoredStateBag = AgentSessionStateBag.Deserialize(json);
// First read caches the value as string
var cachedValue = restoredStateBag.GetValue<string>("key1");
Assert.Equal("hello", cachedValue);
// Act & Assert - request as a different type
Assert.Throws<InvalidOperationException>(() => restoredStateBag.GetValue<Animal>("key1", TestJsonSerializerContext.Default.Options));
}
[Fact]
public void TryGetValue_ComplexTypeAfterSetString_ReturnsFalse()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("animal", "not an animal");
// Act
var found = stateBag.TryGetValue<Animal>("animal", out var result, TestJsonSerializerContext.Default.Options);
// Assert
Assert.False(found);
Assert.Null(result);
}
[Fact]
public void GetValue_TypeMismatch_ExceptionMessageContainsBothTypeNames()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key1", "hello");
// Act
var exception = Assert.Throws<InvalidOperationException>(() => stateBag.GetValue<Animal>("key1", TestJsonSerializerContext.Default.Options));
// Assert
Assert.Contains(typeof(string).FullName!, exception.Message);
Assert.Contains(typeof(Animal).FullName!, exception.Message);
}
#endregion
#region JsonSerializer Integration Tests
[Fact]
public void JsonSerializerSerialize_EmptyStateBag_ReturnsEmptyObject()
{
// Arrange
var stateBag = new AgentSessionStateBag();
// Act
var json = JsonSerializer.Serialize(stateBag, AgentAbstractionsJsonUtilities.DefaultOptions);
// Assert
Assert.Equal("{}", json);
}
[Fact]
public void JsonSerializerSerialize_WithStringValue_ProducesSameOutputAsSerializeMethod()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("stringKey", "stringValue");
// Act
var jsonFromSerializer = JsonSerializer.Serialize(stateBag, AgentAbstractionsJsonUtilities.DefaultOptions);
var jsonFromMethod = stateBag.Serialize().GetRawText();
// Assert
Assert.Equal(jsonFromMethod, jsonFromSerializer);
}
[Fact]
public void JsonSerializerRoundtrip_WithStringValue_PreservesData()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("greeting", "hello world");
// Act
var json = JsonSerializer.Serialize(stateBag, AgentAbstractionsJsonUtilities.DefaultOptions);
var restored = JsonSerializer.Deserialize<AgentSessionStateBag>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(restored);
Assert.Equal("hello world", restored!.GetValue<string>("greeting"));
}
[Fact]
public void JsonSerializerRoundtrip_WithComplexObject_PreservesData()
{
// Arrange
var stateBag = new AgentSessionStateBag();
var animal = new Animal { Id = 10, FullName = "Rex", Species = Species.Tiger };
stateBag.SetValue("animal", animal, TestJsonSerializerContext.Default.Options);
// Act
var json = JsonSerializer.Serialize(stateBag, AgentAbstractionsJsonUtilities.DefaultOptions);
var restored = JsonSerializer.Deserialize<AgentSessionStateBag>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(restored);
var restoredAnimal = restored!.GetValue<Animal>("animal", TestJsonSerializerContext.Default.Options);
Assert.NotNull(restoredAnimal);
Assert.Equal(10, restoredAnimal!.Id);
Assert.Equal("Rex", restoredAnimal.FullName);
Assert.Equal(Species.Tiger, restoredAnimal.Species);
}
[Fact]
public void JsonSerializerDeserialize_NullJson_ReturnsNull()
{
// Arrange
const string Json = "null";
// Act
var stateBag = JsonSerializer.Deserialize<AgentSessionStateBag>(Json, AgentAbstractionsJsonUtilities.DefaultOptions);
// Assert
Assert.Null(stateBag);
}
#if NET10_0_OR_GREATER
[Fact]
public void JsonSerializerSerialize_WithUnknownType_Throws()
{
// Arrange
var stateBag = new AgentSessionStateBag();
stateBag.SetValue("key", new { Name = "Test" }); // Anonymous type which cannot be deserialized
// Act & Assert
Assert.Throws<NotSupportedException>(() => JsonSerializer.Serialize(stateBag, AgentAbstractionsJsonUtilities.DefaultOptions));
}
#endif
#endregion
}
@@ -0,0 +1,185 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
#pragma warning disable CA1861 // Avoid constant arrays as arguments
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Tests for <see cref="AgentSession"/>
/// </summary>
public class AgentSessionTests
{
#region StateBag Tests
[Fact]
public void StateBag_Values_Roundtrips()
{
// Arrange
var session = new TestAgentSession();
// Act & Assert
session.StateBag.SetValue("key1", "value1");
Assert.Equal("value1", session.StateBag.GetValue<string>("key1"));
}
[Fact]
public void StateBag_Default_IsEmpty()
{
// Arrange & Act
var session = new TestAgentSession();
// Assert
Assert.Equal(0, session.StateBag.Count);
}
[Fact]
public void StateBag_MultipleKeys_StoreAndRetrieveIndependently()
{
// Arrange
var session = new TestAgentSession();
// Act
session.StateBag.SetValue("key1", "value1");
session.StateBag.SetValue("key2", "value2");
// Assert
Assert.Equal("value1", session.StateBag.GetValue<string>("key1"));
Assert.Equal("value2", session.StateBag.GetValue<string>("key2"));
}
[Fact]
public void StateBag_OverwriteValue_ReturnsUpdatedValue()
{
// Arrange
var session = new TestAgentSession();
session.StateBag.SetValue("key1", "original");
// Act
session.StateBag.SetValue("key1", "updated");
// Assert
Assert.Equal("updated", session.StateBag.GetValue<string>("key1"));
}
#endregion
#region GetService Method Tests
/// <summary>
/// Verify that GetService returns the session itself when requesting the exact session type.
/// </summary>
[Fact]
public void GetService_RequestingExactThreadType_ReturnsSession()
{
// Arrange
var session = new TestAgentSession();
// Act
var result = session.GetService(typeof(TestAgentSession));
// Assert
Assert.NotNull(result);
Assert.Same(session, result);
}
/// <summary>
/// Verify that GetService returns the session itself when requesting the base AgentSession type.
/// </summary>
[Fact]
public void GetService_RequestingAgentSessionType_ReturnsSession()
{
// Arrange
var session = new TestAgentSession();
// Act
var result = session.GetService(typeof(AgentSession));
// Assert
Assert.NotNull(result);
Assert.Same(session, result);
}
/// <summary>
/// Verify that GetService returns null when requesting an unrelated type.
/// </summary>
[Fact]
public void GetService_RequestingUnrelatedType_ReturnsNull()
{
// Arrange
var session = new TestAgentSession();
// Act
var result = session.GetService(typeof(string));
// Assert
Assert.Null(result);
}
/// <summary>
/// Verify that GetService returns null when a service key is provided, even for matching types.
/// </summary>
[Fact]
public void GetService_WithServiceKey_ReturnsNull()
{
// Arrange
var session = new TestAgentSession();
// Act
var result = session.GetService(typeof(TestAgentSession), "some-key");
// Assert
Assert.Null(result);
}
/// <summary>
/// Verify that GetService throws ArgumentNullException when serviceType is null.
/// </summary>
[Fact]
public void GetService_WithNullServiceType_ThrowsArgumentNullException()
{
// Arrange
var session = new TestAgentSession();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => session.GetService(null!));
}
/// <summary>
/// Verify that GetService generic method works correctly.
/// </summary>
[Fact]
public void GetService_Generic_ReturnsCorrectType()
{
// Arrange
var session = new TestAgentSession();
// Act
var result = session.GetService<TestAgentSession>();
// Assert
Assert.NotNull(result);
Assert.Same(session, result);
}
/// <summary>
/// Verify that GetService generic method returns null for unrelated types.
/// </summary>
[Fact]
public void GetService_Generic_ReturnsNullForUnrelatedType()
{
// Arrange
var session = new TestAgentSession();
// Act
var result = session.GetService<string>();
// Assert
Assert.Null(result);
}
#endregion
private sealed class TestAgentSession : AgentSession;
}
@@ -0,0 +1,559 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="ChatHistoryProvider"/> class.
/// </summary>
public class ChatHistoryProviderTests
{
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
#region GetService Method Tests
[Fact]
public void GetService_RequestingExactProviderType_ReturnsProvider()
{
var provider = new TestChatHistoryProvider();
var result = provider.GetService(typeof(TestChatHistoryProvider));
Assert.NotNull(result);
Assert.Same(provider, result);
}
[Fact]
public void GetService_RequestingBaseProviderType_ReturnsProvider()
{
var provider = new TestChatHistoryProvider();
var result = provider.GetService(typeof(ChatHistoryProvider));
Assert.NotNull(result);
Assert.Same(provider, result);
}
[Fact]
public void GetService_RequestingUnrelatedType_ReturnsNull()
{
var provider = new TestChatHistoryProvider();
var result = provider.GetService(typeof(string));
Assert.Null(result);
}
[Fact]
public void GetService_WithServiceKey_ReturnsNull()
{
var provider = new TestChatHistoryProvider();
var result = provider.GetService(typeof(TestChatHistoryProvider), "some-key");
Assert.Null(result);
}
[Fact]
public void GetService_WithNullServiceType_ThrowsArgumentNullException()
{
var provider = new TestChatHistoryProvider();
Assert.Throws<ArgumentNullException>(() => provider.GetService(null!));
}
[Fact]
public void GetService_Generic_ReturnsCorrectType()
{
var provider = new TestChatHistoryProvider();
var result = provider.GetService<TestChatHistoryProvider>();
Assert.NotNull(result);
Assert.Same(provider, result);
}
[Fact]
public void GetService_Generic_ReturnsNullForUnrelatedType()
{
var provider = new TestChatHistoryProvider();
var result = provider.GetService<string>();
Assert.Null(result);
}
#endregion
#region InvokingContext Tests
[Fact]
public void InvokingContext_Constructor_ThrowsForNullMessages()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, null!));
}
[Fact]
public void InvokingContext_RequestMessages_SetterThrowsForNull()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, messages);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => context.RequestMessages = null!);
}
[Fact]
public void InvokingContext_RequestMessages_SetterRoundtrips()
{
// Arrange
var initialMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var newMessages = new List<ChatMessage> { new(ChatRole.User, "New message") };
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, initialMessages);
// Act
context.RequestMessages = newMessages;
// Assert
Assert.Same(newMessages, context.RequestMessages);
}
[Fact]
public void InvokingContext_Agent_ReturnsConstructorValue()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, messages);
// Assert
Assert.Same(s_mockAgent, context.Agent);
}
[Fact]
public void InvokingContext_Session_ReturnsConstructorValue()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, messages);
// Assert
Assert.Same(s_mockSession, context.Session);
}
[Fact]
public void InvokingContext_Session_CanBeNull()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, null, messages);
// Assert
Assert.Null(context.Session);
}
[Fact]
public void InvokingContext_Constructor_ThrowsForNullAgent()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokingContext(null!, s_mockSession, messages));
}
#endregion
#region InvokedContext Tests
[Fact]
public void InvokedContext_Constructor_ThrowsForNullRequestMessages()
{
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, null!, []));
}
[Fact]
public void InvokedContext_ResponseMessages_Roundtrips()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response message") };
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, responseMessages);
// Assert
Assert.Same(responseMessages, context.ResponseMessages);
}
[Fact]
public void InvokedContext_InvokeException_Roundtrips()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var exception = new InvalidOperationException("Test exception");
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, exception);
// Assert
Assert.Same(exception, context.InvokeException);
}
[Fact]
public void InvokedContext_Agent_ReturnsConstructorValue()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Assert
Assert.Same(s_mockAgent, context.Agent);
}
[Fact]
public void InvokedContext_Session_ReturnsConstructorValue()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, []);
// Assert
Assert.Same(s_mockSession, context.Session);
}
[Fact]
public void InvokedContext_Session_CanBeNull()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, null, requestMessages, []);
// Assert
Assert.Null(context.Session);
}
[Fact]
public void InvokedContext_Constructor_ThrowsForNullAgent()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokedContext(null!, s_mockSession, requestMessages, []));
}
[Fact]
public void InvokedContext_SuccessConstructor_ThrowsForNullResponseMessages()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, (IEnumerable<ChatMessage>)null!));
}
[Fact]
public void InvokedContext_FailureConstructor_ThrowsForNullException()
{
// Arrange
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages, (Exception)null!));
}
#endregion
#region InvokingAsync / InvokedAsync Null Check Tests
[Fact]
public async Task InvokingAsync_NullContext_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var provider = new TestChatHistoryProvider();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => provider.InvokingAsync(null!).AsTask());
}
[Fact]
public async Task InvokedAsync_NullContext_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var provider = new TestChatHistoryProvider();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => provider.InvokedAsync(null!).AsTask());
}
#endregion
#region InvokingCoreAsync Tests
[Fact]
public async Task InvokingCoreAsync_CallsProvideChatHistoryAndReturnsMessagesAsync()
{
// Arrange
var historyMessages = new[] { new ChatMessage(ChatRole.User, "History message") };
var provider = new TestChatHistoryProvider(provideMessages: historyMessages);
var requestMessages = new[] { new ChatMessage(ChatRole.User, "Request message") };
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, requestMessages);
// Act
var result = (await provider.InvokingAsync(context)).ToList();
// Assert
Assert.Equal(2, result.Count);
Assert.Equal("History message", result[0].Text);
Assert.Equal("Request message", result[1].Text);
}
[Fact]
public async Task InvokingCoreAsync_HistoryAppearsBeforeRequestMessagesAsync()
{
// Arrange
var historyMessages = new[]
{
new ChatMessage(ChatRole.User, "Hist1"),
new ChatMessage(ChatRole.Assistant, "Hist2")
};
var provider = new TestChatHistoryProvider(provideMessages: historyMessages);
var requestMessages = new[] { new ChatMessage(ChatRole.User, "Req1") };
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, requestMessages);
// Act
var result = (await provider.InvokingAsync(context)).ToList();
// Assert
Assert.Equal(3, result.Count);
Assert.Equal("Hist1", result[0].Text);
Assert.Equal("Hist2", result[1].Text);
Assert.Equal("Req1", result[2].Text);
}
[Fact]
public async Task InvokingCoreAsync_StampsHistoryMessagesWithChatHistorySourceAsync()
{
// Arrange
var historyMessages = new[] { new ChatMessage(ChatRole.User, "History") };
var provider = new TestChatHistoryProvider(provideMessages: historyMessages);
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
// Act
var result = (await provider.InvokingAsync(context)).ToList();
// Assert
Assert.Single(result);
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result[0].GetAgentRequestMessageSourceType());
}
[Fact]
public async Task InvokingCoreAsync_NoFilterAppliedWhenProvideOutputFilterIsNullAsync()
{
// Arrange
var historyMessages = new[]
{
new ChatMessage(ChatRole.User, "User msg"),
new ChatMessage(ChatRole.System, "System msg"),
new ChatMessage(ChatRole.Assistant, "Assistant msg")
};
var provider = new TestChatHistoryProvider(provideMessages: historyMessages);
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
// Act
var result = (await provider.InvokingAsync(context)).ToList();
// Assert - all 3 history messages returned (no filter)
Assert.Equal(3, result.Count);
}
[Fact]
public async Task InvokingCoreAsync_AppliesProvideOutputFilterWhenProvidedAsync()
{
// Arrange
var historyMessages = new[]
{
new ChatMessage(ChatRole.User, "User msg"),
new ChatMessage(ChatRole.System, "System msg"),
new ChatMessage(ChatRole.Assistant, "Assistant msg")
};
var provider = new TestChatHistoryProvider(
provideMessages: historyMessages,
provideOutputMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.User));
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, []);
// Act
var result = (await provider.InvokingAsync(context)).ToList();
// Assert - only User messages remain after filter
Assert.Single(result);
Assert.Equal("User msg", result[0].Text);
}
[Fact]
public async Task InvokingCoreAsync_ReturnsEmptyHistoryByDefaultAsync()
{
// Arrange - provider that doesn't override ProvideChatHistoryAsync (uses base default)
var provider = new DefaultChatHistoryProvider();
var requestMessages = new[] { new ChatMessage(ChatRole.User, "Hello") };
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, requestMessages);
// Act
var result = (await provider.InvokingAsync(context)).ToList();
// Assert - only the request message (no history)
Assert.Single(result);
Assert.Equal("Hello", result[0].Text);
}
#endregion
#region InvokedCoreAsync Tests
[Fact]
public async Task InvokedCoreAsync_CallsStoreChatHistoryWithFilteredMessagesAsync()
{
// Arrange
var provider = new TestChatHistoryProvider();
var externalMessage = new ChatMessage(ChatRole.User, "External");
var chatHistoryMessage = new ChatMessage(ChatRole.User, "From history")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "source");
var responseMessages = new[] { new ChatMessage(ChatRole.Assistant, "Response") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, new[] { externalMessage, chatHistoryMessage }, responseMessages);
// Act
await provider.InvokedAsync(context);
// Assert - default filter excludes ChatHistory-sourced messages
Assert.NotNull(provider.LastStoredContext);
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Single(storedRequest);
Assert.Equal("External", storedRequest[0].Text);
var storedResponse = provider.LastStoredContext.ResponseMessages!.ToList();
Assert.Single(storedResponse);
Assert.Equal("Response", storedResponse[0].Text);
}
[Fact]
public async Task InvokedCoreAsync_SkipsStorageWhenInvokeExceptionIsNotNullAsync()
{
// Arrange
var provider = new TestChatHistoryProvider();
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "msg")], new InvalidOperationException("Failed"));
// Act
await provider.InvokedAsync(context);
// Assert - StoreChatHistoryAsync was NOT called
Assert.Null(provider.LastStoredContext);
}
[Fact]
public async Task InvokedCoreAsync_UsesCustomStoreInputFilterAsync()
{
// Arrange - filter that only keeps System messages
var provider = new TestChatHistoryProvider(
storeInputRequestMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.System),
storeInputResponseMessageFilter: msgs => msgs.Where(m => m.Role == ChatRole.Assistant));
var messages = new[]
{
new ChatMessage(ChatRole.User, "User msg"),
new ChatMessage(ChatRole.System, "System msg")
};
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, messages, [new ChatMessage(ChatRole.Assistant, "Response"), new ChatMessage(ChatRole.Tool, "Response")]);
// Act
await provider.InvokedAsync(context);
// Assert - only System messages were passed to store
Assert.NotNull(provider.LastStoredContext);
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Single(storedRequest);
Assert.Equal("System msg", storedRequest[0].Text);
var storedResponse = provider.LastStoredContext.ResponseMessages!.ToList();
Assert.Single(storedResponse);
Assert.Equal("Response", storedResponse[0].Text);
}
[Fact]
public async Task InvokedCoreAsync_DefaultFilterExcludesChatHistorySourcedMessagesAsync()
{
// Arrange
var provider = new TestChatHistoryProvider();
var external = new ChatMessage(ChatRole.User, "External");
var fromHistory = new ChatMessage(ChatRole.User, "History")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
var fromContext = new ChatMessage(ChatRole.User, "Context")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, "src");
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [external, fromHistory, fromContext], []);
// Act
await provider.InvokedAsync(context);
// Assert - External and AIContextProvider messages kept, ChatHistory excluded
Assert.NotNull(provider.LastStoredContext);
var storedRequest = provider.LastStoredContext!.RequestMessages.ToList();
Assert.Equal(2, storedRequest.Count);
Assert.Equal("External", storedRequest[0].Text);
Assert.Equal("Context", storedRequest[1].Text);
}
[Fact]
public async Task InvokedCoreAsync_PassesResponseMessagesToStoreAsync()
{
// Arrange
var provider = new TestChatHistoryProvider();
var responseMessages = new[] { new ChatMessage(ChatRole.Assistant, "Resp1"), new ChatMessage(ChatRole.Assistant, "Resp2") };
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "msg")], responseMessages);
// Act
await provider.InvokedAsync(context);
// Assert
Assert.NotNull(provider.LastStoredContext);
Assert.Same(responseMessages, provider.LastStoredContext!.ResponseMessages);
}
#endregion
private sealed class TestChatHistoryProvider : ChatHistoryProvider
{
private readonly IEnumerable<ChatMessage>? _provideMessages;
public InvokedContext? LastStoredContext { get; private set; }
public TestChatHistoryProvider(
IEnumerable<ChatMessage>? provideMessages = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
: base(provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
this._provideMessages = provideMessages;
}
protected override ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new(this._provideMessages ?? []);
protected override ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
this.LastStoredContext = context;
return default;
}
}
/// <summary>
/// A provider that uses only base class defaults (no overrides of ProvideChatHistoryAsync/StoreChatHistoryAsync).
/// </summary>
private sealed class DefaultChatHistoryProvider : ChatHistoryProvider;
}
@@ -0,0 +1,525 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="ChatMessageExtensions"/> class.
/// </summary>
public sealed class ChatMessageExtensionsTests
{
#region GetAgentRequestMessageSourceType Tests
[Fact]
public void GetAgentRequestMessageSourceType_WithNoAdditionalProperties_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSourceType_WithNullAdditionalProperties_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = null
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSourceType_WithEmptyAdditionalProperties_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary()
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSourceType_WithExternalSourceType_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.External, "TestSourceId") }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSourceType_WithAIContextProviderSourceType_ReturnsAIContextProvider()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "TestSourceId") }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, result);
}
[Fact]
public void GetAgentRequestMessageSourceType_WithChatHistorySourceType_ReturnsChatHistory()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSourceId") }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result);
}
[Fact]
public void GetAgentRequestMessageSourceType_WithCustomSourceType_ReturnsCustomSourceType()
{
// Arrange
AgentRequestMessageSourceType customSourceType = new("CustomSourceType");
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(customSourceType, "TestSourceId") }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(customSourceType, result);
Assert.Equal("CustomSourceType", result.Value);
}
[Fact]
public void GetAgentRequestMessageSourceType_WithWrongAttributionType_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, "NotAnAgentRequestMessageSourceAttribution" }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSourceType_WithNullAttributionValue_ReturnsExternal()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, null! }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.External, result);
}
[Fact]
public void GetAgentRequestMessageSourceType_WithMultipleProperties_ReturnsCorrectSourceType()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ "OtherProperty", "SomeValue" },
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSourceId") },
{ "AnotherProperty", 123 }
}
};
// Act
AgentRequestMessageSourceType result = message.GetAgentRequestMessageSourceType();
// Assert
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result);
}
#endregion
#region GetAgentRequestMessageSourceId Tests
[Fact]
public void GetAgentRequestMessageSourceId_WithNoAdditionalProperties_ReturnsNull()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithNullAdditionalProperties_ReturnsNull()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = null
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithEmptyAdditionalProperties_ReturnsNull()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary()
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithAttribution_ReturnsSourceId()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "MyProvider.FullName") }
}
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Equal("MyProvider.FullName", result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithDifferentSourceIds_ReturnsCorrectSourceId()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "CustomHistorySourceId") }
}
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Equal("CustomHistorySourceId", result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithWrongAttributionType_ReturnsNull()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, "NotAnAgentRequestMessageSourceAttribution" }
}
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithNullAttributionValue_ReturnsNull()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, null! }
}
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Null(result);
}
[Fact]
public void GetAgentRequestMessageSourceId_WithMultipleProperties_ReturnsCorrectSourceId()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ "OtherProperty", "SomeValue" },
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.External, "ExpectedSourceId") },
{ "AnotherProperty", 123 }
}
};
// Act
string? result = message.GetAgentRequestMessageSourceId();
// Assert
Assert.Equal("ExpectedSourceId", result);
}
#endregion
#region AsAgentRequestMessageSourcedMessage Tests
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithNoAdditionalProperties_ReturnsClonesMessageWithAttribution()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
ChatMessage result = message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.External, "TestSourceId");
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.External, result.GetAgentRequestMessageSourceType());
Assert.Equal("TestSourceId", result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithNullAdditionalProperties_ReturnsClonesMessageWithAttribution()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = null
};
// Act
ChatMessage result = message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, "ProviderSourceId");
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, result.GetAgentRequestMessageSourceType());
Assert.Equal("ProviderSourceId", result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithMatchingSourceTypeAndSourceId_ReturnsSameInstance()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistoryId") }
}
};
// Act
ChatMessage result = message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "HistoryId");
// Assert
Assert.Same(message, result);
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithDifferentSourceType_ReturnsClonesMessageWithNewAttribution()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.External, "SourceId") }
}
};
// Act
ChatMessage result = message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, "SourceId");
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, result.GetAgentRequestMessageSourceType());
Assert.Equal("SourceId", result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithDifferentSourceId_ReturnsClonesMessageWithNewAttribution()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.External, "OriginalId") }
}
};
// Act
ChatMessage result = message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.External, "NewId");
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.External, result.GetAgentRequestMessageSourceType());
Assert.Equal("NewId", result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithDefaultNullSourceId_ReturnsClonesMessageWithNullSourceId()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
ChatMessage result = message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory);
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result.GetAgentRequestMessageSourceType());
Assert.Null(result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithMatchingSourceTypeAndNullSourceId_ReturnsSameInstance()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.External, null) }
}
};
// Act
ChatMessage result = message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.External);
// Assert
Assert.Same(message, result);
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_DoesNotModifyOriginalMessage()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello");
// Act
ChatMessage result = message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, "ProviderId");
// Assert
Assert.Null(message.AdditionalProperties);
Assert.NotNull(result.AdditionalProperties);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, result.GetAgentRequestMessageSourceType());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_WithWrongAttributionType_ReturnsClonesMessageWithNewAttribution()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
{ AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, "NotAnAttribution" }
}
};
// Act
ChatMessage result = message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.External, "SourceId");
// Assert
Assert.NotSame(message, result);
Assert.Equal(AgentRequestMessageSourceType.External, result.GetAgentRequestMessageSourceType());
Assert.Equal("SourceId", result.GetAgentRequestMessageSourceId());
}
[Fact]
public void AsAgentRequestMessageSourcedMessage_PreservesMessageContent()
{
// Arrange
ChatMessage message = new(ChatRole.Assistant, "Test content");
// Act
ChatMessage result = message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "HistoryId");
// Assert
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.Equal("Test content", result.Text);
}
#endregion
}
@@ -0,0 +1,349 @@
// 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 Moq;
using Moq.Protected;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Unit tests for the <see cref="DelegatingAIAgent"/> class.
/// </summary>
public class DelegatingAIAgentTests
{
private readonly Mock<AIAgent> _innerAgentMock;
private readonly TestDelegatingAIAgent _delegatingAgent;
private readonly AgentResponse _testResponse;
private readonly List<AgentResponseUpdate> _testStreamingResponses;
private readonly AgentSession _testSession;
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingAIAgentTests"/> class.
/// </summary>
public DelegatingAIAgentTests()
{
this._innerAgentMock = new Mock<AIAgent> { CallBase = true };
this._testResponse = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Test response"));
this._testStreamingResponses = [new AgentResponseUpdate(ChatRole.Assistant, "Test streaming response")];
this._testSession = new TestAgentSession();
// Setup inner agent mock
this._innerAgentMock.Protected().SetupGet<string>("IdCore").Returns("test-agent-id");
this._innerAgentMock.Setup(x => x.Name).Returns("Test Agent");
this._innerAgentMock.Setup(x => x.Description).Returns("Test Description");
this._innerAgentMock
.Protected()
.Setup<ValueTask<AgentSession>>("CreateSessionCoreAsync", ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(this._testSession);
this._innerAgentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(this._testResponse);
this._innerAgentMock
.Protected()
.Setup<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentSession?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.Returns(ToAsyncEnumerableAsync(this._testStreamingResponses));
this._delegatingAgent = new TestDelegatingAIAgent(this._innerAgentMock.Object);
}
#region Constructor Tests
/// <summary>
/// Verify that constructor throws ArgumentNullException when innerAgent is null.
/// </summary>
[Fact]
public void RequiresInnerAgent() =>
// Act & Assert
Assert.Throws<ArgumentNullException>("innerAgent", () => new TestDelegatingAIAgent(null!));
/// <summary>
/// Verify that constructor sets the inner agent correctly.
/// </summary>
[Fact]
public void Constructor_WithValidInnerAgent_SetsInnerAgent()
{
// Act
var delegatingAgent = new TestDelegatingAIAgent(this._innerAgentMock.Object);
// Assert
Assert.Same(this._innerAgentMock.Object, delegatingAgent.InnerAgent);
}
#endregion
#region Property Delegation Tests
/// <summary>
/// Verify that Id property delegates to inner agent.
/// </summary>
[Fact]
public void Id_DelegatesToInnerAgent()
{
// Act
var id = this._delegatingAgent.Id;
// Assert
Assert.Equal("test-agent-id", id);
this._innerAgentMock.Protected().VerifyGet<string>("IdCore", Times.Once());
}
/// <summary>
/// Verify that Name property delegates to inner agent.
/// </summary>
[Fact]
public void Name_DelegatesToInnerAgent()
{
// Act
var name = this._delegatingAgent.Name;
// Assert
Assert.Equal("Test Agent", name);
this._innerAgentMock.Verify(x => x.Name, Times.Once);
}
/// <summary>
/// Verify that Description property delegates to inner agent.
/// </summary>
[Fact]
public void Description_DelegatesToInnerAgent()
{
// Act
var description = this._delegatingAgent.Description;
// Assert
Assert.Equal("Test Description", description);
this._innerAgentMock.Verify(x => x.Description, Times.Once);
}
#endregion
#region Method Delegation Tests
/// <summary>
/// Verify that CreateSessionAsync delegates to inner agent.
/// </summary>
[Fact]
public async Task CreateSessionAsync_DelegatesToInnerAgentAsync()
{
// Act
var session = await this._delegatingAgent.CreateSessionAsync();
// Assert
Assert.Same(this._testSession, session);
this._innerAgentMock
.Protected()
.Verify<ValueTask<AgentSession>>("CreateSessionCoreAsync", Times.Once(), ItExpr.IsAny<CancellationToken>());
}
/// <summary>
/// Verify that DeserializeSessionAsync delegates to inner agent.
/// </summary>
[Fact]
public async Task DeserializeSessionAsync_DelegatesToInnerAgentAsync()
{
// Arrange
var serializedSession = JsonSerializer.SerializeToElement("test-session-id", TestJsonSerializerContext.Default.String);
this._innerAgentMock
.Protected()
.Setup<ValueTask<AgentSession>>("DeserializeSessionCoreAsync", ItExpr.IsAny<JsonElement>(), ItExpr.IsAny<JsonSerializerOptions?>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(this._testSession);
// Act
var session = await this._delegatingAgent.DeserializeSessionAsync(serializedSession);
// Assert
Assert.Same(this._testSession, session);
this._innerAgentMock
.Protected()
.Verify<ValueTask<AgentSession>>("DeserializeSessionCoreAsync", Times.Once(), ItExpr.IsAny<JsonElement>(), ItExpr.IsAny<JsonSerializerOptions?>(), ItExpr.IsAny<CancellationToken>());
}
/// <summary>
/// Verify that RunAsync delegates to inner agent with correct parameters.
/// </summary>
[Fact]
public async Task RunAsyncDefaultsToInnerAgentAsync()
{
// Arrange
var expectedMessages = new[] { new ChatMessage(ChatRole.User, "Test message") };
var expectedSession = new TestAgentSession();
var expectedOptions = new AgentRunOptions();
var expectedCancellationToken = new CancellationToken();
var expectedResult = new TaskCompletionSource<AgentResponse>();
var expectedResponse = new AgentResponse();
var innerAgentMock = new Mock<AIAgent>();
innerAgentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.Is<IEnumerable<ChatMessage>>(m => m == expectedMessages),
ItExpr.Is<AgentSession?>(t => t == expectedSession),
ItExpr.Is<AgentRunOptions?>(o => o == expectedOptions),
ItExpr.Is<CancellationToken>(ct => ct == expectedCancellationToken))
.Returns(expectedResult.Task);
var delegatingAgent = new TestDelegatingAIAgent(innerAgentMock.Object);
// Act
var resultTask = delegatingAgent.RunAsync(expectedMessages, expectedSession, expectedOptions, expectedCancellationToken);
// Assert
Assert.False(resultTask.IsCompleted);
expectedResult.SetResult(expectedResponse);
Assert.True(resultTask.IsCompleted);
Assert.Same(expectedResponse, await resultTask);
}
/// <summary>
/// Verify that RunStreamingAsync delegates to inner agent with correct parameters.
/// </summary>
[Fact]
public async Task RunStreamingAsyncDefaultsToInnerAgentAsync()
{
// Arrange
var expectedMessages = new[] { new ChatMessage(ChatRole.User, "Test message") };
var expectedSession = new TestAgentSession();
var expectedOptions = new AgentRunOptions();
var expectedCancellationToken = new CancellationToken();
AgentResponseUpdate[] expectedResults =
[
new(ChatRole.Assistant, "Message 1"),
new(ChatRole.Assistant, "Message 2")
];
var innerAgentMock = new Mock<AIAgent>();
innerAgentMock
.Protected()
.Setup<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
ItExpr.Is<IEnumerable<ChatMessage>>(m => m == expectedMessages),
ItExpr.Is<AgentSession?>(t => t == expectedSession),
ItExpr.Is<AgentRunOptions?>(o => o == expectedOptions),
ItExpr.Is<CancellationToken>(ct => ct == expectedCancellationToken))
.Returns(ToAsyncEnumerableAsync(expectedResults));
var delegatingAgent = new TestDelegatingAIAgent(innerAgentMock.Object);
// Act
var resultAsyncEnumerable = delegatingAgent.RunStreamingAsync(expectedMessages, expectedSession, expectedOptions, expectedCancellationToken);
// Assert
var enumerator = resultAsyncEnumerable.GetAsyncEnumerator();
Assert.True(await enumerator.MoveNextAsync());
Assert.Same(expectedResults[0], enumerator.Current);
Assert.True(await enumerator.MoveNextAsync());
Assert.Same(expectedResults[1], enumerator.Current);
Assert.False(await enumerator.MoveNextAsync());
}
#endregion
#region GetService Tests
/// <summary>
/// Verify that GetService throws ArgumentNullException when serviceType is null.
/// </summary>
[Fact]
public void GetServiceThrowsForNullType() =>
// Act & Assert
Assert.Throws<ArgumentNullException>("serviceType", () => this._delegatingAgent.GetService(null!));
/// <summary>
/// Verify that GetService returns the delegating agent itself when requesting compatible type and key is null.
/// </summary>
[Fact]
public void GetServiceReturnsSelfIfCompatibleWithRequestAndKeyIsNull()
{
// Act
var agent = this._delegatingAgent.GetService<DelegatingAIAgent>();
// Assert
Assert.Same(this._delegatingAgent, agent);
}
/// <summary>
/// Verify that GetService delegates to inner agent when service key is not null.
/// </summary>
[Fact]
public void GetServiceDelegatesToInnerIfKeyIsNotNull()
{
// Arrange
var expectedKey = new object();
var expectedResult = new Mock<AIAgent>().Object;
var innerAgentMock = new Mock<AIAgent>();
innerAgentMock.Setup(x => x.GetService(typeof(AIAgent), expectedKey)).Returns(expectedResult);
var delegatingAgent = new TestDelegatingAIAgent(innerAgentMock.Object);
// Act
var agent = delegatingAgent.GetService<AIAgent>(expectedKey);
// Assert
Assert.Same(expectedResult, agent);
}
/// <summary>
/// Verify that GetService delegates to inner agent when not compatible with request.
/// </summary>
[Fact]
public void GetServiceDelegatesToInnerIfNotCompatibleWithRequest()
{
// Arrange
var expectedResult = TimeZoneInfo.Local;
var expectedKey = new object();
var innerAgentMock = new Mock<AIAgent>();
innerAgentMock
.Setup(x => x.GetService(typeof(TimeZoneInfo), expectedKey))
.Returns(expectedResult);
var delegatingAgent = new TestDelegatingAIAgent(innerAgentMock.Object);
// Act
var tzi = delegatingAgent.GetService<TimeZoneInfo>(expectedKey);
// Assert
Assert.Same(expectedResult, tzi);
}
#endregion
#region Helper Methods
private static async IAsyncEnumerable<T> ToAsyncEnumerableAsync<T>(IEnumerable<T> values)
{
await Task.Yield();
foreach (var value in values)
{
yield return value;
}
}
#endregion
#region Test Implementation
/// <summary>
/// Test implementation of DelegatingAIAgent for testing purposes.
/// </summary>
private sealed class TestDelegatingAIAgent(AIAgent innerAgent) : DelegatingAIAgent(innerAgent)
{
public new AIAgent InnerAgent => base.InnerAgent;
}
private sealed class TestAgentSession : AgentSession;
#endregion
}
@@ -0,0 +1,473 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="InMemoryChatHistoryProvider"/> class.
/// </summary>
public class InMemoryChatHistoryProviderTests
{
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static AgentSession CreateMockSession() => new Mock<AgentSession>().Object;
[Fact]
public void Constructor_DefaultsToBeforeMessageRetrieval_ForNotProvidedTriggerEvent()
{
// Arrange & Act
var reducerMock = new Mock<IChatReducer>();
var provider = new InMemoryChatHistoryProvider(new() { ChatReducer = reducerMock.Object });
// Assert
Assert.Equal(InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval, provider.ReducerTriggerEvent);
}
[Fact]
public void Constructor_Arguments_SetOnPropertiesCorrectly()
{
// Arrange & Act
var reducerMock = new Mock<IChatReducer>();
var provider = new InMemoryChatHistoryProvider(new() { ChatReducer = reducerMock.Object, ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded });
// Assert
Assert.Same(reducerMock.Object, provider.ChatReducer);
Assert.Equal(InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded, provider.ReducerTriggerEvent);
}
[Fact]
public void StateKeys_ReturnsDefaultKey_WhenNoOptionsProvided()
{
// Arrange & Act
var provider = new InMemoryChatHistoryProvider();
// Assert
Assert.Single(provider.StateKeys);
Assert.Contains("InMemoryChatHistoryProvider", provider.StateKeys);
}
[Fact]
public void StateKeys_ReturnsCustomKey_WhenSetViaOptions()
{
// Arrange & Act
var provider = new InMemoryChatHistoryProvider(new() { StateKey = "custom-key" });
// Assert
Assert.Single(provider.StateKeys);
Assert.Contains("custom-key", provider.StateKeys);
}
[Fact]
public async Task InvokedAsyncAddsMessagesAsync()
{
var session = CreateMockSession();
// Arrange
var requestMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.System, "additional context") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "TestSource") } } },
};
var responseMessages = new List<ChatMessage>
{
new(ChatRole.Assistant, "Hi there!")
};
var providerMessages = new List<ChatMessage>()
{
new(ChatRole.System, "original instructions")
};
var provider = new InMemoryChatHistoryProvider();
provider.SetMessages(session, providerMessages);
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages, responseMessages);
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
var messages = provider.GetMessages(session);
Assert.Equal(4, messages.Count);
Assert.Equal("original instructions", messages[0].Text);
Assert.Equal("Hello", messages[1].Text);
Assert.Equal("additional context", messages[2].Text);
Assert.Equal("Hi there!", messages[3].Text);
}
[Fact]
public async Task InvokedAsyncWithEmptyDoesNotFailAsync()
{
var session = CreateMockSession();
// Arrange
var provider = new InMemoryChatHistoryProvider();
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, [], []);
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
Assert.Empty(provider.GetMessages(session));
}
[Fact]
public async Task InvokingAsyncReturnsAllMessagesAsync()
{
var session = CreateMockSession();
// Arrange
var requestMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
};
var provider = new InMemoryChatHistoryProvider();
provider.SetMessages(session,
[
new ChatMessage(ChatRole.User, "Test1"),
new ChatMessage(ChatRole.Assistant, "Test2")
]);
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, session, requestMessages);
var result = (await provider.InvokingAsync(context, CancellationToken.None)).ToList();
// Assert
Assert.Equal(3, result.Count);
Assert.Contains(result, m => m.Text == "Test1");
Assert.Contains(result, m => m.Text == "Test2");
Assert.Contains(result, m => m.Text == "Hello");
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result[0].GetAgentRequestMessageSourceType());
Assert.Equal(AgentRequestMessageSourceType.ChatHistory, result[1].GetAgentRequestMessageSourceType());
Assert.Equal(AgentRequestMessageSourceType.External, result[2].GetAgentRequestMessageSourceType());
}
[Fact]
public void StateInitializer_IsInvoked_WhenSessionHasNoState()
{
// Arrange
var initialMessages = new List<ChatMessage>
{
new(ChatRole.User, "Initial message")
};
var provider = new InMemoryChatHistoryProvider(new()
{
StateInitializer = _ => new InMemoryChatHistoryProvider.State { Messages = initialMessages }
});
// Act
var messages = provider.GetMessages(CreateMockSession());
// Assert
Assert.Single(messages);
Assert.Equal("Initial message", messages[0].Text);
}
[Fact]
public void GetMessages_ReturnsEmptyList_WhenNullSession()
{
// Arrange
var provider = new InMemoryChatHistoryProvider();
// Act
var messages = provider.GetMessages(null);
// Assert
Assert.Empty(messages);
}
[Fact]
public void SetMessages_ThrowsForNullMessages()
{
// Arrange
var provider = new InMemoryChatHistoryProvider();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => provider.SetMessages(CreateMockSession(), null!));
}
[Fact]
public void SetMessages_UpdatesState()
{
var session = CreateMockSession();
// Arrange
var provider = new InMemoryChatHistoryProvider();
var messages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "World")
};
// Act
provider.SetMessages(session, messages);
var retrieved = provider.GetMessages(session);
// Assert
Assert.Equal(2, retrieved.Count);
Assert.Equal("Hello", retrieved[0].Text);
Assert.Equal("World", retrieved[1].Text);
}
[Fact]
public async Task InvokedAsyncWithEmptyMessagesDoesNotChangeProviderAsync()
{
var session = CreateMockSession();
// Arrange
var provider = new InMemoryChatHistoryProvider();
var messages = new List<ChatMessage>();
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, messages, []);
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
Assert.Empty(provider.GetMessages(session));
}
[Fact]
public async Task InvokedAsync_WithNullContext_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var provider = new InMemoryChatHistoryProvider();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => provider.InvokedAsync(null!, CancellationToken.None).AsTask());
}
[Fact]
public async Task AddMessagesAsync_WithReducer_AfterMessageAdded_InvokesReducerAsync()
{
var session = CreateMockSession();
// Arrange
var originalMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!")
};
var reducedMessages = new List<ChatMessage>
{
new(ChatRole.User, "Reduced")
};
var reducerMock = new Mock<IChatReducer>();
reducerMock
.Setup(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(originalMessages)), It.IsAny<CancellationToken>()))
.ReturnsAsync(reducedMessages);
var provider = new InMemoryChatHistoryProvider(new() { ChatReducer = reducerMock.Object, ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded });
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, originalMessages, []);
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
var messages = provider.GetMessages(session);
Assert.Single(messages);
Assert.Equal("Reduced", messages[0].Text);
reducerMock.Verify(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(originalMessages)), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task GetMessagesAsync_WithReducer_BeforeMessagesRetrieval_InvokesReducerAsync()
{
var session = CreateMockSession();
// Arrange
var originalMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!")
};
var reducedMessages = new List<ChatMessage>
{
new(ChatRole.User, "Reduced")
};
var reducerMock = new Mock<IChatReducer>();
reducerMock
.Setup(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(originalMessages)), It.IsAny<CancellationToken>()))
.ReturnsAsync(reducedMessages);
var provider = new InMemoryChatHistoryProvider(new() { ChatReducer = reducerMock.Object, ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval });
provider.SetMessages(session, new List<ChatMessage>(originalMessages));
// Act
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, session, Array.Empty<ChatMessage>());
var result = (await provider.InvokingAsync(invokingContext, CancellationToken.None)).ToList();
// Assert
Assert.Single(result);
Assert.Equal("Reduced", result[0].Text);
reducerMock.Verify(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(originalMessages)), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task AddMessagesAsync_WithReducer_ButWrongTrigger_DoesNotInvokeReducerAsync()
{
var session = CreateMockSession();
// Arrange
var originalMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello")
};
var reducerMock = new Mock<IChatReducer>();
var provider = new InMemoryChatHistoryProvider(new() { ChatReducer = reducerMock.Object, ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval });
// Act
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, originalMessages, []);
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
var messages = provider.GetMessages(session);
Assert.Single(messages);
Assert.Equal("Hello", messages[0].Text);
reducerMock.Verify(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task GetMessagesAsync_WithReducer_ButWrongTrigger_DoesNotInvokeReducerAsync()
{
var session = CreateMockSession();
// Arrange
var originalMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello")
};
var reducerMock = new Mock<IChatReducer>();
var provider = new InMemoryChatHistoryProvider(new() { ChatReducer = reducerMock.Object, ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded });
provider.SetMessages(session, new List<ChatMessage>(originalMessages));
// Act
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, session, Array.Empty<ChatMessage>());
var result = (await provider.InvokingAsync(invokingContext, CancellationToken.None)).ToList();
// Assert
Assert.Single(result);
Assert.Equal("Hello", result[0].Text);
reducerMock.Verify(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task InvokedAsync_WithException_DoesNotAddMessagesAsync()
{
var session = CreateMockSession();
// Arrange
var provider = new InMemoryChatHistoryProvider();
var requestMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello")
};
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages, new InvalidOperationException("Test exception"));
// Act
await provider.InvokedAsync(context, CancellationToken.None);
// Assert
Assert.Empty(provider.GetMessages(session));
}
[Fact]
public async Task InvokingAsync_WithNullContext_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var provider = new InMemoryChatHistoryProvider();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => provider.InvokingAsync(null!, CancellationToken.None).AsTask());
}
[Fact]
public async Task InvokedAsync_DefaultFilter_ExcludesChatHistoryMessagesAsync()
{
// Arrange
var session = CreateMockSession();
var provider = new InMemoryChatHistoryProvider();
var requestMessages = new List<ChatMessage>
{
new(ChatRole.User, "External message"),
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
new(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
};
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages, [new ChatMessage(ChatRole.Assistant, "Response")]);
// Act
await provider.InvokedAsync(context, CancellationToken.None);
// Assert - ChatHistory message excluded, AIContextProvider message included
var messages = provider.GetMessages(session);
Assert.Equal(3, messages.Count);
Assert.Equal("External message", messages[0].Text);
Assert.Equal("From context provider", messages[1].Text);
Assert.Equal("Response", messages[2].Text);
}
[Fact]
public async Task InvokedAsync_CustomFilter_OverridesDefaultAsync()
{
// Arrange
var session = CreateMockSession();
var provider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
StorageInputRequestMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
});
var requestMessages = new List<ChatMessage>
{
new(ChatRole.User, "External message"),
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
new(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
};
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages, [new ChatMessage(ChatRole.Assistant, "Response")]);
// Act
await provider.InvokedAsync(context, CancellationToken.None);
// Assert - Custom filter keeps only External messages (both ChatHistory and AIContextProvider excluded)
var messages = provider.GetMessages(session);
Assert.Equal(2, messages.Count);
Assert.Equal("External message", messages[0].Text);
Assert.Equal("Response", messages[1].Text);
}
[Fact]
public async Task InvokingAsync_OutputFilter_FiltersOutputMessagesAsync()
{
// Arrange
var session = CreateMockSession();
var provider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
{
ProvideOutputMessageFilter = messages => messages.Where(m => m.Role == ChatRole.User)
});
provider.SetMessages(session,
[
new ChatMessage(ChatRole.User, "User message"),
new ChatMessage(ChatRole.Assistant, "Assistant message"),
new ChatMessage(ChatRole.System, "System message")
]);
// Act
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, session, []);
var result = (await provider.InvokingAsync(context, CancellationToken.None)).ToList();
// Assert - Only user messages pass through the output filter
Assert.Single(result);
Assert.Equal("User message", result[0].Text);
}
public class TestAIContent(string testData) : AIContent
{
public string TestData => testData;
}
}
@@ -0,0 +1,323 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="MessageAIContextProvider"/> class.
/// </summary>
public class MessageAIContextProviderTests
{
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
#region InvokingAsync Tests
[Fact]
public async Task InvokingAsync_NullContext_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var provider = new TestMessageProvider();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => provider.InvokingAsync(null!).AsTask());
}
[Fact]
public async Task InvokingAsync_ReturnsInputAndProvidedMessagesAsync()
{
// Arrange
var providedMessages = new[] { new ChatMessage(ChatRole.System, "Context message") };
var provider = new TestMessageProvider(provideMessages: providedMessages);
var context = new MessageAIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "User input")]);
// Act
var result = (await provider.InvokingAsync(context)).ToList();
// Assert - input messages + provided messages merged
Assert.Equal(2, result.Count);
Assert.Equal("User input", result[0].Text);
Assert.Equal("Context message", result[1].Text);
}
[Fact]
public async Task InvokingAsync_ReturnsOnlyInputMessages_WhenNoMessagesProvidedAsync()
{
// Arrange
var provider = new DefaultMessageProvider();
var context = new MessageAIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Hello")]);
// Act
var result = (await provider.InvokingAsync(context)).ToList();
// Assert
Assert.Single(result);
Assert.Equal("Hello", result[0].Text);
}
[Fact]
public async Task InvokingAsync_StampsProvidedMessagesWithAIContextProviderSourceAsync()
{
// Arrange
var providedMessages = new[] { new ChatMessage(ChatRole.System, "Provided") };
var provider = new TestMessageProvider(provideMessages: providedMessages);
var context = new MessageAIContextProvider.InvokingContext(s_mockAgent, s_mockSession, []);
// Act
var result = (await provider.InvokingAsync(context)).ToList();
// Assert
Assert.Single(result);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, result[0].GetAgentRequestMessageSourceType());
}
[Fact]
public async Task InvokingAsync_FiltersInputToExternalOnlyByDefaultAsync()
{
// Arrange
var provider = new TestMessageProvider(captureFilteredContext: true);
var externalMsg = new ChatMessage(ChatRole.User, "External");
var chatHistoryMsg = new ChatMessage(ChatRole.User, "History")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
var contextProviderMsg = new ChatMessage(ChatRole.User, "ContextProvider")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.AIContextProvider, "src");
var context = new MessageAIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [externalMsg, chatHistoryMsg, contextProviderMsg]);
// Act
await provider.InvokingAsync(context);
// Assert - ProvideMessagesAsync received only External messages
Assert.NotNull(provider.LastFilteredContext);
var filteredMessages = provider.LastFilteredContext!.RequestMessages.ToList();
Assert.Single(filteredMessages);
Assert.Equal("External", filteredMessages[0].Text);
}
[Fact]
public async Task InvokingAsync_UsesCustomProvideInputFilterAsync()
{
// Arrange - filter that keeps all messages (not just External)
var provider = new TestMessageProvider(
captureFilteredContext: true,
provideInputMessageFilter: msgs => msgs);
var externalMsg = new ChatMessage(ChatRole.User, "External");
var chatHistoryMsg = new ChatMessage(ChatRole.User, "History")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
var context = new MessageAIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [externalMsg, chatHistoryMsg]);
// Act
await provider.InvokingAsync(context);
// Assert - ProvideMessagesAsync received ALL messages (custom filter keeps everything)
Assert.NotNull(provider.LastFilteredContext);
var filteredMessages = provider.LastFilteredContext!.RequestMessages.ToList();
Assert.Equal(2, filteredMessages.Count);
}
[Fact]
public async Task InvokingAsync_MergesWithOriginalUnfilteredMessagesAsync()
{
// Arrange - default filter is External-only, but the MERGED result should include
// the original unfiltered input messages plus the provided messages
var providedMessages = new[] { new ChatMessage(ChatRole.System, "Provided") };
var provider = new TestMessageProvider(provideMessages: providedMessages);
var externalMsg = new ChatMessage(ChatRole.User, "External");
var chatHistoryMsg = new ChatMessage(ChatRole.User, "History")
.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory, "src");
var context = new MessageAIContextProvider.InvokingContext(s_mockAgent, s_mockSession, [externalMsg, chatHistoryMsg]);
// Act
var result = (await provider.InvokingAsync(context)).ToList();
// Assert - original 2 input messages + 1 provided message
Assert.Equal(3, result.Count);
Assert.Equal("External", result[0].Text);
Assert.Equal("History", result[1].Text);
Assert.Equal("Provided", result[2].Text);
}
#endregion
#region ProvideAIContextAsync Tests
[Fact]
public async Task ProvideAIContextAsync_PreservesInstructionsAndToolsAsync()
{
// Arrange
var providedMessages = new[] { new ChatMessage(ChatRole.System, "Context") };
var provider = new TestMessageProvider(provideMessages: providedMessages);
var inputTool = AIFunctionFactory.Create(() => "a", "inputTool");
var inputContext = new AIContext
{
Messages = [new ChatMessage(ChatRole.User, "Hello")],
Instructions = "Be helpful",
Tools = [inputTool]
};
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, inputContext);
// Act
var result = await provider.InvokingAsync(context);
// Assert - instructions and tools are preserved
Assert.Equal("Be helpful", result.Instructions);
Assert.NotNull(result.Tools);
Assert.Single(result.Tools!);
Assert.Equal("inputTool", result.Tools!.First().Name);
// Messages include original input + provided messages (with stamping)
var messages = result.Messages!.ToList();
Assert.Equal(2, messages.Count);
Assert.Equal("Hello", messages[0].Text);
Assert.Equal("Context", messages[1].Text);
Assert.Equal(AgentRequestMessageSourceType.AIContextProvider, messages[1].GetAgentRequestMessageSourceType());
}
[Fact]
public async Task ProvideAIContextAsync_PreservesNullInstructionsAndToolsAsync()
{
// Arrange
var provider = new DefaultMessageProvider();
var inputContext = new AIContext { Messages = [new ChatMessage(ChatRole.User, "Hello")] };
var context = new AIContextProvider.InvokingContext(s_mockAgent, s_mockSession, inputContext);
// Act
var result = await provider.InvokingAsync(context);
// Assert
Assert.Null(result.Instructions);
Assert.Null(result.Tools);
var messages = result.Messages!.ToList();
Assert.Single(messages);
Assert.Equal("Hello", messages[0].Text);
}
#endregion
#region InvokingContext Tests
[Fact]
public void InvokingContext_Constructor_ThrowsForNullAgent()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new MessageAIContextProvider.InvokingContext(null!, s_mockSession, []));
}
[Fact]
public void InvokingContext_Constructor_ThrowsForNullRequestMessages()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new MessageAIContextProvider.InvokingContext(s_mockAgent, s_mockSession, null!));
}
[Fact]
public void InvokingContext_Constructor_AllowsNullSession()
{
// Act
var context = new MessageAIContextProvider.InvokingContext(s_mockAgent, null, []);
// Assert
Assert.Null(context.Session);
}
[Fact]
public void InvokingContext_Properties_Roundtrip()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
// Act
var context = new MessageAIContextProvider.InvokingContext(s_mockAgent, s_mockSession, messages);
// Assert
Assert.Same(s_mockAgent, context.Agent);
Assert.Same(s_mockSession, context.Session);
Assert.Same(messages, context.RequestMessages);
}
[Fact]
public void InvokingContext_RequestMessages_SetterThrowsForNull()
{
// Arrange
var context = new MessageAIContextProvider.InvokingContext(s_mockAgent, s_mockSession, []);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => context.RequestMessages = null!);
}
[Fact]
public void InvokingContext_RequestMessages_SetterAcceptsValidValue()
{
// Arrange
var context = new MessageAIContextProvider.InvokingContext(s_mockAgent, s_mockSession, []);
var newMessages = new List<ChatMessage> { new(ChatRole.User, "Updated") };
// Act
context.RequestMessages = newMessages;
// Assert
Assert.Same(newMessages, context.RequestMessages);
}
#endregion
#region GetService Tests
[Fact]
public void GetService_ReturnsProviderForMessageAIContextProviderType()
{
// Arrange
var provider = new TestMessageProvider();
// Act & Assert
Assert.Same(provider, provider.GetService(typeof(MessageAIContextProvider)));
Assert.Same(provider, provider.GetService(typeof(AIContextProvider)));
Assert.Same(provider, provider.GetService(typeof(TestMessageProvider)));
}
#endregion
#region Test helpers
private sealed class TestMessageProvider : MessageAIContextProvider
{
private readonly IEnumerable<ChatMessage>? _provideMessages;
private readonly bool _captureFilteredContext;
public InvokingContext? LastFilteredContext { get; private set; }
public TestMessageProvider(
IEnumerable<ChatMessage>? provideMessages = null,
bool captureFilteredContext = false,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideInputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputMessageFilter = null)
: base(provideInputMessageFilter, storeInputMessageFilter)
{
this._provideMessages = provideMessages;
this._captureFilteredContext = captureFilteredContext;
}
protected override ValueTask<IEnumerable<ChatMessage>> ProvideMessagesAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
if (this._captureFilteredContext)
{
this.LastFilteredContext = context;
}
return new(this._provideMessages ?? []);
}
}
/// <summary>
/// A provider that uses only base class defaults (no overrides of ProvideMessagesAsync).
/// </summary>
private sealed class DefaultMessageProvider : MessageAIContextProvider;
#endregion
}
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);MEAI001</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
</ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
</Project>
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Models;
[Description("Some test description")]
internal sealed class Animal
{
public int Id { get; set; }
public string? FullName { get; set; }
public Species Species { get; set; }
}
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Models;
internal enum Species
{
Bear,
Tiger,
Walrus,
}
@@ -0,0 +1,241 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="ProviderSessionState{TState}"/> class.
/// </summary>
public class ProviderSessionStateTests
{
#region Constructor Tests
[Fact]
public void Constructor_ThrowsForNullStateInitializer()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ProviderSessionState<TestState>(null!, "test-key"));
}
[Fact]
public void Constructor_ThrowsForNullStateKey()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new ProviderSessionState<TestState>(_ => new TestState(), null!));
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Constructor_ThrowsForEmptyOrWhitespaceStateKey(string stateKey)
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new ProviderSessionState<TestState>(_ => new TestState(), stateKey));
}
[Fact]
public void Constructor_AcceptsNullJsonSerializerOptions()
{
// Act - should not throw
var sessionState = new ProviderSessionState<TestState>(_ => new TestState(), "test-key", jsonSerializerOptions: null);
// Assert - instance is created and functional
Assert.Equal("test-key", sessionState.StateKey);
}
[Fact]
public void Constructor_AcceptsCustomJsonSerializerOptions()
{
// Arrange
var customOptions = new System.Text.Json.JsonSerializerOptions();
// Act - should not throw
var sessionState = new ProviderSessionState<TestState>(_ => new TestState(), "test-key", customOptions);
// Assert - instance is created and functional
Assert.Equal("test-key", sessionState.StateKey);
}
#endregion
#region GetOrInitializeState Tests
[Fact]
public void GetOrInitializeState_InitializesFromStateInitializerOnFirstCall()
{
// Arrange
var expectedState = new TestState { Value = "initialized" };
var sessionState = new ProviderSessionState<TestState>(_ => expectedState, "test-key");
var session = new TestAgentSession();
// Act
var state = sessionState.GetOrInitializeState(session);
// Assert
Assert.Same(expectedState, state);
}
[Fact]
public void GetOrInitializeState_ReturnsCachedStateFromStateBagOnSecondCall()
{
// Arrange
var callCount = 0;
var sessionState = new ProviderSessionState<TestState>(_ =>
{
callCount++;
return new TestState { Value = $"init-{callCount}" };
}, "test-key");
var session = new TestAgentSession();
// Act
var state1 = sessionState.GetOrInitializeState(session);
var state2 = sessionState.GetOrInitializeState(session);
// Assert - initializer called only once; second call reads from StateBag
Assert.Equal(1, callCount);
Assert.Equal("init-1", state1.Value);
Assert.Equal("init-1", state2.Value);
}
[Fact]
public void GetOrInitializeState_WorksWhenSessionIsNull()
{
// Arrange
var sessionState = new ProviderSessionState<TestState>(_ => new TestState { Value = "no-session" }, "test-key");
// Act
var state = sessionState.GetOrInitializeState(null);
// Assert
Assert.Equal("no-session", state.Value);
}
[Fact]
public void GetOrInitializeState_ReInitializesWhenSessionIsNull()
{
// Arrange - without a session, state can't be cached in StateBag
var callCount = 0;
var sessionState = new ProviderSessionState<TestState>(_ =>
{
callCount++;
return new TestState { Value = $"init-{callCount}" };
}, "test-key");
// Act
sessionState.GetOrInitializeState(null);
sessionState.GetOrInitializeState(null);
// Assert - initializer called each time since there's no session to cache in
Assert.Equal(2, callCount);
}
#endregion
#region SaveState Tests
[Fact]
public void SaveState_SavesToStateBag()
{
// Arrange
var sessionState = new ProviderSessionState<TestState>(_ => new TestState(), "test-key");
var session = new TestAgentSession();
var state = new TestState { Value = "saved" };
// Act
sessionState.SaveState(session, state);
var retrieved = sessionState.GetOrInitializeState(session);
// Assert
Assert.Equal("saved", retrieved.Value);
}
[Fact]
public void SaveState_NoOpWhenSessionIsNull()
{
// Arrange
var sessionState = new ProviderSessionState<TestState>(_ => new TestState { Value = "default" }, "test-key");
// Act - should not throw
sessionState.SaveState(null, new TestState { Value = "saved" });
// Assert - no exception; can't verify further without a session
}
#endregion
#region StateKey Tests
[Fact]
public void StateKey_UsesProvidedKey()
{
// Arrange
var sessionState = new ProviderSessionState<TestState>(_ => new TestState(), "my-provider-key");
// Act & Assert
Assert.Equal("my-provider-key", sessionState.StateKey);
}
[Fact]
public void StateKey_UsesCustomKeyWhenProvided()
{
// Arrange
var sessionState = new ProviderSessionState<TestState>(_ => new TestState(), "custom-key");
// Act & Assert
Assert.Equal("custom-key", sessionState.StateKey);
}
#endregion
#region Isolation Tests
[Fact]
public void GetOrInitializeState_IsolatesStateBetweenDifferentKeys()
{
// Arrange
var sessionState1 = new ProviderSessionState<TestState>(_ => new TestState { Value = "state-1" }, "key-1");
var sessionState2 = new ProviderSessionState<TestState>(_ => new TestState { Value = "state-2" }, "key-2");
var session = new TestAgentSession();
// Act
var state1 = sessionState1.GetOrInitializeState(session);
var state2 = sessionState2.GetOrInitializeState(session);
// Assert - each key maintains independent state
Assert.Equal("state-1", state1.Value);
Assert.Equal("state-2", state2.Value);
}
[Fact]
public void GetOrInitializeState_IsolatesStateBetweenDifferentSessions()
{
// Arrange
var callCount = 0;
var sessionState = new ProviderSessionState<TestState>(_ =>
{
callCount++;
return new TestState { Value = $"init-{callCount}" };
}, "test-key");
var session1 = new TestAgentSession();
var session2 = new TestAgentSession();
// Act
var state1 = sessionState.GetOrInitializeState(session1);
var state2 = sessionState.GetOrInitializeState(session2);
// Assert - each session gets its own state
Assert.Equal(2, callCount);
Assert.Equal("init-1", state1.Value);
Assert.Equal("init-2", state2.Value);
}
#endregion
public sealed class TestState
{
public string Value { get; set; } = string.Empty;
}
private sealed class TestAgentSession : AgentSession;
}
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Agents.AI.Abstractions.UnitTests.Models;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
UseStringEnumConverter = true)]
[JsonSerializable(typeof(AgentResponse))]
[JsonSerializable(typeof(AgentResponseUpdate))]
[JsonSerializable(typeof(AgentRunOptions))]
[JsonSerializable(typeof(Animal))]
[JsonSerializable(typeof(Species))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(Dictionary<string, object?>))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(InMemoryChatHistoryProviderTests.TestAIContent))]
internal sealed partial class TestJsonSerializerContext : JsonSerializerContext;