// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Agents.AI.Hosting.OpenAI.Conversations; using Microsoft.Agents.AI.Hosting.OpenAI.Conversations.Models; using Microsoft.Agents.AI.Hosting.OpenAI.Models; using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; /// /// Unit tests for InMemoryConversationStorage implementation. /// public sealed class InMemoryConversationStorageTests { [Fact] public async Task CreateConversationAsync_SuccessAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_test123", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = new Dictionary { ["key"] = "value" } }; // Act Conversation result = await storage.CreateConversationAsync(conversation); // Assert Assert.NotNull(result); Assert.Equal(conversation.Id, result.Id); Assert.Equal(conversation.CreatedAt, result.CreatedAt); Assert.NotNull(result.Metadata); Assert.Equal("value", result.Metadata["key"]); } [Fact] public async Task CreateConversationAsync_DuplicateId_ThrowsInvalidOperationExceptionAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_duplicate", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); // Act & Assert var exception = await Assert.ThrowsAsync( () => storage.CreateConversationAsync(conversation)); Assert.Contains("already exists", exception.Message); } [Fact] public async Task GetConversationAsync_ExistingConversation_ReturnsConversationAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_get123", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); // Act Conversation? result = await storage.GetConversationAsync("conv_get123"); // Assert Assert.NotNull(result); Assert.Equal(conversation.Id, result.Id); } [Fact] public async Task GetConversationAsync_NonExistentConversation_ReturnsNullAsync() { // Arrange var storage = new InMemoryConversationStorage(); // Act Conversation? result = await storage.GetConversationAsync("conv_nonexistent"); // Assert Assert.Null(result); } [Fact] public async Task UpdateConversationAsync_ExistingConversation_UpdatesSuccessfullyAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_update123", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = new Dictionary { ["original"] = "value" } }; await storage.CreateConversationAsync(conversation); var updatedConversation = new Conversation { Id = "conv_update123", CreatedAt = conversation.CreatedAt, Metadata = new Dictionary { ["updated"] = "newvalue" } }; // Act Conversation? result = await storage.UpdateConversationAsync(updatedConversation); // Assert Assert.NotNull(result); Assert.Equal(updatedConversation.Id, result.Id); Assert.NotNull(result.Metadata); Assert.Equal("newvalue", result.Metadata["updated"]); // Verify the update persisted Conversation? retrieved = await storage.GetConversationAsync("conv_update123"); Assert.NotNull(retrieved); Assert.Equal("newvalue", retrieved.Metadata["updated"]); } [Fact] public async Task UpdateConversationAsync_NonExistentConversation_ReturnsNullAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_nonexistent", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; // Act Conversation? result = await storage.UpdateConversationAsync(conversation); // Assert Assert.Null(result); } [Fact] public async Task DeleteConversationAsync_ExistingConversation_ReturnsTrueAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_delete123", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); // Act bool result = await storage.DeleteConversationAsync("conv_delete123"); // Assert Assert.True(result); // Verify deletion Conversation? retrieved = await storage.GetConversationAsync("conv_delete123"); Assert.Null(retrieved); } [Fact] public async Task DeleteConversationAsync_NonExistentConversation_ReturnsFalseAsync() { // Arrange var storage = new InMemoryConversationStorage(); // Act bool result = await storage.DeleteConversationAsync("conv_nonexistent"); // Assert Assert.False(result); } [Fact] public async Task AddItemsAsync_SuccessAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_items123", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); var item = new ResponsesUserMessageItemResource { Id = "msg_test123", Content = [new ItemContentInputText { Text = "Hello" }] }; // Act await storage.AddItemsAsync("conv_items123", [item]); // Assert ItemResource? result = await storage.GetItemAsync("conv_items123", item.Id); Assert.NotNull(result); Assert.Equal(item.Id, result.Id); } [Fact] public async Task AddItemsAsync_NonExistentConversation_ThrowsInvalidOperationExceptionAsync() { // Arrange var storage = new InMemoryConversationStorage(); var item = new ResponsesUserMessageItemResource { Id = "msg_test123", Content = [new ItemContentInputText { Text = "Hello" }] }; // Act & Assert var exception = await Assert.ThrowsAsync( () => storage.AddItemsAsync("conv_nonexistent", [item])); Assert.Contains("not found", exception.Message); } [Fact] public async Task AddItemsAsync_DuplicateItemId_ThrowsInvalidOperationExceptionAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_dup_items", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); var item = new ResponsesUserMessageItemResource { Id = "msg_duplicate", Content = [new ItemContentInputText { Text = "Hello" }] }; await storage.AddItemsAsync("conv_dup_items", [item]); // Act & Assert var exception = await Assert.ThrowsAsync( () => storage.AddItemsAsync("conv_dup_items", [item])); Assert.Contains("already exists", exception.Message); } [Fact] public async Task GetItemAsync_ExistingItem_ReturnsItemAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_getitem", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); var item = new ResponsesUserMessageItemResource { Id = "msg_getitem123", Content = [new ItemContentInputText { Text = "Test message" }] }; await storage.AddItemsAsync("conv_getitem", [item]); // Act ItemResource? result = await storage.GetItemAsync("conv_getitem", "msg_getitem123"); // Assert Assert.NotNull(result); Assert.Equal(item.Id, result.Id); var userMessage = Assert.IsType(result); Assert.NotEmpty(userMessage.Content); } [Fact] public async Task GetItemAsync_NonExistentItem_ReturnsNullAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_noitem", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); // Act ItemResource? result = await storage.GetItemAsync("conv_noitem", "msg_nonexistent"); // Assert Assert.Null(result); } [Fact] public async Task GetItemAsync_NonExistentConversation_ReturnsNullAsync() { // Arrange var storage = new InMemoryConversationStorage(); // Act ItemResource? result = await storage.GetItemAsync("conv_nonexistent", "msg_any"); // Assert Assert.Null(result); } [Fact] public async Task ListItemsAsync_DefaultParameters_ReturnsDescendingOrderAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_list", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); // Add items in order var item1 = new ResponsesUserMessageItemResource { Id = "msg_001", Content = [new ItemContentInputText { Text = "First" }] }; var item2 = new ResponsesUserMessageItemResource { Id = "msg_002", Content = [new ItemContentInputText { Text = "Second" }] }; var item3 = new ResponsesUserMessageItemResource { Id = "msg_003", Content = [new ItemContentInputText { Text = "Third" }] }; await storage.AddItemsAsync("conv_list", [item1]); await storage.AddItemsAsync("conv_list", [item2]); await storage.AddItemsAsync("conv_list", [item3]); // Act ListResponse result = await storage.ListItemsAsync("conv_list"); // Assert Assert.NotNull(result); Assert.NotNull(result.Data); Assert.Equal(3, result.Data.Count); Assert.Equal("msg_003", result.Data[0].Id); // Descending order Assert.Equal("msg_002", result.Data[1].Id); Assert.Equal("msg_001", result.Data[2].Id); Assert.Equal("msg_003", result.FirstId); Assert.Equal("msg_001", result.LastId); Assert.False(result.HasMore); } [Fact] public async Task ListItemsAsync_AscendingOrder_ReturnsCorrectOrderAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_asc", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); var item1 = new ResponsesUserMessageItemResource { Id = "msg_001", Content = [new ItemContentInputText { Text = "First" }] }; var item2 = new ResponsesUserMessageItemResource { Id = "msg_002", Content = [new ItemContentInputText { Text = "Second" }] }; await storage.AddItemsAsync("conv_asc", [item1]); await storage.AddItemsAsync("conv_asc", [item2]); // Act ListResponse result = await storage.ListItemsAsync("conv_asc", order: SortOrder.Ascending); // Assert Assert.Equal(2, result.Data.Count); Assert.Equal("msg_001", result.Data[0].Id); // Ascending order Assert.Equal("msg_002", result.Data[1].Id); } [Fact] public async Task ListItemsAsync_WithLimit_ReturnsCorrectPageSizeAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_limit", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); for (int i = 1; i <= 10; i++) { var item = new ResponsesUserMessageItemResource { Id = $"msg_{i:D3}", Content = [new ItemContentInputText { Text = $"Message {i}" }] }; await storage.AddItemsAsync("conv_limit", [item]); } // Act ListResponse result = await storage.ListItemsAsync("conv_limit", limit: 5); // Assert Assert.Equal(5, result.Data.Count); Assert.True(result.HasMore); Assert.Equal("msg_010", result.FirstId); // First in descending order Assert.Equal("msg_006", result.LastId); } [Fact] public async Task ListItemsAsync_WithAfter_ReturnsNextPageAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_after", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); for (int i = 1; i <= 10; i++) { var item = new ResponsesUserMessageItemResource { Id = $"msg_{i:D3}", Content = [new ItemContentInputText { Text = $"Message {i}" }] }; await storage.AddItemsAsync("conv_after", [item]); } // Act ListResponse result = await storage.ListItemsAsync("conv_after", limit: 5, after: "msg_006"); // Assert Assert.Equal(5, result.Data.Count); Assert.Equal("msg_005", result.Data[0].Id); // Next items after msg_006 in descending order Assert.Equal("msg_001", result.Data[4].Id); Assert.False(result.HasMore); // No more items after this page } [Fact] public async Task ListItemsAsync_LimitClamping_ClampsToValidRangeAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_clamp", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); for (int i = 1; i <= 5; i++) { var item = new ResponsesUserMessageItemResource { Id = $"msg_{i:D3}", Content = [new ItemContentInputText { Text = $"Message {i}" }] }; await storage.AddItemsAsync("conv_clamp", [item]); } // Act - Test upper bound ListResponse result1 = await storage.ListItemsAsync("conv_clamp", limit: 200); // Act - Test lower bound ListResponse result2 = await storage.ListItemsAsync("conv_clamp", limit: 0); // Assert Assert.Equal(5, result1.Data.Count); // Should return all items (clamped to 100 max, but we only have 5) Assert.NotNull(result2.Data); Assert.NotEmpty(result2.Data); Assert.Single(result2.Data); // Should return at least 1 item (clamped to 1 min) } [Fact] public async Task ListItemsAsync_EmptyConversation_ReturnsEmptyListAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_empty", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); // Act ListResponse result = await storage.ListItemsAsync("conv_empty"); // Assert Assert.NotNull(result); Assert.NotNull(result.Data); Assert.Empty(result.Data); Assert.Null(result.FirstId); Assert.Null(result.LastId); Assert.False(result.HasMore); } [Fact] public async Task ListItemsAsync_NonExistentConversation_ThrowsInvalidOperationExceptionAsync() { // Arrange var storage = new InMemoryConversationStorage(); // Act & Assert var exception = await Assert.ThrowsAsync( () => storage.ListItemsAsync("conv_nonexistent")); Assert.Contains("not found", exception.Message); } [Fact] public async Task DeleteItemAsync_ExistingItem_ReturnsTrueAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_delitem", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); var item = new ResponsesUserMessageItemResource { Id = "msg_delete", Content = [new ItemContentInputText { Text = "Delete me" }] }; await storage.AddItemsAsync("conv_delitem", [item]); // Act bool result = await storage.DeleteItemAsync("conv_delitem", "msg_delete"); // Assert Assert.True(result); // Verify deletion ItemResource? retrieved = await storage.GetItemAsync("conv_delitem", "msg_delete"); Assert.Null(retrieved); } [Fact] public async Task DeleteItemAsync_NonExistentItem_ReturnsFalseAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_delnoitem", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); // Act bool result = await storage.DeleteItemAsync("conv_delnoitem", "msg_nonexistent"); // Assert Assert.False(result); } [Fact] public async Task DeleteItemAsync_NonExistentConversation_ReturnsFalseAsync() { // Arrange var storage = new InMemoryConversationStorage(); // Act bool result = await storage.DeleteItemAsync("conv_nonexistent", "msg_any"); // Assert Assert.False(result); } [Fact] public async Task ConcurrentOperations_ThreadSafeAsync() { // Arrange var storage = new InMemoryConversationStorage(); var conversation = new Conversation { Id = "conv_concurrent", CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Metadata = [] }; await storage.CreateConversationAsync(conversation); // Act - Add items concurrently var tasks = new List(); for (int i = 0; i < 100; i++) { int index = i; tasks.Add(Task.Run(async () => { var item = new ResponsesUserMessageItemResource { Id = $"msg_{index:D3}", Content = [new ItemContentInputText { Text = $"Message {index}" }] }; await storage.AddItemsAsync("conv_concurrent", [item]); })); } await Task.WhenAll(tasks); // Assert ListResponse result = await storage.ListItemsAsync("conv_concurrent", limit: 100); Assert.NotNull(result.Data); Assert.NotEmpty(result.Data); Assert.Equal(100, result.Data.Count); } }