diff --git a/SpongeEngine.KoboldSharp.Tests/Integration/Extra/AbortGenerate.cs b/SpongeEngine.KoboldSharp.Tests/Integration/Extra/AbortGenerate.cs new file mode 100644 index 0000000..79e2192 --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Integration/Extra/AbortGenerate.cs @@ -0,0 +1,70 @@ +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Integration.Extra +{ + [Trait("Category", "Integration")] + [Trait("API", "Native")] + public class AbortGenerate : IntegrationTestBase + { + public AbortGenerate(ITestOutputHelper output) : base(output) {} + + [SkippableFact] + public async Task AbortGenerate_DuringGeneration_ShouldStop() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Arrange - Start a long generation + var request = new KoboldSharpClient.KoboldSharpRequest + { + Prompt = "Write a very long story about a journey", + MaxLength = 200, + Temperature = 0.7f, + TopP = 0.9f + }; + + // Start generation in a separate task + var generationTask = Task.Run(async () => + { + try + { + await foreach (var token in Client.GenerateStreamAsync(request)) + { + Output.WriteLine($"Generated token: {token}"); + } + } + catch (Exception ex) + { + Output.WriteLine($"Generation stopped: {ex.Message}"); + } + }); + + // Give some time for generation to start + await Task.Delay(1000); + + // Act + var result = await Client.AbortGenerateAsync(); + + // Assert + result.Should().BeTrue(); + + // Wait for generation task to complete due to abort + var completedTask = await Task.WhenAny(generationTask, Task.Delay(5000)); + completedTask.Should().Be(generationTask, "Generation should stop after abort"); + } + + [SkippableFact] + public async Task AbortGenerate_WhenNoGeneration_ShouldReturnTrue() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Act + var result = await Client.AbortGenerateAsync(); + + // Assert + result.Should().BeTrue(); + Output.WriteLine("Successfully called abort with no active generation"); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Integration/Extra/LogProbs.cs b/SpongeEngine.KoboldSharp.Tests/Integration/Extra/LogProbs.cs new file mode 100644 index 0000000..2ffdfeb --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Integration/Extra/LogProbs.cs @@ -0,0 +1,122 @@ +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Integration.Extra +{ + [Trait("Category", "Integration")] + [Trait("API", "Native")] + public class LogProbs : IntegrationTestBase + { + public LogProbs(ITestOutputHelper output) : base(output) {} + + [SkippableFact] + public async Task GetPendingOutput_DuringGeneration_ShouldReturnPartialOutput() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Arrange - Start a long generation + var request = new KoboldSharpClient.KoboldSharpRequest + { + Prompt = "Write a long story about an adventure", + MaxLength = 100, + Temperature = 0.7f, + TopP = 0.9f + }; + + // Start generation in background + var generationTask = Task.Run(async () => + { + await Client.GenerateAsync(request); + }); + + // Give some time for generation to start + await Task.Delay(500); + + // Act + var pendingOutput = await Client.GetPendingOutputAsync(); + + // Assert + pendingOutput.Should().NotBeNull(); + if (!string.IsNullOrEmpty(pendingOutput)) + { + Output.WriteLine($"Pending output received: {pendingOutput}"); + } + else + { + Output.WriteLine("No pending output at the moment of check"); + } + + // Cleanup - ensure generation completes or is aborted + if (!generationTask.IsCompleted) + { + await Client.AbortGenerateAsync(); + } + await Task.WhenAny(generationTask, Task.Delay(5000)); + } + + [SkippableFact] + public async Task GetPendingOutput_WithNoGeneration_ShouldReturnEmpty() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // First ensure no generation is running + await Client.AbortGenerateAsync(); + await Task.Delay(500); // Give time for any pending operations to complete + + // Act + var result = await Client.GetPendingOutputAsync(); + + // Assert + result.Should().BeEmpty(); + Output.WriteLine("Successfully verified no pending output when no generation is active"); + } + + [SkippableFact] + public async Task GetPendingOutput_MultipleChecks_ShouldShowProgress() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Arrange - Start a long generation + var request = new KoboldSharpClient.KoboldSharpRequest + { + Prompt = "Write a detailed story about a journey through time", + MaxLength = 150, + Temperature = 0.7f, + TopP = 0.9f + }; + + var generationTask = Task.Run(async () => + { + await Client.GenerateAsync(request); + }); + + // Act - Check output multiple times + var outputs = new List(); + for (int i = 0; i < 3; i++) + { + await Task.Delay(1000); // Wait between checks + var pendingOutput = await Client.GetPendingOutputAsync(); + outputs.Add(pendingOutput); + Output.WriteLine($"Check {i + 1} output length: {pendingOutput.Length}"); + Output.WriteLine($"Content: {pendingOutput}"); + } + + // Assert + outputs.Should().NotBeEmpty(); + if (outputs.Count > 1) + { + // Verify that at least some outputs show different content + outputs.Distinct().Should().HaveCountGreaterThan(1, + "Multiple checks should show generation progress"); + } + + // Cleanup + if (!generationTask.IsCompleted) + { + await Client.AbortGenerateAsync(); + } + await Task.WhenAny(generationTask, Task.Delay(5000)); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Integration/Extra/PendingOutput.cs b/SpongeEngine.KoboldSharp.Tests/Integration/Extra/PendingOutput.cs new file mode 100644 index 0000000..eecbacb --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Integration/Extra/PendingOutput.cs @@ -0,0 +1,122 @@ +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Integration.Extra +{ + [Trait("Category", "Integration")] + [Trait("API", "Native")] + public class PendingOutput : IntegrationTestBase + { + public PendingOutput(ITestOutputHelper output) : base(output) {} + + [SkippableFact] + public async Task GetPendingOutput_DuringGeneration_ShouldReturnPartialOutput() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Arrange - Start a long generation + var request = new KoboldSharpClient.KoboldSharpRequest + { + Prompt = "Write a long story about an adventure", + MaxLength = 100, + Temperature = 0.7f, + TopP = 0.9f + }; + + // Start generation in background + var generationTask = Task.Run(async () => + { + await Client.GenerateAsync(request); + }); + + // Give some time for generation to start + await Task.Delay(500); + + // Act + var pendingOutput = await Client.GetPendingOutputAsync(); + + // Assert + pendingOutput.Should().NotBeNull(); + if (!string.IsNullOrEmpty(pendingOutput)) + { + Output.WriteLine($"Pending output received: {pendingOutput}"); + } + else + { + Output.WriteLine("No pending output at the moment of check"); + } + + // Cleanup - ensure generation completes or is aborted + if (!generationTask.IsCompleted) + { + await Client.AbortGenerateAsync(); + } + await Task.WhenAny(generationTask, Task.Delay(5000)); + } + + [SkippableFact] + public async Task GetPendingOutput_WithNoGeneration_ShouldReturnEmpty() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // First ensure no generation is running + await Client.AbortGenerateAsync(); + await Task.Delay(500); // Give time for any pending operations to complete + + // Act + var result = await Client.GetPendingOutputAsync(); + + // Assert + result.Should().BeEmpty(); + Output.WriteLine("Successfully verified no pending output when no generation is active"); + } + + [SkippableFact] + public async Task GetPendingOutput_MultipleChecks_ShouldShowProgress() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Arrange - Start a long generation + var request = new KoboldSharpClient.KoboldSharpRequest + { + Prompt = "Write a detailed story about a journey through time", + MaxLength = 150, + Temperature = 0.7f, + TopP = 0.9f + }; + + var generationTask = Task.Run(async () => + { + await Client.GenerateAsync(request); + }); + + // Act - Check output multiple times + var outputs = new List(); + for (int i = 0; i < 3; i++) + { + await Task.Delay(1000); // Wait between checks + var pendingOutput = await Client.GetPendingOutputAsync(); + outputs.Add(pendingOutput); + Output.WriteLine($"Check {i + 1} output length: {pendingOutput.Length}"); + Output.WriteLine($"Content: {pendingOutput}"); + } + + // Assert + outputs.Should().NotBeEmpty(); + if (outputs.Count > 1) + { + // Verify that at least some outputs show different content + outputs.Distinct().Should().HaveCountGreaterThan(1, + "Multiple checks should show generation progress"); + } + + // Cleanup + if (!generationTask.IsCompleted) + { + await Client.AbortGenerateAsync(); + } + await Task.WhenAny(generationTask, Task.Delay(5000)); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Integration/Extra/PerfInfo.cs b/SpongeEngine.KoboldSharp.Tests/Integration/Extra/PerfInfo.cs new file mode 100644 index 0000000..af171b9 --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Integration/Extra/PerfInfo.cs @@ -0,0 +1,67 @@ +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Integration.Extra +{ + [Trait("Category", "Integration")] + [Trait("API", "Native")] + public class PerfInfo : IntegrationTestBase + { + public PerfInfo(ITestOutputHelper output) : base(output) {} + + [SkippableFact] + public async Task GetPerfInfo_ShouldReturnValidData() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Act + var result = await Client.GetPerfInfoAsync(); + + // Assert + result.Should().NotBeNull(); + result.TotalGenerations.Should().BeGreaterOrEqualTo(0); + result.Uptime.Should().BeGreaterThan(0); + + // Log performance data + Output.WriteLine($"Last Process Time: {result.LastProcessTime}s"); + Output.WriteLine($"Last Eval Time: {result.LastEvalTime}s"); + Output.WriteLine($"Last Token Count: {result.LastTokenCount}"); + Output.WriteLine($"Total Generations: {result.TotalGenerations}"); + Output.WriteLine($"Queue Size: {result.QueueSize}"); + Output.WriteLine($"Uptime: {result.Uptime}s"); + Output.WriteLine($"Idle Time: {result.IdleTime}s"); + } + + [SkippableFact] + public async Task GetPerfInfo_AfterGeneration_ShouldShowActivity() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Arrange - Get initial performance info + var initialInfo = await Client.GetPerfInfoAsync(); + + // Perform a generation + var request = new KoboldSharpClient.KoboldSharpRequest + { + Prompt = "Test generation for performance monitoring", + MaxLength = 20 + }; + await Client.GenerateAsync(request); + + // Act - Get updated performance info + var updatedInfo = await Client.GetPerfInfoAsync(); + + // Assert + updatedInfo.Should().NotBeNull(); + updatedInfo.TotalGenerations.Should().BeGreaterThan(initialInfo.TotalGenerations); + updatedInfo.LastTokenCount.Should().BeGreaterThan(0); + updatedInfo.LastProcessTime.Should().BeGreaterThan(0); + + Output.WriteLine($"Initial Total Generations: {initialInfo.TotalGenerations}"); + Output.WriteLine($"Updated Total Generations: {updatedInfo.TotalGenerations}"); + Output.WriteLine($"Last Process Time: {updatedInfo.LastProcessTime}s"); + Output.WriteLine($"Last Token Count: {updatedInfo.LastTokenCount}"); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/ImageToImage.cs b/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/ImageToImage.cs new file mode 100644 index 0000000..0a95c33 --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/ImageToImage.cs @@ -0,0 +1,139 @@ +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Integration.StableDiffusion +{ + [Trait("Category", "Integration")] + [Trait("API", "Native")] + public class ImageToImage : IntegrationTestBase + { + // Simple 1x1 pixel base64 encoded PNG + private const string SampleBase64Image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; + + public ImageToImage(ITestOutputHelper output) : base(output) { } + + [SkippableFact] + public async Task ImageToImage_WithBasicPrompt_ShouldGenerateImage() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Arrange + var request = new KoboldSharpClient.StableDiffusionImageToImageRequest + { + InitImages = new List { SampleBase64Image }, + Prompt = "Convert to oil painting style", + DenoisingStrength = 0.75f, + Steps = 20, + CfgScale = 7.0f, + SamplerName = "euler_a" + }; + + // Act + var result = await Client.ImageToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty("At least one image should be generated"); + result.Parameters.Should().NotBeEmpty("Generation parameters should be returned"); + + // Log generation details + Output.WriteLine($"Generated {result.Images.Count} images"); + Output.WriteLine($"Parameters: {result.Info}"); + Output.WriteLine($"First image base64 length: {result.Images[0].Length}"); + } + + [SkippableFact] + public async Task ImageToImage_WithDifferentDenoisingStrengths_ShouldWork() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + var strengths = new[] { 0.3f, 0.7f, 0.9f }; + + foreach (var strength in strengths) + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionImageToImageRequest + { + InitImages = new List { SampleBase64Image }, + Prompt = "Convert to sketch", + DenoisingStrength = strength, + Steps = 20 + }; + + Output.WriteLine($"\nTesting denoising strength: {strength}"); + + // Act + var result = await Client.ImageToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty(); + Output.WriteLine($"Successfully generated image with denoising strength {strength}"); + + // Add delay between generations + await Task.Delay(1000); + } + } + + [SkippableFact] + public async Task ImageToImage_WithNegativePrompt_ShouldGenerateImage() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Arrange + var request = new KoboldSharpClient.StableDiffusionImageToImageRequest + { + InitImages = new List { SampleBase64Image }, + Prompt = "Convert to watercolor painting", + NegativePrompt = "blurry, distorted, low quality", + DenoisingStrength = 0.7f, + Steps = 20 + }; + + // Act + var result = await Client.ImageToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty(); + + Output.WriteLine($"Generated image with negative prompt"); + Output.WriteLine($"Parameters used: {result.Info}"); + } + + [SkippableFact] + public async Task ImageToImage_WithVariousSteps_ShouldShowDifferentResults() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + var stepCounts = new[] { 10, 20, 30 }; + + foreach (var steps in stepCounts) + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionImageToImageRequest + { + InitImages = new List { SampleBase64Image }, + Prompt = "Convert to digital art", + DenoisingStrength = 0.7f, + Steps = steps + }; + + Output.WriteLine($"\nTesting with {steps} steps"); + + // Act + var result = await Client.ImageToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty(); + Output.WriteLine($"Successfully generated image with {steps} steps"); + Output.WriteLine($"Output image size: {result.Images[0].Length} bytes"); + + // Add delay between generations + await Task.Delay(1000); + } + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/StableDiffusionModels.cs b/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/StableDiffusionModels.cs new file mode 100644 index 0000000..81b8efa --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/StableDiffusionModels.cs @@ -0,0 +1,82 @@ +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Integration.StableDiffusion +{ + [Trait("Category", "Integration")] + [Trait("API", "Native")] + public class StableDiffusionModels : IntegrationTestBase + { + public StableDiffusionModels(ITestOutputHelper output) : base(output) {} + + [SkippableFact] + public async Task GetStableDiffusionModels_ShouldReturnValidModels() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Act + var models = await Client.GetStableDiffusionModelsAsync(); + + // Assert + models.Should().NotBeNull(); + + // Log model information + Output.WriteLine($"Found {models.Count} models:"); + foreach (var model in models) + { + Output.WriteLine($"\nModel: {model.Title}"); + Output.WriteLine($"Name: {model.ModelName}"); + Output.WriteLine($"Hash: {model.Hash}"); + Output.WriteLine($"Filename: {model.Filename}"); + } + + if (models.Any()) + { + // If models are found, verify their properties + models.Should().AllSatisfy(model => + { + model.Title.Should().NotBeNullOrEmpty(); + model.ModelName.Should().NotBeNullOrEmpty(); + model.Filename.Should().NotBeNullOrEmpty(); + }); + } + else + { + Output.WriteLine("No Stable Diffusion models found - this may be normal if SD is not configured"); + } + } + + [SkippableFact] + public async Task GetStableDiffusionModels_ShouldHaveConsistentHashes() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Get models twice to verify consistency + var firstResult = await Client.GetStableDiffusionModelsAsync(); + await Task.Delay(1000); // Brief delay between requests + var secondResult = await Client.GetStableDiffusionModelsAsync(); + + // Assert + firstResult.Should().NotBeNull(); + secondResult.Should().NotBeNull(); + + // Compare results + firstResult.Count.Should().Be(secondResult.Count, "Model count should be consistent between requests"); + + if (firstResult.Any()) + { + // Compare model hashes + for (int i = 0; i < firstResult.Count; i++) + { + var model1 = firstResult[i]; + var model2 = secondResult[i]; + + Output.WriteLine($"Comparing model: {model1.Title}"); + model1.Hash.Should().Be(model2.Hash, + $"Hash for model {model1.Title} should be consistent between requests"); + } + } + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/StableDiffusionSamplers.cs b/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/StableDiffusionSamplers.cs new file mode 100644 index 0000000..13296ec --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/StableDiffusionSamplers.cs @@ -0,0 +1,133 @@ +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Integration.StableDiffusion +{ + [Trait("Category", "Integration")] + [Trait("API", "Native")] + public class StableDiffusionSamplers : IntegrationTestBase + { + public StableDiffusionSamplers(ITestOutputHelper output) : base(output) {} + + [SkippableFact] + public async Task GetStableDiffusionSamplers_ShouldReturnValidSamplers() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Act + var samplers = await Client.GetStableDiffusionSamplersAsync(); + + // Assert + samplers.Should().NotBeNull(); + + // Log sampler information + Output.WriteLine($"Found {samplers.Count} samplers:"); + foreach (var sampler in samplers) + { + Output.WriteLine($"\nSampler: {sampler.Name}"); + Output.WriteLine($"Aliases: {string.Join(", ", sampler.Aliases)}"); + Output.WriteLine("Options:"); + foreach (var (key, value) in sampler.Options) + { + Output.WriteLine($" {key}: {value}"); + } + } + + if (samplers.Any()) + { + // Verify basic sampler properties + samplers.Should().AllSatisfy(sampler => + { + sampler.Name.Should().NotBeNullOrEmpty(); + sampler.Aliases.Should().NotBeNull(); + sampler.Options.Should().NotBeNull(); + }); + + // Common samplers that should typically be available + samplers.Should().Contain(s => + s.Name.Contains("Euler", StringComparison.OrdinalIgnoreCase) || + s.Aliases.Any(a => a.Contains("euler", StringComparison.OrdinalIgnoreCase)), + "Should contain at least one Euler-based sampler"); + } + else + { + Output.WriteLine("No Stable Diffusion samplers found - this may be normal if SD is not configured"); + } + } + + [SkippableFact] + public async Task GetStableDiffusionSamplers_ShouldHaveConsistentOptions() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Get samplers twice to verify consistency + var firstResult = await Client.GetStableDiffusionSamplersAsync(); + await Task.Delay(1000); // Brief delay between requests + var secondResult = await Client.GetStableDiffusionSamplersAsync(); + + // Assert + firstResult.Should().NotBeNull(); + secondResult.Should().NotBeNull(); + + // Compare results + firstResult.Count.Should().Be(secondResult.Count, "Sampler count should be consistent between requests"); + + if (firstResult.Any()) + { + // Compare each sampler's properties + for (int i = 0; i < firstResult.Count; i++) + { + var sampler1 = firstResult[i]; + var sampler2 = secondResult[i]; + + Output.WriteLine($"Comparing sampler: {sampler1.Name}"); + sampler1.Name.Should().Be(sampler2.Name); + sampler1.Aliases.Should().BeEquivalentTo(sampler2.Aliases); + sampler1.Options.Should().BeEquivalentTo(sampler2.Options); + } + } + } + + [SkippableFact] + public async Task GetStableDiffusionSamplers_OptionsShouldBeValid() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Act + var samplers = await Client.GetStableDiffusionSamplersAsync(); + + // Assert + samplers.Should().NotBeNull(); + + foreach (var sampler in samplers) + { + Output.WriteLine($"\nValidating sampler: {sampler.Name}"); + + // Verify options contain expected keys + if (sampler.Options.Any()) + { + sampler.Options.Should().ContainKey("steps", + "Sampler should have 'steps' configuration"); + + // Log all options for inspection + foreach (var (key, value) in sampler.Options) + { + Output.WriteLine($"Option {key}: {value}"); + value.Should().NotBeNull("Option values should not be null"); + } + } + else + { + Output.WriteLine("No options found for this sampler"); + } + + // Verify aliases are properly formatted + foreach (var alias in sampler.Aliases) + { + alias.Should().NotBeNullOrWhiteSpace("Aliases should be valid strings"); + } + } + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/TextToImage.cs b/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/TextToImage.cs new file mode 100644 index 0000000..1811eb5 --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Integration/StableDiffusion/TextToImage.cs @@ -0,0 +1,141 @@ +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Integration.StableDiffusion +{ + [Trait("Category", "Integration")] + [Trait("API", "Native")] + public class TextToImage : IntegrationTestBase + { + public TextToImage(ITestOutputHelper output) : base(output) {} + + [SkippableFact] + public async Task TextToImage_WithBasicPrompt_ShouldGenerateImage() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Arrange + var request = new KoboldSharpClient.StableDiffusionGenerationRequest + { + Prompt = "A simple landscape with mountains", + Width = 512, + Height = 512, + Steps = 20, + CfgScale = 7.0f, + SamplerName = "euler_a" + }; + + // Act + var result = await Client.TextToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty("At least one image should be generated"); + result.Parameters.Should().NotBeEmpty("Generation parameters should be returned"); + + // Log generation details + Output.WriteLine($"Generated {result.Images.Count} images"); + Output.WriteLine($"Parameters: {result.Info}"); + Output.WriteLine($"First image base64 length: {result.Images[0].Length}"); + } + + [SkippableFact] + public async Task TextToImage_WithDifferentSamplers_ShouldWork() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Get available samplers + var samplers = await Client.GetStableDiffusionSamplersAsync(); + Skip.If(!samplers.Any(), "No Stable Diffusion samplers available"); + + // Test first two samplers + var samplersToTest = samplers.Take(2).ToList(); + foreach (var sampler in samplersToTest) + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionGenerationRequest + { + Prompt = "A simple test image", + Width = 512, + Height = 512, + Steps = 20, + SamplerName = sampler.Name + }; + + Output.WriteLine($"\nTesting sampler: {sampler.Name}"); + + // Act + var result = await Client.TextToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty(); + Output.WriteLine($"Successfully generated image with sampler {sampler.Name}"); + + // Add delay between generations + await Task.Delay(1000); + } + } + + [SkippableFact] + public async Task TextToImage_WithNegativePrompt_ShouldGenerateImage() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + // Arrange + var request = new KoboldSharpClient.StableDiffusionGenerationRequest + { + Prompt = "A beautiful mountain landscape with clear sky", + NegativePrompt = "clouds, blur, darkness, distortion", + Width = 512, + Height = 512, + Steps = 20, + CfgScale = 7.5f + }; + + // Act + var result = await Client.TextToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty(); + + Output.WriteLine($"Generated image with negative prompt"); + Output.WriteLine($"Parameters used: {result.Info}"); + } + + [SkippableFact] + public async Task TextToImage_WithDifferentSizes_ShouldGenerateImage() + { + Skip.If(!ServerAvailable, "KoboldCpp server is not available"); + + var sizes = new[] { (512, 512), (640, 384), (384, 640) }; + + foreach (var (width, height) in sizes) + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionGenerationRequest + { + Prompt = "A simple landscape", + Width = width, + Height = height, + Steps = 20 + }; + + Output.WriteLine($"\nTesting size: {width}x{height}"); + + // Act + var result = await Client.TextToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty(); + Output.WriteLine($"Successfully generated {width}x{height} image"); + + // Add delay between generations + await Task.Delay(1000); + } + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/SpongeEngine.KoboldSharp.Tests.csproj b/SpongeEngine.KoboldSharp.Tests/SpongeEngine.KoboldSharp.Tests.csproj index 6b7e22b..71dd89e 100644 --- a/SpongeEngine.KoboldSharp.Tests/SpongeEngine.KoboldSharp.Tests.csproj +++ b/SpongeEngine.KoboldSharp.Tests/SpongeEngine.KoboldSharp.Tests.csproj @@ -31,8 +31,6 @@ - - \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Unit/Extra/AbortGenerate.cs b/SpongeEngine.KoboldSharp.Tests/Unit/Extra/AbortGenerate.cs new file mode 100644 index 0000000..47203c0 --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Unit/Extra/AbortGenerate.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using SpongeEngine.LLMSharp.Core.Exceptions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Unit.Extra +{ + public class AbortGenerate : UnitTestBase + { + public AbortGenerate(ITestOutputHelper output) : base(output) {} + + [Fact] + public async Task AbortGenerate_WithNoGenKey_ShouldAbortGeneration() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/api/extra/abort") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("{\"success\":\"true\",\"done\":\"true\"}")); + + // Act + var result = await Client.AbortGenerateAsync(); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public async Task AbortGenerate_WithGenKey_ShouldAbortSpecificGeneration() + { + // Arrange + const string genKey = "test-key"; + + Server + .Given(Request.Create() + .WithPath("/api/extra/abort") + .WithBody(body => body.Contains(genKey)) + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("{\"success\":\"true\",\"done\":\"true\"}")); + + // Act + var result = await Client.AbortGenerateAsync(genKey); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public async Task AbortGenerate_WhenServerError_ShouldThrowException() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/api/extra/abort") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(500) + .WithBody("Internal Server Error")); + + // Act & Assert + await Client.Invoking(c => c.AbortGenerateAsync()) + .Should().ThrowAsync() + .WithMessage("Failed to abort generation"); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Unit/Extra/LogProbs.cs b/SpongeEngine.KoboldSharp.Tests/Unit/Extra/LogProbs.cs new file mode 100644 index 0000000..67ecd1d --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Unit/Extra/LogProbs.cs @@ -0,0 +1,112 @@ +using FluentAssertions; +using SpongeEngine.LLMSharp.Core.Exceptions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Unit.Extra +{ + public class LogProbs : UnitTestBase + { + public LogProbs(ITestOutputHelper output) : base(output) {} + + [Fact] + public async Task GetLastLogProbs_ShouldReturnProbabilityData() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/api/extra/last_logprobs") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody(@"{ + ""logprobs"": { + ""content"": [ + { + ""token"": ""test"", + ""logprob"": -0.5, + ""bytes"": [116, 101, 115, 116], + ""top_logprobs"": [ + { + ""token"": ""test"", + ""logprob"": -0.5, + ""bytes"": [116, 101, 115, 116] + } + ] + } + ], + ""tokens"": [""test""], + ""token_logprobs"": [-0.5], + ""top_logprobs"": [{""test"": -0.5}], + ""text_offset"": [0] + } + }")); + + // Act + var result = await Client.GetLastLogProbsAsync(); + + // Assert + result.Should().NotBeNull(); + result.LogProbabilities.Should().NotBeNull(); + result.LogProbabilities.Content.Should().HaveCount(1); + result.LogProbabilities.Tokens.Should().HaveCount(1); + result.LogProbabilities.TokenLogProbs.Should().HaveCount(1); + + var firstContent = result.LogProbabilities.Content[0]; + firstContent.Token.Should().Be("test"); + firstContent.LogProbability.Should().Be(-0.5f); + firstContent.Bytes.Should().BeEquivalentTo(new[] { 116, 101, 115, 116 }); + } + + [Fact] + public async Task GetLastLogProbs_WithNoGeneration_ShouldReturnEmptyData() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/api/extra/last_logprobs") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody(@"{ + ""logprobs"": { + ""content"": [], + ""tokens"": [], + ""token_logprobs"": [], + ""top_logprobs"": [], + ""text_offset"": [] + } + }")); + + // Act + var result = await Client.GetLastLogProbsAsync(); + + // Assert + result.Should().NotBeNull(); + result.LogProbabilities.Should().NotBeNull(); + result.LogProbabilities.Content.Should().BeEmpty(); + result.LogProbabilities.Tokens.Should().BeEmpty(); + result.LogProbabilities.TokenLogProbs.Should().BeEmpty(); + } + + [Fact] + public async Task GetLastLogProbs_WhenServerError_ShouldThrowException() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/api/extra/last_logprobs") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(500) + .WithBody("Internal Server Error")); + + // Act & Assert + await Client.Invoking(c => c.GetLastLogProbsAsync()) + .Should().ThrowAsync() + .WithMessage("Failed to get last log probabilities"); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Unit/Extra/PendingOutput.cs b/SpongeEngine.KoboldSharp.Tests/Unit/Extra/PendingOutput.cs new file mode 100644 index 0000000..4dcea0e --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Unit/Extra/PendingOutput.cs @@ -0,0 +1,94 @@ +using FluentAssertions; +using SpongeEngine.LLMSharp.Core.Exceptions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Unit.Extra +{ + public class PendingOutput : UnitTestBase + { + public PendingOutput(ITestOutputHelper output) : base(output) {} + + [Fact] + public async Task GetPendingOutput_WithNoGenKey_ShouldReturnCurrentOutput() + { + // Arrange + const string expectedOutput = "Generated text in progress"; + Server + .Given(Request.Create() + .WithPath("/api/extra/generate/check") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody($"{{\"results\": [{{\"text\": \"{expectedOutput}\"}}]}}")); + + // Act + var result = await Client.GetPendingOutputAsync(); + + // Assert + result.Should().Be(expectedOutput); + } + + [Fact] + public async Task GetPendingOutput_WithGenKey_ShouldReturnSpecificOutput() + { + // Arrange + const string genKey = "test-generation"; + const string expectedOutput = "Specific generation output"; + + Server + .Given(Request.Create() + .WithPath("/api/extra/generate/check") + .WithBody(body => body.Contains(genKey)) + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody($"{{\"results\": [{{\"text\": \"{expectedOutput}\"}}]}}")); + + // Act + var result = await Client.GetPendingOutputAsync(genKey); + + // Assert + result.Should().Be(expectedOutput); + } + + [Fact] + public async Task GetPendingOutput_WithNoActiveGeneration_ShouldReturnEmpty() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/api/extra/generate/check") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("{\"results\": []}")); + + // Act + var result = await Client.GetPendingOutputAsync(); + + // Assert + result.Should().BeEmpty(); + } + + [Fact] + public async Task GetPendingOutput_WhenServerError_ShouldThrowException() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/api/extra/generate/check") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(500) + .WithBody("Internal Server Error")); + + // Act & Assert + await Client.Invoking(c => c.GetPendingOutputAsync()) + .Should().ThrowAsync() + .WithMessage("Failed to get pending output"); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Unit/Extra/PerfInfo.cs b/SpongeEngine.KoboldSharp.Tests/Unit/Extra/PerfInfo.cs new file mode 100644 index 0000000..d193686 --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Unit/Extra/PerfInfo.cs @@ -0,0 +1,106 @@ +using FluentAssertions; +using SpongeEngine.LLMSharp.Core.Exceptions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Unit.Extra +{ + public class PerfInfo : UnitTestBase + { + public PerfInfo(ITestOutputHelper output) : base(output) {} + + [Fact] + public async Task GetPerfInfo_ShouldReturnPerformanceData() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/api/extra/perf") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody(@"{ + ""last_process"": 0.5, + ""last_eval"": 0.3, + ""last_token_count"": 100, + ""last_seed"": 12345, + ""total_gens"": 50, + ""stop_reason"": 0, + ""total_img_gens"": 10, + ""queue"": 0, + ""idle"": 1, + ""hordeexitcounter"": 0, + ""uptime"": 3600.0, + ""idletime"": 1800.0, + ""quiet"": false + }")); + + // Act + var result = await Client.GetPerfInfoAsync(); + + // Assert + result.Should().NotBeNull(); + result.LastProcessTime.Should().Be(0.5f); + result.LastEvalTime.Should().Be(0.3f); + result.LastTokenCount.Should().Be(100); + result.LastSeed.Should().Be(12345); + result.TotalGenerations.Should().Be(50); + result.TotalImageGenerations.Should().Be(10); + result.QueueSize.Should().Be(0); + result.IsIdle.Should().Be(1); + result.Uptime.Should().Be(3600.0f); + result.IdleTime.Should().Be(1800.0f); + result.IsQuiet.Should().BeFalse(); + } + + [Fact] + public async Task GetPerfInfo_WithMinimalData_ShouldHandleDefaults() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/api/extra/perf") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody(@"{ + ""last_process"": 0.0, + ""last_eval"": 0.0, + ""last_token_count"": 0, + ""last_seed"": 0, + ""total_gens"": 0, + ""stop_reason"": 0 + }")); + + // Act + var result = await Client.GetPerfInfoAsync(); + + // Assert + result.Should().NotBeNull(); + result.LastProcessTime.Should().Be(0); + result.LastEvalTime.Should().Be(0); + result.LastTokenCount.Should().Be(0); + result.TotalGenerations.Should().Be(0); + } + + [Fact] + public async Task GetPerfInfo_WhenServerError_ShouldThrowException() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/api/extra/perf") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(500) + .WithBody("Internal Server Error")); + + // Act & Assert + await Client.Invoking(c => c.GetPerfInfoAsync()) + .Should().ThrowAsync() + .WithMessage("Failed to get performance info"); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/ImageToImage.cs b/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/ImageToImage.cs new file mode 100644 index 0000000..f1ad297 --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/ImageToImage.cs @@ -0,0 +1,164 @@ +using FluentAssertions; +using SpongeEngine.LLMSharp.Core.Exceptions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Unit.StableDiffusion +{ + public class ImageToImage : UnitTestBase + { + // Simple 1x1 pixel base64 encoded PNG for testing + private const string SampleBase64Image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; + + public ImageToImage(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task ImageToImage_WithValidRequest_ShouldReturnModifiedImage() + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionImageToImageRequest + { + InitImages = new List { SampleBase64Image }, + Prompt = "Convert to oil painting", + DenoisingStrength = 0.75f, + Steps = 20, + CfgScale = 7.0f, + SamplerName = "euler_a" + }; + + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/img2img") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody($@"{{ + ""images"": [""{SampleBase64Image}""], + ""parameters"": {{ + ""prompt"": ""Convert to oil painting"", + ""denoising_strength"": 0.75, + ""steps"": 20 + }}, + ""info"": ""Generation successful"" + }}")); + + // Act + var result = await Client.ImageToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty(); + result.Images.Should().HaveCount(1); + result.Images[0].Should().Be(SampleBase64Image); + result.Parameters.Should().NotBeEmpty(); + result.Info.Should().Be("Generation successful"); + } + + [Fact] + public async Task ImageToImage_WithDifferentDenoisingStrength_ShouldWork() + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionImageToImageRequest + { + InitImages = new List { SampleBase64Image }, + Prompt = "Convert to oil painting", + DenoisingStrength = 0.9f, + Steps = 20 + }; + + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/img2img") + .WithBody(body => body.Contains("\"denoising_strength\":0.9")) + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody($@"{{ + ""images"": [""{SampleBase64Image}""], + ""parameters"": {{}}, + ""info"": """" + }}")); + + // Act + var result = await Client.ImageToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty(); + } + + [Fact] + public async Task ImageToImage_WithNoInitialImage_ShouldThrowException() + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionImageToImageRequest + { + InitImages = new List(), + Prompt = "Convert to oil painting" + }; + + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/img2img") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(400) + .WithBody("No input image provided")); + + // Act & Assert + await Client.Invoking(c => c.ImageToImageAsync(request)) + .Should().ThrowAsync() + .WithMessage("Failed to generate image from image"); + } + + [Fact] + public async Task ImageToImage_WithInvalidImage_ShouldThrowException() + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionImageToImageRequest + { + InitImages = new List { "invalid_base64_data" }, + Prompt = "Convert to oil painting" + }; + + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/img2img") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(400) + .WithBody("Invalid image data")); + + // Act & Assert + await Client.Invoking(c => c.ImageToImageAsync(request)) + .Should().ThrowAsync() + .WithMessage("Failed to generate image from image"); + } + + [Fact] + public async Task ImageToImage_WithServerError_ShouldThrowException() + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionImageToImageRequest + { + InitImages = new List { SampleBase64Image }, + Prompt = "Convert to oil painting" + }; + + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/img2img") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(500) + .WithBody("Internal Server Error")); + + // Act & Assert + await Client.Invoking(c => c.ImageToImageAsync(request)) + .Should().ThrowAsync() + .WithMessage("Failed to generate image from image"); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/StableDiffusionModels.cs b/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/StableDiffusionModels.cs new file mode 100644 index 0000000..e52d7cf --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/StableDiffusionModels.cs @@ -0,0 +1,91 @@ +using FluentAssertions; +using SpongeEngine.LLMSharp.Core.Exceptions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Unit.StableDiffusion +{ + public class StableDiffusionModels : UnitTestBase + { + public StableDiffusionModels(ITestOutputHelper output) : base(output) {} + + [Fact] + public async Task GetStableDiffusionModels_ShouldReturnAvailableModels() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/sd-models") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody(@"[ + { + ""title"": ""Stable Diffusion v1.5"", + ""model_name"": ""sd_v1.5"", + ""hash"": ""hash123"", + ""filename"": ""sd_v1.5.safetensors"" + }, + { + ""title"": ""Stable Diffusion v2.1"", + ""model_name"": ""sd_v2.1"", + ""hash"": ""hash456"", + ""filename"": ""sd_v2.1.safetensors"" + } + ]")); + + // Act + var models = await Client.GetStableDiffusionModelsAsync(); + + // Assert + models.Should().NotBeNull(); + models.Should().HaveCount(2); + + var firstModel = models[0]; + firstModel.Title.Should().Be("Stable Diffusion v1.5"); + firstModel.ModelName.Should().Be("sd_v1.5"); + firstModel.Hash.Should().Be("hash123"); + firstModel.Filename.Should().Be("sd_v1.5.safetensors"); + } + + [Fact] + public async Task GetStableDiffusionModels_WhenNoModels_ShouldReturnEmptyList() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/sd-models") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("[]")); + + // Act + var models = await Client.GetStableDiffusionModelsAsync(); + + // Assert + models.Should().NotBeNull(); + models.Should().BeEmpty(); + } + + [Fact] + public async Task GetStableDiffusionModels_WhenServerError_ShouldThrowException() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/sd-models") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(500) + .WithBody("Internal Server Error")); + + // Act & Assert + await Client.Invoking(c => c.GetStableDiffusionModelsAsync()) + .Should().ThrowAsync() + .WithMessage("Failed to get Stable Diffusion models"); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/StableDiffusionSamplers.cs b/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/StableDiffusionSamplers.cs new file mode 100644 index 0000000..658503e --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/StableDiffusionSamplers.cs @@ -0,0 +1,124 @@ +using FluentAssertions; +using SpongeEngine.LLMSharp.Core.Exceptions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Unit.StableDiffusion +{ + public class StableDiffusionSamplers : UnitTestBase + { + public StableDiffusionSamplers(ITestOutputHelper output) : base(output) {} + + [Fact] + public async Task GetStableDiffusionSamplers_ShouldReturnAvailableSamplers() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/samplers") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody(@"[ + { + ""name"": ""Euler a"", + ""aliases"": [""euler_ancestral"", ""euler a""], + ""options"": { + ""k"": ""auto"", + ""steps"": 20 + } + }, + { + ""name"": ""DDIM"", + ""aliases"": [""ddim""], + ""options"": { + ""steps"": 20 + } + } + ]")); + + // Act + var samplers = await Client.GetStableDiffusionSamplersAsync(); + + // Assert + samplers.Should().NotBeNull(); + samplers.Should().HaveCount(2); + + var firstSampler = samplers[0]; + firstSampler.Name.Should().Be("Euler a"); + firstSampler.Aliases.Should().BeEquivalentTo(new[] { "euler_ancestral", "euler a" }); + firstSampler.Options.Should().ContainKey("k"); + firstSampler.Options.Should().ContainKey("steps"); + } + + [Fact] + public async Task GetStableDiffusionSamplers_WhenNoSamplers_ShouldReturnEmptyList() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/samplers") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody("[]")); + + // Act + var samplers = await Client.GetStableDiffusionSamplersAsync(); + + // Assert + samplers.Should().NotBeNull(); + samplers.Should().BeEmpty(); + } + + [Fact] + public async Task GetStableDiffusionSamplers_WhenServerError_ShouldThrowException() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/samplers") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(500) + .WithBody("Internal Server Error")); + + // Act & Assert + await Client.Invoking(c => c.GetStableDiffusionSamplersAsync()) + .Should().ThrowAsync() + .WithMessage("Failed to get Stable Diffusion samplers"); + } + + [Fact] + public async Task GetStableDiffusionSamplers_WithInvalidResponse_ShouldHandleGracefully() + { + // Arrange + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/samplers") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody(@"[ + { + ""name"": ""Invalid Sampler"", + ""aliases"": null, + ""options"": null + } + ]")); + + // Act + var samplers = await Client.GetStableDiffusionSamplersAsync(); + + // Assert + samplers.Should().NotBeNull(); + samplers.Should().ContainSingle(); + var sampler = samplers[0]; + sampler.Name.Should().Be("Invalid Sampler"); + sampler.Aliases.Should().BeEmpty(); + sampler.Options.Should().BeEmpty(); + } + } +} \ No newline at end of file diff --git a/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/TextToImage.cs b/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/TextToImage.cs new file mode 100644 index 0000000..6a40a5f --- /dev/null +++ b/SpongeEngine.KoboldSharp.Tests/Unit/StableDiffusion/TextToImage.cs @@ -0,0 +1,142 @@ +using FluentAssertions; +using SpongeEngine.LLMSharp.Core.Exceptions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using Xunit; +using Xunit.Abstractions; + +namespace SpongeEngine.KoboldSharp.Tests.Unit.StableDiffusion +{ + public class TextToImage : UnitTestBase + { + public TextToImage(ITestOutputHelper output) : base(output) {} + + [Fact] + public async Task TextToImage_WithValidRequest_ShouldReturnImages() + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionGenerationRequest + { + Prompt = "A beautiful landscape", + Width = 512, + Height = 512, + Steps = 20, + CfgScale = 7.0f, + SamplerName = "euler_a" + }; + + // Mock base64 image data + var mockImageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; + + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/txt2img") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody($@"{{ + ""images"": [""{mockImageBase64}""], + ""parameters"": {{ + ""prompt"": ""A beautiful landscape"", + ""steps"": 20, + ""cfg_scale"": 7.0 + }}, + ""info"": ""Generation successful"" + }}")); + + // Act + var result = await Client.TextToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty(); + result.Images.Should().HaveCount(1); + result.Images[0].Should().Be(mockImageBase64); + result.Parameters.Should().NotBeEmpty(); + result.Info.Should().Be("Generation successful"); + } + + [Fact] + public async Task TextToImage_WithNegativePrompt_ShouldIncludeInRequest() + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionGenerationRequest + { + Prompt = "A beautiful landscape", + NegativePrompt = "ugly, blurry, distorted", + Width = 512, + Height = 512 + }; + + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/txt2img") + .WithBody(body => + body.Contains("negative_prompt") && + body.Contains("ugly, blurry, distorted")) + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(200) + .WithBody(@"{ + ""images"": [""base64data""], + ""parameters"": {}, + ""info"": """" + }")); + + // Act + var result = await Client.TextToImageAsync(request); + + // Assert + result.Should().NotBeNull(); + result.Images.Should().NotBeEmpty(); + } + + [Fact] + public async Task TextToImage_WithInvalidDimensions_ShouldThrowException() + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionGenerationRequest + { + Prompt = "A beautiful landscape", + Width = 0, // Invalid width + Height = 512 + }; + + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/txt2img") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(400) + .WithBody("Invalid dimensions")); + + // Act & Assert + await Client.Invoking(c => c.TextToImageAsync(request)) + .Should().ThrowAsync() + .WithMessage("Failed to generate image from text"); + } + + [Fact] + public async Task TextToImage_WithServerError_ShouldThrowException() + { + // Arrange + var request = new KoboldSharpClient.StableDiffusionGenerationRequest + { + Prompt = "A beautiful landscape" + }; + + Server + .Given(Request.Create() + .WithPath("/sdapi/v1/txt2img") + .UsingPost()) + .RespondWith(Response.Create() + .WithStatusCode(500) + .WithBody("Internal Server Error")); + + // Act & Assert + await Client.Invoking(c => c.TextToImageAsync(request)) + .Should().ThrowAsync() + .WithMessage("Failed to generate image from text"); + } + } +} \ No newline at end of file