// Copyright (c) Microsoft. All rights reserved. using System; using System.Threading; using System.Threading.Tasks; using Moq; namespace Microsoft.Agents.AI.Hosting.UnitTests; /// /// Unit tests for the class. /// public class DelegatingAgentSessionStoreTests { private readonly Mock _innerStoreMock; private readonly Mock _agentMock; private readonly TestDelegatingAgentSessionStore _delegatingStore; private readonly AgentSession _testSession; /// /// Initializes a new instance of the class. /// public DelegatingAgentSessionStoreTests() { this._innerStoreMock = new Mock(); this._agentMock = new Mock(); this._testSession = new TestAgentSession(); // Setup inner store mock this._innerStoreMock .Setup(x => x.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(this._testSession); this._innerStoreMock .Setup(x => x.SaveSessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(ValueTask.CompletedTask); this._delegatingStore = new TestDelegatingAgentSessionStore(this._innerStoreMock.Object); } #region Constructor Tests /// /// Verify that constructor throws ArgumentNullException when innerStore is null. /// [Fact] public void RequiresInnerStore() => // Act & Assert Assert.Throws("innerStore", () => new TestDelegatingAgentSessionStore(null!)); /// /// Verify that constructor sets the inner store correctly. /// [Fact] public void Constructor_WithValidInnerStore_SetsInnerStore() { // Act var delegatingStore = new TestDelegatingAgentSessionStore(this._innerStoreMock.Object); // Assert Assert.Same(this._innerStoreMock.Object, delegatingStore.InnerStore); } #endregion #region Method Delegation Tests /// /// Verify that GetSessionAsync delegates to inner store with correct parameters. /// [Fact] public async Task GetSessionAsyncDelegatesToInnerStoreAsync() { // Arrange const string ExpectedConversationId = "test-conversation-id"; var expectedCancellationToken = new CancellationToken(); this._innerStoreMock .Setup(x => x.GetSessionAsync( It.Is(a => a == this._agentMock.Object), It.Is(c => c == ExpectedConversationId), It.Is(ct => ct == expectedCancellationToken))) .ReturnsAsync(this._testSession); // Act var session = await this._delegatingStore.GetSessionAsync( this._agentMock.Object, ExpectedConversationId, expectedCancellationToken); // Assert Assert.Same(this._testSession, session); this._innerStoreMock.Verify( x => x.GetSessionAsync( this._agentMock.Object, ExpectedConversationId, expectedCancellationToken), Times.Once); } /// /// Verify that SaveSessionAsync delegates to inner store with correct parameters. /// [Fact] public async Task SaveSessionAsyncDelegatesToInnerStoreAsync() { // Arrange const string ExpectedConversationId = "test-conversation-id"; var expectedCancellationToken = new CancellationToken(); var expectedSession = new TestAgentSession(); this._innerStoreMock .Setup(x => x.SaveSessionAsync( It.Is(a => a == this._agentMock.Object), It.Is(c => c == ExpectedConversationId), It.Is(s => s == expectedSession), It.Is(ct => ct == expectedCancellationToken))) .Returns(ValueTask.CompletedTask); // Act await this._delegatingStore.SaveSessionAsync( this._agentMock.Object, ExpectedConversationId, expectedSession, expectedCancellationToken); // Assert this._innerStoreMock.Verify( x => x.SaveSessionAsync( this._agentMock.Object, ExpectedConversationId, expectedSession, expectedCancellationToken), Times.Once); } /// /// Verify that GetSessionAsync awaits the inner store's result before returning. /// [Fact] public async Task GetSessionAsyncAwaitsInnerStoreResultAsync() { // Arrange const string ExpectedConversationId = "test-conversation-id"; var taskCompletionSource = new TaskCompletionSource(); var innerStoreMock = new Mock(); innerStoreMock .Setup(x => x.GetSessionAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(taskCompletionSource.Task)); var delegatingStore = new TestDelegatingAgentSessionStore(innerStoreMock.Object); // Act var resultTask = delegatingStore.GetSessionAsync(this._agentMock.Object, ExpectedConversationId); // Assert Assert.False(resultTask.IsCompleted); taskCompletionSource.SetResult(this._testSession); Assert.True(resultTask.IsCompleted); Assert.Same(this._testSession, await resultTask); } /// /// Verify that SaveSessionAsync awaits the inner store's completion before returning. /// [Fact] public async Task SaveSessionAsyncAwaitsInnerStoreCompletionAsync() { // Arrange const string ExpectedConversationId = "test-conversation-id"; var expectedSession = new TestAgentSession(); var taskCompletionSource = new TaskCompletionSource(); var innerStoreMock = new Mock(); innerStoreMock .Setup(x => x.SaveSessionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(new ValueTask(taskCompletionSource.Task)); var delegatingStore = new TestDelegatingAgentSessionStore(innerStoreMock.Object); // Act var resultTask = delegatingStore.SaveSessionAsync(this._agentMock.Object, ExpectedConversationId, expectedSession); // Assert Assert.False(resultTask.IsCompleted); taskCompletionSource.SetResult(); Assert.True(resultTask.IsCompleted); await resultTask; } #endregion #region GetService Tests /// /// Verify that GetService returns itself when requesting the exact type. /// [Fact] public void GetServiceReturnsItselfForExactType() { // Act var result = this._delegatingStore.GetService(typeof(TestDelegatingAgentSessionStore)); // Assert Assert.Same(this._delegatingStore, result); } /// /// Verify that GetService returns itself when requesting a base type. /// [Fact] public void GetServiceReturnsItselfForBaseType() { // Act var result = this._delegatingStore.GetService(typeof(DelegatingAgentSessionStore)); // Assert Assert.Same(this._delegatingStore, result); } /// /// Verify that GetService returns itself when requesting AgentSessionStore. /// [Fact] public void GetServiceReturnsItselfForAgentSessionStoreType() { // Act var result = this._delegatingStore.GetService(typeof(AgentSessionStore)); // Assert Assert.Same(this._delegatingStore, result); } /// /// Verify that GetService chains to inner store when type is not satisfied by outer store. /// [Fact] public void GetServiceChainsToInnerStore() { // Arrange var innerStore = new ConcreteAgentSessionStore(); var delegatingStore = new TestDelegatingAgentSessionStore(innerStore); // Act var result = delegatingStore.GetService(typeof(ConcreteAgentSessionStore)); // Assert Assert.Same(innerStore, result); } /// /// Verify that GetService chains through multiple delegation layers. /// [Fact] public void GetServiceChainsThoughMultipleDelegationLayers() { // Arrange - create a three-layer chain: outer -> middle -> inner var innerStore = new ConcreteAgentSessionStore(); var middleStore = new AnotherDelegatingAgentSessionStore(innerStore); var outerStore = new TestDelegatingAgentSessionStore(middleStore); // Act - request the innermost store type var result = outerStore.GetService(typeof(ConcreteAgentSessionStore)); // Assert Assert.Same(innerStore, result); } /// /// Verify that GetService can find a store in the middle of the delegation chain. /// [Fact] public void GetServiceFindsMiddleStoreInChain() { // Arrange - create a three-layer chain: outer -> middle -> inner var innerStore = new ConcreteAgentSessionStore(); var middleStore = new AnotherDelegatingAgentSessionStore(innerStore); var outerStore = new TestDelegatingAgentSessionStore(middleStore); // Act - request the middle store type var result = outerStore.GetService(typeof(AnotherDelegatingAgentSessionStore)); // Assert Assert.Same(middleStore, result); } /// /// Verify that GetService returns null when the requested type is not found in the chain. /// [Fact] public void GetServiceReturnsNullWhenTypeNotFound() { // Arrange var innerStore = new ConcreteAgentSessionStore(); var delegatingStore = new TestDelegatingAgentSessionStore(innerStore); // Act var result = delegatingStore.GetService(typeof(string)); // Assert Assert.Null(result); } /// /// Verify that GetService returns null when a service key is provided but not matched. /// [Fact] public void GetServiceReturnsNullWhenServiceKeyProvided() { // Act var result = this._delegatingStore.GetService(typeof(TestDelegatingAgentSessionStore), "some-key"); // Assert Assert.Null(result); } /// /// Verify that GetService throws ArgumentNullException when serviceType is null. /// [Fact] public void GetServiceThrowsWhenServiceTypeIsNull() => Assert.Throws("serviceType", () => this._delegatingStore.GetService(null!)); /// /// Verify that GetService generic method works correctly. /// [Fact] public void GetServiceGenericReturnsItself() { // Act var result = this._delegatingStore.GetService(); // Assert Assert.Same(this._delegatingStore, result); } /// /// Verify that GetService generic method chains to inner store. /// [Fact] public void GetServiceGenericChainsToInnerStore() { // Arrange var innerStore = new ConcreteAgentSessionStore(); var delegatingStore = new TestDelegatingAgentSessionStore(innerStore); // Act var result = delegatingStore.GetService(); // Assert Assert.Same(innerStore, result); } /// /// Verify that GetService generic method returns null when type not found. /// [Fact] public void GetServiceGenericReturnsNullWhenTypeNotFound() { // Act var result = this._delegatingStore.GetService(); // Assert Assert.Null(result); } #endregion #region Test Implementation /// /// Test implementation of DelegatingAgentSessionStore for testing purposes. /// private sealed class TestDelegatingAgentSessionStore(AgentSessionStore innerStore) : DelegatingAgentSessionStore(innerStore) { public new AgentSessionStore InnerStore => base.InnerStore; } /// /// Another delegating store implementation for testing multi-layer chains. /// private sealed class AnotherDelegatingAgentSessionStore(AgentSessionStore innerStore) : DelegatingAgentSessionStore(innerStore); /// /// Concrete (non-delegating) session store for testing GetService chaining. /// private sealed class ConcreteAgentSessionStore : AgentSessionStore { public override ValueTask GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) => new(new TestAgentSession()); public override ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default) => ValueTask.CompletedTask; } private sealed class TestAgentSession : AgentSession; #endregion }