chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+391
@@ -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);
|
||||
}
|
||||
}
|
||||
+490
@@ -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
|
||||
}
|
||||
+95
@@ -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; }
|
||||
}
|
||||
}
|
||||
+509
@@ -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
|
||||
}
|
||||
+470
@@ -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"]);
|
||||
}
|
||||
}
|
||||
+508
@@ -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"));
|
||||
}
|
||||
}
|
||||
+231
@@ -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;
|
||||
}
|
||||
+525
@@ -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
|
||||
}
|
||||
+473
@@ -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;
|
||||
}
|
||||
}
|
||||
+323
@@ -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
|
||||
}
|
||||
+19
@@ -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;
|
||||
Reference in New Issue
Block a user