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
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="AdditionalPropertiesExtensions"/> class.
|
||||
/// </summary>
|
||||
public sealed class AdditionalPropertiesExtensionsTests
|
||||
{
|
||||
#region Add Method Tests
|
||||
|
||||
[Fact]
|
||||
public void Add_WithValidValue_StoresValueUsingTypeName()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass value = new() { Name = "Test" };
|
||||
|
||||
// Act
|
||||
additionalProperties.Add(value);
|
||||
|
||||
// Assert
|
||||
Assert.True(additionalProperties.ContainsKey(typeof(TestClass).FullName!));
|
||||
Assert.Same(value, additionalProperties[typeof(TestClass).FullName!]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithNullDictionary_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary? additionalProperties = null;
|
||||
TestClass value = new() { Name = "Test" };
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => additionalProperties!.Add(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithStringValue_StoresValueCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
const string Value = "test string";
|
||||
|
||||
// Act
|
||||
additionalProperties.Add(Value);
|
||||
|
||||
// Assert
|
||||
Assert.True(additionalProperties.ContainsKey(typeof(string).FullName!));
|
||||
Assert.Equal(Value, additionalProperties[typeof(string).FullName!]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithIntValue_StoresValueCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
const int Value = 42;
|
||||
|
||||
// Act
|
||||
additionalProperties.Add(Value);
|
||||
|
||||
// Assert
|
||||
Assert.True(additionalProperties.ContainsKey(typeof(int).FullName!));
|
||||
Assert.Equal(Value, additionalProperties[typeof(int).FullName!]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_ThrowsArgumentException_WhenSameTypeAddedTwice()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass firstValue = new() { Name = "First" };
|
||||
TestClass secondValue = new() { Name = "Second" };
|
||||
additionalProperties.Add(firstValue);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() => additionalProperties.Add(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithMultipleDifferentTypes_StoresAllValues()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass testClassValue = new() { Name = "Test" };
|
||||
AnotherTestClass anotherValue = new() { Id = 123 };
|
||||
const string StringValue = "test";
|
||||
|
||||
// Act
|
||||
additionalProperties.Add(testClassValue);
|
||||
additionalProperties.Add(anotherValue);
|
||||
additionalProperties.Add(StringValue);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, additionalProperties.Count);
|
||||
Assert.Same(testClassValue, additionalProperties[typeof(TestClass).FullName!]);
|
||||
Assert.Same(anotherValue, additionalProperties[typeof(AnotherTestClass).FullName!]);
|
||||
Assert.Equal(StringValue, additionalProperties[typeof(string).FullName!]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryAdd Method Tests
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_WithValidValue_ReturnsTrueAndStoresValue()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass value = new() { Name = "Test" };
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.TryAdd(value);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.True(additionalProperties.ContainsKey(typeof(TestClass).FullName!));
|
||||
Assert.Same(value, additionalProperties[typeof(TestClass).FullName!]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_WithNullDictionary_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary? additionalProperties = null;
|
||||
TestClass value = new() { Name = "Test" };
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => additionalProperties!.TryAdd(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_WithExistingType_ReturnsFalseAndKeepsOriginalValue()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass firstValue = new() { Name = "First" };
|
||||
TestClass secondValue = new() { Name = "Second" };
|
||||
additionalProperties.Add(firstValue);
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.TryAdd(secondValue);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Single(additionalProperties);
|
||||
Assert.Same(firstValue, additionalProperties[typeof(TestClass).FullName!]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_WithStringValue_ReturnsTrueAndStoresValue()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
const string Value = "test string";
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.TryAdd(Value);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.True(additionalProperties.ContainsKey(typeof(string).FullName!));
|
||||
Assert.Equal(Value, additionalProperties[typeof(string).FullName!]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_WithIntValue_ReturnsTrueAndStoresValue()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
const int Value = 42;
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.TryAdd(Value);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.True(additionalProperties.ContainsKey(typeof(int).FullName!));
|
||||
Assert.Equal(Value, additionalProperties[typeof(int).FullName!]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryAdd_WithMultipleDifferentTypes_StoresAllValues()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass testClassValue = new() { Name = "Test" };
|
||||
AnotherTestClass anotherValue = new() { Id = 123 };
|
||||
const string StringValue = "test";
|
||||
|
||||
// Act
|
||||
bool result1 = additionalProperties.TryAdd(testClassValue);
|
||||
bool result2 = additionalProperties.TryAdd(anotherValue);
|
||||
bool result3 = additionalProperties.TryAdd(StringValue);
|
||||
|
||||
// Assert
|
||||
Assert.True(result1);
|
||||
Assert.True(result2);
|
||||
Assert.True(result3);
|
||||
Assert.Equal(3, additionalProperties.Count);
|
||||
Assert.Same(testClassValue, additionalProperties[typeof(TestClass).FullName!]);
|
||||
Assert.Same(anotherValue, additionalProperties[typeof(AnotherTestClass).FullName!]);
|
||||
Assert.Equal(StringValue, additionalProperties[typeof(string).FullName!]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TryGetValue Method Tests
|
||||
|
||||
[Fact]
|
||||
public void TryGetValue_WithExistingValue_ReturnsTrueAndValue()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass expectedValue = new() { Name = "Test" };
|
||||
additionalProperties.Add(expectedValue);
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.TryGetValue(out TestClass? actualValue);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.NotNull(actualValue);
|
||||
Assert.Same(expectedValue, actualValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetValue_WithNonExistingValue_ReturnsFalseAndNull()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.TryGetValue(out TestClass? actualValue);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Null(actualValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetValue_WithNullDictionary_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary? additionalProperties = null;
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => additionalProperties!.TryGetValue<TestClass>(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetValue_WithStringValue_ReturnsCorrectValue()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
const string ExpectedValue = "test string";
|
||||
additionalProperties.Add(ExpectedValue);
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.TryGetValue(out string? actualValue);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Equal(ExpectedValue, actualValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetValue_WithIntValue_ReturnsCorrectValue()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
const int ExpectedValue = 42;
|
||||
additionalProperties.Add(ExpectedValue);
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.TryGetValue(out int actualValue);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Equal(ExpectedValue, actualValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetValue_WithWrongType_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass testValue = new() { Name = "Test" };
|
||||
additionalProperties.Add(testValue);
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.TryGetValue(out AnotherTestClass? actualValue);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Null(actualValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetValue_AfterTryAddFails_ReturnsOriginalValue()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass firstValue = new() { Name = "First" };
|
||||
TestClass secondValue = new() { Name = "Second" };
|
||||
additionalProperties.Add(firstValue);
|
||||
additionalProperties.TryAdd(secondValue);
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.TryGetValue(out TestClass? actualValue);
|
||||
|
||||
// Assert
|
||||
Assert.Single(additionalProperties);
|
||||
Assert.True(result);
|
||||
Assert.Same(firstValue, actualValue);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Contains Method Tests
|
||||
|
||||
[Fact]
|
||||
public void Contains_WithExistingType_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass value = new() { Name = "Test" };
|
||||
additionalProperties.Add(value);
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.Contains<TestClass>();
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_WithNonExistingType_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.Contains<TestClass>();
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_WithNullDictionary_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary? additionalProperties = null;
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => additionalProperties!.Contains<TestClass>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_WithDifferentType_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass value = new() { Name = "Test" };
|
||||
additionalProperties.Add(value);
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.Contains<AnotherTestClass>();
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Contains_AfterRemove_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass value = new() { Name = "Test" };
|
||||
additionalProperties.Add(value);
|
||||
additionalProperties.Remove<TestClass>();
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.Contains<TestClass>();
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Remove Method Tests
|
||||
|
||||
[Fact]
|
||||
public void Remove_WithExistingType_ReturnsTrueAndRemovesValue()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass value = new() { Name = "Test" };
|
||||
additionalProperties.Add(value);
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.Remove<TestClass>();
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Empty(additionalProperties);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_WithNonExistingType_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.Remove<TestClass>();
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_WithNullDictionary_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary? additionalProperties = null;
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => additionalProperties!.Remove<TestClass>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_OnlyRemovesSpecifiedType()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass testValue = new() { Name = "Test" };
|
||||
AnotherTestClass anotherValue = new() { Id = 123 };
|
||||
additionalProperties.Add(testValue);
|
||||
additionalProperties.Add(anotherValue);
|
||||
|
||||
// Act
|
||||
bool result = additionalProperties.Remove<TestClass>();
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Single(additionalProperties);
|
||||
Assert.False(additionalProperties.Contains<TestClass>());
|
||||
Assert.True(additionalProperties.Contains<AnotherTestClass>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_CalledTwice_ReturnsFalseOnSecondCall()
|
||||
{
|
||||
// Arrange
|
||||
AdditionalPropertiesDictionary additionalProperties = new();
|
||||
TestClass value = new() { Name = "Test" };
|
||||
additionalProperties.Add(value);
|
||||
|
||||
// Act
|
||||
bool firstResult = additionalProperties.Remove<TestClass>();
|
||||
bool secondResult = additionalProperties.Remove<TestClass>();
|
||||
|
||||
// Assert
|
||||
Assert.True(firstResult);
|
||||
Assert.False(secondResult);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Helper Classes
|
||||
|
||||
private sealed class TestClass
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class AnotherTestClass
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="AgentAbstractionsJsonUtilities"/>
|
||||
/// </summary>
|
||||
public class AgentAbstractionsJsonUtilitiesTests
|
||||
{
|
||||
[Fact]
|
||||
public void DefaultOptions_HasExpectedConfiguration()
|
||||
{
|
||||
var options = AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
|
||||
// Must be read-only singleton.
|
||||
Assert.NotNull(options);
|
||||
Assert.Same(options, AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
Assert.True(options.IsReadOnly);
|
||||
|
||||
// Must conform to JsonSerializerDefaults.Web
|
||||
Assert.Equal(JsonNamingPolicy.CamelCase, options.PropertyNamingPolicy);
|
||||
Assert.True(options.PropertyNameCaseInsensitive);
|
||||
Assert.Equal(JsonNumberHandling.AllowReadingFromString, options.NumberHandling);
|
||||
|
||||
// Additional settings
|
||||
Assert.Equal(JsonIgnoreCondition.WhenWritingNull, options.DefaultIgnoreCondition);
|
||||
Assert.Same(JavaScriptEncoder.UnsafeRelaxedJsonEscaping, options.Encoder);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("<script>alert('XSS')</script>", "<script>alert('XSS')</script>")]
|
||||
[InlineData("""{"forecast":"sunny", "temperature":"75"}""", """{\"forecast\":\"sunny\", \"temperature\":\"75\"}""")]
|
||||
[InlineData("""{"message":"Πάντα ῥεῖ."}""", """{\"message\":\"Πάντα ῥεῖ.\"}""")]
|
||||
[InlineData("""{"message":"七転び八起き"}""", """{\"message\":\"七転び八起き\"}""")]
|
||||
[InlineData("""☺️🤖🌍𝄞""", """☺️\uD83E\uDD16\uD83C\uDF0D\uD834\uDD1E""")]
|
||||
public void DefaultOptions_UsesExpectedEscaping(string input, string expectedJsonString)
|
||||
{
|
||||
var options = AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
string json = JsonSerializer.Serialize(input, options);
|
||||
Assert.Equal($@"""{expectedJsonString}""", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultOptions_UsesReflectionWhenDefault()
|
||||
{
|
||||
Type anonType = new { Name = 42 }.GetType();
|
||||
Assert.Equal(JsonSerializer.IsReflectionEnabledByDefault, AgentAbstractionsJsonUtilities.DefaultOptions.TryGetTypeInfo(anonType, out _));
|
||||
}
|
||||
|
||||
// The following two tests validate behaviors of reflection-based serialization
|
||||
// which is only available in .NET Framework builds.
|
||||
#if NETFRAMEWORK
|
||||
[Fact]
|
||||
public void DefaultOptions_AllowsReadingNumbersFromStrings_AndOmitsNulls()
|
||||
{
|
||||
var obj = JsonSerializer.Deserialize<NumberContainer>(
|
||||
"{\"value\":\"42\",\"optional\":null}", // value as string, optional null
|
||||
AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
Assert.NotNull(obj);
|
||||
Assert.Equal(42, obj!.Value);
|
||||
Assert.Null(obj.Optional);
|
||||
Assert.Equal("{\"value\":42}",
|
||||
JsonSerializer.Serialize(obj, AgentAbstractionsJsonUtilities.DefaultOptions)); // null omitted
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultOptions_SerializesEnumsAsStrings()
|
||||
{
|
||||
Assert.Equal("\"Monday\"", JsonSerializer.Serialize(DayOfWeek.Monday, AgentAbstractionsJsonUtilities.DefaultOptions));
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public void DefaultOptions_UsesCamelCasePropertyNames_ForAgentResponse()
|
||||
{
|
||||
var response = new AgentResponse(new ChatMessage(ChatRole.Assistant, "Hello"));
|
||||
string json = JsonSerializer.Serialize(response, AgentAbstractionsJsonUtilities.DefaultOptions);
|
||||
Assert.Contains("\"messages\"", json);
|
||||
Assert.DoesNotContain("\"Messages\"", json);
|
||||
}
|
||||
|
||||
private sealed class NumberContainer
|
||||
{
|
||||
public int Value { get; set; }
|
||||
public string? Optional { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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 _));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);MEAI001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
|
||||
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Models;
|
||||
|
||||
[Description("Some test description")]
|
||||
internal sealed class Animal
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? FullName { get; set; }
|
||||
public Species Species { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Models;
|
||||
|
||||
internal enum Species
|
||||
{
|
||||
Bear,
|
||||
Tiger,
|
||||
Walrus,
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user