// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.CosmosNoSql.UnitTests;
///
/// Contains tests for .
///
/// Test Modes:
/// - Default Mode: Cleans up all test data after each test run (deletes database)
/// - Preserve Mode: Keeps containers and data for inspection in Cosmos DB Emulator Data Explorer
///
/// To enable Preserve Mode, set environment variable: COSMOS_PRESERVE_CONTAINERS=true
/// Example: $env:COSMOS_PRESERVE_CONTAINERS="true"; dotnet test
///
/// In Preserve Mode, you can view the data in Cosmos DB Emulator Data Explorer at:
/// https://localhost:8081/_explorer/index.html
/// Database: AgentFrameworkTests
/// Container: ChatMessages
///
/// Environment Variable Reference:
/// | Variable | Values | Description |
/// |----------|--------|-------------|
/// | COSMOS_PRESERVE_CONTAINERS | true / false | Controls whether to preserve test data after completion |
///
/// Usage Examples:
/// - Run all tests in preserve mode: $env:COSMOS_PRESERVE_CONTAINERS="true"; dotnet test tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/
/// - Run specific test category in preserve mode: $env:COSMOS_PRESERVE_CONTAINERS="true"; dotnet test tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/ --filter "Category=CosmosDB"
/// - Reset to cleanup mode: $env:COSMOS_PRESERVE_CONTAINERS=""; dotnet test tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/
///
[Collection("CosmosDB")]
public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
{
// Cosmos DB Emulator connection settings
private const string EmulatorEndpoint = "https://localhost:8081";
private const string EmulatorKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
private const string TestContainerId = "ChatMessages";
private const string HierarchicalTestContainerId = "HierarchicalChatMessages";
// Use unique database ID per test class instance to avoid conflicts
#pragma warning disable CA1802 // Use literals where appropriate
private static readonly string s_testDatabaseId = $"AgentFrameworkTests-ChatStore-{Guid.NewGuid():N}";
#pragma warning restore CA1802
private string _connectionString = string.Empty;
private bool _emulatorAvailable;
private bool _preserveContainer;
private CosmosClient? _setupClient; // Only used for test setup/cleanup
public async Task InitializeAsync()
{
// Fail fast if emulator is not available
this.SkipIfEmulatorNotAvailable();
// Check environment variable to determine if we should preserve containers
// Set COSMOS_PRESERVE_CONTAINERS=true to keep containers and data for inspection
this._preserveContainer = string.Equals(Environment.GetEnvironmentVariable("COSMOS_PRESERVE_CONTAINERS"), "true", StringComparison.OrdinalIgnoreCase);
this._connectionString = $"AccountEndpoint={EmulatorEndpoint};AccountKey={EmulatorKey}";
try
{
// Only create CosmosClient for test setup - the actual tests will use connection string constructors
this._setupClient = new CosmosClient(EmulatorEndpoint, EmulatorKey);
// Test connection by attempting to create database
var databaseResponse = await this._setupClient.CreateDatabaseIfNotExistsAsync(s_testDatabaseId);
// Create container for simple partitioning tests
await databaseResponse.Database.CreateContainerIfNotExistsAsync(
TestContainerId,
"/conversationId",
throughput: 400);
// Create container for hierarchical partitioning tests with hierarchical partition key
var hierarchicalContainerProperties = new ContainerProperties(HierarchicalTestContainerId, ["/tenantId", "/userId", "/sessionId"]);
await databaseResponse.Database.CreateContainerIfNotExistsAsync(
hierarchicalContainerProperties,
throughput: 400);
this._emulatorAvailable = true;
}
catch (Exception)
{
// Emulator not available, tests will be skipped
this._emulatorAvailable = false;
this._setupClient?.Dispose();
this._setupClient = null;
}
}
public async Task DisposeAsync()
{
if (this._setupClient != null && this._emulatorAvailable)
{
try
{
if (this._preserveContainer)
{
// Preserve mode: Don't delete the database/container, keep data for inspection
// This allows viewing data in the Cosmos DB Emulator Data Explorer
// No cleanup needed - data persists for debugging
}
else
{
// Clean mode: Delete the test database and all data
var database = this._setupClient.GetDatabase(s_testDatabaseId);
await database.DeleteAsync();
}
}
catch (Exception ex)
{
// Ignore cleanup errors during test teardown
Console.WriteLine($"Warning: Cleanup failed: {ex.Message}");
}
finally
{
this._setupClient.Dispose();
}
}
}
public void Dispose()
{
this._setupClient?.Dispose();
GC.SuppressFinalize(this);
}
private void SkipIfEmulatorNotAvailable()
{
// In CI: Skip if COSMOS_EMULATOR_AVAILABLE is not set to "true"
// Locally: Skip if emulator connection check failed
var ciEmulatorAvailable = string.Equals(Environment.GetEnvironmentVariable("COSMOS_EMULATOR_AVAILABLE"), "true", StringComparison.OrdinalIgnoreCase);
Xunit.Skip.If(!ciEmulatorAvailable && !this._emulatorAvailable, "Cosmos DB Emulator is not available");
}
#region Constructor Tests
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithConnectionString_ShouldCreateInstance()
{
// Arrange & Act
this.SkipIfEmulatorNotAvailable();
// Act
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, "test-conversation");
// Assert
Assert.NotNull(provider);
Assert.Equal("test-conversation", provider.ConversationId);
Assert.Equal(s_testDatabaseId, provider.DatabaseId);
Assert.Equal(TestContainerId, provider.ContainerId);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithConnectionStringNoConversationId_ShouldCreateInstance()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
// Act
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId);
// Assert
Assert.NotNull(provider);
Assert.NotNull(provider.ConversationId);
Assert.Equal(s_testDatabaseId, provider.DatabaseId);
Assert.Equal(TestContainerId, provider.ContainerId);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithNullConnectionString_ShouldThrowArgumentException()
{
// Arrange & Act & Assert
Assert.Throws(() =>
new CosmosChatHistoryProvider((string)null!, s_testDatabaseId, TestContainerId, "test-conversation"));
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithEmptyConversationId_ShouldThrowArgumentException()
{
// Arrange & Act & Assert
this.SkipIfEmulatorNotAvailable();
Assert.Throws(() =>
new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, ""));
}
#endregion
#region InvokedAsync Tests
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task InvokedAsync_WithSingleMessage_ShouldAddMessageAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
var conversationId = Guid.NewGuid().ToString();
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversationId);
var message = new ChatMessage(ChatRole.User, "Hello, world!");
var context = new ChatHistoryProvider.InvokedContext([message], [])
{
ResponseMessages = []
};
// Act
await provider.InvokedAsync(context);
// Wait a moment for eventual consistency
await Task.Delay(100);
// Assert
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var messages = await provider.InvokingAsync(invokingContext);
var messageList = messages.ToList();
// Simple assertion - if this fails, we know the deserialization is the issue
if (messageList.Count == 0)
{
// Let's check if we can find ANY items in the container for this conversation
var directQuery = new QueryDefinition("SELECT VALUE COUNT(1) FROM c WHERE c.conversationId = @conversationId")
.WithParameter("@conversationId", conversationId);
var countIterator = this._setupClient!.GetDatabase(s_testDatabaseId).GetContainer(TestContainerId)
.GetItemQueryIterator(directQuery, requestOptions: new QueryRequestOptions
{
PartitionKey = new PartitionKey(conversationId)
});
var countResponse = await countIterator.ReadNextAsync();
var count = countResponse.FirstOrDefault();
// Debug: Let's see what the raw query returns
var rawQuery = new QueryDefinition("SELECT * FROM c WHERE c.conversationId = @conversationId")
.WithParameter("@conversationId", conversationId);
var rawIterator = this._setupClient!.GetDatabase(s_testDatabaseId).GetContainer(TestContainerId)
.GetItemQueryIterator(rawQuery, requestOptions: new QueryRequestOptions
{
PartitionKey = new PartitionKey(conversationId)
});
List rawResults = [];
while (rawIterator.HasMoreResults)
{
var rawResponse = await rawIterator.ReadNextAsync();
rawResults.AddRange(rawResponse);
}
string rawJson = rawResults.Count > 0 ? Newtonsoft.Json.JsonConvert.SerializeObject(rawResults[0], Newtonsoft.Json.Formatting.Indented) : "null";
Assert.Fail($"InvokingAsync returned 0 messages, but direct count query found {count} items for conversation {conversationId}. Raw document: {rawJson}");
}
Assert.Single(messageList);
Assert.Equal("Hello, world!", messageList[0].Text);
Assert.Equal(ChatRole.User, messageList[0].Role);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task InvokedAsync_WithMultipleMessages_ShouldAddAllMessagesAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
var conversationId = Guid.NewGuid().ToString();
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversationId);
var requestMessages = new[]
{
new ChatMessage(ChatRole.User, "First message"),
new ChatMessage(ChatRole.Assistant, "Second message"),
new ChatMessage(ChatRole.User, "Third message")
};
var aiContextProviderMessages = new[]
{
new ChatMessage(ChatRole.System, "System context message")
};
var responseMessages = new[]
{
new ChatMessage(ChatRole.Assistant, "Response message")
};
var context = new ChatHistoryProvider.InvokedContext(requestMessages, [])
{
AIContextProviderMessages = aiContextProviderMessages,
ResponseMessages = responseMessages
};
// Act
await provider.InvokedAsync(context);
// Assert
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var retrievedMessages = await provider.InvokingAsync(invokingContext);
var messageList = retrievedMessages.ToList();
Assert.Equal(5, messageList.Count);
Assert.Equal("First message", messageList[0].Text);
Assert.Equal("Second message", messageList[1].Text);
Assert.Equal("Third message", messageList[2].Text);
Assert.Equal("System context message", messageList[3].Text);
Assert.Equal("Response message", messageList[4].Text);
}
#endregion
#region InvokingAsync Tests
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task InvokingAsync_WithNoMessages_ShouldReturnEmptyAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString());
// Act
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var messages = await provider.InvokingAsync(invokingContext);
// Assert
Assert.Empty(messages);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task InvokingAsync_WithConversationIsolation_ShouldOnlyReturnMessagesForConversationAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
var conversation1 = Guid.NewGuid().ToString();
var conversation2 = Guid.NewGuid().ToString();
using var store1 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversation1);
using var store2 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversation2);
var context1 = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message for conversation 1")], []);
var context2 = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message for conversation 2")], []);
await store1.InvokedAsync(context1);
await store2.InvokedAsync(context2);
// Act
var invokingContext1 = new ChatHistoryProvider.InvokingContext([]);
var invokingContext2 = new ChatHistoryProvider.InvokingContext([]);
var messages1 = await store1.InvokingAsync(invokingContext1);
var messages2 = await store2.InvokingAsync(invokingContext2);
// Assert
var messageList1 = messages1.ToList();
var messageList2 = messages2.ToList();
Assert.Single(messageList1);
Assert.Single(messageList2);
Assert.Equal("Message for conversation 1", messageList1[0].Text);
Assert.Equal("Message for conversation 2", messageList2[0].Text);
}
#endregion
#region Integration Tests
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task FullWorkflow_AddAndGet_ShouldWorkCorrectlyAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
var conversationId = $"test-conversation-{Guid.NewGuid():N}"; // Use unique conversation ID
using var originalStore = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversationId);
var messages = new[]
{
new ChatMessage(ChatRole.System, "You are a helpful assistant."),
new ChatMessage(ChatRole.User, "Hello!"),
new ChatMessage(ChatRole.Assistant, "Hi there! How can I help you today?"),
new ChatMessage(ChatRole.User, "What's the weather like?"),
new ChatMessage(ChatRole.Assistant, "I'm sorry, I don't have access to current weather data.")
};
// Act 1: Add messages
var invokedContext = new ChatHistoryProvider.InvokedContext(messages, []);
await originalStore.InvokedAsync(invokedContext);
// Act 2: Verify messages were added
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var retrievedMessages = await originalStore.InvokingAsync(invokingContext);
var retrievedList = retrievedMessages.ToList();
Assert.Equal(5, retrievedList.Count);
// Act 3: Create new provider instance for same conversation (test persistence)
using var newProvider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, conversationId);
var persistedMessages = await newProvider.InvokingAsync(invokingContext);
var persistedList = persistedMessages.ToList();
// Assert final state
Assert.Equal(5, persistedList.Count);
Assert.Equal("You are a helpful assistant.", persistedList[0].Text);
Assert.Equal("Hello!", persistedList[1].Text);
Assert.Equal("Hi there! How can I help you today?", persistedList[2].Text);
Assert.Equal("What's the weather like?", persistedList[3].Text);
Assert.Equal("I'm sorry, I don't have access to current weather data.", persistedList[4].Text);
}
#endregion
#region Disposal Tests
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Dispose_AfterUse_ShouldNotThrow()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString());
// Act & Assert
provider.Dispose(); // Should not throw
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Dispose_MultipleCalls_ShouldNotThrow()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, Guid.NewGuid().ToString());
// Act & Assert
provider.Dispose(); // First call
provider.Dispose(); // Second call - should not throw
}
#endregion
#region Hierarchical Partitioning Tests
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithHierarchicalConnectionString_ShouldCreateInstance()
{
// Arrange & Act
this.SkipIfEmulatorNotAvailable();
// Act
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", "session-789");
// Assert
Assert.NotNull(provider);
Assert.Equal("session-789", provider.ConversationId);
Assert.Equal(s_testDatabaseId, provider.DatabaseId);
Assert.Equal(HierarchicalTestContainerId, provider.ContainerId);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithHierarchicalEndpoint_ShouldCreateInstance()
{
// Arrange & Act
this.SkipIfEmulatorNotAvailable();
// Act
TokenCredential credential = new DefaultAzureCredential();
using var provider = new CosmosChatHistoryProvider(EmulatorEndpoint, credential, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", "session-789");
// Assert
Assert.NotNull(provider);
Assert.Equal("session-789", provider.ConversationId);
Assert.Equal(s_testDatabaseId, provider.DatabaseId);
Assert.Equal(HierarchicalTestContainerId, provider.ContainerId);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithHierarchicalCosmosClient_ShouldCreateInstance()
{
// Arrange & Act
this.SkipIfEmulatorNotAvailable();
using var cosmosClient = new CosmosClient(EmulatorEndpoint, EmulatorKey);
using var provider = new CosmosChatHistoryProvider(cosmosClient, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", "session-789");
// Assert
Assert.NotNull(provider);
Assert.Equal("session-789", provider.ConversationId);
Assert.Equal(s_testDatabaseId, provider.DatabaseId);
Assert.Equal(HierarchicalTestContainerId, provider.ContainerId);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithHierarchicalNullTenantId_ShouldThrowArgumentException()
{
// Arrange & Act & Assert
this.SkipIfEmulatorNotAvailable();
Assert.Throws(() =>
new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, null!, "user-456", "session-789"));
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithHierarchicalEmptyUserId_ShouldThrowArgumentException()
{
// Arrange & Act & Assert
this.SkipIfEmulatorNotAvailable();
Assert.Throws(() =>
new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "", "session-789"));
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public void Constructor_WithHierarchicalWhitespaceSessionId_ShouldThrowArgumentException()
{
// Arrange & Act & Assert
this.SkipIfEmulatorNotAvailable();
Assert.Throws(() =>
new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-123", "user-456", " "));
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task InvokedAsync_WithHierarchicalPartitioning_ShouldAddMessageWithMetadataAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
const string TenantId = "tenant-123";
const string UserId = "user-456";
const string SessionId = "session-789";
// Test hierarchical partitioning constructor with connection string
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId);
var message = new ChatMessage(ChatRole.User, "Hello from hierarchical partitioning!");
var context = new ChatHistoryProvider.InvokedContext([message], []);
// Act
await provider.InvokedAsync(context);
// Wait a moment for eventual consistency
await Task.Delay(100);
// Assert
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var messages = await provider.InvokingAsync(invokingContext);
var messageList = messages.ToList();
Assert.Single(messageList);
Assert.Equal("Hello from hierarchical partitioning!", messageList[0].Text);
Assert.Equal(ChatRole.User, messageList[0].Role);
// Verify that the document is stored with hierarchical partitioning metadata
var directQuery = new QueryDefinition("SELECT * FROM c WHERE c.conversationId = @conversationId AND c.type = @type")
.WithParameter("@conversationId", SessionId)
.WithParameter("@type", "ChatMessage");
var iterator = this._setupClient!.GetDatabase(s_testDatabaseId).GetContainer(HierarchicalTestContainerId)
.GetItemQueryIterator(directQuery, requestOptions: new QueryRequestOptions
{
PartitionKey = new PartitionKeyBuilder().Add(TenantId).Add(UserId).Add(SessionId).Build()
});
var response = await iterator.ReadNextAsync();
var document = response.FirstOrDefault();
Assert.NotNull(document);
// The document should have hierarchical metadata
Assert.Equal(SessionId, (string)document!.conversationId);
Assert.Equal(TenantId, (string)document!.tenantId);
Assert.Equal(UserId, (string)document!.userId);
Assert.Equal(SessionId, (string)document!.sessionId);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task InvokedAsync_WithHierarchicalMultipleMessages_ShouldAddAllMessagesAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
const string TenantId = "tenant-batch";
const string UserId = "user-batch";
const string SessionId = "session-batch";
// Test hierarchical partitioning constructor with connection string
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId);
var messages = new[]
{
new ChatMessage(ChatRole.User, "First hierarchical message"),
new ChatMessage(ChatRole.Assistant, "Second hierarchical message"),
new ChatMessage(ChatRole.User, "Third hierarchical message")
};
var context = new ChatHistoryProvider.InvokedContext(messages, []);
// Act
await provider.InvokedAsync(context);
// Wait a moment for eventual consistency
await Task.Delay(100);
// Assert
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var retrievedMessages = await provider.InvokingAsync(invokingContext);
var messageList = retrievedMessages.ToList();
Assert.Equal(3, messageList.Count);
Assert.Equal("First hierarchical message", messageList[0].Text);
Assert.Equal("Second hierarchical message", messageList[1].Text);
Assert.Equal("Third hierarchical message", messageList[2].Text);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task InvokingAsync_WithHierarchicalPartitionIsolation_ShouldIsolateMessagesByUserIdAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
const string TenantId = "tenant-isolation";
const string UserId1 = "user-1";
const string UserId2 = "user-2";
const string SessionId = "session-isolation";
// Different userIds create different hierarchical partitions, providing proper isolation
using var store1 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId1, SessionId);
using var store2 = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId2, SessionId);
// Add messages to both stores
var context1 = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message from user 1")], []);
var context2 = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Message from user 2")], []);
await store1.InvokedAsync(context1);
await store2.InvokedAsync(context2);
// Wait a moment for eventual consistency
await Task.Delay(100);
// Act & Assert
var invokingContext1 = new ChatHistoryProvider.InvokingContext([]);
var invokingContext2 = new ChatHistoryProvider.InvokingContext([]);
var messages1 = await store1.InvokingAsync(invokingContext1);
var messageList1 = messages1.ToList();
var messages2 = await store2.InvokingAsync(invokingContext2);
var messageList2 = messages2.ToList();
// With true hierarchical partitioning, each user sees only their own messages
Assert.Single(messageList1);
Assert.Single(messageList2);
Assert.Equal("Message from user 1", messageList1[0].Text);
Assert.Equal("Message from user 2", messageList2[0].Text);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task SerializeDeserialize_WithHierarchicalPartitioning_ShouldPreserveStateAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
const string TenantId = "tenant-serialize";
const string UserId = "user-serialize";
const string SessionId = "session-serialize";
using var originalStore = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, TenantId, UserId, SessionId);
var context = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Test serialization message")], []);
await originalStore.InvokedAsync(context);
// Act - Serialize the provider state
var serializedState = originalStore.Serialize();
// Create a new provider from the serialized state
using var cosmosClient = new CosmosClient(EmulatorEndpoint, EmulatorKey);
var serializerOptions = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
};
using var deserializedStore = CosmosChatHistoryProvider.CreateFromSerializedState(cosmosClient, serializedState, s_testDatabaseId, HierarchicalTestContainerId, serializerOptions);
// Wait a moment for eventual consistency
await Task.Delay(100);
// Assert - The deserialized provider should have the same functionality
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var messages = await deserializedStore.InvokingAsync(invokingContext);
var messageList = messages.ToList();
Assert.Single(messageList);
Assert.Equal("Test serialization message", messageList[0].Text);
Assert.Equal(SessionId, deserializedStore.ConversationId);
Assert.Equal(s_testDatabaseId, deserializedStore.DatabaseId);
Assert.Equal(HierarchicalTestContainerId, deserializedStore.ContainerId);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task HierarchicalAndSimplePartitioning_ShouldCoexistAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
const string SessionId = "coexist-session";
// Create simple provider using simple partitioning container and hierarchical provider using hierarchical container
using var simpleProvider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, SessionId);
using var hierarchicalProvider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, HierarchicalTestContainerId, "tenant-coexist", "user-coexist", SessionId);
// Add messages to both
var simpleContext = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Simple partitioning message")], []);
var hierarchicalContext = new ChatHistoryProvider.InvokedContext([new ChatMessage(ChatRole.User, "Hierarchical partitioning message")], []);
await simpleProvider.InvokedAsync(simpleContext);
await hierarchicalProvider.InvokedAsync(hierarchicalContext);
// Wait a moment for eventual consistency
await Task.Delay(100);
// Act & Assert
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var simpleMessages = await simpleProvider.InvokingAsync(invokingContext);
var simpleMessageList = simpleMessages.ToList();
var hierarchicalMessages = await hierarchicalProvider.InvokingAsync(invokingContext);
var hierarchicalMessageList = hierarchicalMessages.ToList();
// Each should only see its own messages since they use different containers
Assert.Single(simpleMessageList);
Assert.Single(hierarchicalMessageList);
Assert.Equal("Simple partitioning message", simpleMessageList[0].Text);
Assert.Equal("Hierarchical partitioning message", hierarchicalMessageList[0].Text);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task MaxMessagesToRetrieve_ShouldLimitAndReturnMostRecentAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
const string ConversationId = "max-messages-test";
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, ConversationId);
// Add 10 messages
var messages = new List();
for (int i = 1; i <= 10; i++)
{
messages.Add(new ChatMessage(ChatRole.User, $"Message {i}"));
await Task.Delay(10); // Small delay to ensure different timestamps
}
var context = new ChatHistoryProvider.InvokedContext(messages, []);
await provider.InvokedAsync(context);
// Wait for eventual consistency
await Task.Delay(100);
// Act - Set max to 5 and retrieve
provider.MaxMessagesToRetrieve = 5;
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var retrievedMessages = await provider.InvokingAsync(invokingContext);
var messageList = retrievedMessages.ToList();
// Assert - Should get the 5 most recent messages (6-10) in ascending order
Assert.Equal(5, messageList.Count);
Assert.Equal("Message 6", messageList[0].Text);
Assert.Equal("Message 7", messageList[1].Text);
Assert.Equal("Message 8", messageList[2].Text);
Assert.Equal("Message 9", messageList[3].Text);
Assert.Equal("Message 10", messageList[4].Text);
}
[SkippableFact]
[Trait("Category", "CosmosDB")]
public async Task MaxMessagesToRetrieve_Null_ShouldReturnAllMessagesAsync()
{
// Arrange
this.SkipIfEmulatorNotAvailable();
const string ConversationId = "max-messages-null-test";
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId, ConversationId);
// Add 10 messages
var messages = new List();
for (int i = 1; i <= 10; i++)
{
messages.Add(new ChatMessage(ChatRole.User, $"Message {i}"));
}
var context = new ChatHistoryProvider.InvokedContext(messages, []);
await provider.InvokedAsync(context);
// Wait for eventual consistency
await Task.Delay(100);
// Act - No limit set (default null)
var invokingContext = new ChatHistoryProvider.InvokingContext([]);
var retrievedMessages = await provider.InvokingAsync(invokingContext);
var messageList = retrievedMessages.ToList();
// Assert - Should get all 10 messages
Assert.Equal(10, messageList.Count);
Assert.Equal("Message 1", messageList[0].Text);
Assert.Equal("Message 10", messageList[9].Text);
}
#endregion
}