diff --git a/ChatBot.Tests/Models/ChatSessionCompressionTests.cs b/ChatBot.Tests/Models/ChatSessionCompressionTests.cs new file mode 100644 index 0000000..4ef094c --- /dev/null +++ b/ChatBot.Tests/Models/ChatSessionCompressionTests.cs @@ -0,0 +1,240 @@ +using ChatBot.Models; +using ChatBot.Models.Dto; +using ChatBot.Services.Interfaces; +using FluentAssertions; +using Moq; +using OllamaSharp.Models.Chat; +using System.Collections.Concurrent; + +namespace ChatBot.Tests.Models; + +public class ChatSessionCompressionTests +{ + [Fact] + public async Task CompressHistoryAsync_ShouldCompressMessages_WhenCompressionServiceAvailable() + { + // Arrange + var session = new ChatSession(); + var compressionServiceMock = new Mock(); + session.SetCompressionService(compressionServiceMock.Object); + + // Setup compression service to return compressed messages + var compressedMessages = new List + { + new ChatMessage { Role = ChatRole.System.ToString(), Content = "System prompt" }, + new ChatMessage { Role = ChatRole.User.ToString(), Content = "Compressed user message" } + }; + compressionServiceMock + .Setup(x => x.CompressHistoryAsync(It.IsAny>(), It.IsAny(), It.IsAny())) + .ReturnsAsync(compressedMessages); + compressionServiceMock + .Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())) + .Returns(true); + + // Add messages to session + for (int i = 0; i < 10; i++) + { + session.AddMessage(new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" }); + } + + // Act + await session.AddMessageWithCompressionAsync( + new ChatMessage { Role = ChatRole.User, Content = "New message" }, + compressionThreshold: 5, + compressionTarget: 2 + ); + + // Assert + var messages = session.GetAllMessages(); + messages.Should().HaveCount(2); + messages[0].Role.Should().Be(ChatRole.System); + messages[1].Role.Should().Be(ChatRole.User); + messages[1].Content.Should().Be("Compressed user message"); + } + + [Fact] + public async Task CompressHistoryAsync_ShouldFallbackToTrimming_WhenCompressionFails() + { + // Arrange + var session = new ChatSession { MaxHistoryLength = 3 }; + var compressionServiceMock = new Mock(); + session.SetCompressionService(compressionServiceMock.Object); + + // Setup compression service to throw an exception + var exception = new Exception("Compression failed"); + compressionServiceMock + .Setup(x => x.CompressHistoryAsync(It.IsAny>(), It.IsAny(), It.IsAny())) + .ThrowsAsync(exception); + compressionServiceMock + .Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())) + .Returns(true); + + // Add messages to session + for (int i = 0; i < 5; i++) + { + session.AddMessage(new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" }); + } + + // Act + await session.AddMessageWithCompressionAsync( + new ChatMessage { Role = ChatRole.User, Content = "New message" }, + compressionThreshold: 3, + compressionTarget: 2 + ); + + // Assert - Should fall back to simple trimming + var messages = session.GetAllMessages(); + messages.Should().HaveCount(3); + } + + [Fact] + public async Task AddMessageWithCompressionAsync_ShouldNotCompress_WhenBelowThreshold() + { + // Arrange + var session = new ChatSession(); + var compressionServiceMock = new Mock(); + session.SetCompressionService(compressionServiceMock.Object); + + // Setup compression service to return false for ShouldCompress when count is below threshold + compressionServiceMock + .Setup(x => x.ShouldCompress(It.Is(c => c < 5), It.Is(t => t == 5))) + .Returns(false); + + // Add messages to session (below threshold) + session.AddMessage(new ChatMessage { Role = ChatRole.User, Content = "Message 1" }); + session.AddMessage(new ChatMessage { Role = ChatRole.Assistant, Content = "Response 1" }); + + // Act - Set threshold higher than current message count + await session.AddMessageWithCompressionAsync( + new ChatMessage { Role = ChatRole.User, Content = "Message 2" }, + compressionThreshold: 5, + compressionTarget: 2 + ); + + // Assert - Should not call compression service + compressionServiceMock.Verify( + x => x.CompressHistoryAsync(It.IsAny>(), It.IsAny(), It.IsAny()), + Times.Never + ); + + var messages = session.GetAllMessages(); + messages.Should().HaveCount(3); + } + + [Fact] + public async Task AddMessageWithCompressionAsync_ShouldHandleConcurrentAccess() + { + // Arrange + var session = new ChatSession(); + var compressionServiceMock = new Mock(); + session.SetCompressionService(compressionServiceMock.Object); + + // Setup compression service to simulate processing time + var delayedResult = new List + { + new ChatMessage { Role = ChatRole.System.ToString(), Content = "Compressed" } + }; + compressionServiceMock + .Setup(x => x.CompressHistoryAsync(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns(async (List messages, int target, CancellationToken ct) => + { + await Task.Delay(50); + return delayedResult; + }); + compressionServiceMock + .Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())) + .Returns(true); + + var tasks = new List(); + int messageCount = 5; + + // Act - Start multiple concurrent operations + for (int i = 0; i < messageCount; i++) + { + tasks.Add( + session.AddMessageWithCompressionAsync( + new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" }, + compressionThreshold: 2, + compressionTarget: 1 + ) + ); + } + + // Wait for all operations to complete + await Task.WhenAll(tasks); + + // Assert - Should handle concurrent access without exceptions + // and maintain thread safety + session.GetMessageCount().Should().Be(1); + } + + [Fact] + public void SetCompressionService_ShouldNotThrow_WhenCalledMultipleTimes() + { + // Arrange + var session = new ChatSession(); + var compressionService1 = new Mock().Object; + var compressionService2 = new Mock().Object; + + // Act & Assert + session.Invoking(s => s.SetCompressionService(compressionService1)).Should().NotThrow(); + + // Should not throw when setting a different service + session.Invoking(s => s.SetCompressionService(compressionService2)).Should().NotThrow(); + } + + [Fact] + public async Task CompressHistoryAsync_ShouldPreserveSystemMessage_WhenCompressing() + { + // Arrange + var session = new ChatSession(); + var compressionServiceMock = new Mock(); + session.SetCompressionService(compressionServiceMock.Object); + + // Setup compression service to preserve system message + compressionServiceMock + .Setup(x => x.CompressHistoryAsync(It.IsAny>(), It.IsAny(), It.IsAny())) + .Returns((List messages, int target, CancellationToken ct) => + { + var systemMessage = messages.FirstOrDefault(m => m.Role == ChatRole.System.ToString()); + var compressed = new List(); + + if (systemMessage != null) + { + compressed.Add(systemMessage); + } + + compressed.Add(new ChatMessage + { + Role = ChatRole.User.ToString(), + Content = "Compressed user messages" + }); + + return Task.FromResult(compressed); + }); + compressionServiceMock + .Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())) + .Returns(true); + + // Add system message and some user messages + session.AddMessage(new ChatMessage { Role = ChatRole.System, Content = "System prompt" }); + for (int i = 0; i < 10; i++) + { + session.AddMessage(new ChatMessage { Role = ChatRole.User, Content = $"Message {i}" }); + } + + // Act + await session.AddMessageWithCompressionAsync( + new ChatMessage { Role = ChatRole.User, Content = "New message" }, + compressionThreshold: 5, + compressionTarget: 2 + ); + + // Assert - System message should be preserved + var messages = session.GetAllMessages(); + messages.Should().HaveCount(2); + messages[0].Role.Should().Be(ChatRole.System); + messages[0].Content.Should().Be("System prompt"); + messages[1].Content.Should().Be("Compressed user messages"); + } +}