using System.IO.Compression; using System.IO; using System.Windows.Input; using PersonalToolbox.Models; using PersonalToolbox.Services; var tests = new (string Name, Func Run)[] { ("快捷键解析和捕获格式化", TestHotkeys), ("组合校验覆盖循环、重复和缺失引用", TestCombinationValidation), ("路径和网址校验", TestPathValidation), ("配置读写、导出和数据版本校验", TestConfiguration) }; var failed = 0; foreach (var test in tests) { try { await test.Run(); Console.WriteLine($"PASS {test.Name}"); } catch (Exception ex) { failed++; Console.WriteLine($"FAIL {test.Name}"); Console.WriteLine(ex); } } if (failed > 0) { Environment.Exit(1); } static Task TestHotkeys() { Assert(HotkeyParser.TryParse("Ctrl + Alt + T", out _, out _), "Ctrl + Alt + T 应可解析"); Assert(HotkeyParser.Normalize("alt + ctrl + t") == "Ctrl+Alt+T", "快捷键应按固定顺序归一化"); Assert(!HotkeyParser.TryParse("T", out _, out _), "纯单键快捷键不应被接受"); Assert(HotkeyParser.TryFormatFromInput(ModifierKeys.Control | ModifierKeys.Shift, Key.D, out var hotkey), "捕获输入应成功"); Assert(hotkey == "Ctrl+Shift+D", "捕获输入应生成规范文本"); return Task.CompletedTask; } static Task TestCombinationValidation() { var launcher = new ToolLaunchService(); var toolA = CreateTool("tool-a", "工具 A"); var toolB = CreateTool("tool-b", "工具 B"); var child = CreateCombination("combo-child", "子组合", "tool-b"); var parent = CreateCombination("combo-parent", "父组合", "tool-a", "combo-child"); var valid = launcher.ValidateCombination(parent, [toolA, toolB, child, parent]); Assert(valid.Success, "不重复的嵌套组合应通过校验"); child.Combination!.Members.Add(new CombinationMember { ToolId = "tool-a", SortOrder = 1 }); var duplicate = launcher.ValidateCombination(parent, [toolA, toolB, child, parent]); Assert(!duplicate.Success && duplicate.Kind == LaunchResultKind.DuplicateTool, "展开后重复工具应被拦截"); child.Combination.Members.Clear(); child.Combination.Members.Add(new CombinationMember { ToolId = "combo-parent" }); var circular = launcher.ValidateCombination(parent, [toolA, child, parent]); Assert(!circular.Success && circular.Kind == LaunchResultKind.CircularReference, "循环引用应被拦截"); child.Combination.Members.Clear(); child.Combination.Members.Add(new CombinationMember { ToolId = "missing-tool" }); var missing = launcher.ValidateCombination(parent, [toolA, child, parent]); Assert(!missing.Success && missing.Kind == LaunchResultKind.MissingReference, "缺失成员引用应被拦截"); return Task.CompletedTask; } static Task TestPathValidation() { Assert(PathValidationService.IsValidUrl("example.com", out var url), "无协议网址应自动补全"); Assert(url.StartsWith("https://example.com", StringComparison.OrdinalIgnoreCase), "网址应补全为 https"); Assert(!PathValidationService.IsValidUrl("file://c:/test.txt", out _), "第一版只接受 http/https"); var tool = CreateTool("local-missing", "缺失工具"); tool.LaunchTarget = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "missing.exe"); var report = new PathValidationService().ValidateTools([tool]); Assert(report.HasIssues, "缺失路径应生成检查问题"); Assert(tool.PathInvalid, "缺失路径应标记到工具模型"); return Task.CompletedTask; } static async Task TestConfiguration() { var root = Path.Combine(Path.GetTempPath(), "PersonalToolboxTests_" + Guid.NewGuid().ToString("N")); var service = new ConfigurationService(root); var data = new ToolboxData { Settings = new AppSettings { Theme = "Dark", CardSize = "Large", UiScale = 1.2, ShowToolDescriptions = false }, Categories = [ new CategoryItem { Id = "category-test", Name = "测试分类", IconKey = "dev", SortOrder = 0 } ], Tools = [ CreateTool("tool-config", "配置工具") ], AutoRunEntries = [] }; data.Tools[0].CategoryId = "category-test"; await service.SaveAsync(data); var loaded = await service.LoadAsync(); Assert(loaded.Settings.DataVersion == ConfigurationService.CurrentDataVersion, "加载后应升级到当前数据版本"); Assert(loaded.Settings.Theme == "Dark", "设置应完成往返保存"); Assert(loaded.Tools.Any(tool => tool.Id == "tool-config"), "工具应完成往返保存"); var backupDirectory = Path.Combine(root, "backups", "should-not-export"); Directory.CreateDirectory(backupDirectory); await File.WriteAllTextAsync(Path.Combine(backupDirectory, "ignored.txt"), "ignored"); var zipPath = Path.Combine(Path.GetTempPath(), "PersonalToolboxExport_" + Guid.NewGuid().ToString("N") + ".zip"); await service.ExportAsync(zipPath, loaded); using (var archive = ZipFile.OpenRead(zipPath)) { Assert(archive.GetEntry("appsettings.json") is not null, "导出应包含 appsettings.json"); Assert(archive.Entries.All(entry => !entry.FullName.StartsWith("backups/", StringComparison.OrdinalIgnoreCase)), "导出不应包含 backups 目录"); } var futureRoot = Path.Combine(Path.GetTempPath(), "PersonalToolboxTests_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(futureRoot); await File.WriteAllTextAsync(Path.Combine(futureRoot, "appsettings.json"), "{\"dataVersion\":999}"); var futureService = new ConfigurationService(futureRoot); await AssertThrowsAsync(() => futureService.LoadAsync(), "高版本数据应被拒绝"); } static ToolItem CreateTool(string id, string name) { return new ToolItem { Id = id, Name = name, Type = ToolType.Local, CategoryId = "category-test", LaunchTarget = Environment.ProcessPath ?? "dotnet", IconKey = "local" }; } static ToolItem CreateCombination(string id, string name, params string[] members) { return new ToolItem { Id = id, Name = name, Type = ToolType.Combination, CategoryId = "category-test", IconKey = "combination", Combination = new CombinationConfig { Members = members.Select((toolId, index) => new CombinationMember { ToolId = toolId, SortOrder = index }).ToList() } }; } static async Task AssertThrowsAsync(Func action, string message) where TException : Exception { try { await action(); } catch (TException) { return; } throw new InvalidOperationException(message); } static void Assert(bool condition, string message) { if (!condition) { throw new InvalidOperationException(message); } }