// Copyright (c) Microsoft. All rights reserved. using System; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Moq; using Valkey.Glide; namespace Microsoft.Agents.AI.Valkey.UnitTests; /// /// Unit tests for . /// public sealed class ValkeyChatHistoryProviderTests { private static Mock CreateMockConnection(Mock? dbMock = null) { var mockConnection = new Mock(); dbMock ??= new Mock(); mockConnection.Setup(c => c.GetDatabase()).Returns(dbMock.Object); return mockConnection; } // --- Constructor tests --- [Fact] public void Constructor_WithConnection_SetsProperties() { // Arrange & Act var provider = new ValkeyChatHistoryProvider( CreateMockConnection().Object, static (_) => new ValkeyChatHistoryProvider.State("conv-1"), new ValkeyChatHistoryProviderOptions { KeyPrefix = "test_prefix" }); // Assert Assert.NotNull(provider); } [Fact] public void Constructor_WithConnection_NullConnection_Throws() { // Act & Assert Assert.Throws(() => new ValkeyChatHistoryProvider( null!, static (_) => new ValkeyChatHistoryProvider.State("conv-1"))); } [Fact] public void Constructor_WithConnection_NullStateInitializer_Throws() { // Act & Assert Assert.Throws(() => new ValkeyChatHistoryProvider( CreateMockConnection().Object, null!)); } // --- State tests --- [Fact] public void State_NullConversationId_Throws() { Assert.Throws(() => new ValkeyChatHistoryProvider.State(null!)); } [Fact] public void State_EmptyConversationId_Throws() { Assert.Throws(() => new ValkeyChatHistoryProvider.State("")); } [Fact] public void State_ValidConversationId_SetsProperty() { var state = new ValkeyChatHistoryProvider.State("my-conversation"); Assert.Equal("my-conversation", state.ConversationId); } [Fact] public void State_JsonConstructor_RoundTrips() { // Arrange var original = new ValkeyChatHistoryProvider.State("test-conv"); // Act var json = JsonSerializer.Serialize(original); var deserialized = JsonSerializer.Deserialize(json); // Assert Assert.NotNull(deserialized); Assert.Equal("test-conv", deserialized.ConversationId); } // --- StateKeys tests --- [Fact] public void StateKeys_ReturnsProviderTypeName() { var provider = new ValkeyChatHistoryProvider( CreateMockConnection().Object, _ => new ValkeyChatHistoryProvider.State("conv-1")); var keys = provider.StateKeys; Assert.Single(keys); Assert.Equal(nameof(ValkeyChatHistoryProvider), keys[0]); } [Fact] public void StateKeys_WithCustomKey_ReturnsCustomKey() { var provider = new ValkeyChatHistoryProvider( CreateMockConnection().Object, _ => new ValkeyChatHistoryProvider.State("conv-1"), new ValkeyChatHistoryProviderOptions { StateKey = "custom_key" }); var keys = provider.StateKeys; Assert.Single(keys); Assert.Equal("custom_key", keys[0]); } // --- ProvideChatHistoryAsync tests --- [Fact] public async Task ProvideChatHistoryAsync_ReturnsDeserializedMessagesAsync() { // Arrange var dbMock = new Mock(); var msg1 = new ChatMessage(ChatRole.User, "hello"); var msg2 = new ChatMessage(ChatRole.Assistant, "hi there"); var values = new ValkeyValue[] { JsonSerializer.Serialize(msg1), JsonSerializer.Serialize(msg2) }; dbMock.Setup(d => d.ListRangeAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(values); var provider = new ValkeyChatHistoryProvider( CreateMockConnection(dbMock).Object, _ => new ValkeyChatHistoryProvider.State("conv-1")); var context = TestHelpers.CreateChatHistoryInvokingContext(); // Act — should not throw var result = await provider.InvokingAsync(context); var messages = result.ToList(); // Assert — only the valid message + request message Assert.True(messages.Count >= 1); } [Fact] public async Task ProvideChatHistoryAsync_WithMaxMessagesToRetrieve_UsesRangeQueryAsync() { // Arrange var dbMock = new Mock(); dbMock.Setup(d => d.ListRangeAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync([]); var provider = new ValkeyChatHistoryProvider( CreateMockConnection(dbMock).Object, _ => new ValkeyChatHistoryProvider.State("conv-1"), new ValkeyChatHistoryProviderOptions { MaxMessagesToRetrieve = 5 }); var context = TestHelpers.CreateChatHistoryInvokingContext(); // Act await provider.InvokingAsync(context); // Assert — should use -5, -1 range dbMock.Verify(d => d.ListRangeAsync( It.IsAny(), -5, -1), Times.Once); } [Fact] public async Task ProvideChatHistoryAsync_CancellationToken_ThrowsAsync() { // Arrange var provider = new ValkeyChatHistoryProvider( CreateMockConnection().Object, _ => new ValkeyChatHistoryProvider.State("conv-1")); var cts = new CancellationTokenSource(); cts.Cancel(); var context = TestHelpers.CreateChatHistoryInvokingContext(); // Act & Assert await Assert.ThrowsAsync(() => provider.InvokingAsync(context, cts.Token).AsTask()); } // --- StoreChatHistoryAsync tests --- [Fact] public async Task StoreChatHistoryAsync_BatchPushesMessagesAsync() { // Arrange var dbMock = new Mock(); dbMock.Setup(d => d.ListRightPushAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(2); var provider = new ValkeyChatHistoryProvider( CreateMockConnection(dbMock).Object, _ => new ValkeyChatHistoryProvider.State("conv-1")); var context = TestHelpers.CreateChatHistoryInvokedContext( [new ChatMessage(ChatRole.User, "hello")], [new ChatMessage(ChatRole.Assistant, "hi")]); // Act await provider.InvokedAsync(context); // Assert — batch push called once with array dbMock.Verify(d => d.ListRightPushAsync( It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task StoreChatHistoryAsync_WithMaxMessages_TrimsAsync() { // Arrange var dbMock = new Mock(); dbMock.Setup(d => d.ListRightPushAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(1); dbMock.Setup(d => d.ListTrimAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); var provider = new ValkeyChatHistoryProvider( CreateMockConnection(dbMock).Object, _ => new ValkeyChatHistoryProvider.State("conv-1"), new ValkeyChatHistoryProviderOptions { MaxMessages = 10 }); var context = TestHelpers.CreateChatHistoryInvokedContext( [new ChatMessage(ChatRole.User, "hello")], [new ChatMessage(ChatRole.Assistant, "hi")]); // Act await provider.InvokedAsync(context); // Assert — trim called unconditionally when MaxMessages is set dbMock.Verify(d => d.ListTrimAsync( It.IsAny(), -10, -1), Times.Once); } }