Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions LocalLLMServerManager.Tests/BrowserLauncherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,28 @@ public void OpenUrl_WithNonHttpOrHttpsScheme_ReturnsFalseAndShowsToast(string ur
ToastService.Instance.OnToastShow -= handler;
}
}

[Theory]
[InlineData("http://example.com/\"evil\"")]
[InlineData("http://example.com/'evil'")]
[InlineData("http://example.com/evil`cmd`")]
[InlineData("http://example.com/evil$var")]
[InlineData("http://example.com/evil\r\nsomething")]
public void OpenUrl_WithDangerousCharacters_ReturnsFalseAndShowsToast(string url)
{
ToastItem? emittedToast = null;
Action<ToastItem> handler = t => emittedToast = t;
ToastService.Instance.OnToastShow += handler;
try
{
bool result = BrowserLauncher.OpenUrl(url);
Assert.False(result);
Assert.NotNull(emittedToast);
Assert.Contains("dangerous", emittedToast!.Message, StringComparison.OrdinalIgnoreCase);
}
finally
{
ToastService.Instance.OnToastShow -= handler;
}
}
}
174 changes: 174 additions & 0 deletions LocalLLMServerManager.Tests/CanIRunItViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -393,4 +393,178 @@ public async Task OllamaLibraryViewModel_ConfirmationModal_ConfirmDeletesModel()
Assert.Null(vm.ModelToDelete);
Assert.Empty(vm.InstalledModels);
}

[Theory]
[InlineData("Qwen 2.5 32B", 32.0)]
[InlineData("Qwen 2.5 72B", 72.0)]
[InlineData("DeepSeek R1 32B", 32.0)]
[InlineData("DeepSeek R1 70B", 70.0)]
[InlineData("Mistral Small 24B", 24.0)]
[InlineData("Gemma 2 9B", 9.0)]
[InlineData("Gemma 2 27B", 27.0)]
[InlineData("Phi-4 14B", 14.0)]
public void LlmPresets_Switching_SetsParametersCorrectly(string presetName, double expectedParams)
{
var vm = new CanIRunItViewModel();
vm.SelectedPreset = presetName;

Assert.Equal(expectedParams, vm.ParametersBillions);
Assert.Equal("Q4_K_M", vm.SelectedQuantization);
Assert.NotNull(vm.LlmResult);
}

[Theory]
[InlineData("Flux.1 Schnell", 1024, "FP8")]
[InlineData("SDXL", 1024, "FP16")]
[InlineData("SD 3.5 Large", 1024, "FP8")]
[InlineData("SD 1.5", 512, "FP16")]
public void ImagePresets_Switching_SetsResolutionAndQuant(string preset, int expectedRes, string expectedQuant)
{
var vm = new CanIRunItViewModel();
vm.SelectedModality = "Image";
vm.SelectedImagePreset = preset;

Assert.Equal(expectedRes, vm.SelectedImageResolution);
Assert.Equal(expectedQuant, vm.SelectedImageQuantization);
Assert.NotNull(vm.DiffusionResult);
}

[Theory]
[InlineData("Wan 2.2 1.3B")]
[InlineData("LTX-Video")]
[InlineData("HunyuanVideo")]
public void VideoPresets_Switching_UpdatesCalculation(string preset)
{
var vm = new CanIRunItViewModel();
vm.SelectedModality = "Video";
vm.SelectedVideoPreset = preset;
vm.SelectedVideoResolution = "480p";
vm.SelectedVideoQuantization = "Q4";

Assert.NotNull(vm.VideoResult);
Assert.Equal(preset, vm.VideoResult.ModelName);
}

[Theory]
[InlineData("Faster-Whisper Large-v3-Turbo")]
[InlineData("AllTalk XTTS-v2")]
[InlineData("MusicGen Melody")]
public void AudioPresets_Switching_UpdatesCalculation(string preset)
{
var vm = new CanIRunItViewModel();
vm.SelectedModality = "Audio";
vm.SelectedAudioPreset = preset;

Assert.NotNull(vm.AudioResult);
Assert.Equal(preset, vm.AudioResult.EngineName);
}

[Fact]
public void ThreeDPresets_Switching_UpdatesCalculation()
{
var vm = new CanIRunItViewModel();
vm.SelectedModality = "ThreeD";
vm.SelectedThreeDPreset = "Hunyuan3D-2";

Assert.NotNull(vm.ThreeDResult);
Assert.Equal("Hunyuan3D-2", vm.ThreeDResult.ModelName);
}

[Theory]
[InlineData("DIFFUSION", "Image")]
[InlineData("IMAGE", "Image")]
[InlineData("VIDEO", "Video")]
[InlineData("SPEECH", "Audio")]
[InlineData("TTS", "Audio")]
[InlineData("AUDIO", "Audio")]
[InlineData("3D", "ThreeD")]
[InlineData("THREED", "ThreeD")]
[InlineData("UNKNOWN_MODALITY", "LLM")]
public void SelectModality_StringCommand_MapsCorrectly(string input, string expectedModality)
{
var vm = new CanIRunItViewModel();
vm.SelectModality(input);

Assert.Equal(expectedModality, vm.SelectedModality);
}

[Fact]
public void SelectModality_EmptyInput_DoesNothing()
{
var vm = new CanIRunItViewModel();
vm.SelectedModality = "Video";
vm.SelectModality("");
Assert.Equal("Video", vm.SelectedModality);
}

[Fact]
public void ModalityCommands_RelayExecution_SwitchesState()
{
var vm = new CanIRunItViewModel();
vm.Select3DModality();
Assert.Equal("ThreeD", vm.SelectedModality);

vm.SelectLlmModality();
Assert.Equal("LLM", vm.SelectedModality);
}

[Theory]
[InlineData("my-custom-model-8b.Q8_0.gguf", 8.0, "Q8_0")]
[InlineData("qwen2.5-14b-instruct.fp16.bin", 14.0, "FP16")]
[InlineData("custom-model-27b-chat.gguf", 27.0, "Q4_K_M")]
[InlineData("tiny-model-1.5b.fp8.safetensors", 1.5, "FP8")]
[InlineData("unknown-model-without-params.gguf", 8.0, "Q4_K_M")]
public void InspectModel_CustomLlmNames_ExtractsParamsAndQuant(string name, double expectedB, string expectedQuant)
{
var vm = new CanIRunItViewModel();
vm.InspectModel(name, "LLM");

Assert.Equal("Custom", vm.SelectedPreset);
Assert.Equal(expectedB, vm.ParametersBillions);
Assert.Equal(expectedQuant, vm.SelectedQuantization);
}

[Fact]
public async Task RefreshTelemetryAsync_QueriesServiceAndUpdateHardware()
{
var mockTelemetry = new Mock<ITelemetryService>();
mockTelemetry.Setup(t => t.QueryGpuVramAsync(It.IsAny<string>(), It.IsAny<HttpClient>()))
.ReturnsAsync(new GpuTelemetryInfo("NVIDIA RTX 4080", 2.0, 16.0, 14.0, 10));

var vm = new CanIRunItViewModel(mockTelemetry.Object);
using var client = new HttpClient();
await vm.RefreshTelemetryAsync("http://localhost:5246", client);

Assert.Equal("NVIDIA RTX 4080", vm.GpuName);
Assert.Equal(16384.0, vm.TotalVramMb);
}

[Fact]
public void LowMemory_Verdicts_CoverFormattingBranches()
{
var vm = new CanIRunItViewModel();
// Set VRAM and RAM very low to trigger OOM/CPU verdicts across modalities
vm.UpdateHardwareTelemetry(512.0, 512.0, 1024.0, 1024.0, "Low-End iGPU");

vm.SelectModality("LLM");
vm.SelectedPreset = "DeepSeek R1 671B";
Assert.Equal(FitVerdict.OutOfMemory, vm.LlmResult?.FitVerdict);
Assert.Contains("Out of Memory", vm.VerdictSummaryText);

vm.SelectModality("Image");
vm.SelectedImagePreset = "Flux.1 Dev";
Assert.Equal(FitVerdict.OutOfMemory, vm.DiffusionResult?.FitVerdict);
Assert.Contains("Out of Memory", vm.VerdictSummaryText);

vm.SelectModality("Video");
vm.SelectedVideoPreset = "Wan 2.2 14B";
Assert.Equal(FitVerdict.OutOfMemory, vm.VideoResult?.FitVerdict);
Assert.Contains("Out of Memory", vm.VerdictSummaryText);

vm.SelectModality("Audio");
Assert.NotNull(vm.AudioResult);

vm.SelectModality("ThreeD");
Assert.NotNull(vm.ThreeDResult);
}
}
127 changes: 127 additions & 0 deletions LocalLLMServerManager.Tests/CivitaiSearchViewModelTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using LocalLLMServerManager.Shared.Interfaces;
using LocalLLMServerManager.Shared.Models;
using LocalLLMServerManager.Shared.Services;
using LocalLLMServerManager.Shared.ViewModels;
using Moq;
using Moq.Protected;
using Xunit;

namespace LocalLLMServerManager.Tests;

public class CivitaiSearchViewModelTests
{
[Fact]
public void InitialState_DefaultValues_ConfiguredCorrectly()
{
var mockCivitai = new Mock<ICivitaiSearchService>();
var vm = new CivitaiSearchViewModel(mockCivitai.Object);

Assert.Equal("", vm.CivitaiSearchQuery);
Assert.Equal("Checkpoint", vm.SelectedCivitaiType);
Assert.True(vm.IsFullVramActive);
Assert.True(vm.IsPartialOffloadActive);
Assert.True(vm.IsCpuOnlyActive);
Assert.True(vm.IsOomActive);
Assert.Equal(16384.0, vm.TotalVramMb);
Assert.Equal(32768.0, vm.TotalRamMb);
}

[Fact]
public void ToggleFitVerdict_TogglesFilterFlagsAndAppliesFilter()
{
var mockCivitai = new Mock<ICivitaiSearchService>();
var vm = new CivitaiSearchViewModel(mockCivitai.Object);

vm.ToggleFitVerdict("full");
Assert.False(vm.IsFullVramActive);

vm.ToggleFitVerdict("partial");
Assert.False(vm.IsPartialOffloadActive);

vm.ToggleFitVerdict("cpu");
Assert.False(vm.IsCpuOnlyActive);

vm.ToggleFitVerdict("oom");
Assert.False(vm.IsOomActive);
}

[Fact]
public void NavigateToCanIRunIt_And_InspectModel_InvokeCallbacks()
{
var mockCivitai = new Mock<ICivitaiSearchService>();
var vm = new CivitaiSearchViewModel(mockCivitai.Object);

string? inspectedModel = null;
string? inspectedModality = null;
vm.OnInspectModelRequested = (m, mod) =>
{
inspectedModel = m;
inspectedModality = mod;
};

vm.NavigateToCanIRunIt("Flux-Dev-Checkpoint");
Assert.Equal("Flux-Dev-Checkpoint", inspectedModel);
Assert.Equal("Image", inspectedModality);

var item = new CivitaiModelItem(42, "SDXL Turbo", "Checkpoint", "http://img.jpg", "http://dl.safetensors", "sdxl.safetensors", 4.8, 1200);
vm.InspectModel(item);
Assert.Equal("SDXL Turbo", inspectedModel);
}

[Fact]
public void UpdateHardwareTelemetry_UpdatesVramAndRecalculatesBadges()
{
var mockCivitai = new Mock<ICivitaiSearchService>();
var vm = new CivitaiSearchViewModel(mockCivitai.Object);

vm.CivitaiResults.Add(new CivitaiModelItem(1, "Flux Dev", "Checkpoint", "http://img", "http://dl", "flux.safetensors", 5.0, 500));
vm.UpdateHardwareTelemetry(8192.0, 16384.0);

Assert.Equal(8192.0, vm.TotalVramMb);
Assert.Equal(16384.0, vm.TotalRamMb);
Assert.NotNull(vm.CivitaiResults[0].FitBadge);
}

[Fact]
public async Task SearchCivitaiAsync_PopulatesResultsAndAppliesFilter()
{
var mockCivitai = new Mock<ICivitaiSearchService>();
mockCivitai.Setup(s => s.SearchModelsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<HttpClient>()))
.ReturnsAsync(new List<CivitaiModelItem>
{
new CivitaiModelItem(10, "SD 1.5 Photoreal", "Checkpoint", "http://img.png", "http://dl.gguf", "sd15.safetensors", 4.5, 300)
});

var vm = new CivitaiSearchViewModel(mockCivitai.Object);
using var client = new HttpClient();
await vm.SearchCivitaiAsync("http://localhost:5246", client);

Assert.Single(vm.CivitaiResults);
Assert.Single(vm.FilteredCivitaiResults);
Assert.Equal("SD 1.5 Photoreal", vm.CivitaiResults[0].Name);
}

[Fact]
public async Task DownloadCivitaiModelAsync_SendsDownloadRequest()
{
var mockCivitai = new Mock<ICivitaiSearchService>();
var vm = new CivitaiSearchViewModel(mockCivitai.Object);

var handlerMock = new Mock<HttpMessageHandler>();
handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync",
ItExpr.Is<HttpRequestMessage>(r => r.RequestUri != null && r.RequestUri.ToString().Contains("/api/civitai/download")),
ItExpr.IsAny<System.Threading.CancellationToken>())
.ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK });

var client = new HttpClient(handlerMock.Object);
var item = new CivitaiModelItem(10, "SD 1.5 Photoreal", "Checkpoint", "http://img.png", "http://dl.gguf", "sd15.safetensors", 4.5, 300);

await vm.DownloadCivitaiModelAsync(item, "http://localhost:5246", client);
}
}
23 changes: 21 additions & 2 deletions LocalLLMServerManager.Tests/ComponentManagerAndEndpointsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Net.Http.Json;
using LocalLLMServerManager.Services;
using LocalLLMServerManager.Shared.Models;
using Moq;
using Moq.Protected;
using Xunit;

namespace LocalLLMServerManager.Tests;
Expand Down Expand Up @@ -72,13 +74,30 @@ public async Task UninstallInvalidComponent_ReturnsBadRequest()
[Fact]
public async Task SettingsViewModel_RefreshComponentStatusesAsync_UpdatesInstalledFlags()
{
var client = _fixture.CreateClient();
var jsonResponse = @"[
{ ""id"": ""video-generation"", ""name"": ""Video"", ""installed"": true },
{ ""id"": ""audio-tts"", ""name"": ""Audio"", ""installed"": true }
]";

var handlerMock = new Moq.Mock<HttpMessageHandler>();
handlerMock.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
Moq.Protected.ItExpr.Is<HttpRequestMessage>(r => r.RequestUri != null && r.RequestUri.ToString().Contains("/api/components")),
Moq.Protected.ItExpr.IsAny<System.Threading.CancellationToken>())
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(jsonResponse, System.Text.Encoding.UTF8, "application/json")
});

var client = new HttpClient(handlerMock.Object);
var vm = new LocalLLMServerManager.Shared.ViewModels.SettingsViewModel();

Assert.False(vm.IsVideoPackInstalled);
Assert.False(vm.IsAudioPackInstalled);

await vm.RefreshComponentStatusesAsync("", client);
await vm.RefreshComponentStatusesAsync("http://localhost:5246", client);

Assert.True(vm.IsVideoPackInstalled);
Assert.True(vm.IsAudioPackInstalled);
Expand Down
Loading
Loading