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