test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled

This commit is contained in:
2026-01-24 03:05:12 +11:00
parent f78f2388b3
commit 539852f81c
2584 changed files with 287471 additions and 0 deletions

View File

@@ -0,0 +1,410 @@
// 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<AgentThread> _agentThreadMock;
private readonly AgentResponse _invokeResponse;
private readonly List<AgentResponseUpdate> _invokeStreamingResponses = [];
/// <summary>
/// Initializes a new instance of the <see cref="AIAgentTests"/> class.
/// </summary>
public AIAgentTests()
{
this._agentThreadMock = new Mock<AgentThread>(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<AgentThread?>(t => t == this._agentThreadMock.Object),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(this._invokeResponse);
this._agentMock
.Protected()
.Setup<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.Is<AgentThread?>(t => t == this._agentThreadMock.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._agentThreadMock.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<AgentThread?>(t => t == this._agentThreadMock.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._agentThreadMock.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<AgentThread?>(t => t == this._agentThreadMock.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._agentThreadMock.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<AgentThread?>(t => t == this._agentThreadMock.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._agentThreadMock.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<AgentThread?>(t => t == this._agentThreadMock.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._agentThreadMock.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<AgentThread?>(t => t == this._agentThreadMock.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._agentThreadMock.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<AgentThread?>(t => t == this._agentThreadMock.Object),
ItExpr.Is<AgentRunOptions?>(o => o == options),
ItExpr.Is<CancellationToken>(ct => ct == cancellationToken));
}
[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
/// <summary>
/// Typed mock thread.
/// </summary>
public abstract class TestAgentThread : AgentThread;
private sealed class MockAgent : AIAgent
{
public MockAgent(string? id = null)
{
this.IdCore = id;
}
protected override string? IdCore { get; }
public override async ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public override async ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
protected override Task<AgentResponse> RunCoreAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
protected override IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = 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;
}
}
}

View File

@@ -0,0 +1,165 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
public class AIContextProviderTests
{
[Fact]
public async Task InvokedAsync_ReturnsCompletedTaskAsync()
{
var provider = new TestAIContextProvider();
var messages = new ReadOnlyCollection<ChatMessage>([]);
var task = provider.InvokedAsync(new(messages, aiContextProviderMessages: null));
Assert.Equal(default, task);
}
[Fact]
public void Serialize_ReturnsEmptyElement()
{
var provider = new TestAIContextProvider();
var actual = provider.Serialize();
Assert.Equal(default, actual);
}
[Fact]
public void InvokingContext_Constructor_ThrowsForNullMessages()
{
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokingContext(null!));
}
[Fact]
public void InvokedContext_Constructor_ThrowsForNullMessages()
{
Assert.Throws<ArgumentNullException>(() => new AIContextProvider.InvokedContext(null!, aiContextProviderMessages: null));
}
#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
private sealed class TestAIContextProvider : AIContextProvider
{
public override ValueTask<AIContext> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
return default;
}
}
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) Microsoft. All rights reserved.
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);
Assert.Equal(2, context.Messages.Count);
Assert.Equal("Hello", context.Messages[0].Text);
Assert.Equal("Hi there!", context.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);
Assert.Equal(2, context.Tools.Count);
Assert.Equal("Function1", context.Tools[0].Name);
Assert.Equal("Function2", context.Tools[1].Name);
}
}

View File

@@ -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
}

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,349 @@
// 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),
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.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);
}
[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 },
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);
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);
}
#if NETFRAMEWORK
/// <summary>
/// Since Json Serialization using reflection is disabled in .net core builds, and we are using a custom type here that wouldn't
/// be registered with the default source generated serializer, this test will only pass in .net framework builds where reflection-based
/// serialization is available.
/// </summary>
[Fact]
public void ParseAsStructuredOutputSuccess()
{
// 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 = response.Deserialize<Animal>();
// Assert.
Assert.NotNull(animal);
Assert.Equal(expectedResult.Id, animal.Id);
Assert.Equal(expectedResult.FullName, animal.FullName);
Assert.Equal(expectedResult.Species, animal.Species);
}
#endif
[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 = response.Deserialize<Animal>(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 ParseAsStructuredOutputFailsWithEmptyString()
{
// Arrange.
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, string.Empty));
// Act & Assert.
var exception = Assert.Throws<InvalidOperationException>(() => response.Deserialize<Animal>(TestJsonSerializerContext.Default.Options));
Assert.Equal("The response did not contain JSON to be deserialized.", exception.Message);
}
[Fact]
public void ParseAsStructuredOutputFailsWithInvalidJson()
{
// Arrange.
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, "invalid json"));
// Act & Assert.
Assert.Throws<JsonException>(() => response.Deserialize<Animal>(TestJsonSerializerContext.Default.Options));
}
[Fact]
public void ParseAsStructuredOutputFailsWithIncorrectTypedJson()
{
// Arrange.
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, "[]"));
// Act & Assert.
Assert.Throws<JsonException>(() => response.Deserialize<Animal>(TestJsonSerializerContext.Default.Options));
}
#if NETFRAMEWORK
/// <summary>
/// Since Json Serialization using reflection is disabled in .net core builds, and we are using a custom type here that wouldn't
/// be registered with the default source generated serializer, this test will only pass in .net framework builds where reflection-based
/// serialization is available.
/// </summary>
[Fact]
public void TryParseAsStructuredOutputSuccess()
{
// 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.
response.TryDeserialize(out Animal? animal);
// Assert.
Assert.NotNull(animal);
Assert.Equal(expectedResult.Id, animal.Id);
Assert.Equal(expectedResult.FullName, animal.FullName);
Assert.Equal(expectedResult.Species, animal.Species);
}
#endif
[Fact]
public void TryParseAsStructuredOutputWithJSOSuccess()
{
// 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.
response.TryDeserialize(TestJsonSerializerContext.Default.Options, out Animal? animal);
// 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 TryParseAsStructuredOutputFailsWithEmptyText()
{
// Arrange.
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, string.Empty));
// Act & Assert.
Assert.False(response.TryDeserialize<Animal>(TestJsonSerializerContext.Default.Options, out _));
}
[Fact]
public void TryParseAsStructuredOutputFailsWithIncorrectTypedJson()
{
// Arrange.
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, "[]"));
// Act & Assert.
Assert.False(response.TryDeserialize<Animal>(TestJsonSerializerContext.Default.Options, out _));
}
}

View File

@@ -0,0 +1,310 @@
// 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);
}
private static async IAsyncEnumerable<AgentResponseUpdate> YieldAsync(IEnumerable<AgentResponseUpdate> updates)
{
foreach (AgentResponseUpdate update in updates)
{
await Task.Yield();
yield return update;
}
}
}

View File

@@ -0,0 +1,202 @@
// 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);
}
[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.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);
}
[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);
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
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 = new AgentRunOptions(options);
// 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 CloningConstructorThrowsIfNull() =>
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AgentRunOptions(null!));
[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());
}
}

View File

@@ -0,0 +1,139 @@
// 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="AgentThread"/>
/// </summary>
public class AgentThreadTests
{
[Fact]
public void Serialize_ReturnsDefaultJsonElement()
{
var thread = new TestAgentThread();
var result = thread.Serialize();
Assert.Equal(default, result);
}
#region GetService Method Tests
/// <summary>
/// Verify that GetService returns the thread itself when requesting the exact thread type.
/// </summary>
[Fact]
public void GetService_RequestingExactThreadType_ReturnsThread()
{
// Arrange
var thread = new TestAgentThread();
// Act
var result = thread.GetService(typeof(TestAgentThread));
// Assert
Assert.NotNull(result);
Assert.Same(thread, result);
}
/// <summary>
/// Verify that GetService returns the thread itself when requesting the base AgentThread type.
/// </summary>
[Fact]
public void GetService_RequestingAgentThreadType_ReturnsThread()
{
// Arrange
var thread = new TestAgentThread();
// Act
var result = thread.GetService(typeof(AgentThread));
// Assert
Assert.NotNull(result);
Assert.Same(thread, result);
}
/// <summary>
/// Verify that GetService returns null when requesting an unrelated type.
/// </summary>
[Fact]
public void GetService_RequestingUnrelatedType_ReturnsNull()
{
// Arrange
var thread = new TestAgentThread();
// Act
var result = thread.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 thread = new TestAgentThread();
// Act
var result = thread.GetService(typeof(TestAgentThread), "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 thread = new TestAgentThread();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => thread.GetService(null!));
}
/// <summary>
/// Verify that GetService generic method works correctly.
/// </summary>
[Fact]
public void GetService_Generic_ReturnsCorrectType()
{
// Arrange
var thread = new TestAgentThread();
// Act
var result = thread.GetService<TestAgentThread>();
// Assert
Assert.NotNull(result);
Assert.Same(thread, result);
}
/// <summary>
/// Verify that GetService generic method returns null for unrelated types.
/// </summary>
[Fact]
public void GetService_Generic_ReturnsNullForUnrelatedType()
{
// Arrange
var thread = new TestAgentThread();
// Act
var result = thread.GetService<string>();
// Assert
Assert.Null(result);
}
#endregion
private sealed class TestAgentThread : AgentThread;
}

View File

@@ -0,0 +1,205 @@
// 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;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for the <see cref="ChatMessageStoreMessageFilter"/> class.
/// </summary>
public sealed class ChatMessageStoreMessageFilterTests
{
[Fact]
public void Constructor_WithNullInnerStore_ThrowsArgumentNullException()
{
// Arrange, Act & Assert
Assert.Throws<ArgumentNullException>(() => new ChatMessageStoreMessageFilter(null!));
}
[Fact]
public void Constructor_WithOnlyInnerStore_Throws()
{
// Arrange
var innerStoreMock = new Mock<ChatMessageStore>();
// Act & Assert
Assert.Throws<ArgumentException>(() => new ChatMessageStoreMessageFilter(innerStoreMock.Object));
}
[Fact]
public void Constructor_WithAllParameters_CreatesInstance()
{
// Arrange
var innerStoreMock = new Mock<ChatMessageStore>();
IEnumerable<ChatMessage> InvokingFilter(IEnumerable<ChatMessage> msgs) => msgs;
ChatMessageStore.InvokedContext InvokedFilter(ChatMessageStore.InvokedContext ctx) => ctx;
// Act
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, InvokingFilter, InvokedFilter);
// Assert
Assert.NotNull(filter);
}
[Fact]
public async Task InvokingAsync_WithNoOpFilters_ReturnsInnerStoreMessagesAsync()
{
// Arrange
var innerStoreMock = new Mock<ChatMessageStore>();
var expectedMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!")
};
var context = new ChatMessageStore.InvokingContext([new ChatMessage(ChatRole.User, "Test")]);
innerStoreMock
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedMessages);
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, x => x, x => x);
// Act
var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList();
// Assert
Assert.Equal(2, result.Count);
Assert.Equal("Hello", result[0].Text);
Assert.Equal("Hi there!", result[1].Text);
innerStoreMock.Verify(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task InvokingAsync_WithInvokingFilter_AppliesFilterAsync()
{
// Arrange
var innerStoreMock = new Mock<ChatMessageStore>();
var innerMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!"),
new(ChatRole.User, "How are you?")
};
var context = new ChatMessageStore.InvokingContext([new ChatMessage(ChatRole.User, "Test")]);
innerStoreMock
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
.ReturnsAsync(innerMessages);
// Filter to only user messages
IEnumerable<ChatMessage> InvokingFilter(IEnumerable<ChatMessage> msgs) => msgs.Where(m => m.Role == ChatRole.User);
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, InvokingFilter);
// Act
var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList();
// Assert
Assert.Equal(2, result.Count);
Assert.All(result, msg => Assert.Equal(ChatRole.User, msg.Role));
innerStoreMock.Verify(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task InvokingAsync_WithInvokingFilter_CanModifyMessagesAsync()
{
// Arrange
var innerStoreMock = new Mock<ChatMessageStore>();
var innerMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there!")
};
var context = new ChatMessageStore.InvokingContext([new ChatMessage(ChatRole.User, "Test")]);
innerStoreMock
.Setup(s => s.InvokingAsync(context, It.IsAny<CancellationToken>()))
.ReturnsAsync(innerMessages);
// Filter that transforms messages
IEnumerable<ChatMessage> InvokingFilter(IEnumerable<ChatMessage> msgs) =>
msgs.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}"));
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, InvokingFilter);
// Act
var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList();
// Assert
Assert.Equal(2, result.Count);
Assert.Equal("[FILTERED] Hello", result[0].Text);
Assert.Equal("[FILTERED] Hi there!", result[1].Text);
}
[Fact]
public async Task InvokedAsync_WithInvokedFilter_AppliesFilterAsync()
{
// Arrange
var innerStoreMock = new Mock<ChatMessageStore>();
var requestMessages = new List<ChatMessage> { new(ChatRole.User, "Hello") };
var chatMessageStoreMessages = new List<ChatMessage> { new(ChatRole.System, "System") };
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response") };
var context = new ChatMessageStore.InvokedContext(requestMessages, chatMessageStoreMessages)
{
ResponseMessages = responseMessages
};
ChatMessageStore.InvokedContext? capturedContext = null;
innerStoreMock
.Setup(s => s.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()))
.Callback<ChatMessageStore.InvokedContext, CancellationToken>((ctx, ct) => capturedContext = ctx)
.Returns(default(ValueTask));
// Filter that modifies the context
ChatMessageStore.InvokedContext InvokedFilter(ChatMessageStore.InvokedContext ctx)
{
var modifiedRequestMessages = ctx.RequestMessages.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList();
return new ChatMessageStore.InvokedContext(modifiedRequestMessages, ctx.ChatMessageStoreMessages)
{
ResponseMessages = ctx.ResponseMessages,
AIContextProviderMessages = ctx.AIContextProviderMessages,
InvokeException = ctx.InvokeException
};
}
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, invokedMessagesFilter: InvokedFilter);
// Act
await filter.InvokedAsync(context, CancellationToken.None);
// Assert
Assert.NotNull(capturedContext);
Assert.Single(capturedContext.RequestMessages);
Assert.Equal("[FILTERED] Hello", capturedContext.RequestMessages.First().Text);
innerStoreMock.Verify(s => s.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public void Serialize_DelegatesToInnerStore()
{
// Arrange
var innerStoreMock = new Mock<ChatMessageStore>();
var expectedJson = JsonSerializer.SerializeToElement("data", TestJsonSerializerContext.Default.String);
innerStoreMock
.Setup(s => s.Serialize(It.IsAny<JsonSerializerOptions>()))
.Returns(expectedJson);
var filter = new ChatMessageStoreMessageFilter(innerStoreMock.Object, x => x, x => x);
// Act
var result = filter.Serialize();
// Assert
Assert.Equal(expectedJson.GetRawText(), result.GetRawText());
innerStoreMock.Verify(s => s.Serialize(null), Times.Once);
}
}

View File

@@ -0,0 +1,90 @@
// 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>
/// Contains tests for the <see cref="ChatMessageStore"/> class.
/// </summary>
public class ChatMessageStoreTests
{
#region GetService Method Tests
[Fact]
public void GetService_RequestingExactStoreType_ReturnsStore()
{
var store = new TestChatMessageStore();
var result = store.GetService(typeof(TestChatMessageStore));
Assert.NotNull(result);
Assert.Same(store, result);
}
[Fact]
public void GetService_RequestingBaseStoreType_ReturnsStore()
{
var store = new TestChatMessageStore();
var result = store.GetService(typeof(ChatMessageStore));
Assert.NotNull(result);
Assert.Same(store, result);
}
[Fact]
public void GetService_RequestingUnrelatedType_ReturnsNull()
{
var store = new TestChatMessageStore();
var result = store.GetService(typeof(string));
Assert.Null(result);
}
[Fact]
public void GetService_WithServiceKey_ReturnsNull()
{
var store = new TestChatMessageStore();
var result = store.GetService(typeof(TestChatMessageStore), "some-key");
Assert.Null(result);
}
[Fact]
public void GetService_WithNullServiceType_ThrowsArgumentNullException()
{
var store = new TestChatMessageStore();
Assert.Throws<ArgumentNullException>(() => store.GetService(null!));
}
[Fact]
public void GetService_Generic_ReturnsCorrectType()
{
var store = new TestChatMessageStore();
var result = store.GetService<TestChatMessageStore>();
Assert.NotNull(result);
Assert.Same(store, result);
}
[Fact]
public void GetService_Generic_ReturnsNullForUnrelatedType()
{
var store = new TestChatMessageStore();
var result = store.GetService<string>();
Assert.Null(result);
}
#endregion
private sealed class TestChatMessageStore : ChatMessageStore
{
public override ValueTask<IEnumerable<ChatMessage>> InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
=> new(Array.Empty<ChatMessage>());
public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
=> default;
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> default;
}
}

View File

@@ -0,0 +1,320 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
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 AgentThread _testThread;
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingAIAgentTests"/> class.
/// </summary>
public DelegatingAIAgentTests()
{
this._innerAgentMock = new Mock<AIAgent>();
this._testResponse = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Test response"));
this._testStreamingResponses = [new AgentResponseUpdate(ChatRole.Assistant, "Test streaming response")];
this._testThread = new TestAgentThread();
// 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.Setup(x => x.GetNewThreadAsync()).ReturnsAsync(this._testThread);
this._innerAgentMock
.Protected()
.Setup<Task<AgentResponse>>("RunCoreAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentThread?>(),
ItExpr.IsAny<AgentRunOptions?>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(this._testResponse);
this._innerAgentMock
.Protected()
.Setup<IAsyncEnumerable<AgentResponseUpdate>>("RunCoreStreamingAsync",
ItExpr.IsAny<IEnumerable<ChatMessage>>(),
ItExpr.IsAny<AgentThread?>(),
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 GetNewThreadAsync delegates to inner agent.
/// </summary>
[Fact]
public async Task GetNewThreadAsync_DelegatesToInnerAgentAsync()
{
// Act
var thread = await this._delegatingAgent.GetNewThreadAsync();
// Assert
Assert.Same(this._testThread, thread);
this._innerAgentMock.Verify(x => x.GetNewThreadAsync(), Times.Once);
}
/// <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 expectedThread = new TestAgentThread();
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<AgentThread?>(t => t == expectedThread),
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, expectedThread, 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 expectedThread = new TestAgentThread();
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<AgentThread?>(t => t == expectedThread),
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, expectedThread, 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 TestAgentThread : AgentThread;
#endregion
}

View File

@@ -0,0 +1,155 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Contains tests for <see cref="InMemoryAgentThread"/>.
/// </summary>
public class InMemoryAgentThreadTests
{
#region Constructor and Property Tests
[Fact]
public void Constructor_SetsDefaultMessageStore()
{
// Arrange & Act
var thread = new TestInMemoryAgentThread();
// Assert
Assert.NotNull(thread.GetMessageStore());
Assert.Empty(thread.GetMessageStore());
}
[Fact]
public void Constructor_WithMessageStore_SetsProperty()
{
// Arrange
InMemoryChatMessageStore store = [new(ChatRole.User, "Hello")];
// Act
var thread = new TestInMemoryAgentThread(store);
// Assert
Assert.Same(store, thread.GetMessageStore());
Assert.Single(thread.GetMessageStore());
Assert.Equal("Hello", thread.GetMessageStore()[0].Text);
}
[Fact]
public void Constructor_WithMessages_SetsProperty()
{
// Arrange
var messages = new List<ChatMessage> { new(ChatRole.User, "Hi") };
// Act
var thread = new TestInMemoryAgentThread(messages);
// Assert
Assert.NotNull(thread.GetMessageStore());
Assert.Single(thread.GetMessageStore());
Assert.Equal("Hi", thread.GetMessageStore()[0].Text);
}
[Fact]
public void Constructor_WithSerializedState_SetsProperty()
{
// Arrange
InMemoryChatMessageStore store = [new(ChatRole.User, "TestMsg")];
var storeState = store.Serialize();
var threadStateWrapper = new InMemoryAgentThread.InMemoryAgentThreadState { StoreState = storeState };
var json = JsonSerializer.SerializeToElement(threadStateWrapper, TestJsonSerializerContext.Default.InMemoryAgentThreadState);
// Act
var thread = new TestInMemoryAgentThread(json);
// Assert
Assert.NotNull(thread.GetMessageStore());
Assert.Single(thread.GetMessageStore());
Assert.Equal("TestMsg", thread.GetMessageStore()[0].Text);
}
[Fact]
public void Constructor_WithInvalidJson_ThrowsArgumentException()
{
// Arrange
var invalidJson = JsonSerializer.SerializeToElement(42, TestJsonSerializerContext.Default.Int32);
// Act & Assert
Assert.Throws<ArgumentException>(() => new TestInMemoryAgentThread(invalidJson));
}
#endregion
#region SerializeAsync Tests
[Fact]
public void Serialize_ReturnsCorrectJson_WhenMessagesExist()
{
// Arrange
var thread = new TestInMemoryAgentThread([new(ChatRole.User, "TestContent")]);
// Act
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.True(json.TryGetProperty("storeState", out var storeStateProperty));
Assert.Equal(JsonValueKind.Object, storeStateProperty.ValueKind);
Assert.True(storeStateProperty.TryGetProperty("messages", out var messagesProperty));
Assert.Equal(JsonValueKind.Array, messagesProperty.ValueKind);
var messagesList = messagesProperty.EnumerateArray().ToList();
Assert.Single(messagesList);
}
[Fact]
public void Serialize_ReturnsEmptyMessages_WhenNoMessages()
{
// Arrange
var thread = new TestInMemoryAgentThread();
// Act
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.True(json.TryGetProperty("storeState", out var storeStateProperty));
Assert.Equal(JsonValueKind.Object, storeStateProperty.ValueKind);
Assert.True(storeStateProperty.TryGetProperty("messages", out var messagesProperty));
Assert.Equal(JsonValueKind.Array, messagesProperty.ValueKind);
Assert.Empty(messagesProperty.EnumerateArray());
}
#endregion
#region GetService Tests
[Fact]
public void GetService_RequestingChatMessageStore_ReturnsChatMessageStore()
{
// Arrange
var thread = new TestInMemoryAgentThread();
// Act & Assert
Assert.NotNull(thread.GetService(typeof(ChatMessageStore)));
Assert.Same(thread.GetMessageStore(), thread.GetService(typeof(ChatMessageStore)));
Assert.Same(thread.GetMessageStore(), thread.GetService(typeof(InMemoryChatMessageStore)));
}
#endregion
// Sealed test subclass to expose protected members for testing
private sealed class TestInMemoryAgentThread : InMemoryAgentThread
{
public TestInMemoryAgentThread() { }
public TestInMemoryAgentThread(InMemoryChatMessageStore? store) : base(store) { }
public TestInMemoryAgentThread(IEnumerable<ChatMessage> messages) : base(messages) { }
public TestInMemoryAgentThread(JsonElement serializedThreadState) : base(serializedThreadState) { }
public InMemoryChatMessageStore GetMessageStore() => this.MessageStore;
}
}

View File

@@ -0,0 +1,621 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
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="InMemoryChatMessageStore"/> class.
/// </summary>
public class InMemoryChatMessageStoreTests
{
[Fact]
public void Constructor_Throws_ForNullReducer() =>
// Arrange & Act & Assert
Assert.Throws<ArgumentNullException>(() => new InMemoryChatMessageStore(null!));
[Fact]
public void Constructor_DefaultsToBeforeMessageRetrieval_ForNotProvidedTriggerEvent()
{
// Arrange & Act
var reducerMock = new Mock<IChatReducer>();
var store = new InMemoryChatMessageStore(reducerMock.Object);
// Assert
Assert.Equal(InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval, store.ReducerTriggerEvent);
}
[Fact]
public void Constructor_Arguments_SetOnPropertiesCorrectly()
{
// Arrange & Act
var reducerMock = new Mock<IChatReducer>();
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded);
// Assert
Assert.Same(reducerMock.Object, store.ChatReducer);
Assert.Equal(InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded, store.ReducerTriggerEvent);
}
[Fact]
public async Task InvokedAsyncAddsMessagesAsync()
{
var requestMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello")
};
var responseMessages = new List<ChatMessage>
{
new(ChatRole.Assistant, "Hi there!")
};
var messageStoreMessages = new List<ChatMessage>()
{
new(ChatRole.System, "original instructions")
};
var aiContextProviderMessages = new List<ChatMessage>()
{
new(ChatRole.System, "additional context")
};
var store = new InMemoryChatMessageStore();
store.Add(messageStoreMessages[0]);
var context = new ChatMessageStore.InvokedContext(requestMessages, messageStoreMessages)
{
AIContextProviderMessages = aiContextProviderMessages,
ResponseMessages = responseMessages
};
await store.InvokedAsync(context, CancellationToken.None);
Assert.Equal(4, store.Count);
Assert.Equal("original instructions", store[0].Text);
Assert.Equal("Hello", store[1].Text);
Assert.Equal("additional context", store[2].Text);
Assert.Equal("Hi there!", store[3].Text);
}
[Fact]
public async Task InvokedAsyncWithEmptyDoesNotFailAsync()
{
var store = new InMemoryChatMessageStore();
var context = new ChatMessageStore.InvokedContext([], []);
await store.InvokedAsync(context, CancellationToken.None);
Assert.Empty(store);
}
[Fact]
public async Task InvokingAsyncReturnsAllMessagesAsync()
{
var store = new InMemoryChatMessageStore
{
new ChatMessage(ChatRole.User, "Test1"),
new ChatMessage(ChatRole.Assistant, "Test2")
};
var context = new ChatMessageStore.InvokingContext([]);
var result = (await store.InvokingAsync(context, CancellationToken.None)).ToList();
Assert.Equal(2, result.Count);
Assert.Contains(result, m => m.Text == "Test1");
Assert.Contains(result, m => m.Text == "Test2");
}
[Fact]
public async Task DeserializeConstructorWithEmptyElementAsync()
{
var emptyObject = JsonSerializer.Deserialize("{}", TestJsonSerializerContext.Default.JsonElement);
var newStore = new InMemoryChatMessageStore(emptyObject);
Assert.Empty(newStore);
}
[Fact]
public async Task SerializeAndDeserializeConstructorRoundtripsAsync()
{
var store = new InMemoryChatMessageStore
{
new ChatMessage(ChatRole.User, "A"),
new ChatMessage(ChatRole.Assistant, "B")
};
var jsonElement = store.Serialize();
var newStore = new InMemoryChatMessageStore(jsonElement);
Assert.Equal(2, newStore.Count);
Assert.Equal("A", newStore[0].Text);
Assert.Equal("B", newStore[1].Text);
}
[Fact]
public async Task SerializeAndDeserializeConstructorRoundtripsWithCustomAIContentAsync()
{
JsonSerializerOptions options = new(TestJsonSerializerContext.Default.Options)
{
TypeInfoResolver = JsonTypeInfoResolver.Combine(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver, TestJsonSerializerContext.Default),
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
options.AddAIContentType<TestAIContent>(typeDiscriminatorId: "testContent");
var store = new InMemoryChatMessageStore
{
new ChatMessage(ChatRole.User, [new TestAIContent("foo data")]),
};
var jsonElement = store.Serialize(options);
var newStore = new InMemoryChatMessageStore(jsonElement, options);
Assert.Single(newStore);
var actualTestAIContent = Assert.IsType<TestAIContent>(newStore[0].Contents[0]);
Assert.Equal("foo data", actualTestAIContent.TestData);
}
[Fact]
public async Task SerializeAndDeserializeWorksWithExperimentalContentTypesAsync()
{
var store = new InMemoryChatMessageStore
{
new ChatMessage(ChatRole.User, [new FunctionApprovalRequestContent("call123", new FunctionCallContent("call123", "some_func"))]),
new ChatMessage(ChatRole.Assistant, [new FunctionApprovalResponseContent("call123", true, new FunctionCallContent("call123", "some_func"))])
};
var jsonElement = store.Serialize();
var newStore = new InMemoryChatMessageStore(jsonElement);
Assert.Equal(2, newStore.Count);
Assert.IsType<FunctionApprovalRequestContent>(newStore[0].Contents[0]);
Assert.IsType<FunctionApprovalResponseContent>(newStore[1].Contents[0]);
}
[Fact]
public async Task InvokedAsyncWithEmptyMessagesDoesNotChangeStoreAsync()
{
var store = new InMemoryChatMessageStore();
var messages = new List<ChatMessage>();
var context = new ChatMessageStore.InvokedContext(messages, []);
await store.InvokedAsync(context, CancellationToken.None);
Assert.Empty(store);
}
[Fact]
public async Task InvokedAsync_WithNullContext_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var store = new InMemoryChatMessageStore();
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => store.InvokedAsync(null!, CancellationToken.None).AsTask());
}
[Fact]
public void DeserializeContructor_WithNullSerializedState_CreatesEmptyStore()
{
// Act
var store = new InMemoryChatMessageStore(new JsonElement());
// Assert
Assert.Empty(store);
}
[Fact]
public async Task DeserializeContructor_WithEmptyMessages_DoesNotAddMessagesAsync()
{
// Arrange
var stateWithEmptyMessages = JsonSerializer.SerializeToElement(
new Dictionary<string, object> { ["messages"] = new List<ChatMessage>() },
TestJsonSerializerContext.Default.IDictionaryStringObject);
// Act
var store = new InMemoryChatMessageStore(stateWithEmptyMessages);
// Assert
Assert.Empty(store);
}
[Fact]
public async Task DeserializeConstructor_WithNullMessages_DoesNotAddMessagesAsync()
{
// Arrange
var stateWithNullMessages = JsonSerializer.SerializeToElement(
new Dictionary<string, object> { ["messages"] = null! },
TestJsonSerializerContext.Default.DictionaryStringObject);
// Act
var store = new InMemoryChatMessageStore(stateWithNullMessages);
// Assert
Assert.Empty(store);
}
[Fact]
public async Task DeserializeConstructor_WithValidMessages_AddsMessagesAsync()
{
// Arrange
var messages = new List<ChatMessage>
{
new(ChatRole.User, "User message"),
new(ChatRole.Assistant, "Assistant message")
};
var state = new Dictionary<string, object> { ["messages"] = messages };
var serializedState = JsonSerializer.SerializeToElement(
state,
TestJsonSerializerContext.Default.DictionaryStringObject);
// Act
var store = new InMemoryChatMessageStore(serializedState);
// Assert
Assert.Equal(2, store.Count);
Assert.Equal("User message", store[0].Text);
Assert.Equal("Assistant message", store[1].Text);
}
[Fact]
public void IndexerGet_ReturnsCorrectMessage()
{
// Arrange
var store = new InMemoryChatMessageStore();
var message1 = new ChatMessage(ChatRole.User, "First");
var message2 = new ChatMessage(ChatRole.Assistant, "Second");
store.Add(message1);
store.Add(message2);
// Act & Assert
Assert.Same(message1, store[0]);
Assert.Same(message2, store[1]);
}
[Fact]
public void IndexerSet_UpdatesMessage()
{
// Arrange
var store = new InMemoryChatMessageStore();
var originalMessage = new ChatMessage(ChatRole.User, "Original");
var newMessage = new ChatMessage(ChatRole.User, "Updated");
store.Add(originalMessage);
// Act
store[0] = newMessage;
// Assert
Assert.Same(newMessage, store[0]);
Assert.Equal("Updated", store[0].Text);
}
[Fact]
public void IsReadOnly_ReturnsFalse()
{
// Arrange
var store = new InMemoryChatMessageStore();
// Act & Assert
Assert.False(store.IsReadOnly);
}
[Fact]
public void IndexOf_ReturnsCorrectIndex()
{
// Arrange
var store = new InMemoryChatMessageStore();
var message1 = new ChatMessage(ChatRole.User, "First");
var message2 = new ChatMessage(ChatRole.Assistant, "Second");
var message3 = new ChatMessage(ChatRole.User, "Third");
store.Add(message1);
store.Add(message2);
// Act & Assert
Assert.Equal(0, store.IndexOf(message1));
Assert.Equal(1, store.IndexOf(message2));
Assert.Equal(-1, store.IndexOf(message3)); // Not in store
}
[Fact]
public void Insert_InsertsMessageAtCorrectIndex()
{
// Arrange
var store = new InMemoryChatMessageStore();
var message1 = new ChatMessage(ChatRole.User, "First");
var message2 = new ChatMessage(ChatRole.Assistant, "Second");
var insertMessage = new ChatMessage(ChatRole.User, "Inserted");
store.Add(message1);
store.Add(message2);
// Act
store.Insert(1, insertMessage);
// Assert
Assert.Equal(3, store.Count);
Assert.Same(message1, store[0]);
Assert.Same(insertMessage, store[1]);
Assert.Same(message2, store[2]);
}
[Fact]
public void RemoveAt_RemovesMessageAtIndex()
{
// Arrange
var store = new InMemoryChatMessageStore();
var message1 = new ChatMessage(ChatRole.User, "First");
var message2 = new ChatMessage(ChatRole.Assistant, "Second");
var message3 = new ChatMessage(ChatRole.User, "Third");
store.Add(message1);
store.Add(message2);
store.Add(message3);
// Act
store.RemoveAt(1);
// Assert
Assert.Equal(2, store.Count);
Assert.Same(message1, store[0]);
Assert.Same(message3, store[1]);
}
[Fact]
public void Clear_RemovesAllMessages()
{
// Arrange
var store = new InMemoryChatMessageStore
{
new ChatMessage(ChatRole.User, "First"),
new ChatMessage(ChatRole.Assistant, "Second")
};
// Act
store.Clear();
// Assert
Assert.Empty(store);
}
[Fact]
public void Contains_ReturnsTrueForExistingMessage()
{
// Arrange
var store = new InMemoryChatMessageStore();
var message1 = new ChatMessage(ChatRole.User, "First");
var message2 = new ChatMessage(ChatRole.Assistant, "Second");
store.Add(message1);
// Act & Assert
Assert.Contains(message1, store);
Assert.DoesNotContain(message2, store);
}
[Fact]
public void CopyTo_CopiesMessagesToArray()
{
// Arrange
var store = new InMemoryChatMessageStore();
var message1 = new ChatMessage(ChatRole.User, "First");
var message2 = new ChatMessage(ChatRole.Assistant, "Second");
store.Add(message1);
store.Add(message2);
var array = new ChatMessage[4];
// Act
store.CopyTo(array, 1);
// Assert
Assert.Null(array[0]);
Assert.Same(message1, array[1]);
Assert.Same(message2, array[2]);
Assert.Null(array[3]);
}
[Fact]
public void Remove_RemovesSpecificMessage()
{
// Arrange
var store = new InMemoryChatMessageStore();
var message1 = new ChatMessage(ChatRole.User, "First");
var message2 = new ChatMessage(ChatRole.Assistant, "Second");
var message3 = new ChatMessage(ChatRole.User, "Third");
store.Add(message1);
store.Add(message2);
store.Add(message3);
// Act
var removed = store.Remove(message2);
// Assert
Assert.True(removed);
Assert.Equal(2, store.Count);
Assert.Same(message1, store[0]);
Assert.Same(message3, store[1]);
}
[Fact]
public void Remove_ReturnsFalseForNonExistentMessage()
{
// Arrange
var store = new InMemoryChatMessageStore();
var message1 = new ChatMessage(ChatRole.User, "First");
var message2 = new ChatMessage(ChatRole.Assistant, "Second");
store.Add(message1);
// Act
var removed = store.Remove(message2);
// Assert
Assert.False(removed);
Assert.Single(store);
}
[Fact]
public void GetEnumerator_Generic_ReturnsAllMessages()
{
// Arrange
var store = new InMemoryChatMessageStore();
var message1 = new ChatMessage(ChatRole.User, "First");
var message2 = new ChatMessage(ChatRole.Assistant, "Second");
store.Add(message1);
store.Add(message2);
// Act
var messages = new List<ChatMessage>();
messages.AddRange(store);
// Assert
Assert.Equal(2, messages.Count);
Assert.Same(message1, messages[0]);
Assert.Same(message2, messages[1]);
}
[Fact]
public void GetEnumerator_NonGeneric_ReturnsAllMessages()
{
// Arrange
var store = new InMemoryChatMessageStore();
var message1 = new ChatMessage(ChatRole.User, "First");
var message2 = new ChatMessage(ChatRole.Assistant, "Second");
store.Add(message1);
store.Add(message2);
// Act
var messages = new List<ChatMessage>();
var enumerator = ((System.Collections.IEnumerable)store).GetEnumerator();
while (enumerator.MoveNext())
{
messages.Add((ChatMessage)enumerator.Current);
}
// Assert
Assert.Equal(2, messages.Count);
Assert.Same(message1, messages[0]);
Assert.Same(message2, messages[1]);
}
[Fact]
public async Task AddMessagesAsync_WithReducer_AfterMessageAdded_InvokesReducerAsync()
{
// 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 store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded);
// Act
var context = new ChatMessageStore.InvokedContext(originalMessages, []);
await store.InvokedAsync(context, CancellationToken.None);
// Assert
Assert.Single(store);
Assert.Equal("Reduced", store[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()
{
// 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 store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval);
// Add messages directly to the store for this test
foreach (var msg in originalMessages)
{
store.Add(msg);
}
// Act
var invokingContext = new ChatMessageStore.InvokingContext(Array.Empty<ChatMessage>());
var result = (await store.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()
{
// Arrange
var originalMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello")
};
var reducerMock = new Mock<IChatReducer>();
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval);
// Act
var context = new ChatMessageStore.InvokedContext(originalMessages, []);
await store.InvokedAsync(context, CancellationToken.None);
// Assert
Assert.Single(store);
Assert.Equal("Hello", store[0].Text);
reducerMock.Verify(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task GetMessagesAsync_WithReducer_ButWrongTrigger_DoesNotInvokeReducerAsync()
{
// Arrange
var originalMessages = new List<ChatMessage>
{
new(ChatRole.User, "Hello")
};
var reducerMock = new Mock<IChatReducer>();
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded)
{
originalMessages[0]
};
// Act
var invokingContext = new ChatMessageStore.InvokingContext(Array.Empty<ChatMessage>());
var result = (await store.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);
}
public class TestAIContent(string testData) : AIContent
{
public string TestData => testData;
}
}

View File

@@ -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>

View File

@@ -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; }
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Models;
internal enum Species
{
Bear,
Tiger,
Walrus,
}

View File

@@ -0,0 +1,119 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
/// <summary>
/// Tests for <see cref="ServiceIdAgentThread"/>.
/// </summary>
public class ServiceIdAgentThreadTests
{
#region Constructor and Property Tests
[Fact]
public void Constructor_SetsDefaults()
{
// Arrange & Act
var thread = new TestServiceIdAgentThread();
// Assert
Assert.Null(thread.GetServiceThreadId());
}
[Fact]
public void Constructor_WithServiceThreadId_SetsProperty()
{
// Arrange & Act
var thread = new TestServiceIdAgentThread("service-id-123");
// Assert
Assert.Equal("service-id-123", thread.GetServiceThreadId());
}
[Fact]
public void Constructor_WithSerializedId_SetsProperty()
{
// Arrange
var serviceThreadWrapper = new ServiceIdAgentThread.ServiceIdAgentThreadState { ServiceThreadId = "service-id-456" };
var json = JsonSerializer.SerializeToElement(serviceThreadWrapper, TestJsonSerializerContext.Default.ServiceIdAgentThreadState);
// Act
var thread = new TestServiceIdAgentThread(json);
// Assert
Assert.Equal("service-id-456", thread.GetServiceThreadId());
}
[Fact]
public void Constructor_WithSerializedUndefinedId_SetsProperty()
{
// Arrange
var emptyObject = new EmptyObject();
var json = JsonSerializer.SerializeToElement(emptyObject, TestJsonSerializerContext.Default.EmptyObject);
// Act
var thread = new TestServiceIdAgentThread(json);
// Assert
Assert.Null(thread.GetServiceThreadId());
}
[Fact]
public void Constructor_WithInvalidJson_ThrowsArgumentException()
{
// Arrange
var invalidJson = JsonSerializer.SerializeToElement(42, TestJsonSerializerContext.Default.Int32);
// Act & Assert
Assert.Throws<ArgumentException>(() => new TestServiceIdAgentThread(invalidJson));
}
#endregion
#region SerializeAsync Tests
[Fact]
public void Serialize_ReturnsCorrectJson_WhenServiceThreadIdIsSet()
{
// Arrange
var thread = new TestServiceIdAgentThread("service-id-789");
// Act
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.True(json.TryGetProperty("serviceThreadId", out var idProperty));
Assert.Equal("service-id-789", idProperty.GetString());
}
[Fact]
public void Serialize_ReturnsUndefinedServiceThreadId_WhenNotSet()
{
// Arrange
var thread = new TestServiceIdAgentThread();
// Act
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.False(json.TryGetProperty("serviceThreadId", out _));
}
#endregion
// Sealed test subclass to expose protected members for testing
private sealed class TestServiceIdAgentThread : ServiceIdAgentThread
{
public TestServiceIdAgentThread() { }
public TestServiceIdAgentThread(string serviceThreadId) : base(serviceThreadId) { }
public TestServiceIdAgentThread(JsonElement serializedThreadState) : base(serializedThreadState) { }
public string? GetServiceThreadId() => this.ServiceThreadId;
}
// Helper class to represent empty objects
internal sealed class EmptyObject;
}

View File

@@ -0,0 +1,26 @@
// 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(JsonElement))]
[JsonSerializable(typeof(Dictionary<string, object?>))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(InMemoryAgentThread.InMemoryAgentThreadState))]
[JsonSerializable(typeof(ServiceIdAgentThread.ServiceIdAgentThreadState))]
[JsonSerializable(typeof(ServiceIdAgentThreadTests.EmptyObject))]
[JsonSerializable(typeof(InMemoryChatMessageStoreTests.TestAIContent))]
internal sealed partial class TestJsonSerializerContext : JsonSerializerContext;