using System.Text.Json; using FluentAssertions; using AxCopilot.Models; using Xunit; namespace AxCopilot.Tests.Services; public class SettingsServiceTests { // ─── AppSettings 기본값 ────────────────────────────────────────────────── [Fact] public void AppSettings_DefaultHotkey_IsAltSpace() { new AppSettings().Hotkey.Should().Be("Alt+Space"); } [Fact] public void LauncherSettings_DefaultMaxResults_IsSeven() { new LauncherSettings().MaxResults.Should().Be(7); } [Fact] public void LauncherSettings_DefaultTheme_IsSystem() { new LauncherSettings().Theme.Should().Be("system"); } [Fact] public void LauncherSettings_DefaultOpacity_IsValid() { var opacity = new LauncherSettings().Opacity; opacity.Should().BeInRange(0.0, 1.0); } [Fact] public void LauncherSettings_DefaultBrowserSessionRestore_IsEnabled() { new LauncherSettings().EnableBrowserSessionRestore.Should().BeTrue(); } [Fact] public void LauncherSettings_DefaultAdaptiveWorkspaceRestoreThrottle_IsEnabled() { new LauncherSettings().EnableAdaptiveWorkspaceRestoreThrottle.Should().BeTrue(); } [Fact] public void LauncherSettings_DefaultWorkspaceRestoreDelayRange_IsValid() { var settings = new LauncherSettings(); settings.WorkspaceRestoreBaseDelayMs.Should().Be(250); settings.WorkspaceRestoreMaxDelayMs.Should().BeGreaterOrEqualTo(settings.WorkspaceRestoreBaseDelayMs); } [Fact] public void AppSettings_DefaultMonitorMismatch_IsWarn() { new AppSettings().MonitorMismatch.Should().Be("warn"); } [Fact] public void AppSettings_DefaultCleanupPeriodDays_IsThirty() { new AppSettings().CleanupPeriodDays.Should().Be(30); } [Fact] public void AppSettings_DefaultIndexPaths_NotEmpty() { new AppSettings().IndexPaths.Should().NotBeEmpty(); } [Fact] public void LlmSettings_DefaultMaxContextTokens_IsAuto() { new LlmSettings().MaxContextTokens.Should().Be(0); } // ─── LauncherSettings 테마 ─────────────────────────────────────────────── [Theory] [InlineData("system")] [InlineData("dark")] [InlineData("light")] public void LauncherSettings_Theme_AcceptsValidValues(string theme) { var settings = new LauncherSettings { Theme = theme }; settings.Theme.Should().Be(theme); } // ─── JSON 직렬화 라운드트립 ────────────────────────────────────────────── [Fact] public void AppSettings_Serialization_PreservesHotkey() { var original = new AppSettings { Hotkey = "Ctrl+Space" }; var restored = RoundTrip(original); restored.Hotkey.Should().Be("Ctrl+Space"); } [Fact] public void AppSettings_Serialization_PreservesTheme() { var original = new AppSettings { Launcher = new LauncherSettings { Theme = "dark" } }; var restored = RoundTrip(original); restored.Launcher.Theme.Should().Be("dark"); } [Fact] public void AppSettings_Serialization_PreservesMaxResults() { var original = new AppSettings { Launcher = new LauncherSettings { MaxResults = 15 } }; var restored = RoundTrip(original); restored.Launcher.MaxResults.Should().Be(15); } [Fact] public void AppSettings_Serialization_PreservesAliases() { var original = new AppSettings { Aliases = [ new() { Key = "@test", Type = "url", Target = "https://example.com" } ] }; var restored = RoundTrip(original); restored.Aliases.Should().HaveCount(1); restored.Aliases[0].Key.Should().Be("@test"); restored.Aliases[0].Target.Should().Be("https://example.com"); } [Fact] public void AppSettings_Serialization_PreservesCleanupPeriodDays() { var original = new AppSettings { CleanupPeriodDays = 14 }; var restored = RoundTrip(original); restored.CleanupPeriodDays.Should().Be(14); } // ─── AliasEntry ────────────────────────────────────────────────────────── [Fact] public void AliasEntry_DefaultShowWindow_IsFalse() { new AliasEntry().ShowWindow.Should().BeFalse(); } [Theory] [InlineData("url")] [InlineData("folder")] [InlineData("app")] [InlineData("batch")] [InlineData("api")] [InlineData("clipboard")] public void AliasEntry_Type_AcceptsAllTypes(string type) { var entry = new AliasEntry { Type = type }; entry.Type.Should().Be(type); } // ─── WorkspaceProfile / WindowSnapshot ─────────────────────────────────── [Fact] public void WorkspaceProfile_DefaultWindows_IsEmpty() { new WorkspaceProfile().Windows.Should().BeEmpty(); } [Fact] public void WindowSnapshot_DefaultShowCmd_IsNormal() { new WindowSnapshot().ShowCmd.Should().Be("Normal"); } [Fact] public void WindowSnapshot_DefaultMonitor_IsZero() { new WindowSnapshot().Monitor.Should().Be(0); } [Fact] public void WindowRect_DefaultValues_AreZero() { var rect = new WindowRect(); rect.X.Should().Be(0); rect.Y.Should().Be(0); rect.Width.Should().Be(0); rect.Height.Should().Be(0); } [Fact] public void WorkspaceProfile_Serialization_RoundTrip() { var profile = new WorkspaceProfile { Name = "작업 프로필", Windows = [ new() { Exe = "notepad.exe", ShowCmd = "Maximized", Monitor = 1 } ] }; var json = JsonSerializer.Serialize(profile, JsonOptions); var restored = JsonSerializer.Deserialize(json, JsonOptions)!; restored.Name.Should().Be("작업 프로필"); restored.Windows.Should().HaveCount(1); restored.Windows[0].Exe.Should().Be("notepad.exe"); restored.Windows[0].ShowCmd.Should().Be("Maximized"); restored.Windows[0].Monitor.Should().Be(1); } [Fact] public void WorkspaceProfile_Serialization_PreservesBrowserState() { var profile = new WorkspaceProfile { Name = "브라우저 프로필", Windows = [ new() { Exe = "msedge.exe", Title = "업무 포털 - Microsoft Edge", Browser = new BrowserWindowState { Kind = "edge", UserDataDir = @"C:\Users\tester\AppData\Local\Microsoft\Edge\User Data", ProfileDirectory = "Profile 3", ActiveUrl = "https://portal.example.com", ActiveTabIndex = 1, TabUrls = ["https://mail.example.com", "https://portal.example.com"] } } ] }; var json = JsonSerializer.Serialize(profile, JsonOptions); var restored = JsonSerializer.Deserialize(json, JsonOptions)!; restored.Windows.Should().HaveCount(1); restored.Windows[0].Browser.Should().NotBeNull(); restored.Windows[0].Browser!.Kind.Should().Be("edge"); restored.Windows[0].Browser!.ProfileDirectory.Should().Be("Profile 3"); restored.Windows[0].Browser!.ActiveTabIndex.Should().Be(1); restored.Windows[0].Browser!.TabUrls.Should().ContainInOrder( "https://mail.example.com", "https://portal.example.com"); } [Fact] public void WorkspaceProfile_Serialization_PreservesAppState() { var profile = new WorkspaceProfile { Name = "앱 상태 프로필", Windows = [ new() { Exe = "explorer.exe", Title = "문서 - 파일 탐색기", AppState = new AppWindowState { Kind = "explorer", PrimaryPath = @"C:\Docs", ActivePathIndex = 0, Paths = [@"C:\Docs", @"C:\Docs\Archive"] } } ] }; var json = JsonSerializer.Serialize(profile, JsonOptions); var restored = JsonSerializer.Deserialize(json, JsonOptions)!; restored.Windows.Should().HaveCount(1); restored.Windows[0].AppState.Should().NotBeNull(); restored.Windows[0].AppState!.Kind.Should().Be("explorer"); restored.Windows[0].AppState!.PrimaryPath.Should().Be(@"C:\Docs"); restored.Windows[0].AppState!.Paths.Should().ContainInOrder(@"C:\Docs", @"C:\Docs\Archive"); } // ─── ClipboardTransformer ──────────────────────────────────────────────── [Fact] public void ClipboardTransformer_DefaultTimeout_IsFiveSeconds() { new ClipboardTransformer().Timeout.Should().Be(5000); } [Fact] public void ClipboardTransformer_DefaultType_IsRegex() { new ClipboardTransformer().Type.Should().Be("regex"); } [Fact] public void NormalizeLlmThresholds_ClampsConfiguredValues() { var llm = new LlmSettings { ReadOnlySignatureLoopThreshold = 1, ReadOnlyStagnationThreshold = 99, NoProgressRecoveryThreshold = 2, NoProgressAbortThreshold = 200, NoProgressRecoveryMaxRetries = 9, ToolExecutionTimeoutMs = 1000 }; InvokeNormalizeLlmThresholds(llm); llm.ReadOnlySignatureLoopThreshold.Should().Be(2); llm.ReadOnlyStagnationThreshold.Should().Be(20); llm.NoProgressRecoveryThreshold.Should().Be(4); llm.NoProgressAbortThreshold.Should().Be(50); llm.NoProgressRecoveryMaxRetries.Should().Be(5); llm.ToolExecutionTimeoutMs.Should().Be(5000); } [Fact] public void NormalizeLlmThresholds_PreservesZeroAsUnset() { var llm = new LlmSettings { ReadOnlySignatureLoopThreshold = 0, ReadOnlyStagnationThreshold = 0, NoProgressRecoveryThreshold = 0, NoProgressAbortThreshold = 0, NoProgressRecoveryMaxRetries = 0, ToolExecutionTimeoutMs = 0 }; InvokeNormalizeLlmThresholds(llm); llm.ReadOnlySignatureLoopThreshold.Should().Be(0); llm.ReadOnlyStagnationThreshold.Should().Be(0); llm.NoProgressRecoveryThreshold.Should().Be(0); llm.NoProgressAbortThreshold.Should().Be(0); llm.NoProgressRecoveryMaxRetries.Should().Be(0); llm.ToolExecutionTimeoutMs.Should().Be(0); } // ─── Helpers ───────────────────────────────────────────────────────────── private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = false, PropertyNameCaseInsensitive = true }; private static AppSettings RoundTrip(AppSettings original) { var json = JsonSerializer.Serialize(original, JsonOptions); return JsonSerializer.Deserialize(json, JsonOptions)!; } private static void InvokeNormalizeLlmThresholds(LlmSettings llm) { var method = typeof(AxCopilot.Services.SettingsService) .GetMethod("NormalizeLlmThresholds", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); method.Should().NotBeNull(); method!.Invoke(null, [llm]); } }