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

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

View File

@@ -0,0 +1,128 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.UnitTests;
public class ChatClientAgentContinuationTokenTests
{
[Fact]
public void ToBytes_Roundtrip()
{
// Arrange
ResponseContinuationToken originalToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3, 4, 5 });
ChatClientAgentContinuationToken chatClientToken = new(originalToken)
{
InputMessages =
[
new ChatMessage(ChatRole.User, "Hello!"),
new ChatMessage(ChatRole.User, "How are you?")
],
ResponseUpdates =
[
new ChatResponseUpdate(ChatRole.Assistant, "I'm fine, thank you."),
new ChatResponseUpdate(ChatRole.Assistant, "How can I assist you today?")
]
};
// Act
ReadOnlyMemory<byte> bytes = chatClientToken.ToBytes();
ChatClientAgentContinuationToken tokenFromBytes = ChatClientAgentContinuationToken.FromToken(ResponseContinuationToken.FromBytes(bytes));
// Assert
Assert.NotNull(tokenFromBytes);
Assert.Equal(chatClientToken.ToBytes().ToArray(), tokenFromBytes.ToBytes().ToArray());
// Verify InnerToken
Assert.Equal(chatClientToken.InnerToken.ToBytes().ToArray(), tokenFromBytes.InnerToken.ToBytes().ToArray());
// Verify InputMessages
Assert.NotNull(tokenFromBytes.InputMessages);
Assert.Equal(chatClientToken.InputMessages.Count(), tokenFromBytes.InputMessages.Count());
for (int i = 0; i < chatClientToken.InputMessages.Count(); i++)
{
Assert.Equal(chatClientToken.InputMessages.ElementAt(i).Role, tokenFromBytes.InputMessages.ElementAt(i).Role);
Assert.Equal(chatClientToken.InputMessages.ElementAt(i).Text, tokenFromBytes.InputMessages.ElementAt(i).Text);
}
// Verify ResponseUpdates
Assert.NotNull(tokenFromBytes.ResponseUpdates);
Assert.Equal(chatClientToken.ResponseUpdates.Count, tokenFromBytes.ResponseUpdates.Count);
for (int i = 0; i < chatClientToken.ResponseUpdates.Count; i++)
{
Assert.Equal(chatClientToken.ResponseUpdates.ElementAt(i).Role, tokenFromBytes.ResponseUpdates.ElementAt(i).Role);
Assert.Equal(chatClientToken.ResponseUpdates.ElementAt(i).Text, tokenFromBytes.ResponseUpdates.ElementAt(i).Text);
}
}
[Fact]
public void Serialization_Roundtrip()
{
// Arrange
ResponseContinuationToken originalToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3, 4, 5 });
ChatClientAgentContinuationToken chatClientToken = new(originalToken)
{
InputMessages =
[
new ChatMessage(ChatRole.User, "Hello!"),
new ChatMessage(ChatRole.User, "How are you?")
],
ResponseUpdates =
[
new ChatResponseUpdate(ChatRole.Assistant, "I'm fine, thank you."),
new ChatResponseUpdate(ChatRole.Assistant, "How can I assist you today?")
]
};
// Act
string json = JsonSerializer.Serialize(chatClientToken, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
ResponseContinuationToken? deserializedToken = (ResponseContinuationToken?)JsonSerializer.Deserialize(json, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ResponseContinuationToken)));
ChatClientAgentContinuationToken deserializedChatClientToken = ChatClientAgentContinuationToken.FromToken(deserializedToken!);
// Assert
Assert.NotNull(deserializedChatClientToken);
Assert.Equal(chatClientToken.ToBytes().ToArray(), deserializedChatClientToken.ToBytes().ToArray());
// Verify InnerToken
Assert.Equal(chatClientToken.InnerToken.ToBytes().ToArray(), deserializedChatClientToken.InnerToken.ToBytes().ToArray());
// Verify InputMessages
Assert.NotNull(deserializedChatClientToken.InputMessages);
Assert.Equal(chatClientToken.InputMessages.Count(), deserializedChatClientToken.InputMessages.Count());
for (int i = 0; i < chatClientToken.InputMessages.Count(); i++)
{
Assert.Equal(chatClientToken.InputMessages.ElementAt(i).Role, deserializedChatClientToken.InputMessages.ElementAt(i).Role);
Assert.Equal(chatClientToken.InputMessages.ElementAt(i).Text, deserializedChatClientToken.InputMessages.ElementAt(i).Text);
}
// Verify ResponseUpdates
Assert.NotNull(deserializedChatClientToken.ResponseUpdates);
Assert.Equal(chatClientToken.ResponseUpdates.Count, deserializedChatClientToken.ResponseUpdates.Count);
for (int i = 0; i < chatClientToken.ResponseUpdates.Count; i++)
{
Assert.Equal(chatClientToken.ResponseUpdates.ElementAt(i).Role, deserializedChatClientToken.ResponseUpdates.ElementAt(i).Role);
Assert.Equal(chatClientToken.ResponseUpdates.ElementAt(i).Text, deserializedChatClientToken.ResponseUpdates.ElementAt(i).Text);
}
}
[Fact]
public void FromToken_WithChatClientAgentContinuationToken_ReturnsSameInstance()
{
// Arrange
ChatClientAgentContinuationToken originalToken = new(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3, 4, 5 }));
// Act
ChatClientAgentContinuationToken fromToken = ChatClientAgentContinuationToken.FromToken(originalToken);
// Assert
Assert.Same(originalToken, fromToken);
}
}

View File

@@ -0,0 +1,186 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Unit tests for the <see cref="ChatClientAgentOptions"/> class.
/// </summary>
public class ChatClientAgentOptionsTests
{
[Fact]
public void DefaultConstructor_InitializesWithNullValues()
{
// Act
var options = new ChatClientAgentOptions();
// Assert
Assert.Null(options.Name);
Assert.Null(options.Description);
Assert.Null(options.ChatOptions);
Assert.Null(options.ChatMessageStoreFactory);
Assert.Null(options.AIContextProviderFactory);
}
[Fact]
public void Constructor_WithNullValues_SetsPropertiesCorrectly()
{
// Act
var options = new ChatClientAgentOptions() { Name = null, Description = null, ChatOptions = new() { Tools = null, Instructions = null } };
// Assert
Assert.Null(options.Name);
Assert.Null(options.Description);
Assert.Null(options.AIContextProviderFactory);
Assert.Null(options.ChatMessageStoreFactory);
Assert.NotNull(options.ChatOptions);
Assert.Null(options.ChatOptions.Instructions);
Assert.Null(options.ChatOptions.Tools);
}
[Fact]
public void Constructor_WithToolsOnly_SetsChatOptionsWithTools()
{
// Arrange
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
// Act
var options = new ChatClientAgentOptions()
{
Name = null,
Description = null,
ChatOptions = new() { Tools = tools }
};
// Assert
Assert.Null(options.Name);
Assert.Null(options.Description);
Assert.NotNull(options.ChatOptions);
AssertSameTools(tools, options.ChatOptions.Tools);
}
[Fact]
public void Constructor_WithAllParameters_SetsAllPropertiesCorrectly()
{
// Arrange
const string Instructions = "Test instructions";
const string Name = "Test name";
const string Description = "Test description";
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
// Act
var options = new ChatClientAgentOptions()
{
Name = Name,
Description = Description,
ChatOptions = new() { Tools = tools, Instructions = Instructions }
};
// Assert
Assert.Equal(Name, options.Name);
Assert.Equal(Instructions, options.ChatOptions.Instructions);
Assert.Equal(Description, options.Description);
Assert.NotNull(options.ChatOptions);
AssertSameTools(tools, options.ChatOptions.Tools);
}
[Fact]
public void Constructor_WithNameAndDescriptionOnly_DoesNotCreateChatOptions()
{
// Arrange
const string Name = "Test name";
const string Description = "Test description";
// Act
var options = new ChatClientAgentOptions()
{
Name = Name,
Description = Description,
};
// Assert
Assert.Equal(Name, options.Name);
Assert.Equal(Description, options.Description);
Assert.Null(options.ChatOptions);
}
[Fact]
public void Clone_CreatesDeepCopyWithSameValues()
{
// Arrange
const string Name = "Test name";
const string Description = "Test description";
var tools = new List<AITool> { AIFunctionFactory.Create(() => "test") };
static ValueTask<ChatMessageStore> ChatMessageStoreFactoryAsync(
ChatClientAgentOptions.ChatMessageStoreFactoryContext ctx, CancellationToken ct) => new(new Mock<ChatMessageStore>().Object);
static ValueTask<AIContextProvider> AIContextProviderFactoryAsync(
ChatClientAgentOptions.AIContextProviderFactoryContext ctx, CancellationToken ct) => new(new Mock<AIContextProvider>().Object);
var original = new ChatClientAgentOptions()
{
Name = Name,
Description = Description,
ChatOptions = new() { Tools = tools },
Id = "test-id",
ChatMessageStoreFactory = ChatMessageStoreFactoryAsync,
AIContextProviderFactory = AIContextProviderFactoryAsync
};
// Act
var clone = original.Clone();
// Assert
Assert.NotSame(original, clone);
Assert.Equal(original.Id, clone.Id);
Assert.Equal(original.Name, clone.Name);
Assert.Equal(original.Description, clone.Description);
Assert.Same(original.ChatMessageStoreFactory, clone.ChatMessageStoreFactory);
Assert.Same(original.AIContextProviderFactory, clone.AIContextProviderFactory);
// ChatOptions should be cloned, not the same reference
Assert.NotSame(original.ChatOptions, clone.ChatOptions);
Assert.Equal(original.ChatOptions?.Instructions, clone.ChatOptions?.Instructions);
Assert.Equal(original.ChatOptions?.Tools, clone.ChatOptions?.Tools);
}
[Fact]
public void Clone_WithoutProvidingChatOptions_ClonesCorrectly()
{
// Arrange
var original = new ChatClientAgentOptions
{
Id = "test-id",
Name = "Test name",
Description = "Test description"
};
// Act
var clone = original.Clone();
// Assert
Assert.NotSame(original, clone);
Assert.Equal(original.Id, clone.Id);
Assert.Equal(original.Name, clone.Name);
Assert.Equal(original.Description, clone.Description);
Assert.Null(original.ChatOptions);
Assert.Null(clone.ChatMessageStoreFactory);
Assert.Null(clone.AIContextProviderFactory);
}
private static void AssertSameTools(IList<AITool>? expected, IList<AITool>? actual)
{
var index = 0;
foreach (var tool in expected ?? [])
{
Assert.Same(tool, actual?[index]);
index++;
}
}
}

View File

@@ -0,0 +1,335 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
public class ChatClientAgentRunOptionsTests
{
/// <summary>
/// Verify that ChatClientAgentRunOptions constructor works with null chatOptions.
/// </summary>
[Fact]
public void ConstructorWorksWithNullChatOptions()
{
// Act
var runOptions = new ChatClientAgentRunOptions();
// Assert
Assert.Null(runOptions.ChatOptions);
}
/// <summary>
/// Verify that ChatClientAgentRunOptions ChatOptions property is set and mutable.
/// </summary>
[Fact]
public void ChatOptionsPropertyIsReadOnly()
{
// Arrange
var chatOptions = new ChatOptions { MaxOutputTokens = 100 };
var runOptions = new ChatClientAgentRunOptions(chatOptions);
chatOptions.MaxOutputTokens = 200; // Change the property to verify mutability
// Act & Assert
Assert.Same(chatOptions, runOptions.ChatOptions);
// Verify that the property doesn't have a setter by checking if it's the same instance
var retrievedOptions = runOptions.ChatOptions!;
Assert.Same(chatOptions, retrievedOptions);
Assert.Equal(200, retrievedOptions.MaxOutputTokens); // Ensure the change is reflected
}
#region ChatClientFactory Tests
/// <summary>
/// Tests that ChatClientFactory is called and transforms the client for RunAsync.
/// </summary>
[Fact]
public async Task RunAsync_WithChatClientFactory_UsesTransformedClientAsync()
{
// Arrange
var originalClient = new Mock<IChatClient>();
var transformedClient = new Mock<IChatClient>();
var factoryCallCount = 0;
// Setup the original client to throw if called (should not be used)
originalClient.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Throws(new InvalidOperationException("Original client should not be called"));
// Setup the transformed client to return a response
transformedClient.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Transformed response")]));
// Create the factory that transforms the client
IChatClient ClientFactory(IChatClient client)
{
factoryCallCount++;
Assert.Same(originalClient.Object, client); // Verify original client is passed
return transformedClient.Object;
}
var agent = new ChatClientAgent(originalClient.Object, new ChatClientAgentOptions() { UseProvidedChatClientAsIs = true });
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
var options = new ChatClientAgentRunOptions { ChatClientFactory = ClientFactory };
// Act
var response = await agent.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.NotNull(response);
Assert.Equal(1, factoryCallCount); // Factory should be called exactly once
transformedClient.Verify(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Once);
originalClient.Verify(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Never);
}
/// <summary>
/// Tests that ChatClientFactory is called and transforms the client for RunStreamingAsync.
/// </summary>
[Fact]
public async Task RunStreamingAsync_WithChatClientFactory_UsesTransformedClientAsync()
{
// Arrange
var originalClient = new Mock<IChatClient>();
var transformedClient = new Mock<IChatClient>();
var factoryCallCount = 0;
// Setup the original client to throw if called (should not be used)
originalClient.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Throws(new InvalidOperationException("Original client should not be called"));
// Setup the transformed client to return streaming responses
var streamingResponses = new[]
{
new ChatResponseUpdate { Contents = [new TextContent("Streaming ")] },
new ChatResponseUpdate { Contents = [new TextContent("response")] }
};
transformedClient.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(streamingResponses.ToAsyncEnumerable());
// Create the factory that transforms the client
IChatClient ClientFactory(IChatClient client)
{
factoryCallCount++;
Assert.Same(originalClient.Object, client); // Verify original client is passed
return transformedClient.Object;
}
var agent = new ChatClientAgent(originalClient.Object, new ChatClientAgentOptions() { UseProvidedChatClientAsIs = true });
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
var options = new ChatClientAgentRunOptions { ChatClientFactory = ClientFactory };
// Act
var responseUpdates = new List<AgentResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(messages, null, options, CancellationToken.None))
{
responseUpdates.Add(update);
}
// Assert
Assert.NotEmpty(responseUpdates);
Assert.Equal(1, factoryCallCount); // Factory should be called exactly once
transformedClient.Verify(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Once);
originalClient.Verify(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Never);
}
/// <summary>
/// Tests that without ChatClientFactory, the original client is used for RunAsync.
/// </summary>
[Fact]
public async Task RunAsync_WithoutChatClientFactory_UsesOriginalClientAsync()
{
// Arrange
var originalClient = new Mock<IChatClient>();
originalClient.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Original response")]));
var agent = new ChatClientAgent(originalClient.Object);
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
// Act - No ChatClientFactory provided
var response = await agent.RunAsync(messages, null, null, CancellationToken.None);
// Assert
Assert.NotNull(response);
originalClient.Verify(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Once);
}
/// <summary>
/// Tests that without ChatClientFactory, the original client is used for RunStreamingAsync.
/// </summary>
[Fact]
public async Task RunStreamingAsync_WithoutChatClientFactory_UsesOriginalClientAsync()
{
// Arrange
var originalClient = new Mock<IChatClient>();
var streamingResponses = new[]
{
new ChatResponseUpdate { Contents = [new TextContent("Original ")] },
new ChatResponseUpdate { Contents = [new TextContent("streaming")] }
};
originalClient.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(streamingResponses.ToAsyncEnumerable());
var agent = new ChatClientAgent(originalClient.Object);
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
// Act - No ChatClientFactory provided
var responseUpdates = new List<AgentResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(messages, null, null, CancellationToken.None))
{
responseUpdates.Add(update);
}
// Assert
Assert.NotEmpty(responseUpdates);
originalClient.Verify(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Once);
}
/// <summary>
/// Tests that ChatClientFactory is called for each separate RunAsync call.
/// </summary>
[Fact]
public async Task RunAsync_MultipleCalls_ChatClientFactoryCalledEachTimeAsync()
{
// Arrange
var originalClient = new Mock<IChatClient>();
var transformedClient = new Mock<IChatClient>();
var factoryCallCount = 0;
transformedClient.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Response")]));
IChatClient ClientFactory(IChatClient client)
{
factoryCallCount++;
return transformedClient.Object;
}
var agent = new ChatClientAgent(originalClient.Object);
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
var options = new ChatClientAgentRunOptions { ChatClientFactory = ClientFactory };
// Act - Call RunAsync multiple times
await agent.RunAsync(messages, null, options, CancellationToken.None);
await agent.RunAsync(messages, null, options, CancellationToken.None);
// Assert
Assert.Equal(2, factoryCallCount); // Factory should be called for each run
transformedClient.Verify(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Exactly(2));
}
/// <summary>
/// Tests that subsequent calls without ChatClientFactory use the original client.
/// </summary>
[Fact]
public async Task RunAsync_AfterFactoryCall_WithoutFactory_UsesOriginalClientAsync()
{
// Arrange
var originalClient = new Mock<IChatClient>();
var transformedClient = new Mock<IChatClient>();
originalClient.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Original response")]));
transformedClient.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Transformed response")]));
IChatClient ClientFactory(IChatClient client) => transformedClient.Object;
var agent = new ChatClientAgent(originalClient.Object);
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
var optionsWithFactory = new ChatClientAgentRunOptions { ChatClientFactory = ClientFactory };
// Act - First call with factory, second call without
await agent.RunAsync(messages, null, optionsWithFactory, CancellationToken.None);
await agent.RunAsync(messages, null, null, CancellationToken.None);
// Assert
transformedClient.Verify(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Once);
originalClient.Verify(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Once);
}
/// <summary>
/// Tests that ChatClientFactory returning null throws an exception.
/// </summary>
[Fact]
public async Task RunAsync_ChatClientFactoryReturnsNull_ThrowsExceptionAsync()
{
// Arrange
var originalClient = new Mock<IChatClient>();
static IChatClient ClientFactory(IChatClient client) => null!;
var agent = new ChatClientAgent(originalClient.Object);
var messages = new List<ChatMessage> { new(ChatRole.User, "Test message") };
var options = new ChatClientAgentRunOptions { ChatClientFactory = ClientFactory };
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
await agent.RunAsync(messages, null, options, CancellationToken.None));
}
#endregion
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,330 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
#pragma warning disable CA1861 // Avoid constant arrays as arguments
namespace Microsoft.Agents.AI.UnitTests;
public class ChatClientAgentThreadTests
{
#region Constructor and Property Tests
[Fact]
public void ConstructorSetsDefaults()
{
// Arrange & Act
var thread = new ChatClientAgentThread();
// Assert
Assert.Null(thread.ConversationId);
Assert.Null(thread.MessageStore);
}
[Fact]
public void SetConversationIdRoundtrips()
{
// Arrange
var thread = new ChatClientAgentThread();
const string ConversationId = "test-thread-id";
// Act
thread.ConversationId = ConversationId;
// Assert
Assert.Equal(ConversationId, thread.ConversationId);
Assert.Null(thread.MessageStore);
}
[Fact]
public void SetChatMessageStoreRoundtrips()
{
// Arrange
var thread = new ChatClientAgentThread();
var messageStore = new InMemoryChatMessageStore();
// Act
thread.MessageStore = messageStore;
// Assert
Assert.Same(messageStore, thread.MessageStore);
Assert.Null(thread.ConversationId);
}
[Fact]
public void SetConversationIdThrowsWhenMessageStoreIsSet()
{
// Arrange
var thread = new ChatClientAgentThread
{
MessageStore = new InMemoryChatMessageStore()
};
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => thread.ConversationId = "new-thread-id");
Assert.Equal("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.", exception.Message);
Assert.NotNull(thread.MessageStore);
}
[Fact]
public void SetChatMessageStoreThrowsWhenConversationIdIsSet()
{
// Arrange
var thread = new ChatClientAgentThread
{
ConversationId = "existing-thread-id"
};
var store = new InMemoryChatMessageStore();
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => thread.MessageStore = store);
Assert.Equal("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.", exception.Message);
Assert.NotNull(thread.ConversationId);
}
#endregion Constructor and Property Tests
#region Deserialize Tests
[Fact]
public async Task VerifyDeserializeWithMessagesAsync()
{
// Arrange
var json = JsonSerializer.Deserialize("""
{
"storeState": { "messages": [{"authorName": "testAuthor"}] }
}
""", TestJsonSerializerContext.Default.JsonElement);
// Act.
var thread = await ChatClientAgentThread.DeserializeAsync(json);
// Assert
Assert.Null(thread.ConversationId);
var messageStore = thread.MessageStore as InMemoryChatMessageStore;
Assert.NotNull(messageStore);
Assert.Single(messageStore);
Assert.Equal("testAuthor", messageStore[0].AuthorName);
}
[Fact]
public async Task VerifyDeserializeWithIdAsync()
{
// Arrange
var json = JsonSerializer.Deserialize("""
{
"conversationId": "TestConvId"
}
""", TestJsonSerializerContext.Default.JsonElement);
// Act
var thread = await ChatClientAgentThread.DeserializeAsync(json);
// Assert
Assert.Equal("TestConvId", thread.ConversationId);
Assert.Null(thread.MessageStore);
}
[Fact]
public async Task VerifyDeserializeWithAIContextProviderAsync()
{
// Arrange
var json = JsonSerializer.Deserialize("""
{
"conversationId": "TestConvId",
"aiContextProviderState": ["CP1"]
}
""", TestJsonSerializerContext.Default.JsonElement);
Mock<AIContextProvider> mockProvider = new();
// Act
var thread = await ChatClientAgentThread.DeserializeAsync(json, aiContextProviderFactory: (_, _, _) => new(mockProvider.Object));
// Assert
Assert.Null(thread.MessageStore);
Assert.Same(thread.AIContextProvider, mockProvider.Object);
}
[Fact]
public async Task DeserializeWithInvalidJsonThrowsAsync()
{
// Arrange
var invalidJson = JsonSerializer.Deserialize("[42]", TestJsonSerializerContext.Default.JsonElement);
var thread = new ChatClientAgentThread();
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() => ChatClientAgentThread.DeserializeAsync(invalidJson));
}
#endregion Deserialize Tests
#region Serialize Tests
/// <summary>
/// Verify thread serialization to JSON when the thread has an id.
/// </summary>
[Fact]
public void VerifyThreadSerializationWithId()
{
// Arrange
var thread = new ChatClientAgentThread { ConversationId = "TestConvId" };
// Act
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.True(json.TryGetProperty("conversationId", out var idProperty));
Assert.Equal("TestConvId", idProperty.GetString());
Assert.False(json.TryGetProperty("storeState", out _));
}
/// <summary>
/// Verify thread serialization to JSON when the thread has messages.
/// </summary>
[Fact]
public void VerifyThreadSerializationWithMessages()
{
// Arrange
InMemoryChatMessageStore store = [new(ChatRole.User, "TestContent") { AuthorName = "TestAuthor" }];
var thread = new ChatClientAgentThread { MessageStore = store };
// Act
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.False(json.TryGetProperty("conversationId", out _));
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.Single(messagesProperty.EnumerateArray());
var message = messagesProperty.EnumerateArray().First();
Assert.Equal("TestAuthor", message.GetProperty("authorName").GetString());
Assert.True(message.TryGetProperty("contents", out var contentsProperty));
Assert.Equal(JsonValueKind.Array, contentsProperty.ValueKind);
Assert.Single(contentsProperty.EnumerateArray());
var textContent = contentsProperty.EnumerateArray().First();
Assert.Equal("TestContent", textContent.GetProperty("text").GetString());
}
[Fact]
public void VerifyThreadSerializationWithWithAIContextProvider()
{
// Arrange
Mock<AIContextProvider> mockProvider = new();
mockProvider
.Setup(m => m.Serialize(It.IsAny<JsonSerializerOptions?>()))
.Returns(JsonSerializer.SerializeToElement(["CP1"], TestJsonSerializerContext.Default.StringArray));
var thread = new ChatClientAgentThread
{
AIContextProvider = mockProvider.Object
};
// Act
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.True(json.TryGetProperty("aiContextProviderState", out var providerStateProperty));
Assert.Equal(JsonValueKind.Array, providerStateProperty.ValueKind);
Assert.Single(providerStateProperty.EnumerateArray());
Assert.Equal("CP1", providerStateProperty.EnumerateArray().First().GetString());
mockProvider.Verify(m => m.Serialize(It.IsAny<JsonSerializerOptions?>()), Times.Once);
}
/// <summary>
/// Verify thread serialization to JSON with custom options.
/// </summary>
[Fact]
public void VerifyThreadSerializationWithCustomOptions()
{
// Arrange
var thread = new ChatClientAgentThread();
JsonSerializerOptions options = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
var storeStateElement = JsonSerializer.SerializeToElement(
new Dictionary<string, object> { ["Key"] = "TestValue" },
TestJsonSerializerContext.Default.DictionaryStringObject);
var messageStoreMock = new Mock<ChatMessageStore>();
messageStoreMock
.Setup(m => m.Serialize(options))
.Returns(storeStateElement);
thread.MessageStore = messageStoreMock.Object;
// Act
var json = thread.Serialize(options);
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
Assert.False(json.TryGetProperty("conversationId", out var idProperty));
Assert.True(json.TryGetProperty("storeState", out var storeStateProperty));
Assert.Equal(JsonValueKind.Object, storeStateProperty.ValueKind);
Assert.True(storeStateProperty.TryGetProperty("Key", out var keyProperty));
Assert.Equal("TestValue", keyProperty.GetString());
messageStoreMock.Verify(m => m.Serialize(options), Times.Once);
}
#endregion Serialize Tests
#region GetService Tests
[Fact]
public void GetService_RequestingAIContextProvider_ReturnsAIContextProvider()
{
// Arrange
var thread = new ChatClientAgentThread();
var mockProvider = new Mock<AIContextProvider>();
mockProvider
.Setup(m => m.GetService(It.Is<Type>(x => x == typeof(AIContextProvider)), null))
.Returns(mockProvider.Object);
thread.AIContextProvider = mockProvider.Object;
// Act
var result = thread.GetService(typeof(AIContextProvider));
// Assert
Assert.NotNull(result);
Assert.Same(mockProvider.Object, result);
}
[Fact]
public void GetService_RequestingChatMessageStore_ReturnsChatMessageStore()
{
// Arrange
var thread = new ChatClientAgentThread();
var messageStore = new InMemoryChatMessageStore();
thread.MessageStore = messageStore;
// Act
var result = thread.GetService(typeof(ChatMessageStore));
// Assert
Assert.NotNull(result);
Assert.Same(messageStore, result);
}
#endregion
}

View File

@@ -0,0 +1,808 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Contains unit tests for ChatClientAgent background responses functionality.
/// </summary>
public class ChatClientAgent_BackgroundResponsesTests
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task RunAsync_PropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
{
// Arrange
var continuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }));
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ContinuationToken = null, ConversationId = "conversation-id" });
AgentRunOptions agentRunOptions;
if (providePropsViaChatOptions)
{
ChatOptions chatOptions = new()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken
};
agentRunOptions = new ChatClientAgentRunOptions(chatOptions);
}
else
{
agentRunOptions = new AgentRunOptions()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken
};
}
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentThread thread = new() { ConversationId = "conversation-id" };
// Act
await agent.RunAsync(thread, options: agentRunOptions);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.True(capturedChatOptions.AllowBackgroundResponses);
Assert.Same(continuationToken.InnerToken, capturedChatOptions.ContinuationToken);
}
[Fact]
public async Task RunAsync_WhenPropertiesSetInBothLocations_PrioritizesAgentRunOptionsOverChatOptionsAsync()
{
// Arrange
var continuationToken1 = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }));
var continuationToken2 = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }));
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ContinuationToken = null, ConversationId = "conversation-id" });
ChatOptions chatOptions = new()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken1
};
ChatClientAgentRunOptions agentRunOptions = new(chatOptions)
{
AllowBackgroundResponses = false,
ContinuationToken = continuationToken2
};
ChatClientAgentThread thread = new() { ConversationId = "conversation-id" };
ChatClientAgent agent = new(mockChatClient.Object);
// Act
await agent.RunAsync(thread, options: agentRunOptions);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.False(capturedChatOptions.AllowBackgroundResponses);
Assert.Same(continuationToken2.InnerToken, capturedChatOptions.ContinuationToken);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task RunStreamingAsync_PropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
{
// Arrange
ChatResponseUpdate[] returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "wh") { ConversationId = "conversation-id" },
new ChatResponseUpdate(role: ChatRole.Assistant, content: "at?") { ConversationId = "conversation-id" },
];
var continuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) { InputMessages = [new ChatMessage()] };
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
.Returns(ToAsyncEnumerableAsync(returnUpdates));
AgentRunOptions agentRunOptions;
if (providePropsViaChatOptions)
{
ChatOptions chatOptions = new()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken
};
agentRunOptions = new ChatClientAgentRunOptions(chatOptions);
}
else
{
agentRunOptions = new AgentRunOptions()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken
};
}
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentThread thread = new() { ConversationId = "conversation-id" };
// Act
await foreach (var _ in agent.RunStreamingAsync(thread, options: agentRunOptions))
{
}
// Assert
Assert.NotNull(capturedChatOptions);
Assert.True(capturedChatOptions.AllowBackgroundResponses);
Assert.Same(continuationToken.InnerToken, capturedChatOptions.ContinuationToken);
}
[Fact]
public async Task RunStreamingAsync_WhenPropertiesSetInBothLocations_PrioritizesAgentRunOptionsOverChatOptionsAsync()
{
// Arrange
ChatResponseUpdate[] returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "wh") { ConversationId = "conversation-id" },
];
var continuationToken1 = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) { InputMessages = [new ChatMessage()] };
var continuationToken2 = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) { InputMessages = [new ChatMessage()] };
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
.Returns(ToAsyncEnumerableAsync(returnUpdates));
ChatOptions chatOptions = new()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken1
};
ChatClientAgentRunOptions agentRunOptions = new(chatOptions)
{
AllowBackgroundResponses = false,
ContinuationToken = continuationToken2
};
ChatClientAgent agent = new(mockChatClient.Object);
var thread = new ChatClientAgentThread() { ConversationId = "conversation-id" };
// Act
await foreach (var _ in agent.RunStreamingAsync(thread, options: agentRunOptions))
{
}
// Assert
Assert.NotNull(capturedChatOptions);
Assert.False(capturedChatOptions.AllowBackgroundResponses);
Assert.Same(continuationToken2.InnerToken, capturedChatOptions.ContinuationToken);
}
[Fact]
public async Task RunAsync_WhenContinuationTokenReceivedFromChatResponse_WrapsContinuationTokenAsync()
{
// Arrange
var continuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "partial")]) { ContinuationToken = continuationToken });
ChatClientAgent agent = new(mockChatClient.Object);
var runOptions = new ChatClientAgentRunOptions(new ChatOptions { AllowBackgroundResponses = true });
ChatClientAgentThread thread = new();
// Act
var response = await agent.RunAsync([new(ChatRole.User, "hi")], thread, options: runOptions);
// Assert
Assert.Same(continuationToken, (response.ContinuationToken as ChatClientAgentContinuationToken)?.InnerToken);
}
[Fact]
public async Task RunStreamingAsync_WhenContinuationTokenReceived_WrapsContinuationTokenAsync()
{
// Arrange
var token1 = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
ChatResponseUpdate[] expectedUpdates =
[
new ChatResponseUpdate(ChatRole.Assistant, "pa") { ContinuationToken = token1 },
new ChatResponseUpdate(ChatRole.Assistant, "rt") { ContinuationToken = null } // terminal
];
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(expectedUpdates));
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentThread thread = new();
// Act
var actualUpdates = new List<AgentResponseUpdate>();
await foreach (var u in agent.RunStreamingAsync([new(ChatRole.User, "hi")], thread, options: new ChatClientAgentRunOptions(new ChatOptions { AllowBackgroundResponses = true })))
{
actualUpdates.Add(u);
}
// Assert
Assert.Equal(2, actualUpdates.Count);
Assert.Same(token1, (actualUpdates[0].ContinuationToken as ChatClientAgentContinuationToken)?.InnerToken);
Assert.Null(actualUpdates[1].ContinuationToken); // last update has null token
}
[Fact]
public async Task RunAsync_WhenMessagesProvidedWithContinuationToken_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
ChatClientAgent agent = new(mockChatClient.Object);
AgentRunOptions runOptions = new() { ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) };
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(inputMessages, options: runOptions));
// Verify that the IChatClient was never called due to early validation
mockChatClient.Verify(
c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunStreamingAsync_WhenMessagesProvidedWithContinuationToken_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
ChatClientAgent agent = new(mockChatClient.Object);
AgentRunOptions runOptions = new() { ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) };
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await foreach (var update in agent.RunStreamingAsync(inputMessages, options: runOptions))
{
// Should not reach here
}
});
// Verify that the IChatClient was never called due to early validation
mockChatClient.Verify(
c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunAsync_WhenContinuationTokenProvided_SkipsThreadMessagePopulationAsync()
{
// Arrange
List<ChatMessage> capturedMessages = [];
// Create a mock message store that would normally provide messages
var mockMessageStore = new Mock<ChatMessageStore>();
mockMessageStore
.Setup(ms => ms.InvokingAsync(It.IsAny<ChatMessageStore.InvokingContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync([new(ChatRole.User, "Message from message store")]);
// Create a mock AI context provider that would normally provide context
var mockContextProvider = new Mock<AIContextProvider>();
mockContextProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AIContext
{
Messages = [new(ChatRole.System, "Message from AI context")],
Instructions = "context instructions"
});
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "continued response")]));
ChatClientAgent agent = new(mockChatClient.Object);
// Create a thread with both message store and AI context provider
ChatClientAgentThread thread = new()
{
MessageStore = mockMessageStore.Object,
AIContextProvider = mockContextProvider.Object
};
AgentRunOptions runOptions = new()
{
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }))
};
// Act
await agent.RunAsync([], thread, options: runOptions);
// Assert
// With continuation token, thread message population should be skipped
Assert.Empty(capturedMessages);
// Verify that message store was never called due to continuation token
mockMessageStore.Verify(
ms => ms.InvokingAsync(It.IsAny<ChatMessageStore.InvokingContext>(), It.IsAny<CancellationToken>()),
Times.Never);
// Verify that AI context provider was never called due to continuation token
mockContextProvider.Verify(
p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunStreamingAsync_WhenContinuationTokenProvided_SkipsThreadMessagePopulationAsync()
{
// Arrange
List<ChatMessage> capturedMessages = [];
// Create a mock message store that would normally provide messages
var mockMessageStore = new Mock<ChatMessageStore>();
mockMessageStore
.Setup(ms => ms.InvokingAsync(It.IsAny<ChatMessageStore.InvokingContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync([new(ChatRole.User, "Message from message store")]);
// Create a mock AI context provider that would normally provide context
var mockContextProvider = new Mock<AIContextProvider>();
mockContextProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AIContext
{
Messages = [new(ChatRole.System, "Message from AI context")],
Instructions = "context instructions"
});
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedMessages.AddRange(msgs))
.Returns(ToAsyncEnumerableAsync([new ChatResponseUpdate(role: ChatRole.Assistant, content: "continued response")]));
ChatClientAgent agent = new(mockChatClient.Object);
// Create a thread with both message store and AI context provider
ChatClientAgentThread thread = new()
{
MessageStore = mockMessageStore.Object,
AIContextProvider = mockContextProvider.Object
};
AgentRunOptions runOptions = new()
{
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })) { InputMessages = [new ChatMessage()] }
};
// Act
await agent.RunStreamingAsync(thread, options: runOptions).ToListAsync();
// Assert
// With continuation token, thread message population should be skipped
Assert.Empty(capturedMessages);
// Verify that message store was never called due to continuation token
mockMessageStore.Verify(
ms => ms.InvokingAsync(It.IsAny<ChatMessageStore.InvokingContext>(), It.IsAny<CancellationToken>()),
Times.Never);
// Verify that AI context provider was never called due to continuation token
mockContextProvider.Verify(
p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunAsync_WhenNoThreadProvidedForBackgroundResponses_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
ChatClientAgent agent = new(mockChatClient.Object);
AgentRunOptions runOptions = new() { AllowBackgroundResponses = true };
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(inputMessages, options: runOptions));
// Verify that the IChatClient was never called due to early validation
mockChatClient.Verify(
c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunStreamingAsync_WhenNoThreadProvidedForBackgroundResponses_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
ChatClientAgent agent = new(mockChatClient.Object);
AgentRunOptions runOptions = new() { AllowBackgroundResponses = true };
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await foreach (var update in agent.RunStreamingAsync(inputMessages, options: runOptions))
{
// Should not reach here
}
});
// Verify that the IChatClient was never called due to early validation
mockChatClient.Verify(
c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunStreamingAsync_WhenInputMessagesPresentInContinuationToken_ResumesStreamingAsync()
{
// Arrange
ChatResponseUpdate[] returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "continuation") { ConversationId = "conversation-id" },
];
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(returnUpdates));
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentThread thread = new() { ConversationId = "conversation-id" };
AgentRunOptions runOptions = new()
{
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }))
{
InputMessages = [new ChatMessage(ChatRole.User, "previous message")]
}
};
// Act
var updates = new List<AgentResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(thread, options: runOptions))
{
updates.Add(update);
}
// Assert
Assert.Single(updates);
// Verify that the IChatClient was called
mockChatClient.Verify(
c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_WhenResponseUpdatesPresentInContinuationToken_ResumesStreamingAsync()
{
// Arrange
ChatResponseUpdate[] returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "continuation") { ConversationId = "conversation-id" },
];
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(returnUpdates));
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentThread thread = new() { ConversationId = "conversation-id" };
AgentRunOptions runOptions = new()
{
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }))
{
ResponseUpdates = [new ChatResponseUpdate(ChatRole.Assistant, "previous update")]
}
};
// Act
var updates = new List<AgentResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(thread, options: runOptions))
{
updates.Add(update);
}
// Assert
Assert.Single(updates);
// Verify that the IChatClient was called
mockChatClient.Verify(
c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_WhenResumingStreaming_UsesUpdatesFromInitialRunForContextProviderAndMessageStoreAsync()
{
// Arrange
ChatResponseUpdate[] returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "upon"),
new ChatResponseUpdate(role: ChatRole.Assistant, content: " a"),
new ChatResponseUpdate(role: ChatRole.Assistant, content: " time"),
];
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(returnUpdates));
ChatClientAgent agent = new(mockChatClient.Object);
List<ChatMessage> capturedMessagesAddedToStore = [];
var mockMessageStore = new Mock<ChatMessageStore>();
mockMessageStore
.Setup(ms => ms.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()))
.Callback<ChatMessageStore.InvokedContext, CancellationToken>((ctx, ct) => capturedMessagesAddedToStore.AddRange(ctx.ResponseMessages ?? []))
.Returns(new ValueTask());
AIContextProvider.InvokedContext? capturedInvokedContext = null;
var mockContextProvider = new Mock<AIContextProvider>();
mockContextProvider
.Setup(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Callback<AIContextProvider.InvokedContext, CancellationToken>((context, ct) => capturedInvokedContext = context)
.Returns(new ValueTask());
ChatClientAgentThread thread = new()
{
MessageStore = mockMessageStore.Object,
AIContextProvider = mockContextProvider.Object
};
AgentRunOptions runOptions = new()
{
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }))
{
ResponseUpdates = [new ChatResponseUpdate(ChatRole.Assistant, "once ")]
}
};
// Act
await agent.RunStreamingAsync(thread, options: runOptions).ToListAsync();
// Assert
mockMessageStore.Verify(ms => ms.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
Assert.Single(capturedMessagesAddedToStore);
Assert.Contains("once upon a time", capturedMessagesAddedToStore[0].Text);
mockContextProvider.Verify(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
Assert.NotNull(capturedInvokedContext?.ResponseMessages);
Assert.Single(capturedInvokedContext.ResponseMessages);
Assert.Contains("once upon a time", capturedInvokedContext.ResponseMessages.ElementAt(0).Text);
}
[Fact]
public async Task RunStreamingAsync_WhenResumingStreaming_UsesInputMessagesFromInitialRunForContextProviderAndMessageStoreAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(Array.Empty<ChatResponseUpdate>()));
ChatClientAgent agent = new(mockChatClient.Object);
List<ChatMessage> capturedMessagesAddedToStore = [];
var mockMessageStore = new Mock<ChatMessageStore>();
mockMessageStore
.Setup(ms => ms.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()))
.Callback<ChatMessageStore.InvokedContext, CancellationToken>((ctx, ct) => capturedMessagesAddedToStore.AddRange(ctx.RequestMessages))
.Returns(new ValueTask());
AIContextProvider.InvokedContext? capturedInvokedContext = null;
var mockContextProvider = new Mock<AIContextProvider>();
mockContextProvider
.Setup(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()))
.Callback<AIContextProvider.InvokedContext, CancellationToken>((context, ct) => capturedInvokedContext = context)
.Returns(new ValueTask());
ChatClientAgentThread thread = new()
{
MessageStore = mockMessageStore.Object,
AIContextProvider = mockContextProvider.Object
};
AgentRunOptions runOptions = new()
{
ContinuationToken = new ChatClientAgentContinuationToken(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }))
{
InputMessages = [new ChatMessage(ChatRole.User, "Tell me a story")],
}
};
// Act
await agent.RunStreamingAsync(thread, options: runOptions).ToListAsync();
// Assert
mockMessageStore.Verify(ms => ms.InvokedAsync(It.IsAny<ChatMessageStore.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
Assert.Single(capturedMessagesAddedToStore);
Assert.Contains("Tell me a story", capturedMessagesAddedToStore[0].Text);
mockContextProvider.Verify(cp => cp.InvokedAsync(It.IsAny<AIContextProvider.InvokedContext>(), It.IsAny<CancellationToken>()), Times.Once);
Assert.NotNull(capturedInvokedContext?.RequestMessages);
Assert.Single(capturedInvokedContext.RequestMessages);
Assert.Contains("Tell me a story", capturedInvokedContext.RequestMessages.ElementAt(0).Text);
}
[Fact]
public async Task RunStreamingAsync_WhenResumingStreaming_SavesInputMessagesAndUpdatesInContinuationTokenAsync()
{
// Arrange
List<ChatResponseUpdate> returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "Once") { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) },
new ChatResponseUpdate(role: ChatRole.Assistant, content: " upon") { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) },
new ChatResponseUpdate(role: ChatRole.Assistant, content: " a") { ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) },
new ChatResponseUpdate(role: ChatRole.Assistant, content: " time"){ ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }) },
];
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(returnUpdates));
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentThread thread = new() { };
List<ChatClientAgentContinuationToken> capturedContinuationTokens = [];
ChatMessage userMessage = new(ChatRole.User, "Tell me a story");
// Act
// Do the initial run
await foreach (var update in agent.RunStreamingAsync(userMessage, thread))
{
capturedContinuationTokens.Add(Assert.IsType<ChatClientAgentContinuationToken>(update.ContinuationToken));
break;
}
// Now resume the run using the captured continuation token
returnUpdates.RemoveAt(0); // remove the first mock update as it was already processed
var options = new AgentRunOptions { ContinuationToken = capturedContinuationTokens[0] };
await foreach (var update in agent.RunStreamingAsync(thread, options: options))
{
capturedContinuationTokens.Add(Assert.IsType<ChatClientAgentContinuationToken>(update.ContinuationToken));
}
// Assert
Assert.Equal(4, capturedContinuationTokens.Count);
// Verify that the first continuation token has the initial input and first update
Assert.NotNull(capturedContinuationTokens[0].InputMessages);
Assert.Single(capturedContinuationTokens[0].InputMessages!);
Assert.Equal("Tell me a story", capturedContinuationTokens[0].InputMessages!.Last().Text);
Assert.NotNull(capturedContinuationTokens[0].ResponseUpdates);
Assert.Single(capturedContinuationTokens[0].ResponseUpdates!);
Assert.Equal("Once", capturedContinuationTokens[0].ResponseUpdates![0].Text);
// Verify the last continuation token has the input and all updates
var lastToken = capturedContinuationTokens[^1];
Assert.NotNull(lastToken.InputMessages);
Assert.Single(lastToken.InputMessages!);
Assert.Equal("Tell me a story", lastToken.InputMessages!.Last().Text);
Assert.NotNull(lastToken.ResponseUpdates);
Assert.Equal(4, lastToken.ResponseUpdates!.Count);
Assert.Equal("Once", lastToken.ResponseUpdates!.ElementAt(0).Text);
Assert.Equal(" upon", lastToken.ResponseUpdates!.ElementAt(1).Text);
Assert.Equal(" a", lastToken.ResponseUpdates!.ElementAt(2).Text);
Assert.Equal(" time", lastToken.ResponseUpdates!.ElementAt(3).Text);
}
private static async IAsyncEnumerable<T> ToAsyncEnumerableAsync<T>(IEnumerable<T> values)
{
await Task.Yield();
foreach (var update in values)
{
yield return update;
}
}
}

View File

@@ -0,0 +1,371 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
using Xunit.Sdk;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Contains unit tests that verify the chat history management functionality of the <see cref="ChatClientAgent"/> class,
/// e.g. that it correctly reads and updates chat history in any available <see cref="ChatMessageStore"/> or that
/// it uses conversation id correctly for service managed chat history.
/// </summary>
public class ChatClientAgent_ChatHistoryManagementTests
{
#region ConversationId Tests
/// <summary>
/// Verify that RunAsync does not throw when providing a ConversationId via both AgentThread and
/// via ChatOptions and the two are the same.
/// </summary>
[Fact]
public async Task RunAsync_DoesNotThrow_WhenSpecifyingTwoSameConversationIdsAsync()
{
// Arrange
var chatOptions = new ChatOptions { ConversationId = "ConvId" };
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.Is<ChatOptions>(opts => opts.ConversationId == "ConvId"),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
// Act & Assert
var response = await agent.RunAsync([new(ChatRole.User, "test")], thread, options: new ChatClientAgentRunOptions(chatOptions));
Assert.NotNull(response);
}
/// <summary>
/// Verify that RunAsync throws when providing a ConversationId via both AgentThread and
/// via ChatOptions and the two are different.
/// </summary>
[Fact]
public async Task RunAsync_Throws_WhenSpecifyingTwoDifferentConversationIdsAsync()
{
// Arrange
var chatOptions = new ChatOptions { ConversationId = "ConvId" };
Mock<IChatClient> mockService = new();
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ThreadId" };
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], thread, options: new ChatClientAgentRunOptions(chatOptions)));
}
/// <summary>
/// Verify that RunAsync clones the ChatOptions when providing a thread with a ConversationId and a ChatOptions.
/// </summary>
[Fact]
public async Task RunAsync_ClonesChatOptions_ToAddConversationIdAsync()
{
// Arrange
var chatOptions = new ChatOptions { MaxOutputTokens = 100 };
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.Is<ChatOptions>(opts => opts.MaxOutputTokens == 100 && opts.ConversationId == "ConvId"),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
// Act
await agent.RunAsync([new(ChatRole.User, "test")], thread, options: new ChatClientAgentRunOptions(chatOptions));
// Assert
Assert.Null(chatOptions.ConversationId);
}
/// <summary>
/// Verify that RunAsync throws if a thread is provided that uses a conversation id already, but the service does not return one on invoke.
/// </summary>
[Fact]
public async Task RunAsync_Throws_ForMissingConversationIdWithConversationIdThreadAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new() { ConversationId = "ConvId" };
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], thread));
}
/// <summary>
/// Verify that RunAsync sets the ConversationId on the thread when the service returns one.
/// </summary>
[Fact]
public async Task RunAsync_SetsConversationIdOnThread_WhenReturnedByChatClientAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
ChatClientAgentThread thread = new();
// Act
await agent.RunAsync([new(ChatRole.User, "test")], thread);
// Assert
Assert.Equal("ConvId", thread.ConversationId);
}
#endregion
#region ChatMessageStore Tests
/// <summary>
/// Verify that RunAsync uses the default InMemoryChatMessageStore when the chat client returns no conversation id.
/// </summary>
[Fact]
public async Task RunAsync_UsesDefaultInMemoryChatMessageStore_WhenNoConversationIdReturnedByChatClientAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
});
// Act
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
await agent.RunAsync([new(ChatRole.User, "test")], thread);
// Assert
var messageStore = Assert.IsType<InMemoryChatMessageStore>(thread!.MessageStore);
Assert.Equal(2, messageStore.Count);
Assert.Equal("test", messageStore[0].Text);
Assert.Equal("response", messageStore[1].Text);
}
/// <summary>
/// Verify that RunAsync uses the ChatMessageStore factory when the chat client returns no conversation id.
/// </summary>
[Fact]
public async Task RunAsync_UsesChatMessageStoreFactory_WhenProvidedAndNoConversationIdReturnedByChatClientAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
Mock<ChatMessageStore> mockChatMessageStore = new();
mockChatMessageStore.Setup(s => s.InvokingAsync(
It.IsAny<ChatMessageStore.InvokingContext>(),
It.IsAny<CancellationToken>())).ReturnsAsync([new ChatMessage(ChatRole.User, "Existing Chat History")]);
mockChatMessageStore.Setup(s => s.InvokedAsync(
It.IsAny<ChatMessageStore.InvokedContext>(),
It.IsAny<CancellationToken>())).Returns(new ValueTask());
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockChatMessageStore.Object);
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
// Act
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
await agent.RunAsync([new(ChatRole.User, "test")], thread);
// Assert
Assert.IsType<ChatMessageStore>(thread!.MessageStore, exactMatch: false);
mockService.Verify(
x => x.GetResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Count() == 2 && msgs.Any(m => m.Text == "Existing Chat History") && msgs.Any(m => m.Text == "test")),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
mockChatMessageStore.Verify(s => s.InvokingAsync(
It.Is<ChatMessageStore.InvokingContext>(x => x.RequestMessages.Count() == 1),
It.IsAny<CancellationToken>()),
Times.Once);
mockChatMessageStore.Verify(s => s.InvokedAsync(
It.Is<ChatMessageStore.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ChatMessageStoreMessages != null && x.ChatMessageStoreMessages.Count() == 1 && x.ResponseMessages!.Count() == 1),
It.IsAny<CancellationToken>()),
Times.Once);
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>()), Times.Once);
}
/// <summary>
/// Verify that RunAsync notifies the ChatMessageStore on failure.
/// </summary>
[Fact]
public async Task RunAsync_NotifiesChatMessageStore_OnFailureAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Throws(new InvalidOperationException("Test Error"));
Mock<ChatMessageStore> mockChatMessageStore = new();
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockChatMessageStore.Object);
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
// Act
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], thread));
// Assert
Assert.IsType<ChatMessageStore>(thread!.MessageStore, exactMatch: false);
mockChatMessageStore.Verify(s => s.InvokedAsync(
It.Is<ChatMessageStore.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ResponseMessages == null && x.InvokeException!.Message == "Test Error"),
It.IsAny<CancellationToken>()),
Times.Once);
mockFactory.Verify(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>()), Times.Once);
}
/// <summary>
/// Verify that RunAsync throws when a ChatMessageStore Factory is provided and the chat client returns a conversation id.
/// </summary>
[Fact]
public async Task RunAsync_Throws_WhenChatMessageStoreFactoryProvidedAndConversationIdReturnedByChatClientAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ConversationId = "ConvId" });
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(new InMemoryChatMessageStore());
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
// Act & Assert
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync([new(ChatRole.User, "test")], thread));
Assert.Equal("Only the ConversationId or MessageStore may be set, but not both and switching from one to another is not supported.", exception.Message);
}
#endregion
#region ChatMessageStore Override Tests
/// <summary>
/// Tests that RunAsync uses an override ChatMessageStore provided via AdditionalProperties instead of the store from a factory
/// if one is supplied.
/// </summary>
[Fact]
public async Task RunAsync_UsesOverrideChatMessageStore_WhenProvidedViaAdditionalPropertiesAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
// Arrange a chat message store to override the factory provided one.
Mock<ChatMessageStore> mockOverrideChatMessageStore = new();
mockOverrideChatMessageStore.Setup(s => s.InvokingAsync(
It.IsAny<ChatMessageStore.InvokingContext>(),
It.IsAny<CancellationToken>())).ReturnsAsync([new ChatMessage(ChatRole.User, "Existing Chat History")]);
mockOverrideChatMessageStore.Setup(s => s.InvokedAsync(
It.IsAny<ChatMessageStore.InvokedContext>(),
It.IsAny<CancellationToken>())).Returns(new ValueTask());
// Arrange a chat message store to provide to the agent via a factory at construction time.
// This one shouldn't be used since it is being overridden.
Mock<ChatMessageStore> mockFactoryChatMessageStore = new();
mockFactoryChatMessageStore.Setup(s => s.InvokingAsync(
It.IsAny<ChatMessageStore.InvokingContext>(),
It.IsAny<CancellationToken>())).ThrowsAsync(FailException.ForFailure("Base ChatMessageStore shouldn't be used."));
mockFactoryChatMessageStore.Setup(s => s.InvokedAsync(
It.IsAny<ChatMessageStore.InvokedContext>(),
It.IsAny<CancellationToken>())).Throws(FailException.ForFailure("Base ChatMessageStore shouldn't be used."));
Mock<Func<ChatClientAgentOptions.ChatMessageStoreFactoryContext, CancellationToken, ValueTask<ChatMessageStore>>> mockFactory = new();
mockFactory.Setup(f => f(It.IsAny<ChatClientAgentOptions.ChatMessageStoreFactoryContext>(), It.IsAny<CancellationToken>())).ReturnsAsync(mockFactoryChatMessageStore.Object);
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = new() { Instructions = "test instructions" },
ChatMessageStoreFactory = mockFactory.Object
});
// Act
ChatClientAgentThread? thread = await agent.GetNewThreadAsync() as ChatClientAgentThread;
var additionalProperties = new AdditionalPropertiesDictionary();
additionalProperties.Add(mockOverrideChatMessageStore.Object);
await agent.RunAsync([new(ChatRole.User, "test")], thread, options: new AgentRunOptions { AdditionalProperties = additionalProperties });
// Assert
Assert.Same(mockFactoryChatMessageStore.Object, thread!.MessageStore);
mockService.Verify(
x => x.GetResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Count() == 2 && msgs.Any(m => m.Text == "Existing Chat History") && msgs.Any(m => m.Text == "test")),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
mockOverrideChatMessageStore.Verify(s => s.InvokingAsync(
It.Is<ChatMessageStore.InvokingContext>(x => x.RequestMessages.Count() == 1),
It.IsAny<CancellationToken>()),
Times.Once);
mockOverrideChatMessageStore.Verify(s => s.InvokedAsync(
It.Is<ChatMessageStore.InvokedContext>(x => x.RequestMessages.Count() == 1 && x.ChatMessageStoreMessages != null && x.ChatMessageStoreMessages.Count() == 1 && x.ResponseMessages!.Count() == 1),
It.IsAny<CancellationToken>()),
Times.Once);
mockFactoryChatMessageStore.Verify(s => s.InvokingAsync(
It.IsAny<ChatMessageStore.InvokingContext>(),
It.IsAny<CancellationToken>()),
Times.Never);
mockFactoryChatMessageStore.Verify(s => s.InvokedAsync(
It.IsAny<ChatMessageStore.InvokedContext>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
#endregion
}

View File

@@ -0,0 +1,442 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Contains tests for <see cref="ChatOptions"/> merging in <see cref="ChatClientAgent"/>.
/// </summary>
public class ChatClientAgent_ChatOptionsMergingTests
{
/// <summary>
/// Verify that ChatOptions merging works when agent has ChatOptions but request doesn't.
/// </summary>
[Fact]
public async Task ChatOptionsMergingUsesAgentOptionsWhenRequestHasNoneAsync()
{
// Arrange
var agentChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f, Instructions = "test instructions" };
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.Equal(100, capturedChatOptions.MaxOutputTokens);
Assert.Equal(0.7f, capturedChatOptions.Temperature);
Assert.Equal("test instructions", capturedChatOptions.Instructions);
}
[Fact]
public async Task ChatOptionsMergingUsesAgentOptionsConstructorWhenRequestHasNoneAsync()
{
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.Equal("test instructions", capturedChatOptions.Instructions);
}
/// <summary>
/// Verify that ChatOptions merging works when request has ChatOptions but agent doesn't.
/// </summary>
[Fact]
public async Task ChatOptionsMergingUsesRequestOptionsWhenAgentHasNoneAsync()
{
// Arrange
var requestChatOptions = new ChatOptions { MaxOutputTokens = 200, Temperature = 0.3f, Instructions = "test instructions" };
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object);
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
// Assert
Assert.NotNull(capturedChatOptions);
Assert.Equivalent(requestChatOptions, capturedChatOptions); // Should be the same instance since no merging needed
Assert.Equal(200, capturedChatOptions.MaxOutputTokens);
Assert.Equal(0.3f, capturedChatOptions.Temperature);
Assert.Equal("test instructions", capturedChatOptions.Instructions);
}
/// <summary>
/// Verify that <see cref="ChatOptions"/> merging prioritizes <see cref="AgentRunOptions"/> over request <see cref="ChatOptions"/> and that in turn over agent level <see cref="ChatOptions"/>.
/// </summary>
[Fact]
public async Task ChatOptionsMergingPrioritizesRequestOptionsOverAgentOptionsAsync()
{
// Arrange
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
MaxOutputTokens = 100,
Temperature = 0.7f,
TopP = 0.9f,
ModelId = "agent-model",
AdditionalProperties = new AdditionalPropertiesDictionary { ["key1"] = "agent-value", ["key2"] = "agent-value", ["key3"] = "agent-value" }
};
var requestChatOptions = new ChatOptions
{
// TopP and ModelId not set, should use agent values
MaxOutputTokens = 200,
Temperature = 0.3f,
AdditionalProperties = new AdditionalPropertiesDictionary { ["key2"] = "request-value", ["key3"] = "request-value" },
Instructions = "request instructions"
};
var agentRunOptionsAdditionalProperties = new AdditionalPropertiesDictionary { ["key3"] = "runoptions-value" };
var expectedChatOptionsMerge = new ChatOptions
{
MaxOutputTokens = 200, // Request value takes priority
Temperature = 0.3f, // Request value takes priority
// Check that each level of precedence is respected in AdditionalProperties
AdditionalProperties = new AdditionalPropertiesDictionary { ["key1"] = "agent-value", ["key2"] = "request-value", ["key3"] = "runoptions-value" },
TopP = 0.9f, // Agent value used when request doesn't specify
ModelId = "agent-model", // Agent value used when request doesn't specify
Instructions = "test instructions\nrequest instructions" // Request is in addition to agent instructions
};
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions) { AdditionalProperties = agentRunOptionsAdditionalProperties });
// Assert
Assert.NotNull(capturedChatOptions);
Assert.Equivalent(expectedChatOptionsMerge, capturedChatOptions); // Should be the same instance (modified in place)
Assert.Equal(200, capturedChatOptions.MaxOutputTokens); // Request value takes priority
Assert.Equal(0.3f, capturedChatOptions.Temperature); // Request value takes priority
Assert.NotNull(capturedChatOptions.AdditionalProperties);
Assert.Equal("agent-value", capturedChatOptions.AdditionalProperties["key1"]); // Agent value used when request doesn't specify
Assert.Equal("request-value", capturedChatOptions.AdditionalProperties["key2"]); // Request ChatOptions value takes priority over agent ChatOptions value
Assert.Equal("runoptions-value", capturedChatOptions.AdditionalProperties["key3"]); // Run options value takes priority over request and agent ChatOptions values
Assert.Equal(0.9f, capturedChatOptions.TopP); // Agent value used when request doesn't specify
Assert.Equal("agent-model", capturedChatOptions.ModelId); // Agent value used when request doesn't specify
}
/// <summary>
/// Verify that ChatOptions merging returns null when both agent and request have no ChatOptions.
/// </summary>
[Fact]
public async Task ChatOptionsMergingReturnsNullWhenBothAgentAndRequestHaveNoneAsync()
{
// Arrange
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object);
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages);
// Assert
Assert.Null(capturedChatOptions);
}
/// <summary>
/// Verify that ChatOptions merging concatenates Tools from agent and request.
/// </summary>
[Fact]
public async Task ChatOptionsMergingConcatenatesToolsFromAgentAndRequestAsync()
{
// Arrange
var agentTool = AIFunctionFactory.Create(() => "agent tool");
var requestTool = AIFunctionFactory.Create(() => "request tool");
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
Tools = [agentTool]
};
var requestChatOptions = new ChatOptions
{
Tools = [requestTool]
};
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
// Assert
Assert.NotNull(capturedChatOptions);
Assert.NotNull(capturedChatOptions.Tools);
Assert.Equal(2, capturedChatOptions.Tools.Count);
// Request tools should come first, then agent tools
Assert.Contains(requestTool, capturedChatOptions.Tools);
Assert.Contains(agentTool, capturedChatOptions.Tools);
}
/// <summary>
/// Verify that ChatOptions merging uses agent Tools when request has no Tools.
/// </summary>
[Fact]
public async Task ChatOptionsMergingUsesAgentToolsWhenRequestHasNoToolsAsync()
{
// Arrange
var agentTool = AIFunctionFactory.Create(() => "agent tool");
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
Tools = [agentTool]
};
var requestChatOptions = new ChatOptions
{
// No Tools specified
MaxOutputTokens = 100
};
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
// Assert
Assert.NotNull(capturedChatOptions);
Assert.NotNull(capturedChatOptions.Tools);
Assert.Single(capturedChatOptions.Tools);
Assert.Contains(agentTool, capturedChatOptions.Tools); // Should contain the agent's tool
}
/// <summary>
/// Verify that ChatOptions merging uses RawRepresentationFactory from request first, with fallback to agent.
/// </summary>
[Theory]
[InlineData("MockAgentSetting", "MockRequestSetting", "MockRequestSetting")]
[InlineData("MockAgentSetting", null, "MockAgentSetting")]
[InlineData(null, "MockRequestSetting", "MockRequestSetting")]
public async Task ChatOptionsMergingUsesRawRepresentationFactoryWithFallbackAsync(string? agentSetting, string? requestSetting, string expectedSetting)
{
// Arrange
var agentChatOptions = new ChatOptions
{
Instructions = "test instructions",
RawRepresentationFactory = _ => agentSetting
};
var requestChatOptions = new ChatOptions
{
RawRepresentationFactory = _ => requestSetting
};
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
// Assert
Assert.NotNull(capturedChatOptions);
Assert.NotNull(capturedChatOptions.RawRepresentationFactory);
Assert.Equal(expectedSetting, capturedChatOptions.RawRepresentationFactory(null!));
}
/// <summary>
/// Verify that ChatOptions merging handles all scalar properties correctly.
/// </summary>
[Fact]
public async Task ChatOptionsMergingHandlesAllScalarPropertiesCorrectlyAsync()
{
// Arrange
var agentChatOptions = new ChatOptions
{
MaxOutputTokens = 100,
Temperature = 0.7f,
TopP = 0.9f,
TopK = 50,
PresencePenalty = 0.1f,
FrequencyPenalty = 0.2f,
Instructions = "agent instructions",
ModelId = "agent-model",
Seed = 12345,
ConversationId = "agent-conversation",
AllowMultipleToolCalls = true,
StopSequences = ["agent-stop"]
};
var requestChatOptions = new ChatOptions
{
MaxOutputTokens = 200,
Temperature = 0.3f,
Instructions = "request instructions",
// Other properties not set, should use agent values
StopSequences = ["request-stop"]
};
var expectedChatOptionsMerge = new ChatOptions
{
MaxOutputTokens = 200,
Temperature = 0.3f,
// Agent value used when request doesn't specify
TopP = 0.9f,
TopK = 50,
PresencePenalty = 0.1f,
FrequencyPenalty = 0.2f,
Instructions = "agent instructions\nrequest instructions",
ModelId = "agent-model",
Seed = 12345,
ConversationId = "agent-conversation",
AllowMultipleToolCalls = true,
// Merged StopSequences
StopSequences = ["request-stop", "agent-stop"]
};
Mock<IChatClient> mockService = new();
ChatOptions? capturedChatOptions = null;
mockService.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedChatOptions = opts)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
ChatClientAgent agent = new(mockService.Object, options: new()
{
ChatOptions = agentChatOptions
});
var messages = new List<ChatMessage> { new(ChatRole.User, "test") };
// Act
await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
// Assert
Assert.NotNull(capturedChatOptions);
Assert.Equivalent(expectedChatOptionsMerge, capturedChatOptions); // Should be the equivalent instance (modified in place)
// Request values should take priority
Assert.Equal(200, capturedChatOptions.MaxOutputTokens);
Assert.Equal(0.3f, capturedChatOptions.Temperature);
// Merge StopSequences
Assert.Equal(["request-stop", "agent-stop"], capturedChatOptions.StopSequences);
// Agent values should be used when request doesn't specify
Assert.Equal(0.9f, capturedChatOptions.TopP);
Assert.Equal(50, capturedChatOptions.TopK);
Assert.Equal(0.1f, capturedChatOptions.PresencePenalty);
Assert.Equal(0.2f, capturedChatOptions.FrequencyPenalty);
Assert.Equal("agent-model", capturedChatOptions.ModelId);
Assert.Equal(12345, capturedChatOptions.Seed);
Assert.Equal("agent-conversation", capturedChatOptions.ConversationId);
Assert.Equal(true, capturedChatOptions.AllowMultipleToolCalls);
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Contains unit tests for the ChatClientAgent.DeserializeThread methods.
/// </summary>
public class ChatClientAgent_DeserializeThreadTests
{
[Fact]
public async Task DeserializeThread_UsesAIContextProviderFactory_IfProvidedAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
var mockContextProvider = new Mock<AIContextProvider>();
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
AIContextProviderFactory = (_, _) =>
{
factoryCalled = true;
return new ValueTask<AIContextProvider>(mockContextProvider.Object);
}
});
var json = JsonSerializer.Deserialize("""
{
"aiContextProviderState": ["CP1"]
}
""", TestJsonSerializerContext.Default.JsonElement);
// Act
var thread = await agent.DeserializeThreadAsync(json);
// Assert
Assert.True(factoryCalled, "AIContextProviderFactory was not called.");
Assert.IsType<ChatClientAgentThread>(thread);
var typedThread = (ChatClientAgentThread)thread;
Assert.Same(mockContextProvider.Object, typedThread.AIContextProvider);
}
[Fact]
public async Task DeserializeThread_UsesChatMessageStoreFactory_IfProvidedAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
var mockMessageStore = new Mock<ChatMessageStore>();
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
ChatMessageStoreFactory = (_, _) =>
{
factoryCalled = true;
return new ValueTask<ChatMessageStore>(mockMessageStore.Object);
}
});
var json = JsonSerializer.Deserialize("""
{
"storeState": { }
}
""", TestJsonSerializerContext.Default.JsonElement);
// Act
var thread = await agent.DeserializeThreadAsync(json);
// Assert
Assert.True(factoryCalled, "ChatMessageStoreFactory was not called.");
Assert.IsType<ChatClientAgentThread>(thread);
var typedThread = (ChatClientAgentThread)thread;
Assert.Same(mockMessageStore.Object, typedThread.MessageStore);
}
}

View File

@@ -0,0 +1,101 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Contains unit tests for the ChatClientAgent.GetNewThreadAsync methods.
/// </summary>
public class ChatClientAgent_GetNewThreadTests
{
[Fact]
public async Task GetNewThread_UsesAIContextProviderFactory_IfProvidedAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
var mockContextProvider = new Mock<AIContextProvider>();
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
AIContextProviderFactory = (_, _) =>
{
factoryCalled = true;
return new ValueTask<AIContextProvider>(mockContextProvider.Object);
}
});
// Act
var thread = await agent.GetNewThreadAsync();
// Assert
Assert.True(factoryCalled, "AIContextProviderFactory was not called.");
Assert.IsType<ChatClientAgentThread>(thread);
var typedThread = (ChatClientAgentThread)thread;
Assert.Same(mockContextProvider.Object, typedThread.AIContextProvider);
}
[Fact]
public async Task GetNewThread_UsesChatMessageStoreFactory_IfProvidedAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
var mockMessageStore = new Mock<ChatMessageStore>();
var factoryCalled = false;
var agent = new ChatClientAgent(mockChatClient.Object, new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Test instructions" },
ChatMessageStoreFactory = (_, _) =>
{
factoryCalled = true;
return new ValueTask<ChatMessageStore>(mockMessageStore.Object);
}
});
// Act
var thread = await agent.GetNewThreadAsync();
// Assert
Assert.True(factoryCalled, "ChatMessageStoreFactory was not called.");
Assert.IsType<ChatClientAgentThread>(thread);
var typedThread = (ChatClientAgentThread)thread;
Assert.Same(mockMessageStore.Object, typedThread.MessageStore);
}
[Fact]
public async Task GetNewThread_UsesChatMessageStore_FromTypedOverloadAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
var mockMessageStore = new Mock<ChatMessageStore>();
var agent = new ChatClientAgent(mockChatClient.Object);
// Act
var thread = await agent.GetNewThreadAsync(mockMessageStore.Object);
// Assert
Assert.IsType<ChatClientAgentThread>(thread);
var typedThread = (ChatClientAgentThread)thread;
Assert.Same(mockMessageStore.Object, typedThread.MessageStore);
}
[Fact]
public async Task GetNewThread_UsesConversationId_FromTypedOverloadAsync()
{
// Arrange
var mockChatClient = new Mock<IChatClient>();
const string TestConversationId = "test_conversation_id";
var agent = new ChatClientAgent(mockChatClient.Object);
// Act
var thread = await agent.GetNewThreadAsync(TestConversationId);
// Assert
Assert.IsType<ChatClientAgentThread>(thread);
var typedThread = (ChatClientAgentThread)thread;
Assert.Equal(TestConversationId, typedThread.ConversationId);
}
}

View File

@@ -0,0 +1,456 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Tests for <see cref="ChatClientAgent"/> run methods with <see cref="ChatClientAgentRunOptions"/>.
/// </summary>
public sealed partial class ChatClientAgent_RunWithCustomOptionsTests
{
#region RunAsync Tests
[Fact]
public async Task RunAsync_WithThreadAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
ChatClientAgentRunOptions options = new();
// Act
AgentResponse result = await agent.RunAsync(thread, options);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsync_WithStringMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
ChatClientAgentRunOptions options = new();
// Act
AgentResponse result = await agent.RunAsync("Test message", thread, options);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Any(m => m.Text == "Test message")),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsync_WithChatMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage message = new(ChatRole.User, "Test message");
ChatClientAgentRunOptions options = new();
// Act
AgentResponse result = await agent.RunAsync(message, thread, options);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Contains(message)),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsync_WithMessagesCollectionAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
IEnumerable<ChatMessage> messages = [new(ChatRole.User, "Message 1"), new(ChatRole.User, "Message 2")];
ChatClientAgentRunOptions options = new();
// Act
AgentResponse result = await agent.RunAsync(messages, thread, options);
// Assert
Assert.NotNull(result);
Assert.Single(result.Messages);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsync_WithChatOptionsInRunOptions_UsesChatOptionsAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")]));
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentRunOptions options = new(new ChatOptions { Temperature = 0.5f });
// Act
AgentResponse result = await agent.RunAsync("Test", null, options);
// Assert
Assert.NotNull(result);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.Is<ChatOptions>(opts => opts.Temperature == 0.5f),
It.IsAny<CancellationToken>()),
Times.Once);
}
#endregion
#region RunStreamingAsync Tests
[Fact]
public async Task RunStreamingAsync_WithThreadAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
ChatClientAgentRunOptions options = new();
// Act
var updates = new List<AgentResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(thread, options))
{
updates.Add(update);
}
// Assert
Assert.NotEmpty(updates);
mockChatClient.Verify(
x => x.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_WithStringMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
ChatClientAgentRunOptions options = new();
// Act
var updates = new List<AgentResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync("Test message", thread, options))
{
updates.Add(update);
}
// Assert
Assert.NotEmpty(updates);
mockChatClient.Verify(
x => x.GetStreamingResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Any(m => m.Text == "Test message")),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_WithChatMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage message = new(ChatRole.User, "Test message");
ChatClientAgentRunOptions options = new();
// Act
var updates = new List<AgentResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(message, thread, options))
{
updates.Add(update);
}
// Assert
Assert.NotEmpty(updates);
mockChatClient.Verify(
x => x.GetStreamingResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Contains(message)),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_WithMessagesCollectionAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).Returns(GetAsyncUpdatesAsync());
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
IEnumerable<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Message 1"), new ChatMessage(ChatRole.User, "Message 2")];
ChatClientAgentRunOptions options = new();
// Act
var updates = new List<AgentResponseUpdate>();
await foreach (var update in agent.RunStreamingAsync(messages, thread, options))
{
updates.Add(update);
}
// Assert
Assert.NotEmpty(updates);
mockChatClient.Verify(
x => x.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
#endregion
#region Helper Methods
private static async IAsyncEnumerable<ChatResponseUpdate> GetAsyncUpdatesAsync()
{
yield return new ChatResponseUpdate { Contents = new[] { new TextContent("Hello") } };
yield return new ChatResponseUpdate { Contents = new[] { new TextContent(" World") } };
await Task.CompletedTask;
}
#endregion
#region RunAsync{T} Tests
[Fact]
public async Task RunAsyncOfT_WithThreadAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
ChatClientAgentRunOptions options = new();
// Act
AgentResponse<Animal> agentResponse = await agent.RunAsync<Animal>(thread, JsonContext_WithCustomRunOptions.Default.Options, options);
// Assert
Assert.NotNull(agentResponse);
Assert.Single(agentResponse.Messages);
Assert.Equal("Tigger", agentResponse.Result.FullName);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsyncOfT_WithStringMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
ChatClientAgentRunOptions options = new();
// Act
AgentResponse<Animal> agentResponse = await agent.RunAsync<Animal>("Test message", thread, JsonContext_WithCustomRunOptions.Default.Options, options);
// Assert
Assert.NotNull(agentResponse);
Assert.Single(agentResponse.Messages);
Assert.Equal("Tigger", agentResponse.Result.FullName);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Any(m => m.Text == "Test message")),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsyncOfT_WithChatMessageAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
ChatMessage message = new(ChatRole.User, "Test message");
ChatClientAgentRunOptions options = new();
// Act
AgentResponse<Animal> agentResponse = await agent.RunAsync<Animal>(message, thread, JsonContext_WithCustomRunOptions.Default.Options, options);
// Assert
Assert.NotNull(agentResponse);
Assert.Single(agentResponse.Messages);
Assert.Equal("Tigger", agentResponse.Result.FullName);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.Is<IEnumerable<ChatMessage>>(msgs => msgs.Contains(message)),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task RunAsyncOfT_WithMessagesCollectionAndOptions_CallsBaseMethodAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
mockChatClient.Setup(
s => s.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")]));
ChatClientAgent agent = new(mockChatClient.Object);
AgentThread thread = await agent.GetNewThreadAsync();
IEnumerable<ChatMessage> messages = [new(ChatRole.User, "Message 1"), new(ChatRole.User, "Message 2")];
ChatClientAgentRunOptions options = new();
// Act
AgentResponse<Animal> agentResponse = await agent.RunAsync<Animal>(messages, thread, JsonContext_WithCustomRunOptions.Default.Options, options);
// Assert
Assert.NotNull(agentResponse);
Assert.Single(agentResponse.Messages);
Assert.Equal("Tigger", agentResponse.Result.FullName);
mockChatClient.Verify(
x => x.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Once);
}
#endregion
private sealed class Animal
{
public int Id { get; set; }
public string? FullName { get; set; }
public Species Species { get; set; }
}
private enum Species
{
Bear,
Tiger,
Walrus,
}
[JsonSourceGenerationOptions(UseStringEnumConverter = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Animal))]
private sealed partial class JsonContext_WithCustomRunOptions : JsonSerializerContext;
}

View File

@@ -0,0 +1,215 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Contains unit tests for the <see cref="ChatClientBuilderExtensions"/> class.
/// </summary>
public sealed class ChatClientBuilderExtensionsTests
{
[Fact]
public void BuildAIAgent_WithBasicParameters_CreatesAgent()
{
// Arrange
var innerChatClientMock = new Mock<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
// Act
var agent = builder.BuildAIAgent(
instructions: "Test instructions",
name: "TestAgent",
description: "Test description"
);
// Assert
Assert.NotNull(agent);
Assert.Equal("TestAgent", agent.Name);
Assert.Equal("Test description", agent.Description);
Assert.Equal("Test instructions", agent.Instructions);
}
[Fact]
public void BuildAIAgent_WithTools_SetsToolsInOptions()
{
// Arrange
var innerChatClientMock = new Mock<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
var tools = new List<AITool> { new Mock<AITool>().Object };
// Act
var agent = builder.BuildAIAgent(tools: tools);
// Assert
Assert.NotNull(agent);
Assert.NotNull(agent.ChatOptions);
Assert.Equal(tools, agent.ChatOptions.Tools);
}
[Fact]
public void BuildAIAgent_WithAllParameters_CreatesAgentCorrectly()
{
// Arrange
var innerChatClientMock = new Mock<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
var tools = new List<AITool> { new Mock<AITool>().Object };
var loggerFactoryMock = new Mock<ILoggerFactory>();
var serviceProviderMock = new Mock<IServiceProvider>();
// Act
var agent = builder.BuildAIAgent(
instructions: "Complex instructions",
name: "ComplexAgent",
description: "Complex description",
tools: tools,
loggerFactory: loggerFactoryMock.Object,
services: serviceProviderMock.Object
);
// Assert
Assert.NotNull(agent);
Assert.Equal("ComplexAgent", agent.Name);
Assert.Equal("Complex description", agent.Description);
Assert.Equal("Complex instructions", agent.Instructions);
Assert.NotNull(agent.ChatOptions);
Assert.Equal(tools, agent.ChatOptions.Tools);
}
[Fact]
public void BuildAIAgent_WithOptions_CreatesAgentWithOptions()
{
// Arrange
var innerChatClientMock = new Mock<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
var options = new ChatClientAgentOptions
{
Name = "AgentWithOptions",
Description = "Desc",
ChatOptions = new() { Instructions = "Instr" },
UseProvidedChatClientAsIs = true
};
// Act
var agent = builder.BuildAIAgent(options);
// Assert
Assert.NotNull(agent);
Assert.Equal("AgentWithOptions", agent.Name);
Assert.Equal("Desc", agent.Description);
Assert.Equal("Instr", agent.Instructions);
}
[Fact]
public void BuildAIAgent_WithOptionsAndServices_CreatesAgentCorrectly()
{
// Arrange
var innerChatClientMock = new Mock<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
var loggerFactoryMock = new Mock<ILoggerFactory>();
var serviceProviderMock = new Mock<IServiceProvider>();
var options = new ChatClientAgentOptions
{
Name = "ServiceAgent",
ChatOptions = new() { Instructions = "Service instructions" }
};
// Act
var agent = builder.BuildAIAgent(
options: options,
loggerFactory: loggerFactoryMock.Object,
services: serviceProviderMock.Object
);
// Assert
Assert.NotNull(agent);
Assert.Equal("ServiceAgent", agent.Name);
Assert.Equal("Service instructions", agent.Instructions);
}
[Fact]
public void BuildAIAgent_WithNullBuilder_Throws()
{
// Arrange
ChatClientBuilder builder = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => builder.BuildAIAgent(instructions: "instructions"));
}
[Fact]
public void BuildAIAgent_WithNullBuilderAndOptions_Throws()
{
// Arrange
ChatClientBuilder builder = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => builder.BuildAIAgent(options: new() { ChatOptions = new() { Instructions = "instructions" } }));
}
[Fact]
public void BuildAIAgent_WithMiddleware_BuildsCorrectPipeline()
{
// Arrange
var innerChatClientMock = new Mock<IChatClient>();
var middlewareChatClientMock = new Mock<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
// Add middleware that returns our mock
builder.Use((client, services) => middlewareChatClientMock.Object);
// Act
var agent = builder.BuildAIAgent(
new ChatClientAgentOptions
{
ChatOptions = new() { Instructions = "Middleware test" },
UseProvidedChatClientAsIs = true
}
);
// Assert
Assert.NotNull(agent);
Assert.Equal("Middleware test", agent.Instructions);
// When UseProvidedChatClientAsIs is true, the agent should use the middleware chat client directly
Assert.Same(middlewareChatClientMock.Object, agent.ChatClient);
}
[Fact]
public void BuildAIAgent_WithNullOptions_CreatesAgentWithDefaults()
{
// Arrange
var innerChatClientMock = new Mock<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
// Act
var agent = builder.BuildAIAgent(options: null);
// Assert
Assert.NotNull(agent);
Assert.Null(agent.Name);
Assert.Null(agent.Description);
Assert.Null(agent.Instructions);
}
[Fact]
public void BuildAIAgent_WithEmptyParameters_CreatesMinimalAgent()
{
// Arrange
var innerChatClientMock = new Mock<IChatClient>();
var builder = new ChatClientBuilder(innerChatClientMock.Object);
// Act
var agent = builder.BuildAIAgent();
// Assert
Assert.NotNull(agent);
Assert.Null(agent.Name);
Assert.Null(agent.Description);
Assert.Null(agent.Instructions);
Assert.Null(agent.ChatOptions);
}
}

View File

@@ -0,0 +1,94 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests;
/// <summary>
/// Contains unit tests for the ChatClientExtensions class.
/// </summary>
public sealed class ChatClientExtensionsTests
{
[Fact]
public void CreateAIAgent_WithBasicParameters_CreatesAgent()
{
// Arrange
var chatClientMock = new Mock<IChatClient>();
// Act
var agent = chatClientMock.Object.AsAIAgent(
instructions: "Test instructions",
name: "TestAgent",
description: "Test description"
);
// Assert
Assert.NotNull(agent);
Assert.Equal("TestAgent", agent.Name);
Assert.Equal("Test description", agent.Description);
Assert.Equal("Test instructions", agent.Instructions);
}
[Fact]
public void CreateAIAgent_WithTools_SetsToolsInOptions()
{
// Arrange
var chatClientMock = new Mock<IChatClient>();
var tools = new List<AITool> { new Mock<AITool>().Object };
// Act
var agent = chatClientMock.Object.AsAIAgent(tools: tools);
// Assert
Assert.NotNull(agent);
Assert.NotNull(agent.ChatOptions);
Assert.Equal(tools, agent.ChatOptions.Tools);
}
[Fact]
public void CreateAIAgent_WithOptions_CreatesAgentWithOptions()
{
// Arrange
var chatClientMock = new Mock<IChatClient>();
var options = new ChatClientAgentOptions
{
Name = "AgentWithOptions",
Description = "Desc",
ChatOptions = new() { Instructions = "Instr" },
UseProvidedChatClientAsIs = true
};
// Act
var agent = chatClientMock.Object.AsAIAgent(options);
// Assert
Assert.NotNull(agent);
Assert.Equal("AgentWithOptions", agent.Name);
Assert.Equal("Desc", agent.Description);
Assert.Equal("Instr", agent.Instructions);
Assert.Same(chatClientMock.Object, agent.ChatClient);
}
[Fact]
public void CreateAIAgent_WithNullClient_Throws()
{
// Arrange
IChatClient chatClient = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => chatClient.AsAIAgent(instructions: "instructions"));
}
[Fact]
public void CreateAIAgent_WithNullClientAndOptions_Throws()
{
// Arrange
IChatClient chatClient = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => chatClient.AsAIAgent(options: new() { ChatOptions = new() { Instructions = "instructions" } }));
}
}