diff --git a/SemanticKernelPooling.Tests.Mocks/MockAIConfiguration.cs b/SemanticKernelPooling.Tests.Mocks/MockAIConfiguration.cs
new file mode 100644
index 0000000..9a82210
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Mocks/MockAIConfiguration.cs
@@ -0,0 +1,36 @@
+namespace SemanticKernelPooling.Tests.Mocks;
+
+///
+/// Represents a mock configuration for testing kernel pool behavior.
+/// This configuration simulates the settings required for AI service providers without actual credentials.
+///
+///
+/// This mock configuration is used in unit tests to verify the kernel pooling behavior
+/// without requiring actual AI service provider credentials or connections.
+///
+public record MockAIConfiguration : AIServiceProviderConfiguration
+{
+ ///
+ /// Gets or initializes the API key used for testing.
+ /// In mock scenarios, this can be any non-null string.
+ ///
+ public required string ApiKey { get; init; }
+
+ ///
+ /// Gets or initializes the model identifier used for testing.
+ /// In mock scenarios, this can be any non-null string.
+ ///
+ public required string ModelId { get; init; }
+
+ ///
+ /// Gets or initializes the service identifier used for testing.
+ /// In mock scenarios, this can be any non-null string.
+ ///
+ public required string ServiceId { get; init; }
+
+ ///
+ /// Gets or initializes the endpoint URL used for testing.
+ /// In mock scenarios, this can be any valid URL string.
+ ///
+ public required string Endpoint { get; init; }
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs b/SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs
new file mode 100644
index 0000000..86ef5fe
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Mocks/MockKernelPool.cs
@@ -0,0 +1,63 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel;
+
+namespace SemanticKernelPooling.Tests.Mocks;
+
+///
+/// Provides a mock implementation of the kernel pool for testing purposes.
+/// This implementation simulates the behavior of a real kernel pool without requiring actual AI service connections.
+///
+///
+///
+/// The mock kernel pool tracks kernel creation and provides a controllable environment for testing
+/// kernel pool behaviors including creation, disposal, and reuse patterns.
+///
+///
+/// Example usage:
+///
+/// var config = new MockAIConfiguration { ... };
+/// var pool = new MockKernelPool(config, loggerFactory, "path/to/responses");
+///
+///
+///
+public class MockKernelPool : AIServicePool
+{
+ private readonly ILoggerFactory _loggerFactory;
+ private int _kernelCreationCount;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration for the mock kernel pool.
+ /// The factory for creating loggers.
+ /// The path to mock response files. Defaults to "MockResponses".
+ public MockKernelPool(
+ MockAIConfiguration config,
+ ILoggerFactory loggerFactory)
+ : base(config)
+ {
+ _loggerFactory = loggerFactory;
+ }
+
+ ///
+ /// Registers a mock chat completion service with the kernel builder.
+ ///
+ /// The kernel builder to configure.
+ /// The mock configuration to use.
+ /// Optional HTTP client to use. If null, a mock client will be created.
+ protected override void RegisterChatCompletionService(
+ IKernelBuilder kernelBuilder,
+ MockAIConfiguration config,
+ HttpClient? httpClient)
+ {
+ Interlocked.Increment(ref _kernelCreationCount);
+ }
+
+ ///
+ protected override ILogger Logger => _loggerFactory.CreateLogger();
+
+ ///
+ /// Gets the number of kernels that have been created by this pool.
+ ///
+ public int KernelCreationCount => _kernelCreationCount;
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.Tests.Mocks/SemanticKernelPooling.Tests.Mocks.csproj b/SemanticKernelPooling.Tests.Mocks/SemanticKernelPooling.Tests.Mocks.csproj
new file mode 100644
index 0000000..a7dc87e
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Mocks/SemanticKernelPooling.Tests.Mocks.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs b/SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs
new file mode 100644
index 0000000..1090fba
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Mocks/ServiceExtension.cs
@@ -0,0 +1,58 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace SemanticKernelPooling.Tests.Mocks;
+
+///
+/// Provides extension methods for registering mock kernel pools with the service collection.
+///
+///
+/// This extension follows the same pattern as the production service extensions but provides
+/// mock implementations for testing purposes.
+///
+public static class ServiceExtension
+{
+ ///
+ /// Registers mock kernel pool implementations for testing purposes.
+ ///
+ /// The DI service provider.
+ /// The service provider to enable method chaining.
+ ///
+ ///
+ /// This method registers mock implementations for both OpenAI and Azure OpenAI service types,
+ /// allowing tests to run without actual service dependencies.
+ ///
+ ///
+ /// Example usage:
+ ///
+ /// services.AddSingleton(mockConfiguration.Object);
+ /// services.UseSemanticKernelPooling();
+ /// serviceProvider.UseMockKernelPool();
+ ///
+ ///
+ ///
+ public static IServiceProvider UseMockKernelPool(
+ this IServiceProvider serviceProvider,
+ string responsesPath = "MockResponses")
+ {
+ var registrar = serviceProvider.GetRequiredService();
+
+ // Register factory for both mock providers
+ var factory = (AIServiceProviderConfiguration config, ILoggerFactory loggerFactory) =>
+ new MockKernelPool((MockAIConfiguration)config, loggerFactory);
+
+ registrar.RegisterKernelPoolFactory(AIServiceProviderType.OpenAI, factory);
+ registrar.RegisterKernelPoolFactory(AIServiceProviderType.AzureOpenAI, factory);
+
+ // Register configuration readers
+ var configReader = (IConfigurationSection section) =>
+ section.Get() ??
+ throw new InvalidOperationException("Mock configuration not found.");
+
+ registrar.RegisterConfigurationReader(AIServiceProviderType.OpenAI, configReader);
+ registrar.RegisterConfigurationReader(AIServiceProviderType.AzureOpenAI, configReader);
+
+ return serviceProvider;
+ }
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs
new file mode 100644
index 0000000..e5db807
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByNameTests.cs
@@ -0,0 +1,323 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.SemanticKernel;
+using Xunit.Abstractions;
+
+namespace SemanticKernelPooling.Tests.Unit;
+
+[Collection("KernelPoolManagerTests")]
+public class KernelPoolManagerGetByNameTests
+{
+ private IServiceProvider _serviceProvider => _fixture.ServiceProvider;
+ private readonly SemanticKernelPoolTestFixture _fixture;
+ private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper;
+ private const string ValidUniqueName = "MockOpenAI";
+ private int PoolSize => _fixture.PoolSize;
+
+ public KernelPoolManagerGetByNameTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper)
+ {
+ _fixture = fixture;
+ _fixture.ConfigureLogging(outputHelper);
+ _fixture.InitializeServices();
+ OutputHelper.WriteLine("Initializing KernelPoolManagerGetByNameTests");
+ }
+
+ [Fact]
+ public async Task GetKernelByName_WithValidName_ReturnsKernelWithCorrectServiceType()
+ {
+ OutputHelper.WriteLine($"Starting test: GetKernelByName with valid name '{ValidUniqueName}'");
+
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+
+ using var kernelWrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ OutputHelper.WriteLine($"Retrieved kernel details:");
+ OutputHelper.WriteLine($"- Service Type: {kernelWrapper.ServiceProviderType}");
+ OutputHelper.WriteLine($"- Kernel Instance: {kernelWrapper.Kernel.GetHashCode()}");
+
+ Assert.NotNull(kernelWrapper);
+ Assert.NotNull(kernelWrapper.Kernel);
+ Assert.Equal(AIServiceProviderType.OpenAI, kernelWrapper.ServiceProviderType);
+ OutputHelper.WriteLine($"Successfully verified kernel properties");
+ }
+
+ [Fact]
+ public async Task GetKernelByName_WhenHoldingTwoKernels_BothAreDifferentInstances()
+ {
+ OutputHelper.WriteLine($"Starting test: GetKernelByName requesting two kernels simultaneously");
+
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+
+ using var wrapper1 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ OutputHelper.WriteLine($"First kernel obtained:");
+ OutputHelper.WriteLine($"- Instance Hash: {wrapper1.Kernel.GetHashCode()}");
+
+ using var wrapper2 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ OutputHelper.WriteLine($"Second kernel obtained:");
+ OutputHelper.WriteLine($"- Instance Hash: {wrapper2.Kernel.GetHashCode()}");
+
+ Assert.NotSame(wrapper1.Kernel, wrapper2.Kernel);
+ OutputHelper.WriteLine("Verified both kernels are different instances");
+ }
+
+ [Fact]
+ public async Task GetKernelByName_WhenDisposingAndReacquiring_ReusesKernel()
+ {
+ OutputHelper.WriteLine($"Starting test: GetKernelByName with disposal and reacquisition");
+
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+
+ Kernel firstKernel;
+ using (var wrapper1 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName))
+ {
+ firstKernel = wrapper1.Kernel;
+ OutputHelper.WriteLine($"First kernel obtained:");
+ OutputHelper.WriteLine($"- Instance Hash: {firstKernel.GetHashCode()}");
+ }
+ OutputHelper.WriteLine("First kernel disposed and returned to pool");
+
+ using var wrapper2 = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ OutputHelper.WriteLine($"Second kernel obtained:");
+ OutputHelper.WriteLine($"- Instance Hash: {wrapper2.Kernel.GetHashCode()}");
+
+ Assert.Same(firstKernel, wrapper2.Kernel);
+ OutputHelper.WriteLine("Verified second kernel is same instance as first");
+ }
+
+ [Fact]
+ public async Task GetKernelByName_WithInvalidName_ThrowsInvalidOperationException()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByName with invalid name");
+
+ // Arrange
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ const string invalidName = "NonExistentKernel";
+ OutputHelper.WriteLine($"Attempting to get kernel with invalid name: {invalidName}");
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(
+ async () => await kernelPoolManager.GetKernelByNameAsync(invalidName));
+
+ OutputHelper.WriteLine($"Exception details:");
+ OutputHelper.WriteLine($"- Type: {exception.GetType().Name}");
+ OutputHelper.WriteLine($"- Message: {exception.Message}");
+
+ // Verify the exception type and a more generic error message
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.Contains($"No configuration found for kernel pool name {invalidName}", exception.Message);
+
+ OutputHelper.WriteLine("Successfully verified exception type and message");
+ }
+
+ [Fact]
+ public async Task GetKernelByName_WhenPoolExhausted_WaitsForTimeoutThenThrows()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByName with pool exhaustion and timeout");
+
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var kernels = new List();
+
+ try
+ {
+ // Exhaust all pools
+ for (int i = 0; i < PoolSize; i++)
+ {
+ var wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ kernels.Add(wrapper);
+ OutputHelper.WriteLine($"Obtained kernel {i + 1}: Hash {wrapper.Kernel.GetHashCode()}");
+ }
+
+ var startTime = DateTime.UtcNow;
+ OutputHelper.WriteLine($"All pools exhausted, attempting to get another kernel at {startTime:HH:mm:ss.fff}");
+
+ await Assert.ThrowsAsync(
+ async () => await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName));
+
+ var duration = DateTime.UtcNow - startTime;
+ OutputHelper.WriteLine($"Request failed after {duration.TotalSeconds:F2} seconds");
+ }
+ finally
+ {
+ foreach (var kernel in kernels)
+ {
+ kernel.Dispose();
+ }
+ }
+ }
+
+ [Fact]
+ public async Task GetKernelByName_WhenKernelDisposedMultipleTimes_HandlesGracefully()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByName with multiple dispose calls");
+
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+
+ var wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ var kernelHash = wrapper.Kernel.GetHashCode();
+ OutputHelper.WriteLine($"Obtained kernel: Hash {kernelHash}");
+
+ // First dispose - should work normally
+ wrapper.Dispose();
+ OutputHelper.WriteLine("First dispose completed");
+
+ // Second dispose - should not throw
+ wrapper.Dispose();
+ OutputHelper.WriteLine("Second dispose completed without error");
+
+ // Verify we can get the same kernel back
+ using var newWrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ OutputHelper.WriteLine($"Retrieved new kernel: Hash {newWrapper.Kernel.GetHashCode()}");
+
+ Assert.Equal(kernelHash, newWrapper.Kernel.GetHashCode());
+ }
+
+ [Fact]
+ public async Task GetKernelByName_UnderLoad_MaintainsPoolSize()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByName under load conditions");
+
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var kernelHashes = new HashSet();
+ const int numberOfIterations = 10;
+
+ for (int i = 0; i < numberOfIterations; i++)
+ {
+ OutputHelper.WriteLine($"Iteration {i + 1} of {numberOfIterations}");
+ var wrappers = new List();
+
+ // Request maximum pool size kernels
+ for (int j = 0; j < PoolSize; j++)
+ {
+ var wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ wrappers.Add(wrapper);
+ kernelHashes.Add(wrapper.Kernel.GetHashCode());
+ OutputHelper.WriteLine($"Got kernel: Hash {wrapper.Kernel.GetHashCode()}");
+ }
+
+ // Dispose all kernels
+ foreach (var wrapper in wrappers)
+ {
+ wrapper.Dispose();
+ }
+ OutputHelper.WriteLine("All kernels returned to pool");
+ }
+
+ // Verify we never created more unique kernels than the pool size
+ OutputHelper.WriteLine($"Total unique kernels created: {kernelHashes.Count}");
+ Assert.Equal(PoolSize, kernelHashes.Count);
+ }
+
+ [Fact(Skip = "Need to add possibility to cancel kernel request")]
+ public async Task GetKernelByName_WithCancellation_StopsWaiting()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByName with cancellation");
+
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var cts = new CancellationTokenSource();
+
+ // Hold all available kernels
+ var heldKernels = new List();
+ for (int i = 0; i < PoolSize; i++)
+ {
+ var wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ heldKernels.Add(wrapper);
+ OutputHelper.WriteLine($"Holding kernel {i + 1}: Hash {wrapper.Kernel.GetHashCode()}");
+ }
+
+ try
+ {
+ // Try to get another kernel with cancellation
+ var getKernelTask = Task.Run(async () =>
+ {
+ try
+ {
+ return await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ }
+ catch (OperationCanceledException)
+ {
+ OutputHelper.WriteLine("Task was cancelled as expected");
+ throw;
+ }
+ }, cts.Token);
+
+ // Cancel after a short delay
+ await Task.Delay(100);
+ cts.Cancel();
+ OutputHelper.WriteLine("Cancellation requested");
+
+ // Verify the operation was cancelled
+ await Assert.ThrowsAsync(() => getKernelTask);
+ }
+ finally
+ {
+ // Cleanup
+ foreach (var wrapper in heldKernels)
+ {
+ wrapper.Dispose();
+ }
+ cts.Dispose();
+ }
+ }
+
+ [Fact]
+ public async Task GetKernelByName_WhenDisposedInDifferentThread_HandlesCorrectly()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByName with disposal in different thread");
+
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ KernelWrapper wrapper = null!;
+ int kernelHash = 0;
+
+ // Get kernel in main thread
+ wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ kernelHash = wrapper.Kernel.GetHashCode();
+ OutputHelper.WriteLine($"Got kernel in main thread: Hash {kernelHash}");
+
+ // Dispose in different thread
+ await Task.Run(() =>
+ {
+ OutputHelper.WriteLine($"Disposing kernel in separate thread: Hash {kernelHash}");
+ wrapper.Dispose();
+ });
+
+ // Verify we can get the same kernel back
+ using var newWrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ OutputHelper.WriteLine($"Retrieved kernel back in main thread: Hash {newWrapper.Kernel.GetHashCode()}");
+ Assert.Equal(kernelHash, newWrapper.Kernel.GetHashCode());
+ }
+
+ [Fact]
+ public async Task GetKernelByName_WithRapidRequestAndDispose_HandlesRaceConditions()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByName with rapid request/dispose cycles");
+
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ const int numberOfCycles = 20;
+ var tasks = new List();
+ var startTime = DateTime.UtcNow;
+
+ for (int i = 0; i < numberOfCycles; i++)
+ {
+ var cycleIndex = i;
+ var task = Task.Run(async () =>
+ {
+ OutputHelper.WriteLine($"Cycle {cycleIndex}: Requesting kernel at {(DateTime.UtcNow - startTime).TotalMilliseconds:F2}ms");
+
+ using var wrapper = await kernelPoolManager.GetKernelByNameAsync(ValidUniqueName);
+ var acquiredTime = DateTime.UtcNow;
+ OutputHelper.WriteLine($"Cycle {cycleIndex}: Acquired kernel Hash {wrapper.Kernel.GetHashCode()} at {(acquiredTime - startTime).TotalMilliseconds:F2}ms");
+
+ // Add small random delay to simulate some work
+ await Task.Delay(Random.Shared.Next(10, 50));
+
+ // Log before disposal
+ var disposingTime = DateTime.UtcNow;
+ OutputHelper.WriteLine($"Cycle {cycleIndex}: Disposing kernel Hash {wrapper.Kernel.GetHashCode()} at {(disposingTime - startTime).TotalMilliseconds:F2}ms");
+ });
+ tasks.Add(task);
+ }
+
+ await Task.WhenAll(tasks);
+ var endTime = DateTime.UtcNow;
+ OutputHelper.WriteLine($"All cycles completed after {(endTime - startTime).TotalMilliseconds:F2}ms");
+ }
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs
new file mode 100644
index 0000000..51d5b81
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByScopeTests.cs
@@ -0,0 +1,250 @@
+using Microsoft.Extensions.DependencyInjection;
+using Xunit.Abstractions;
+
+namespace SemanticKernelPooling.Tests.Unit;
+
+[Collection("KernelPoolManagerTests")]
+public class KernelPoolManagerGetByScopeTests
+{
+ private IServiceProvider _serviceProvider => _fixture.ServiceProvider;
+ private readonly SemanticKernelPoolTestFixture _fixture;
+ private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper;
+ private string TestScope1 => _fixture.TestScope1;
+ private int PoolSize => _fixture.PoolSize;
+ private int TotalPoolSize => PoolSize * 2;
+
+ public KernelPoolManagerGetByScopeTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper)
+ {
+ _fixture = fixture;
+ _fixture.ConfigureLogging(outputHelper);
+ _fixture.InitializeServices();
+ OutputHelper.WriteLine("Initializing KernelPoolManagerGetByScopeTests");
+ }
+
+ [Fact]
+ public async Task GetKernelByScope_WithValidScope_ReturnsValidKernel()
+ {
+ OutputHelper.WriteLine($"Starting test: GetKernelByScope with scope '{TestScope1}'");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+
+ using var wrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+
+ OutputHelper.WriteLine($"Retrieved kernel details:");
+ OutputHelper.WriteLine($"- Scope: {string.Join(", ", wrapper.Scopes)}");
+ OutputHelper.WriteLine($"- Kernel Instance: {wrapper.Kernel.GetHashCode()}");
+
+ Assert.NotNull(wrapper);
+ Assert.NotNull(wrapper.Kernel);
+ Assert.Contains(TestScope1, wrapper.Scopes);
+
+ OutputHelper.WriteLine("Successfully verified kernel properties");
+ }
+
+ [Fact]
+ public async Task GetKernelByScope_WithInvalidScope_ThrowsInvalidOperationException()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByScope with invalid scope");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ const string invalidScope = "non-existent-scope";
+
+ var exception = await Assert.ThrowsAsync(
+ async () => await kernelPoolManager.GetKernelByScopeAsync(invalidScope));
+
+ OutputHelper.WriteLine($"Exception details: {exception.Message}");
+ Assert.Contains($"No configuration found for scope {invalidScope}", exception.Message);
+ }
+
+ [Fact]
+ public async Task GetKernelByScope_WhenRequestingTwo_AlternateBetweenPools()
+ {
+ // Arrange
+ OutputHelper.WriteLine("Starting test: GetKernelByScope alternates between pools");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+
+ // Act
+ using var wrapper1 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+ OutputHelper.WriteLine($"First kernel obtained:");
+ OutputHelper.WriteLine($"- Pool Unique Name: {wrapper1.UniqueName}");
+ OutputHelper.WriteLine($"- Instance Hash: {wrapper1.Kernel.GetHashCode()}");
+
+ using var wrapper2 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+ OutputHelper.WriteLine($"Second kernel obtained:");
+ OutputHelper.WriteLine($"- Pool Unique Name: {wrapper2.UniqueName}");
+ OutputHelper.WriteLine($"- Instance Hash: {wrapper2.Kernel.GetHashCode()}");
+
+ // Assert
+ Assert.NotNull(wrapper1.Kernel);
+ Assert.NotNull(wrapper2.Kernel);
+ Assert.NotEqual(wrapper1.Kernel.GetHashCode(), wrapper2.Kernel.GetHashCode());
+
+ Assert.Contains(TestScope1, wrapper1.Scopes);
+ Assert.Contains(TestScope1, wrapper2.Scopes);
+
+ // Verify they're from different pools by checking their scopes
+ Assert.NotEqual(wrapper1.UniqueName, wrapper2.UniqueName);
+
+ OutputHelper.WriteLine($"Verified kernels came from different pools - Pool 1: {wrapper1.UniqueName}, Pool 2: {wrapper2.UniqueName}");
+ }
+
+ [Fact]
+ public async Task GetKernelByScope_WhenAllPoolsExhausted_WaitsForTimeoutThenThrows()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByScope with all pools exhausted");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var kernels = new List();
+
+ try
+ {
+ // Exhaust all pools
+ for (int i = 0; i < TotalPoolSize; i++)
+ {
+ var wrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+ kernels.Add(wrapper);
+ OutputHelper.WriteLine($"Obtained kernel {i + 1}: Hash {wrapper.Kernel.GetHashCode()}");
+ }
+
+ var startTime = DateTime.UtcNow;
+ OutputHelper.WriteLine($"All pools exhausted, attempting to get another kernel at {startTime:HH:mm:ss.fff}");
+
+ await Assert.ThrowsAsync(
+ async () => await kernelPoolManager.GetKernelByScopeAsync(TestScope1));
+
+ var duration = DateTime.UtcNow - startTime;
+ OutputHelper.WriteLine($"Request failed after {duration.TotalSeconds:F2} seconds");
+ }
+ finally
+ {
+ foreach (var kernel in kernels)
+ {
+ kernel.Dispose();
+ }
+ }
+ }
+
+ [Fact]
+ public async Task GetKernelByScope_WithMultipleRequests_DistributesLoad()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByScope load distribution");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var poolUsage = new Dictionary();
+
+ for (int i = 0; i < 10; i++)
+ {
+ using var wrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+ if (!poolUsage.ContainsKey(wrapper.UniqueName))
+ {
+ poolUsage[wrapper.UniqueName] = 0;
+ }
+ poolUsage[wrapper.UniqueName]++;
+
+ OutputHelper.WriteLine($"Request {i + 1} used pool: {wrapper.UniqueName}");
+ }
+
+ foreach (var usage in poolUsage)
+ {
+ OutputHelper.WriteLine($"Pool {usage.Key} was used {usage.Value} times");
+ }
+
+ Assert.True(poolUsage.Count > 1, "Load should be distributed across multiple pools");
+ }
+
+ [Fact]
+ public async Task GetKernelByScope_WithConcurrentRequests_HandlesThemCorrectly()
+ {
+ OutputHelper.WriteLine($"Starting test: GetKernelByScope with concurrent requests exceeding pool size");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var tasks = new List>();
+ const int extraRequests = 3; // Number of requests exceeding pool size
+ var concurrentRequests = TotalPoolSize + extraRequests;
+ var successfulRequests = new List();
+ var failedRequests = 0;
+
+ OutputHelper.WriteLine($"Total pool size: {TotalPoolSize}");
+ OutputHelper.WriteLine($"Extra requests: {extraRequests}");
+ OutputHelper.WriteLine($"Total concurrent requests: {concurrentRequests}");
+
+ try
+ {
+ // Create multiple concurrent requests
+ for (int i = 0; i < concurrentRequests; i++)
+ {
+ var requestId = i; // Capture for logging
+ var task = Task.Run(async () =>
+ {
+ try
+ {
+ OutputHelper.WriteLine($"Starting request {requestId + 1}/{concurrentRequests}");
+ return await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+ }
+ catch (InvalidOperationException ex)
+ {
+ OutputHelper.WriteLine($"Request {requestId + 1} failed as expected: {ex.Message}");
+ Interlocked.Increment(ref failedRequests);
+ throw;
+ }
+ });
+ tasks.Add(task);
+ }
+
+ // Wait for all tasks and collect results
+ var results = await Task.WhenAll(
+ tasks.Select(async t =>
+ {
+ try
+ {
+ return await t;
+ }
+ catch
+ {
+ return null;
+ }
+ })
+ );
+
+ // Count successful requests
+ successfulRequests.AddRange(results.Where(r => r != null)!);
+ var uniqueKernels = successfulRequests.Select(w => w.Kernel.GetHashCode()).Distinct().Count();
+
+ OutputHelper.WriteLine($"\nRequest Summary:");
+ OutputHelper.WriteLine($"- Total requests: {concurrentRequests}");
+ OutputHelper.WriteLine($"- Successful requests: {successfulRequests.Count}");
+ OutputHelper.WriteLine($"- Failed requests: {failedRequests}");
+ OutputHelper.WriteLine($"- Unique kernels obtained: {uniqueKernels}");
+
+ // Verify results
+ Assert.Equal(TotalPoolSize, successfulRequests.Count);
+ Assert.Equal(extraRequests, failedRequests);
+ Assert.True(uniqueKernels <= TotalPoolSize,
+ $"Got {uniqueKernels} unique kernels, expected maximum of {TotalPoolSize}");
+ }
+ finally
+ {
+ // Dispose successful kernels
+ foreach (var kernel in successfulRequests)
+ {
+ kernel.Dispose();
+ }
+ OutputHelper.WriteLine("Cleaned up all successful kernel requests");
+ }
+ }
+
+ [Fact]
+ public async Task GetKernelByScope_WhenDisposedAndReacquired_ReusesKernels()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByScope kernel reuse");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var seenKernels = new HashSet();
+ const int configCount = 2; // Number of OpenAI configurations
+
+ for (int i = 0; i < TotalPoolSize; i++)
+ {
+ using var wrapper = await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+ var kernelHash = wrapper.Kernel.GetHashCode();
+ OutputHelper.WriteLine($"Request {i + 1} got kernel with hash: {kernelHash} from {wrapper.UniqueName}");
+ seenKernels.Add(kernelHash);
+ }
+
+ OutputHelper.WriteLine($"Total unique kernels created: {seenKernels.Count}");
+ Assert.Equal(TotalPoolSize / configCount, seenKernels.Count);
+ }
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs
new file mode 100644
index 0000000..86e94d8
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerGetByServiceTypeTests.cs
@@ -0,0 +1,251 @@
+using Microsoft.Extensions.DependencyInjection;
+using Xunit.Abstractions;
+
+namespace SemanticKernelPooling.Tests.Unit;
+
+[Collection("KernelPoolManagerTests")]
+public class KernelPoolManagerGetByServiceTypeTests
+{
+ private IServiceProvider _serviceProvider => _fixture.ServiceProvider;
+ private readonly SemanticKernelPoolTestFixture _fixture;
+ private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper;
+ private int PoolSize => _fixture.PoolSize;
+ private int TotalPoolSize => PoolSize * 2; // Total across both configs
+ private const AIServiceProviderType TestProviderType = AIServiceProviderType.OpenAI;
+
+ public KernelPoolManagerGetByServiceTypeTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper)
+ {
+ _fixture = fixture;
+ _fixture.ConfigureLogging(outputHelper);
+ _fixture.InitializeServices();
+ OutputHelper.WriteLine("Initializing KernelPoolManagerGetByServiceTypeTests");
+ }
+
+ [Fact]
+ public async Task GetKernelByType_WithValidType_ReturnsValidKernel()
+ {
+ // Arrange
+ OutputHelper.WriteLine($"Starting test: GetKernelByType with service type '{TestProviderType}'");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+
+ // Act
+ using var wrapper = await kernelPoolManager.GetKernelAsync(TestProviderType);
+
+ // Log the details
+ OutputHelper.WriteLine($"Retrieved kernel details:");
+ OutputHelper.WriteLine($"- Service Type: {wrapper.ServiceProviderType}");
+ OutputHelper.WriteLine($"- Kernel Instance: {wrapper.Kernel.GetHashCode()}");
+
+ // Assert
+ Assert.NotNull(wrapper);
+ Assert.NotNull(wrapper.Kernel);
+ Assert.Equal(TestProviderType, wrapper.ServiceProviderType);
+
+ OutputHelper.WriteLine("Successfully verified kernel properties");
+ }
+
+ [Fact(Skip = "need to fix GetByType round robin")]
+ public async Task GetKernelByType_WhenRequestingTwo_AlternatesBetweenPools()
+ {
+ // Arrange
+ OutputHelper.WriteLine("Starting test: GetKernelByType alternates between pools");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+
+ // Act
+ using var wrapper1 = await kernelPoolManager.GetKernelAsync(TestProviderType);
+ OutputHelper.WriteLine($"First kernel obtained:");
+ OutputHelper.WriteLine($"- Pool Unique Name: {wrapper1.UniqueName}");
+ OutputHelper.WriteLine($"- Instance Hash: {wrapper1.Kernel.GetHashCode()}");
+
+ using var wrapper2 = await kernelPoolManager.GetKernelAsync(TestProviderType);
+ OutputHelper.WriteLine($"Second kernel obtained:");
+ OutputHelper.WriteLine($"- Pool Unique Name: {wrapper2.UniqueName}");
+ OutputHelper.WriteLine($"- Instance Hash: {wrapper2.Kernel.GetHashCode()}");
+
+ // Assert
+ Assert.NotNull(wrapper1.Kernel);
+ Assert.NotNull(wrapper2.Kernel);
+ Assert.NotEqual(wrapper1.Kernel.GetHashCode(), wrapper2.Kernel.GetHashCode());
+
+ // Verify they're from different pools by checking their scopes
+ Assert.NotEqual(wrapper1.UniqueName, wrapper2.UniqueName);
+
+ OutputHelper.WriteLine($"Verified kernels came from different pools - Pool 1: {wrapper1.UniqueName}, Pool 2: {wrapper2.UniqueName}");
+ }
+
+ [Fact(Skip = "need to fix GetByType round robin")]
+ public async Task GetKernelByType_WithInvalidType_ThrowsInvalidOperationException()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByType with invalid service type");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ const AIServiceProviderType invalidType = (AIServiceProviderType)999;
+
+ var exception = await Assert.ThrowsAsync(
+ async () => await kernelPoolManager.GetKernelAsync(invalidType));
+
+ OutputHelper.WriteLine($"Exception details: {exception.Message}");
+ Assert.Contains("No configuration found for kernel pool type", exception.Message);
+ }
+
+ [Fact(Skip = "need to fix GetByType round robin")]
+ public async Task GetKernelByType_WhenAllPoolsExhausted_WaitsForTimeoutThenThrows()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByType with all pools exhausted");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var kernels = new List();
+
+ try
+ {
+ // Exhaust all pools
+ for (int i = 0; i < TotalPoolSize; i++)
+ {
+ var wrapper = await kernelPoolManager.GetKernelAsync(TestProviderType);
+ kernels.Add(wrapper);
+ OutputHelper.WriteLine($"Obtained kernel {i + 1}: Hash {wrapper.Kernel.GetHashCode()}");
+ }
+
+ var startTime = DateTime.UtcNow;
+ OutputHelper.WriteLine($"All pools exhausted, attempting to get another kernel at {startTime:HH:mm:ss.fff}");
+
+ await Assert.ThrowsAsync(
+ async () => await kernelPoolManager.GetKernelAsync(TestProviderType));
+
+ var duration = DateTime.UtcNow - startTime;
+ OutputHelper.WriteLine($"Request failed after {duration.TotalSeconds:F2} seconds");
+ }
+ finally
+ {
+ foreach (var kernel in kernels)
+ {
+ kernel.Dispose();
+ }
+ }
+ }
+
+ [Fact(Skip = "need to fix GetByType round robin")]
+ public async Task GetKernelByType_WithMultipleRequests_DistributesLoad()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByType load distribution");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var poolUsage = new Dictionary();
+
+ for (int i = 0; i < 10; i++)
+ {
+ using var wrapper = await kernelPoolManager.GetKernelAsync(TestProviderType);
+ if (!poolUsage.ContainsKey(wrapper.UniqueName))
+ {
+ poolUsage[wrapper.UniqueName] = 0;
+ }
+ poolUsage[wrapper.UniqueName]++;
+
+ OutputHelper.WriteLine($"Request {i + 1} used pool: {wrapper.UniqueName}");
+ }
+
+ foreach (var usage in poolUsage)
+ {
+ OutputHelper.WriteLine($"Pool {usage.Key} was used {usage.Value} times");
+ }
+
+ Assert.True(poolUsage.Count > 1, "Load should be distributed across multiple pools");
+ }
+
+ [Fact(Skip = "need to fix GetByType round robin")]
+ public async Task GetKernelByType_WithConcurrentRequests_HandlesThemCorrectly()
+ {
+ OutputHelper.WriteLine($"Starting test: GetKernelByType with concurrent requests exceeding pool size");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var tasks = new List>();
+ const int extraRequests = 3; // Number of requests exceeding pool size
+ var concurrentRequests = TotalPoolSize + extraRequests;
+ var successfulRequests = new List();
+ var failedRequests = 0;
+
+ OutputHelper.WriteLine($"Total pool size: {TotalPoolSize}");
+ OutputHelper.WriteLine($"Extra requests: {extraRequests}");
+ OutputHelper.WriteLine($"Total concurrent requests: {concurrentRequests}");
+
+ try
+ {
+ // Create multiple concurrent requests
+ for (int i = 0; i < concurrentRequests; i++)
+ {
+ var requestId = i; // Capture for logging
+ var task = Task.Run(async () =>
+ {
+ try
+ {
+ OutputHelper.WriteLine($"Starting request {requestId + 1}/{concurrentRequests}");
+ return await kernelPoolManager.GetKernelAsync(TestProviderType);
+ }
+ catch (InvalidOperationException ex)
+ {
+ OutputHelper.WriteLine($"Request {requestId + 1} failed as expected: {ex.Message}");
+ Interlocked.Increment(ref failedRequests);
+ throw;
+ }
+ });
+ tasks.Add(task);
+ }
+
+ // Wait for all tasks and collect results
+ var results = await Task.WhenAll(
+ tasks.Select(async t =>
+ {
+ try
+ {
+ return await t;
+ }
+ catch
+ {
+ return null;
+ }
+ })
+ );
+
+ // Count successful requests
+ successfulRequests.AddRange(results.Where(r => r != null)!);
+ var uniqueKernels = successfulRequests.Select(w => w.Kernel.GetHashCode()).Distinct().Count();
+
+ OutputHelper.WriteLine($"\nRequest Summary:");
+ OutputHelper.WriteLine($"- Total requests: {concurrentRequests}");
+ OutputHelper.WriteLine($"- Successful requests: {successfulRequests.Count}");
+ OutputHelper.WriteLine($"- Failed requests: {failedRequests}");
+ OutputHelper.WriteLine($"- Unique kernels obtained: {uniqueKernels}");
+
+ // Verify results
+ Assert.Equal(TotalPoolSize, successfulRequests.Count);
+ Assert.Equal(extraRequests, failedRequests);
+ Assert.True(uniqueKernels <= TotalPoolSize,
+ $"Got {uniqueKernels} unique kernels, expected maximum of {TotalPoolSize}");
+ }
+ finally
+ {
+ // Dispose successful kernels
+ foreach (var kernel in successfulRequests)
+ {
+ kernel.Dispose();
+ }
+ OutputHelper.WriteLine("Cleaned up all successful kernel requests");
+ }
+ }
+
+ [Fact(Skip = "need to fix GetByType round robin")]
+ public async Task GetKernelByType_WhenDisposedAndReacquired_ReusesKernels()
+ {
+ OutputHelper.WriteLine("Starting test: GetKernelByType kernel reuse");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var seenKernels = new HashSet();
+ const int configCount = 2; // Number of OpenAI configurations
+
+ for (int i = 0; i < TotalPoolSize; i++)
+ {
+ using var wrapper = await kernelPoolManager.GetKernelAsync(TestProviderType);
+ var kernelHash = wrapper.Kernel.GetHashCode();
+ OutputHelper.WriteLine($"Request {i + 1} got kernel with hash: {kernelHash} from {wrapper.UniqueName}");
+ seenKernels.Add(kernelHash);
+ }
+
+ OutputHelper.WriteLine($"Total unique kernels created: {seenKernels.Count}");
+ Assert.Equal(TotalPoolSize / configCount, seenKernels.Count);
+ }
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs
new file mode 100644
index 0000000..9eb0fb7
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerInitializationTests.cs
@@ -0,0 +1,162 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.SemanticKernel;
+using SemanticKernelPooling.Tests.Mocks;
+using Xunit.Abstractions;
+
+namespace SemanticKernelPooling.Tests.Unit;
+
+[Collection("KernelPoolManagerTests")]
+public class KernelPoolManagerInitializationTests
+{
+ private IServiceProvider _serviceProvider => _fixture.ServiceProvider;
+ private readonly SemanticKernelPoolTestFixture _fixture;
+ private ITestOutputHelper OutputHelper => _fixture.TestOutputHelper;
+ private string TestScope1 => _fixture.TestScope1;
+ private string TestScope2 => _fixture.TestScope2;
+
+ public KernelPoolManagerInitializationTests(SemanticKernelPoolTestFixture fixture, ITestOutputHelper outputHelper)
+ {
+ _fixture = fixture;
+ _fixture.ConfigureLogging(outputHelper);
+ _fixture.InitializeServices();
+ OutputHelper.WriteLine("Initializing KernelPoolManagerInitializationTests");
+ }
+
+ [Fact]
+ public async Task RegisterForPreKernelCreation_WithScope_ExecutesForMatchingScope()
+ {
+ // Arrange
+ OutputHelper.WriteLine($"Starting test: RegisterForPreKernelCreation with scope '{TestScope1}'");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var executionCount = 0;
+
+ // Register scoped pre-initialization action
+ kernelPoolManager.RegisterForPreKernelCreation(TestScope1,
+ (builder, config, options, scopes) =>
+ {
+ OutputHelper.WriteLine("Executing preRegisteration");
+ Assert.NotNull(scopes); // Verify scopes is not null
+ Assert.Contains(TestScope1, scopes);
+ Interlocked.Increment(ref executionCount);
+ OutputHelper.WriteLine($"Pre-initialization action executed for scopes: {string.Join(", ", scopes)}");
+ });
+
+ // Act
+ using var wrapper1 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+ using var wrapper2 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+
+ using var wrapperDifferentScope = await kernelPoolManager.GetKernelByScopeAsync(TestScope2);
+
+ OutputHelper.WriteLine("wrapper 1 unique name: " + wrapper1.UniqueName);
+ OutputHelper.WriteLine("wrapper 2 unique name: " + wrapper2.UniqueName);
+ OutputHelper.WriteLine("wrapper Different Scope unique name: " + wrapperDifferentScope.UniqueName);
+
+ // Assert
+ OutputHelper.WriteLine($"Execution count: {executionCount}");
+ Assert.Equal(2, executionCount);
+ Assert.DoesNotContain(TestScope1, wrapperDifferentScope.Scopes);
+ }
+
+ [Fact]
+ public async Task RegisterForAfterKernelCreation_WithScope_ExecutesForMatchingScope()
+ {
+ // Arrange
+ OutputHelper.WriteLine($"Starting test: RegisterForAfterKernelCreation with scope '{TestScope1}'");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var executionCount = 0;
+ Kernel? lastConfiguredKernel = null;
+
+ // Register scoped post-initialization action
+ kernelPoolManager.RegisterForAfterKernelCreation(TestScope1,
+ (kernel, config, scopes) =>
+ {
+ OutputHelper.WriteLine("Executing AfterRegisteration");
+ Assert.NotNull(scopes); // Verify scopes is not null
+ Assert.Contains(TestScope1, scopes);
+ Interlocked.Increment(ref executionCount);
+ lastConfiguredKernel = kernel;
+ OutputHelper.WriteLine($"Post-initialization action executed for scopes: {string.Join(", ", scopes)}");
+ });
+
+ // Act
+ using var wrapper1 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+ using var wrapper2 = await kernelPoolManager.GetKernelByScopeAsync(TestScope1);
+
+ using var wrapperDifferentScope= await kernelPoolManager.GetKernelByScopeAsync(TestScope2);
+
+ OutputHelper.WriteLine("wrapper 1 unique name: " + wrapper1.UniqueName);
+ OutputHelper.WriteLine("wrapper 2 unique name: " + wrapper2.UniqueName);
+ OutputHelper.WriteLine("wrapper Different Scope unique name: " + wrapperDifferentScope.UniqueName);
+
+ // Assert
+ OutputHelper.WriteLine($"Execution count: {executionCount}");
+ Assert.Equal(2, executionCount);
+ Assert.Same(lastConfiguredKernel, wrapper2.Kernel);
+ Assert.DoesNotContain(TestScope1, wrapperDifferentScope.Scopes);
+ }
+
+ [Fact(Skip ="fix register by configuration type")]
+ public async Task RegisterForPreKernelCreation_WithType_ExecutesForMatchingType()
+ {
+ // Arrange
+ OutputHelper.WriteLine("Starting test: RegisterForPreKernelCreation with type MockAIConfiguration");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var providerType = AIServiceProviderType.OpenAI;
+ var executionCount = 0;
+
+ // Register type-specific pre-initialization action
+ kernelPoolManager.RegisterForPreKernelCreation(
+ (builder, config, options) =>
+ {
+ OutputHelper.WriteLine("Executing preRegistration for MockAIConfiguration");
+ Assert.IsType(config);
+ Interlocked.Increment(ref executionCount);
+ OutputHelper.WriteLine($"Pre-initialization action executed for config: {config.UniqueName}");
+ });
+
+ // Act
+ using var wrapper1 = await kernelPoolManager.GetKernelAsync(providerType);
+ using var wrapper2 = await kernelPoolManager.GetKernelAsync(providerType);
+
+ OutputHelper.WriteLine($"wrapper 1 type: {providerType}, unique name: {wrapper1.UniqueName}");
+ OutputHelper.WriteLine($"wrapper 2 type: {providerType}, unique name: {wrapper2.UniqueName}");
+
+ // Assert
+ OutputHelper.WriteLine($"Execution count: {executionCount}");
+ Assert.Equal(2, executionCount);
+ }
+
+ [Fact(Skip = "fix register by configuration type")]
+ public async Task RegisterForAfterKernelCreation_WithType_ExecutesForMatchingType()
+ {
+ // Arrange
+ OutputHelper.WriteLine("Starting test: RegisterForAfterKernelCreation with type MockAIConfiguration");
+ var kernelPoolManager = _serviceProvider.GetRequiredService();
+ var providerType = AIServiceProviderType.OpenAI;
+ var executionCount = 0;
+ Kernel? lastConfiguredKernel = null;
+
+ // Register type-specific post-initialization action
+ kernelPoolManager.RegisterForAfterKernelCreation(
+ (kernel, config) =>
+ {
+ OutputHelper.WriteLine("Executing afterRegistration for MockAIConfiguration");
+ Assert.IsType(config);
+ Interlocked.Increment(ref executionCount);
+ lastConfiguredKernel = kernel;
+ OutputHelper.WriteLine($"Post-initialization action executed for config: {config.UniqueName}");
+ });
+
+ // Act
+ using var wrapper1 = await kernelPoolManager.GetKernelAsync(providerType);
+ using var wrapper2 = await kernelPoolManager.GetKernelAsync(providerType);
+
+ OutputHelper.WriteLine($"wrapper 1 type: {providerType}, unique name: {wrapper1.UniqueName}");
+ OutputHelper.WriteLine($"wrapper 2 type: {providerType}, unique name: {wrapper2.UniqueName}");
+
+ // Assert
+ OutputHelper.WriteLine($"Execution count: {executionCount}");
+ Assert.Equal(2, executionCount);
+ Assert.Same(lastConfiguredKernel, wrapper2.Kernel);
+ }
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTestCollection.cs b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTestCollection.cs
new file mode 100644
index 0000000..f41ea42
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/KernelPoolManagerTestCollection.cs
@@ -0,0 +1,7 @@
+namespace SemanticKernelPooling.Tests.Unit;
+
+[CollectionDefinition("KernelPoolManagerTests")]
+public class KernelPoolManagerTestCollection : ICollectionFixture
+{
+ // This class is just a marker for the collection, it remains empty
+}
diff --git a/SemanticKernelPooling.Tests.Unit/Logging/XUnitLogger.cs b/SemanticKernelPooling.Tests.Unit/Logging/XUnitLogger.cs
new file mode 100644
index 0000000..db73e76
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/Logging/XUnitLogger.cs
@@ -0,0 +1,49 @@
+using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+
+namespace SemanticKernelPooling.Tests.Unit.Logging
+{
+ internal class XUnitLogger : ILogger
+ {
+ private readonly ITestOutputHelper _testOutputHelper;
+ private readonly string _categoryName;
+
+ public XUnitLogger(ITestOutputHelper testOutputHelper, string categoryName)
+ {
+ _testOutputHelper = testOutputHelper;
+ _categoryName = categoryName;
+ }
+
+ public IDisposable BeginScope(TState state) where TState : notnull
+ => NoopDisposable.Instance;
+
+ public bool IsEnabled(LogLevel logLevel)
+ => true;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ try
+ {
+ _testOutputHelper.WriteLine($"{_categoryName} [{eventId}] {formatter(state, exception!)}");
+
+ if (exception != null)
+ _testOutputHelper.WriteLine(exception.ToString());
+ }
+ catch (InvalidOperationException)
+ {
+ //no active test, ignore!
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex.Message);
+ }
+ }
+
+ private class NoopDisposable : IDisposable
+ {
+ public static readonly NoopDisposable Instance = new NoopDisposable();
+ public void Dispose()
+ { }
+ }
+ }
+}
diff --git a/SemanticKernelPooling.Tests.Unit/Logging/XUnitLoggerProvider.cs b/SemanticKernelPooling.Tests.Unit/Logging/XUnitLoggerProvider.cs
new file mode 100644
index 0000000..4b003d4
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/Logging/XUnitLoggerProvider.cs
@@ -0,0 +1,19 @@
+using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+
+namespace SemanticKernelPooling.Tests.Unit.Logging;
+
+public class XUnitLoggerProvider : ILoggerProvider
+{
+ private readonly ITestOutputHelper _testOutputHelper;
+
+ public XUnitLoggerProvider(ITestOutputHelper testOutputHelper)
+ {
+ _testOutputHelper = testOutputHelper;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ => new XUnitLogger(_testOutputHelper, categoryName);
+
+ public void Dispose(){ }
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs b/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs
new file mode 100644
index 0000000..65eed73
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/SemanticKernelPoolTestFixture.cs
@@ -0,0 +1,137 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using SemanticKernelPooling.Tests.Mocks;
+using SemanticKernelPooling.Tests.Unit.Logging;
+using System.Net;
+using System.Text.Json;
+using Xunit.Abstractions;
+
+namespace SemanticKernelPooling.Tests.Unit;
+
+///
+/// Test fixture for Semantic Kernel Pool tests that provides configured services,
+/// logging, and mock response handling capabilities.
+///
+public class SemanticKernelPoolTestFixture : IDisposable
+{
+ ///
+ /// Gets the configured service provider containing all required services for testing.
+ ///
+ public IServiceProvider ServiceProvider { get; private set; } = null!;
+
+ ///
+ /// Gets the test output helper for logging test information.
+ ///
+ public ITestOutputHelper TestOutputHelper
+ {
+ get
+ {
+ if (_testOutputHelper == null)
+ throw new InvalidOperationException("TestOutputHelper not configured. Call ConfigureLogging first.");
+ return _testOutputHelper;
+ }
+ private set => _testOutputHelper = value;
+ }
+
+ private readonly string _mockResponsesPath;
+ private ILoggerProvider? _loggerProvider;
+ private ITestOutputHelper? _testOutputHelper;
+
+ ///
+ /// The scope used for test configurations.
+ ///
+ public string TestScope1 = "test_scope_1";
+ public string TestScope2 = "test_scope_2";
+
+ ///
+ /// The size of the kernel pool for testing.
+ ///
+ public int PoolSize = 2;
+
+ ///
+ /// The mock deployment name used in test configurations.
+ ///
+ public const string DeploymentName = "mock-deployment";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SemanticKernelPoolTestFixture()
+ {
+ _mockResponsesPath = Path.Combine(Path.GetTempPath(), "MockResponses");
+ Directory.CreateDirectory(_mockResponsesPath);
+ InitializeServices();
+ }
+
+ public void InitializeServices()
+ {
+ var services = new ServiceCollection();
+
+ services.AddLogging(builder =>
+ {
+ builder.ClearProviders();
+ builder.SetMinimumLevel(LogLevel.Debug);
+ });
+
+ var configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json", optional: false)
+ .Build();
+
+ services.AddSingleton(configuration);
+ services.UseSemanticKernelPooling();
+
+ ServiceProvider = services.BuildServiceProvider();
+ ServiceProvider.UseMockKernelPool(_mockResponsesPath);
+ }
+
+ ///
+ /// Configures logging for a specific test, ensuring test output is properly captured.
+ ///
+ /// The test output helper provided by XUnit.
+ public void ConfigureLogging(ITestOutputHelper testOutputHelper)
+ {
+ TestOutputHelper = testOutputHelper; // Set the property
+ var loggerFactory = ServiceProvider.GetRequiredService();
+
+ _loggerProvider?.Dispose();
+ _loggerProvider = new XUnitLoggerProvider(testOutputHelper);
+ loggerFactory.AddProvider(_loggerProvider);
+
+ TestOutputHelper.WriteLine("Logging configured successfully");
+ }
+
+ ///
+ /// Performs cleanup of managed and unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ if (ServiceProvider is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ _loggerProvider?.Dispose();
+
+ try
+ {
+ if (Directory.Exists(_mockResponsesPath))
+ {
+ Directory.Delete(_mockResponsesPath, true);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Since we're disposing, we might not have access to TestOutputHelper,
+ // but we'll try to log if we can
+ try
+ {
+ TestOutputHelper.WriteLine($"Error cleaning up mock responses directory: {ex.Message}");
+ }
+ catch
+ {
+ // Nothing we can do here if TestOutputHelper is not available
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj b/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj
new file mode 100644
index 0000000..cfa01fa
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/SemanticKernelPooling.Tests.Unit.csproj
@@ -0,0 +1,43 @@
+
+
+
+ net9.0
+ enable
+ enable
+ false
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SemanticKernelPooling.Tests.Unit/TestOutputHelper.cs b/SemanticKernelPooling.Tests.Unit/TestOutputHelper.cs
new file mode 100644
index 0000000..9002b80
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/TestOutputHelper.cs
@@ -0,0 +1,36 @@
+using Xunit.Abstractions;
+
+namespace SemanticKernelPooling.Tests.Unit;
+
+public class TestOutputHelper : ITestOutputHelper
+{
+ private readonly ITestOutputHelper _testOutputHelper;
+
+ public TestOutputHelper(ITestOutputHelper testOutputHelper)
+ {
+ _testOutputHelper = testOutputHelper;
+ }
+ public void WriteLine(string message)
+ {
+ try
+ {
+ _testOutputHelper.WriteLine(message);
+ }
+ catch
+ {
+ Console.WriteLine(message);
+ }
+ }
+
+ public void WriteLine(string format, params object[] args)
+ {
+ try
+ {
+ _testOutputHelper.WriteLine(format, args);
+ }
+ catch
+ {
+ Console.WriteLine(format, args);
+ }
+ }
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.Tests.Unit/appsettings.json b/SemanticKernelPooling.Tests.Unit/appsettings.json
new file mode 100644
index 0000000..2d76854
--- /dev/null
+++ b/SemanticKernelPooling.Tests.Unit/appsettings.json
@@ -0,0 +1,62 @@
+{
+ "AIServiceProviderConfigurations": [
+ {
+ "UniqueName": "MockOpenAI",
+ "ServiceType": "OpenAI",
+ "InstanceCount": 2,
+ "DeploymentTextEmbedding": "mock-embedding",
+ "MaxWaitForKernelInSeconds": 1,
+ "ApiKey": "mock-api-key",
+ "ModelId": "mock-model",
+ "ServiceId": "mock-service-id",
+ "Endpoint": "https://mock.endpoint",
+ "Scopes": [
+ "test_scope_1"
+ ]
+ },
+ {
+ "UniqueName": "MockOpenAI2",
+ "ServiceType": "OpenAI",
+ "InstanceCount": 2,
+ "DeploymentTextEmbedding": "mock-embedding",
+ "MaxWaitForKernelInSeconds": 1,
+ "ApiKey": "mock-api-key",
+ "ModelId": "mock-model",
+ "ServiceId": "mock-service-id",
+ "Endpoint": "https://mock.endpoint",
+ "Scopes": [
+ "test_scope_2"
+ ]
+ },
+ {
+ "UniqueName": "MockAzureOpenAI",
+ "ServiceType": "AzureOpenAI",
+ "InstanceCount": 2,
+ "DeploymentTextEmbedding": "mock-embedding",
+ "MaxWaitForKernelInSeconds": 1,
+ "ApiKey": "mock-api-key",
+ "ModelId": "mock-model",
+ "ServiceId": "mock-service-id",
+ "Endpoint": "https://mock.openai.azure.com",
+ "DeploymentName": "DeploymentName",
+ "Scopes": [
+ "test_scope_1"
+ ]
+ },
+ {
+ "UniqueName": "MockAzureOpenAI2",
+ "ServiceType": "AzureOpenAI",
+ "InstanceCount": 2,
+ "DeploymentTextEmbedding": "mock-embedding",
+ "MaxWaitForKernelInSeconds": 1,
+ "ApiKey": "mock-api-key",
+ "ModelId": "mock-model",
+ "ServiceId": "mock-service-id",
+ "Endpoint": "https://mock.openai.azure.com",
+ "DeploymentName": "DeploymentName",
+ "Scopes": [
+ "test_scope_2"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/SemanticKernelPooling.sln b/SemanticKernelPooling.sln
index 64605d6..852d18b 100644
--- a/SemanticKernelPooling.sln
+++ b/SemanticKernelPooling.sln
@@ -26,6 +26,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{6ACB62
assets\package-icon.png = assets\package-icon.png
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernelPooling.Tests.Mocks", "SemanticKernelPooling.Tests.Mocks\SemanticKernelPooling.Tests.Mocks.csproj", "{D03855DF-C58C-40F2-A361-CDAF7FCEC88C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SemanticKernelPooling.Tests.Unit", "SemanticKernelPooling.Tests.Unit\SemanticKernelPooling.Tests.Unit.csproj", "{B8F6A4BF-2626-4367-95D4-FB00A8254785}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -56,6 +60,14 @@ Global
{EF7CC525-73D8-4416-A52F-D8C44BEA539B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF7CC525-73D8-4416-A52F-D8C44BEA539B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF7CC525-73D8-4416-A52F-D8C44BEA539B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D03855DF-C58C-40F2-A361-CDAF7FCEC88C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D03855DF-C58C-40F2-A361-CDAF7FCEC88C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D03855DF-C58C-40F2-A361-CDAF7FCEC88C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D03855DF-C58C-40F2-A361-CDAF7FCEC88C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B8F6A4BF-2626-4367-95D4-FB00A8254785}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B8F6A4BF-2626-4367-95D4-FB00A8254785}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B8F6A4BF-2626-4367-95D4-FB00A8254785}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B8F6A4BF-2626-4367-95D4-FB00A8254785}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/SemanticKernelPooling/AIServicePool.cs b/SemanticKernelPooling/AIServicePool.cs
index 3f8af73..2aff9bb 100644
--- a/SemanticKernelPooling/AIServicePool.cs
+++ b/SemanticKernelPooling/AIServicePool.cs
@@ -61,7 +61,13 @@ private Kernel CreateKernel()
{
var kernelBuilder = Kernel.CreateBuilder();
- TServiceProviderConfiguration newConfig = AIServiceProviderAIConfiguration with { UniqueName = $"{AIServiceProviderAIConfiguration.UniqueName}{CurrentNumberOfKernels}" };
+ TServiceProviderConfiguration newConfig = AIServiceProviderAIConfiguration with {
+ UniqueName = $"{AIServiceProviderAIConfiguration.UniqueName}{CurrentNumberOfKernels}",
+ Scopes = AIServiceProviderAIConfiguration.Scopes,
+ InstanceCount = AIServiceProviderAIConfiguration.InstanceCount,
+ MaxWaitForKernelInSeconds = AIServiceProviderAIConfiguration.MaxWaitForKernelInSeconds,
+ DeploymentTextEmbedding = AIServiceProviderAIConfiguration.DeploymentTextEmbedding,
+ };
bool shouldAutoAddChatCompletionService = true;
KernelBuilderOptions options = new();
diff --git a/SemanticKernelPooling/KernelPoolManager.cs b/SemanticKernelPooling/KernelPoolManager.cs
index 5961f54..97df982 100644
--- a/SemanticKernelPooling/KernelPoolManager.cs
+++ b/SemanticKernelPooling/KernelPoolManager.cs
@@ -109,7 +109,7 @@ private async Task GetKernelAsync(AIServiceProviderConfiguration
public async Task GetKernelByNameAsync(string uniqueName)
{
//get the service configuration
- var config = AIConfigurations.First(c => c.UniqueName == uniqueName);
+ var config = AIConfigurations.FirstOrDefault(c => c.UniqueName == uniqueName);
if (config == null)
throw new InvalidOperationException($"No configuration found for kernel pool name {uniqueName}");
diff --git a/SemanticKernelPooling/KernelWrapper.cs b/SemanticKernelPooling/KernelWrapper.cs
index 8a5f688..e32403d 100644
--- a/SemanticKernelPooling/KernelWrapper.cs
+++ b/SemanticKernelPooling/KernelWrapper.cs
@@ -51,4 +51,9 @@ public void Dispose()
///
// ReSharper disable once UnusedMember.Global
public AIServiceProviderType ServiceProviderType { get; } = pool.ServiceProviderType;
+
+ ///
+ /// The Unique Name of the kernel pool of this wrapper.
+ ///
+ public string UniqueName { get; } = pool.UniqueName;
}
\ No newline at end of file