Add WinUI and core source
build-winui / winui (push) Has been cancelled

This commit is contained in:
QWQLwToo
2026-06-26 13:27:13 +08:00
parent f59190251d
commit 7ecc6a8923
262 changed files with 137492 additions and 0 deletions
@@ -0,0 +1,81 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Data.Sqlite;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Settings;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class AgreementAcceptanceStoreTests
{
[TestMethod]
public void OldBooleanAgreementSettingRequiresResign()
{
var settings = new AppSettings
{
UserAgreementAccepted = true
};
Assert.IsFalse(AgreementDocument.SettingsMatchCurrent(settings));
settings.UserAgreementVersion = AgreementDocument.CurrentVersion;
Assert.IsTrue(AgreementDocument.SettingsMatchCurrent(settings));
}
[TestMethod]
public async Task AgreementAcceptanceUsesMainSqliteAndSurvivesLogCleanup()
{
using var workspace = TestWorkspace.Create("ymhut-agreement-store");
var paths = AppPaths.ForCurrentUser(workspace.Root);
var store = new AgreementAcceptanceStore(paths);
var logService = new SqliteLogService(paths);
var acceptedAt = new DateTimeOffset(2026, 6, 15, 9, 30, 0, TimeSpan.Zero);
await store.RecordAcceptedAsync(
AgreementDocument.CurrentVersion,
AgreementDocument.CurrentRevision,
"2.0.6.2",
"zh-CN",
acceptedAt);
await logService.WriteAsync("Information", "test", "log before cleanup");
await logService.ClearAllAsync();
var latest = await store.GetLatestAsync();
Assert.AreEqual(Path.Combine(paths.Logs, AppDatabasePaths.MainDatabaseFileName), store.DatabasePath);
Assert.IsNotNull(latest);
Assert.AreEqual(AgreementDocument.CurrentVersion, latest.Version);
Assert.AreEqual(AgreementDocument.CurrentRevision, latest.Revision);
Assert.AreEqual("2.0.6.2", latest.AppVersion);
Assert.AreEqual("zh-CN", latest.Language);
Assert.AreEqual(acceptedAt, latest.AcceptedAt);
}
private sealed class TestWorkspace : IDisposable
{
private TestWorkspace(string root)
{
Root = root;
}
public string Root { get; }
public static TestWorkspace Create(string prefix)
{
var root = Path.Combine(Path.GetTempPath(), prefix, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
return new TestWorkspace(root);
}
public void Dispose()
{
SqliteConnection.ClearAllPools();
if (Directory.Exists(Root))
{
Directory.Delete(Root, recursive: true);
}
}
}
}
@@ -0,0 +1,185 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.Settings;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class AppSettingsStoreTests
{
[TestMethod]
public async Task NewSettingsDefaultToDashboardHome()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
var store = new AppSettingsStore(root);
var settings = await store.LoadAsync();
Assert.AreEqual("dashboard", settings.HomePageMode);
}
[TestMethod]
public async Task RecordRecentToolKeepsNewestFirstAndUnique()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
var store = new AppSettingsStore(root);
await store.RecordRecentToolAsync("json_formatter");
await store.RecordRecentToolAsync("safe_browser");
await store.RecordRecentToolAsync("json_formatter");
var settings = await store.LoadAsync();
CollectionAssert.AreEqual(
new[] { "json_formatter", "safe_browser" },
settings.RecentToolIds);
}
[TestMethod]
public async Task LoadAsyncImportsLegacySettingsOnce()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"), "WinUI");
var legacyPath = Path.Combine(Path.GetDirectoryName(root)!, "YMhut Box", "settings.json");
Directory.CreateDirectory(Path.GetDirectoryName(legacyPath)!);
await File.WriteAllTextAsync(legacyPath, """
{
"theme": "Dark",
"recentToolIds": [
"safe_browser"
],
"legacyImportCompleted": false
}
""");
var store = new AppSettingsStore(root);
var settings = await store.LoadAsync();
Assert.IsTrue(settings.LegacyImportCompleted);
Assert.AreEqual("Dark", settings.Theme);
CollectionAssert.AreEqual(new[] { "safe_browser" }, settings.RecentToolIds);
}
[TestMethod]
public async Task LoadAsyncImportsLegacySharedPreferences()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"), "WinUI");
var legacyPath = Path.Combine(Path.GetDirectoryName(root)!, "ymhut_box", "shared_preferences.json");
Directory.CreateDirectory(Path.GetDirectoryName(legacyPath)!);
var legacyPrefix = string.Concat("flut", "ter.");
await File.WriteAllTextAsync(legacyPath, """
{
"__PREFIX__theme": "dark",
"__PREFIX__pitch_black": true,
"__PREFIX__seed_color": -10458044,
"__PREFIX__card_opacity": 0.64,
"__PREFIX__background_image": "D:\\Images\\wallpaper.png",
"__PREFIX__background_opacity": 0.42,
"__PREFIX__user_agreement_accepted": true,
"__PREFIX__sidebar_collapsed": true,
"__PREFIX__desktop_titlebar": false,
"__PREFIX__recent_tool_ids": [
"media_player",
"safe_browser"
],
"__PREFIX__pinned_tool_ids": [
"json_formatter"
],
"__PREFIX__window_x": 12.5,
"__PREFIX__window_y": 24.5,
"__PREFIX__window_width": 1280,
"__PREFIX__window_height": 760,
"__PREFIX__window_maximized": true,
"__PREFIX__proxy_enabled": true,
"__PREFIX__proxy_mode": "manual",
"__PREFIX__proxy_host": "127.0.0.1",
"__PREFIX__proxy_port": 7891,
"__PREFIX__animations_enabled": false,
"__PREFIX__font_size": 1.15,
"__PREFIX__tool_display_mode": "list",
"__PREFIX__close_behavior": "minimize_then_exit",
"__PREFIX__restore_window_position": false,
"__PREFIX__auto_start": true,
"__PREFIX__log_retention_count": 300,
"__PREFIX__data_refresh_interval": 45,
"__PREFIX__update_notification": false,
"__PREFIX__hardware_acceleration_enabled": false,
"__PREFIX__proxy_test_timeout_seconds": 9
}
""".Replace("__PREFIX__", legacyPrefix));
var store = new AppSettingsStore(root);
var settings = await store.LoadAsync();
Assert.AreEqual("Dark", settings.Theme);
Assert.IsTrue(settings.PitchBlack);
Assert.AreEqual(-10458044, settings.SeedColor);
Assert.AreEqual(0.64, settings.CardOpacity, 0.001);
Assert.AreEqual("D:\\Images\\wallpaper.png", settings.BackgroundImage);
Assert.AreEqual(0.42, settings.BackgroundOpacity, 0.001);
Assert.IsTrue(settings.UserAgreementAccepted);
Assert.IsTrue(settings.SidebarCollapsed);
Assert.IsFalse(settings.DesktopTitlebar);
CollectionAssert.AreEqual(new[] { "media_player", "safe_browser" }, settings.RecentToolIds);
CollectionAssert.AreEqual(new[] { "json_formatter" }, settings.PinnedToolIds);
Assert.AreEqual(12.5, settings.WindowX);
Assert.AreEqual(24.5, settings.WindowY);
Assert.AreEqual(1280, settings.WindowWidth);
Assert.AreEqual(760, settings.WindowHeight);
Assert.IsTrue(settings.WindowMaximized);
Assert.IsTrue(settings.ProxyEnabled);
Assert.AreEqual("manual", settings.ProxyMode);
Assert.AreEqual("127.0.0.1", settings.ProxyHost);
Assert.AreEqual(7891, settings.ProxyPort);
Assert.IsFalse(settings.AnimationsEnabled);
Assert.AreEqual(1.15, settings.FontSize, 0.001);
Assert.AreEqual("list", settings.ToolDisplayMode);
Assert.AreEqual("minimize_then_exit", settings.CloseBehavior);
Assert.IsFalse(settings.RestoreWindowPosition);
Assert.IsTrue(settings.AutoStart);
Assert.AreEqual(300, settings.LogRetentionCount);
Assert.AreEqual(45, settings.DataRefreshInterval);
Assert.IsFalse(settings.UpdateNotification);
Assert.IsFalse(settings.HardwareAccelerationEnabled);
Assert.AreEqual(9, settings.ProxyTestTimeoutSeconds);
}
[TestMethod]
public void PinnedToolSettingsTogglesUniquelyAndTrims()
{
var pinned = Enumerable.Range(0, 70)
.Select(index => $"tool_{index:00}")
.ToList();
pinned.Insert(10, "json_formatter");
PinnedToolSettings.Toggle(pinned, "json_formatter", wasPinned: false);
Assert.HasCount(64, pinned);
Assert.AreEqual("json_formatter", pinned[0]);
Assert.AreEqual(1, pinned.Count(id => string.Equals(id, "json_formatter", StringComparison.OrdinalIgnoreCase)));
PinnedToolSettings.Toggle(pinned, "JSON_FORMATTER", wasPinned: true);
Assert.HasCount(63, pinned);
Assert.IsFalse(pinned.Any(id => string.Equals(id, "json_formatter", StringComparison.OrdinalIgnoreCase)));
}
[TestMethod]
public async Task LoadAsyncNormalizesGlobalMaterialSettings()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
await File.WriteAllTextAsync(Path.Combine(root, "settings.json"), """
{
"legacyImportCompleted": true,
"windowBackdrop": "desktop_acrylic",
"settingsPanelMaterial": "frosted_glass",
"topBarMaterial": "frosted_glass"
}
""");
var store = new AppSettingsStore(root);
var settings = await store.LoadAsync();
Assert.AreEqual("acrylic", settings.WindowBackdrop);
Assert.AreEqual("glass", settings.SettingsPanelMaterial);
Assert.AreEqual("glass", settings.TopBarMaterial);
}
}
+3
View File
@@ -0,0 +1,3 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
@@ -0,0 +1,377 @@
using System.Net;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.App;
using YMhut.Box.Core.DevEnvironments;
using YMhut.Box.Core.Downloads;
using YMhut.Box.Core.Net;
using YMhut.Box.Core.Updates;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class DownloadAndDevEnvironmentTests
{
[TestMethod]
public void DevEnvironmentVersionParserRecognizesSupportedTools()
{
Assert.AreEqual("1.23.4", DevEnvironmentVersionParser.Parse("go", "go version go1.23.4 windows/amd64"));
Assert.AreEqual("3.12.7", DevEnvironmentVersionParser.Parse("python", "Python 3.12.7"));
Assert.AreEqual("21.0.2", DevEnvironmentVersionParser.Parse("java", "openjdk version \"21.0.2\" 2024-01-16"));
Assert.AreEqual("27.3.1", DevEnvironmentVersionParser.Parse("docker", "Docker version 27.3.1, build ce12230"));
Assert.AreEqual("8.0.36", DevEnvironmentVersionParser.Parse("mysql", "mysql Ver 8.0.36 for Win64 on x86_64"));
Assert.AreEqual("22.11.0", DevEnvironmentVersionParser.Parse("node", "v22.11.0"));
Assert.AreEqual("10.0.100", DevEnvironmentVersionParser.Parse("dotnet", "10.0.100\r\n9.0.308"));
Assert.AreEqual("2.47.1.windows.2", DevEnvironmentVersionParser.Parse("git", "git version 2.47.1.windows.2"));
Assert.AreEqual("1.82.0", DevEnvironmentVersionParser.Parse("rust", "rustc 1.82.0 (f6e511eec 2024-10-15)"));
Assert.AreEqual("3.31.0", DevEnvironmentVersionParser.Parse("cmake", "cmake version 3.31.0"));
}
[TestMethod]
public async Task DevEnvironmentCatalogReadsOfficialVersionMetadata()
{
var workspace = TempWorkspace();
var paths = AppPaths.ForCurrentUser(workspace);
var service = new DevEnvironmentCatalogService(
paths,
new FakeHttpService(uri => uri.AbsoluteUri switch
{
"https://go.dev/dl/?mode=json" => """
[
{
"version": "go1.23.4",
"stable": true,
"files": [
{ "filename": "go1.23.4.windows-amd64.msi", "sha256": "abc" },
{ "filename": "go1.23.4.src.tar.gz", "sha256": "def" }
]
}
]
""",
"https://nodejs.org/dist/index.json" => """
[
{ "version": "v22.11.0", "date": "2024-10-29" }
]
""",
"https://api.github.com/repos/git-for-windows/git/releases" => """
[
{
"tag_name": "v2.47.1.windows.2",
"published_at": "2024-10-01T00:00:00Z",
"html_url": "https://github.com/git-for-windows/git/releases/tag/v2.47.1.windows.2",
"assets": [
{
"name": "Git-2.47.1-64-bit.exe",
"browser_download_url": "https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.2/Git-2.47.1-64-bit.exe",
"size": 1024
}
]
}
]
""",
_ => """
{
"releases-index": [
{
"channel-version": "10.0",
"latest-sdk": "10.0.100",
"release-date": "2025-11-11"
}
]
}
"""
}));
var go = await service.GetVersionsAsync("go");
var node = await service.GetVersionsAsync("node");
var dotnet = await service.GetVersionsAsync("dotnet");
Assert.AreEqual("1.23.4", go[0].Version);
Assert.AreEqual("go1.23.4.windows-amd64.msi", go[0].Installer?.FileName);
Assert.AreEqual("go1.23.4.src.tar.gz", go[0].SourceArchive?.FileName);
Assert.AreEqual("22.11.0", node[0].Version);
Assert.AreEqual("node-v22.11.0-x64.msi", node[0].Installer?.FileName);
Assert.IsTrue(node[0].AllCandidates.Any(candidate => candidate.SourceType == "MirrorCN"));
Assert.AreEqual("10.0.100", dotnet[0].Version);
Assert.AreEqual("dotnet-sdk-10.0.100-win-x64.exe", dotnet[0].Installer?.FileName);
}
[TestMethod]
public async Task DevEnvironmentCatalogReadsGithubReleaseAssets()
{
var workspace = TempWorkspace();
var service = new DevEnvironmentCatalogService(
AppPaths.ForCurrentUser(workspace),
new FakeHttpService(_ => """
[
{
"tag_name": "v3.31.0",
"published_at": "2024-11-01T00:00:00Z",
"html_url": "https://github.com/Kitware/CMake/releases/tag/v3.31.0",
"assets": [
{
"name": "cmake-3.31.0-windows-x86_64.msi",
"browser_download_url": "https://github.com/Kitware/CMake/releases/download/v3.31.0/cmake-3.31.0-windows-x86_64.msi",
"size": 2048
},
{
"name": "cmake-3.31.0.tar.gz",
"browser_download_url": "https://github.com/Kitware/CMake/releases/download/v3.31.0/cmake-3.31.0.tar.gz",
"size": 4096
}
]
}
]
"""));
var cmake = await service.GetVersionsAsync("cmake");
Assert.HasCount(1, cmake);
Assert.AreEqual("3.31.0", cmake[0].Version);
Assert.IsTrue(cmake[0].AllCandidates.Any(candidate => candidate.Mode == DevEnvironmentInstallMode.QuickInstall && candidate.SourceType == "GitHub"));
Assert.IsTrue(cmake[0].AllCandidates.Any(candidate => candidate.Mode == DevEnvironmentInstallMode.SourceBuild && candidate.Source.SizeBytes == 4096));
}
[TestMethod]
public async Task DownloadQueueStorePersistsAndRestoresItems()
{
var workspace = TempWorkspace();
var store = new DownloadQueueStore(AppPaths.ForCurrentUser(workspace));
var item = DownloadItem.Create(
new DownloadSource("https://example.com/tool.exe", "Tool", "tool.exe"),
Path.Combine(workspace, "tool.exe"),
"installer")
.WithResumeMetadata(
"\"abc\"",
"Wed, 10 Jun 2026 08:00:00 GMT",
"bytes",
2048,
"https://cdn.example.com/tool.exe",
true)
.WithProgress(DownloadState.Completed, 1024, 2048, 0);
await store.SaveAsync([item]);
var loaded = await store.LoadAsync();
Assert.HasCount(1, loaded);
Assert.AreEqual(item.Id, loaded[0].Id);
Assert.AreEqual(DownloadState.Completed, loaded[0].State);
Assert.AreEqual("tool.exe", loaded[0].Source.FileName);
Assert.AreEqual("installer", loaded[0].InstallCommand);
Assert.AreEqual(Path.Combine(workspace, "tool.exe.partial"), loaded[0].EffectivePartialPath);
Assert.AreEqual("\"abc\"", loaded[0].ETag);
Assert.AreEqual("Wed, 10 Jun 2026 08:00:00 GMT", loaded[0].LastModified);
Assert.AreEqual("bytes", loaded[0].AcceptRanges);
Assert.AreEqual(2048, loaded[0].ContentLength);
Assert.AreEqual("https://cdn.example.com/tool.exe", loaded[0].FinalUrl);
Assert.IsTrue(loaded[0].ResumeSupported);
}
[TestMethod]
public async Task DownloadQueueStorePersistsSettings()
{
var workspace = TempWorkspace();
var store = new DownloadQueueStore(AppPaths.ForCurrentUser(workspace));
var directory = Path.Combine(workspace, "custom");
await store.SaveSettingsAsync(new DownloadSettings(directory, 99));
var loaded = await store.LoadSettingsAsync();
Assert.AreEqual(directory, loaded.DefaultDirectory);
Assert.AreEqual(5, loaded.MaxConcurrentDownloads);
}
[TestMethod]
public void DownloadProgressAndProtocolRoundTrip()
{
var progress = new DownloadProgressSnapshot(
"a",
DownloadState.Running,
512,
1024,
2048,
ETag: "\"abc\"",
LastModified: "Wed, 10 Jun 2026 08:00:00 GMT",
AcceptRanges: "bytes",
ContentLength: 1024,
FinalUrl: "https://cdn.example.com/a.zip");
var message = new DownloadHostMessage(DownloadHostProtocol.Progress, progress.Id, DownloadHostProtocol.Version, Progress: progress);
var serialized = DownloadHostProtocol.Serialize(message);
var roundTrip = DownloadHostProtocol.Deserialize(serialized);
Assert.AreEqual(0.5, progress.Progress, 0.001);
Assert.IsNotNull(roundTrip);
Assert.AreEqual(DownloadHostProtocol.Progress, roundTrip.Type);
Assert.AreEqual(DownloadState.Running, roundTrip.Progress?.State);
Assert.AreEqual(2048, roundTrip.Progress?.BytesPerSecond);
Assert.AreEqual("\"abc\"", roundTrip.Progress?.ETag);
Assert.AreEqual("bytes", roundTrip.Progress?.AcceptRanges);
Assert.AreEqual("https://cdn.example.com/a.zip", roundTrip.Progress?.FinalUrl);
}
[TestMethod]
public async Task DirectDownloadValidatorParsesInternetShortcut()
{
using var validator = new DirectDownloadValidator(new HttpClient(new StubHttpHandler(request =>
{
var content = new ByteArrayContent(Encoding.UTF8.GetBytes("package"));
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/zip");
return new HttpResponseMessage(HttpStatusCode.OK)
{
RequestMessage = request,
Content = content
};
})));
var result = await validator.ValidateAsync("""
[InternetShortcut]
URL=https://example.com/package.zip
""");
Assert.IsTrue(result.IsDirectDownload);
Assert.IsTrue(result.WasInternetShortcut);
Assert.AreEqual("https://example.com/package.zip", result.EffectiveUrl);
}
[TestMethod]
public async Task DirectDownloadValidatorRejectsHtmlDownloadPage()
{
using var validator = new DirectDownloadValidator(new HttpClient(new StubHttpHandler(request =>
{
var content = new ByteArrayContent(Encoding.UTF8.GetBytes("<!doctype html><html></html>"));
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/html");
return new HttpResponseMessage(HttpStatusCode.OK)
{
RequestMessage = request,
Content = content
};
})));
var result = await validator.ValidateAsync("https://example.com/download");
Assert.IsFalse(result.IsDirectDownload);
StringAssert.Contains(result.Message, "web page");
}
[TestMethod]
public async Task ReleaseManifestSerializerRoundTripsFullInstallerManifest()
{
var workspace = TempWorkspace();
var path = Path.Combine(workspace, "manifest.json");
var manifest = new ReleaseManifest(
"2.0.7.0",
"2.0.7.0",
"stable",
DateTimeOffset.Parse("2026-06-13T00:00:00Z"),
[
new ReleaseFileEntry("downloads/YMhut_Box_WinUI_Setup_2.0.7.0.exe", 445_000_000, "installer"),
new ReleaseFileEntry("downloads/YMhut_Box_WinUI_2.0.7.0.msix", 571_000_000, "msix")
],
[],
"https://update.ymhut.cn/downloads/",
"Full");
await ReleaseManifestSerializer.WriteManifestAsync(path, manifest);
var loaded = await ReleaseManifestSerializer.ReadManifestAsync(path);
Assert.AreEqual("2.0.7.0", loaded.Version);
Assert.AreEqual("stable", loaded.Channel);
Assert.AreEqual("Full", loaded.Flavor);
Assert.HasCount(2, loaded.Files);
Assert.IsTrue(loaded.Files.All(file =>
file.Path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ||
file.Path.EndsWith(".msix", StringComparison.OrdinalIgnoreCase)));
}
[TestMethod]
public void UpdateInfoEndpointPolicyPrefersCanonicalUpdateInfo()
{
var endpoints = UpdateInfoEndpointPolicy.BuildDefaultUris("https://update.ymhut.cn/");
CollectionAssert.AreEqual(
new[]
{
"https://update.ymhut.cn/api/client/bootstrap",
"https://update.ymhut.cn/update-info.json",
"https://update.ymhut.cn/update-info",
"https://update.ymhut.cn/api/update-info"
},
endpoints.Select(endpoint => endpoint.ToString()).ToArray());
}
[TestMethod]
public async Task HttpRedirectResolverFollowsMultiHopAndRelativeLocations()
{
using var client = new HttpClient(new StubHttpHandler(request =>
{
return request.RequestUri!.AbsoluteUri switch
{
"https://media.example/start" => new HttpResponseMessage(HttpStatusCode.Redirect)
{
Headers = { Location = new Uri("/hop-1", UriKind.Relative) },
RequestMessage = request
},
"https://media.example/hop-1" => new HttpResponseMessage(HttpStatusCode.TemporaryRedirect)
{
Headers = { Location = new Uri("https://cdn.example/final.mp4") },
RequestMessage = request
},
"https://cdn.example/final.mp4" => new HttpResponseMessage(HttpStatusCode.OK)
{
RequestMessage = request,
Content = new ByteArrayContent(Array.Empty<byte>())
},
_ => new HttpResponseMessage(HttpStatusCode.NotFound)
{
RequestMessage = request,
Content = new StringContent("missing")
}
};
}));
var resolved = await HttpRedirectResolver.ResolveAsync(new Uri("https://media.example/start"), client);
Assert.AreEqual("https://cdn.example/final.mp4", resolved.ToString());
}
private static string TempWorkspace()
{
var path = Path.Combine(Path.GetTempPath(), "ymhut-box-download-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(path);
return path;
}
private sealed class FakeHttpService(Func<Uri, string> handler) : IHttpService
{
public Task<HttpServiceResult> GetAsync(Uri uri, CancellationToken cancellationToken = default)
{
return Task.FromResult(new HttpServiceResult(HttpStatusCode.OK, handler(uri), new Dictionary<string, string[]>(), TimeSpan.Zero));
}
public Task<string> GetStringAsync(Uri uri, CancellationToken cancellationToken = default)
{
return Task.FromResult(handler(uri));
}
public Task<HttpServiceResult> SendAsync(
Uri uri,
string method = "GET",
string? body = null,
IReadOnlyDictionary<string, string>? headers = null,
bool ensureSuccess = true,
HttpRequestPolicy? policy = null,
CancellationToken cancellationToken = default)
{
return GetAsync(uri, cancellationToken);
}
}
private sealed class StubHttpHandler(Func<HttpRequestMessage, HttpResponseMessage> handler) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(handler(request));
}
}
}
+409
View File
@@ -0,0 +1,409 @@
using System.IO.Compression;
using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Feedback;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.System;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class FeedbackServiceTests
{
[TestMethod]
public void FeedbackCodeCreatesServerCompatibleCode()
{
var code = FeedbackCode.Create(DateTimeOffset.Parse("2026-06-04T10:00:00+08:00"));
Assert.IsTrue(Regex.IsMatch(code, "^FB-20260604-[A-F0-9]{6}$"));
Assert.AreEqual("FB-20260604-ABC123", FeedbackCode.Normalize(" fb-20260604-abc123 "));
Assert.IsTrue(FeedbackCode.IsValid("fb-20260604-abc123"));
Assert.IsFalse(FeedbackCode.IsValid("FB-20260604-XYZ123"));
}
[TestMethod]
public async Task FeedbackPackageIncludesSelectedAttachmentsAndLocalizedLogs()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-tests", Guid.NewGuid().ToString("N"));
var paths = AppPaths.ForCurrentUser(tempRoot);
var logs = new JsonFileLogService(paths);
var service = new FeedbackPackageService(logs, new FakeSystemMetricsService(), new ToolCatalog());
await logs.WriteAsync("Information", "navigation", "Open toolbox", "https://example.com/private?token=secret");
var package = await service.BuildPackageAsync(new FeedbackRequest(
"同步状态卡住",
"issue",
"major",
"dev@example.com",
"首页一直显示后台校验中。",
IncludeTodayLogs: true,
IncludeToolStatus: true,
IncludeSystemSummary: true,
Language: "zh-CN",
FeedbackCode: "FB-20260604-ABC123"));
try
{
Assert.IsTrue(File.Exists(package.PackagePath));
Assert.IsGreaterThan(0, package.PackageBytes);
Assert.AreEqual(
Path.GetFullPath(FeedbackPackageService.FeedbackPackagesRoot()),
Path.GetFullPath(Path.GetDirectoryName(package.PackagePath)!));
Assert.IsTrue(package.IncludedFiles.ContainsKey("feedback.json"));
Assert.IsTrue(package.IncludedFiles.ContainsKey("tool-status.json"));
Assert.IsTrue(package.IncludedFiles.ContainsKey("system-summary.json"));
Assert.IsTrue(package.IncludedFiles.Keys.Any(name => name.StartsWith("logs-", StringComparison.OrdinalIgnoreCase)));
Assert.IsTrue(package.SummaryText.Contains("首页一直显示后台校验中。", StringComparison.Ordinal));
Assert.IsTrue(package.SummaryText.Contains("FB-20260604-ABC123", StringComparison.Ordinal));
using var archive = ZipFile.OpenRead(package.PackagePath);
Assert.IsNotNull(archive.GetEntry("feedback.json"));
Assert.IsNotNull(archive.GetEntry("summary.txt"));
var feedbackJson = ReadEntry(archive.GetEntry("feedback.json")!);
using var feedbackDocument = JsonDocument.Parse(feedbackJson);
Assert.AreEqual("FB-20260604-ABC123", feedbackDocument.RootElement.GetProperty("feedbackCode").GetString());
Assert.AreEqual("FB-20260604-ABC123", feedbackDocument.RootElement.GetProperty("request").GetProperty("feedbackCode").GetString());
var logsEntry = archive.Entries.Single(entry => entry.FullName.StartsWith("logs-", StringComparison.OrdinalIgnoreCase));
var logsJson = ReadEntry(logsEntry);
using var logsDocument = JsonDocument.Parse(logsJson);
var firstLog = logsDocument.RootElement[0];
Assert.AreEqual("导航", firstLog.GetProperty("display").GetProperty("category").GetString());
Assert.AreEqual("打开工具箱", firstLog.GetProperty("display").GetProperty("message").GetString());
Assert.AreEqual("远程服务", firstLog.GetProperty("display").GetProperty("detail").GetString());
Assert.AreEqual("远程服务", firstLog.GetProperty("raw").GetProperty("detail").GetString());
Assert.IsFalse(logsJson.Contains("example.com/private", StringComparison.OrdinalIgnoreCase));
var toolStatus = ReadEntry(archive.GetEntry("tool-status.json")!);
StringAssert.Contains(toolStatus, "\"id\": \"compression_codec\"");
StringAssert.Contains(toolStatus, "\"exists\": true");
}
finally
{
DeleteIfExists(package.PackagePath);
Directory.Delete(tempRoot, recursive: true);
}
}
[TestMethod]
public async Task FeedbackPackageCryptoRoundTripsLocalZip()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
var sourceZip = Path.Combine(tempRoot, "feedback.zip");
var decryptedZip = Path.Combine(tempRoot, "feedback-decrypted.zip");
try
{
using (var archive = ZipFile.Open(sourceZip, ZipArchiveMode.Create))
{
var entry = archive.CreateEntry("summary.txt");
await using var stream = entry.Open();
await using var writer = new StreamWriter(stream);
await writer.WriteAsync("反馈包加密测试");
}
var encrypted = await FeedbackPackageCrypto.EncryptPackageAsync(sourceZip);
Assert.IsTrue(File.Exists(encrypted.PackagePath));
Assert.AreEqual(".ymfb", Path.GetExtension(encrypted.PackagePath));
Assert.IsGreaterThan(new FileInfo(sourceZip).Length, encrypted.PackageBytes);
await FeedbackPackageCrypto.DecryptPackageAsync(encrypted.PackagePath, decryptedZip);
CollectionAssert.AreEqual(
await File.ReadAllBytesAsync(sourceZip),
await File.ReadAllBytesAsync(decryptedZip));
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public async Task FeedbackPackageRejectsScreenshotOverFiveMegabytes()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-tests", Guid.NewGuid().ToString("N"));
var paths = AppPaths.ForCurrentUser(tempRoot);
var logs = new JsonFileLogService(paths);
var service = new FeedbackPackageService(logs, new FakeSystemMetricsService(), new ToolCatalog());
var screenshot = Path.Combine(tempRoot, "large.png");
await File.WriteAllBytesAsync(screenshot, new byte[FeedbackPackageService.MaxScreenshotBytes + 1]);
try
{
await Assert.ThrowsExactlyAsync<InvalidOperationException>(() =>
service.BuildPackageAsync(new FeedbackRequest(
"截图过大",
"issue",
"normal",
string.Empty,
"测试截图限制。",
IncludeTodayLogs: false,
IncludeToolStatus: false,
IncludeSystemSummary: false,
ScreenshotPath: screenshot)));
}
finally
{
Directory.Delete(tempRoot, recursive: true);
}
}
[TestMethod]
public void FeedbackSubmissionSignatureIsStable()
{
var signature = FeedbackSubmissionService.Sign("1760000000", "abc123", new string('a', 64), "{\"ok\":true}");
Assert.AreEqual(64, signature.Length);
Assert.IsTrue(signature.All(character => Uri.IsHexDigit(character) && !char.IsUpper(character)));
Assert.AreEqual(signature, FeedbackSubmissionService.Sign("1760000000", "abc123", new string('a', 64), "{\"ok\":true}"));
}
[TestMethod]
public void FeedbackStatusResponseParsesExtendedServerFields()
{
var response = ParseStatusResponse("""
{
"ok": true,
"code": "FB-20260604-ABC123",
"status": "investigating",
"statusLabel": "处理中",
"statusDetail": "已收到日志,正在定位。",
"category": "issue",
"priority": "major",
"hasReply": true,
"reply": "请先保留现场文件。",
"receivedAt": "2026-06-04T10:00:00+08:00",
"updatedAt": "2026-06-04T10:30:00+08:00",
"mailSent": true
}
""");
Assert.IsTrue(response.Ok);
Assert.AreEqual("FB-20260604-ABC123", response.Code);
Assert.AreEqual("investigating", response.Status);
Assert.AreEqual("处理中", response.StatusLabel);
Assert.AreEqual("已收到日志,正在定位。", response.StatusDetail);
Assert.AreEqual("issue", response.Category);
Assert.AreEqual("major", response.Priority);
Assert.IsTrue(response.HasReply);
Assert.AreEqual("请先保留现场文件。", response.Reply);
Assert.AreEqual("2026-06-04T10:30:00+08:00", response.UpdatedAt);
Assert.IsTrue(response.MailSent);
}
[TestMethod]
public async Task FeedbackRecordStoreReadsOldRecordsWithoutExtendedFields()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-record-tests", Guid.NewGuid().ToString("N"));
var storagePath = Path.Combine(tempRoot, "feedback-records.json");
Directory.CreateDirectory(tempRoot);
await File.WriteAllTextAsync(storagePath, """
[
{
"code": "FB-20260604-ABC123",
"title": "Old record",
"type": "issue",
"severity": "normal",
"createdAtUtc": "2026-06-04T02:00:00+00:00",
"updatedAtUtc": "2026-06-04T02:10:00+00:00",
"state": "Sent",
"statusLabel": "Sent",
"lastMessage": "The service received it."
}
]
""");
try
{
var record = await new FeedbackRecordStore(storagePath).FindAsync("fb-20260604-abc123");
Assert.IsNotNull(record);
Assert.AreEqual("FB-20260604-ABC123", record.Code);
Assert.AreEqual("Sent", record.StatusLabel);
Assert.IsNull(record.ServiceStatusDetail);
Assert.IsNull(record.ServiceCategory);
Assert.IsNull(record.ServicePriority);
Assert.IsNull(record.ServiceMailSent);
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public async Task FeedbackRecordStoreKeepsStatusMessageAndPublicReplySeparate()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-record-tests", Guid.NewGuid().ToString("N"));
var storagePath = Path.Combine(tempRoot, "feedback-records.json");
var store = new FeedbackRecordStore(storagePath);
var now = DateTimeOffset.Parse("2026-06-04T03:00:00Z");
try
{
await store.UpsertAsync(new FeedbackRecord(
"FB-20260604-ABC123",
"Status query",
"issue",
"major",
now,
now,
FeedbackRecordState.StatusUpdated,
"处理中",
"已收到日志,正在定位。",
LastQueryAtUtc: now,
ServiceStatus: "investigating",
PublicReply: "请先保留现场文件。",
ServiceStatusDetail: "已收到日志,正在定位。",
ServiceCategory: "issue",
ServicePriority: "major",
ServiceMailSent: true,
ServiceReceivedAt: "2026-06-04T10:00:00+08:00",
ServiceUpdatedAt: "2026-06-04T10:30:00+08:00"));
var record = await store.FindAsync("FB-20260604-ABC123");
Assert.IsNotNull(record);
Assert.AreEqual("已收到日志,正在定位。", record.LastMessage);
Assert.IsFalse(record.LastMessage.Contains("请先保留现场文件。", StringComparison.Ordinal));
Assert.AreEqual("请先保留现场文件。", record.PublicReply);
Assert.AreEqual("investigating", record.ServiceStatus);
Assert.AreEqual("issue", record.ServiceCategory);
Assert.AreEqual("major", record.ServicePriority);
Assert.IsTrue(record.ServiceMailSent);
Assert.AreEqual("2026-06-04T10:30:00+08:00", record.ServiceUpdatedAt);
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public async Task FeedbackRecordStoreArchivesOldRecordsAndKeepsThemQueryable()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-feedback-record-tests", Guid.NewGuid().ToString("N"));
var storagePath = Path.Combine(tempRoot, "feedback-records.json");
var store = new FeedbackRecordStore(storagePath);
var now = DateTimeOffset.Parse("2026-06-05T12:00:00Z");
var oldCreated = now.AddDays(-11);
try
{
await store.UpsertAsync(new FeedbackRecord(
"FB-20260525-ABC123",
"旧反馈",
"issue",
"normal",
oldCreated,
oldCreated,
FeedbackRecordState.Sent,
"发送成功",
"服务端已接收。",
PackagePath: Path.Combine(tempRoot, "feedback.zip"),
PackageSha256: new string('a', 64),
PackageBytes: 128,
SubmittedAtUtc: oldCreated));
var archived = await store.ArchiveOlderThanAsync(TimeSpan.FromDays(10), now);
var record = archived.Single();
Assert.IsTrue(record.IsArchived);
Assert.AreEqual(FeedbackRecordState.Archived, record.State);
Assert.AreEqual("已归档", record.StatusLabel);
Assert.IsNotNull(record.ArchivedAtUtc);
var updated = await store.UpsertAsync(record with
{
State = FeedbackRecordState.StatusUpdated,
StatusLabel = "状态已更新",
LastMessage = "当前状态:已处理。",
UpdatedAtUtc = now.AddMinutes(5),
LastQueryAtUtc = now.AddMinutes(5)
});
Assert.IsTrue(updated.IsArchived);
Assert.AreEqual(FeedbackRecordState.StatusUpdated, updated.State);
Assert.AreEqual("状态已更新", updated.StatusLabel);
Assert.AreEqual("当前状态:已处理。", updated.LastMessage);
var reloaded = await new FeedbackRecordStore(storagePath).FindAsync("fb-20260525-abc123");
Assert.IsNotNull(reloaded);
Assert.IsTrue(reloaded.IsArchived);
Assert.AreEqual("FB-20260525-ABC123", reloaded.Code);
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
private static string ReadEntry(ZipArchiveEntry entry)
{
using var stream = entry.Open();
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
private static FeedbackStatusResponse ParseStatusResponse(string json)
{
var method = typeof(FeedbackSubmissionService).GetMethod("ParseStatusResponse", BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(method);
return (FeedbackStatusResponse)method.Invoke(null, new object?[] { json })!;
}
private static void DeleteIfExists(string path)
{
if (File.Exists(path))
{
File.Delete(path);
}
}
private sealed class FakeSystemMetricsService : ISystemMetricsService
{
public SystemMetricsSnapshot Capture()
{
return new SystemMetricsSnapshot(
"TEST-PC",
"Windows Test",
8,
1024,
2048,
42.5,
16UL * 1024 * 1024 * 1024,
8UL * 1024 * 1024 * 1024,
8UL * 1024 * 1024 * 1024,
50,
1.2,
TimeSpan.FromMinutes(12),
24,
512,
256UL * 1024 * 1024 * 1024,
128UL * 1024 * 1024 * 1024,
128UL * 1024 * 1024 * 1024,
50,
true,
".NET 10.0",
DateTimeOffset.Parse("2026-06-04T10:00:00+08:00"));
}
}
}
@@ -0,0 +1,266 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.App;
namespace YMhut.Box.Tests;
[TestClass]
[DoNotParallelize]
public sealed class InstallLayoutPathsTests
{
[TestMethod]
public void ResolveInstallRootPrefersCurrentInstallRootEnvironment()
{
using var workspace = TestWorkspace.Create("ymhut-install-root-tests");
var installRoot = workspace.CreateDirectory("install");
var baseRoot = workspace.CreateDirectory("runtime");
using var environment = EnvironmentScope.Capture(
InstallLayoutPaths.InstallRootEnvironmentVariable,
InstallLayoutPaths.ArchivedLayoutEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
environment.Set(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, null);
Assert.AreEqual(installRoot, InstallLayoutPaths.ResolveInstallRoot(baseRoot));
Assert.AreEqual(installRoot, InstallLayoutPaths.CandidateRoots(baseRoot).First());
}
[TestMethod]
public void ResolveInstallRootIgnoresLegacyArchivedLayoutForHealth()
{
using var workspace = TestWorkspace.Create("ymhut-archived-layout-tests");
var archivedRoot = workspace.CreateDirectory("archived");
var baseRoot = workspace.CreateDirectory("runtime");
workspace.CreateFile(["runtime", "YMhutBox.exe"], string.Empty);
using var environment = EnvironmentScope.Capture(
InstallLayoutPaths.InstallRootEnvironmentVariable,
InstallLayoutPaths.ArchivedLayoutEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, null);
environment.Set(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, archivedRoot);
Assert.AreEqual(baseRoot, InstallLayoutPaths.ResolveInstallRoot(baseRoot));
Assert.IsFalse(InstallLayoutPaths.CandidateRoots(baseRoot).Contains(archivedRoot));
Assert.AreEqual(archivedRoot, InstallLayoutPaths.ResolveArchivedLayoutRoot());
}
[TestMethod]
public void ResolveAssetsRootAndExecutableUseInstallRoot()
{
using var workspace = TestWorkspace.Create("ymhut-install-assets-tests");
var installRoot = workspace.CreateDirectory("install");
var assetsRoot = workspace.CreateDirectory("install", "Assets");
var executable = workspace.CreateFile(["install", "YMhutBox.exe"], string.Empty);
var runtimeRoot = workspace.CreateDirectory("runtime");
using var environment = EnvironmentScope.Capture(
InstallLayoutPaths.InstallRootEnvironmentVariable,
InstallLayoutPaths.ArchivedLayoutEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
environment.Set(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, null);
Assert.AreEqual(assetsRoot, InstallLayoutPaths.ResolveAssetsRoot(runtimeRoot));
Assert.AreEqual(executable, InstallLayoutPaths.ResolveInstalledExecutablePath(runtimeRoot));
}
[TestMethod]
public void RuntimeLayoutPolicySkipsLargeToolsAndUserDataRoots()
{
foreach (var relativePath in new[]
{
"Tools/vendor/tool.exe",
"Metadata/tools.json",
"data/settings.json",
"FeedbackPackages/feedback.zip"
})
{
Assert.IsTrue(RuntimeLayoutPolicy.ShouldSkipRelativePath(relativePath), relativePath);
Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile(relativePath), relativePath);
}
Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("unins000.exe"));
Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("YMhutBox.exe"));
Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("YMhutBox.dll"));
Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("resources.pri"));
Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("zh-CN/Microsoft.ui.xaml.dll.mui"));
Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("lang/zh-CN/Microsoft.ui.xaml.dll.mui"));
Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile("runtimes/win-x64/native/Microsoft.ui.xaml.dll"));
}
[TestMethod]
public void InstallManifestParserReadsReleaseRequiredFilesAndPayloadFiles()
{
var manifest = InstallManifest.Parse(
"""
[Release]
Version=2.0.6
Build=2
Channel=stable
PackageVersion=2.0.6.2
[RequiredFiles]
YMhutBox.exe
YMhutBox.dll
[Files]
YMhutBox.exe
lang/zh-CN/Microsoft.ui.xaml.dll.mui
Tools/SampleTool/tool.exe
""");
Assert.IsNotNull(manifest.Release);
Assert.AreEqual("2.0.6", manifest.Release.Version);
Assert.AreEqual("2", manifest.Release.Build);
Assert.AreEqual("stable", manifest.Release.Channel);
Assert.AreEqual("2.0.6.2", manifest.Release.PackageVersion);
Assert.IsTrue(manifest.RequiredFiles.SequenceEqual(["YMhutBox.exe", "YMhutBox.dll"]));
Assert.IsTrue(manifest.ContainsFile(@"lang\zh-CN\Microsoft.ui.xaml.dll.mui"));
Assert.IsTrue(manifest.ContainsFile("Tools/SampleTool/tool.exe"));
}
[TestMethod]
public void RuntimeLayoutPolicySkipsRuntimePayloadRoots()
{
foreach (var relativePath in new[]
{
"lang/en-US/Microsoft.UI.Xaml.Phone.dll.mui",
"Microsoft.UI.Xaml/Microsoft.UI.Xaml.dll",
"runtimes/win-x64/native/WebView2Loader.dll",
"worker/YMhut.Box.Worker.exe"
})
{
Assert.IsFalse(RuntimeLayoutPolicy.ShouldCopyFile(relativePath), relativePath);
}
}
[TestMethod]
public void SourceFilesDoNotContainCommonMojibakeSequences()
{
var root = FindRepositoryRoot();
var scanRoots = new[]
{
Path.Combine(root, "src", "YMhut.Box.Core"),
Path.Combine(root, "src", "box-winUI"),
Path.Combine(root, "installer")
};
var extensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".cs", ".xaml", ".iss" };
var mojibakeMarkers = new[]
{
"Ã",
"Â",
"â",
"鈥",
"锛",
"銆",
"鐑",
"澶",
"璁",
"鏍",
"棰",
"缃"
};
var offenders = scanRoots
.Where(Directory.Exists)
.SelectMany(scanRoot => Directory.EnumerateFiles(scanRoot, "*.*", SearchOption.AllDirectories))
.Where(path => extensions.Contains(Path.GetExtension(path)))
.Where(path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
.Where(path => !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
.SelectMany(path => FindMojibakeMarkers(path, mojibakeMarkers))
.Take(12)
.ToArray();
Assert.IsEmpty(offenders, string.Join(Environment.NewLine, offenders));
}
private static IEnumerable<string> FindMojibakeMarkers(string path, IReadOnlyList<string> markers)
{
var text = File.ReadAllText(path);
foreach (var marker in markers)
{
if (text.Contains(marker, StringComparison.Ordinal))
{
yield return $"{Path.GetFileName(path)} contains '{marker}'";
}
}
}
private static string FindRepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
if (Directory.Exists(Path.Combine(directory.FullName, "src")) &&
Directory.Exists(Path.Combine(directory.FullName, "installer")))
{
return directory.FullName;
}
directory = directory.Parent;
}
Assert.Fail("Repository root could not be found from test output directory.");
return AppContext.BaseDirectory;
}
private sealed class EnvironmentScope : IDisposable
{
private readonly Dictionary<string, string?> _snapshot;
private EnvironmentScope(IEnumerable<string> names)
{
_snapshot = names.ToDictionary(name => name, Environment.GetEnvironmentVariable, StringComparer.Ordinal);
}
public static EnvironmentScope Capture(params string[] names) => new(names);
public void Set(string name, string? value) => Environment.SetEnvironmentVariable(name, value);
public void Dispose()
{
foreach (var item in _snapshot)
{
Environment.SetEnvironmentVariable(item.Key, item.Value);
}
}
}
private sealed class TestWorkspace : IDisposable
{
private TestWorkspace(string root)
{
Root = root;
}
public string Root { get; }
public static TestWorkspace Create(string prefix)
{
var root = Path.Combine(Path.GetTempPath(), prefix, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
return new TestWorkspace(root);
}
public string CreateDirectory(params string[] parts)
{
var directory = Path.Combine([Root, .. parts]);
Directory.CreateDirectory(directory);
return directory;
}
public string CreateFile(string[] parts, string contents)
{
var path = Path.Combine([Root, .. parts]);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, contents);
return path;
}
public void Dispose()
{
if (Directory.Exists(Root))
{
Directory.Delete(Root, recursive: true);
}
}
}
}
+131
View File
@@ -0,0 +1,131 @@
using Microsoft.Data.Sqlite;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Logging;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class LogServiceTests
{
[TestMethod]
public void LogDisplayLocalizerUsesEmptyDetailTextInsteadOfUnknownError()
{
Assert.AreEqual("暂无详情", LogDisplayLocalizer.Detail(null, "zh-CN"));
Assert.AreEqual("No details", LogDisplayLocalizer.Detail(" ", "en-US"));
Assert.AreEqual("真实详情", LogDisplayLocalizer.Detail("真实详情", "zh-CN"));
}
[TestMethod]
public void LogDisplayLocalizerTranslatesCommonEnglishMessagesInChineseMode()
{
Assert.AreEqual("打开启动自检结果", LogDisplayLocalizer.Message("Open startup check results", "zh-CN"));
Assert.AreEqual("工具目录已重建", LogDisplayLocalizer.Message("Tool catalog rebuilt", "zh-CN"));
StringAssert.Contains(LogDisplayLocalizer.Message("External tool success: launch", "zh-CN"), "外部工具成功");
Assert.AreEqual("外部工具成功", LogDisplayLocalizer.Message("External tool success", "zh-CN"));
StringAssert.Contains(LogDisplayLocalizer.Message("Tool result canceled: export", "zh-CN"), "工具结果已取消");
Assert.AreEqual("工具工作进程已就绪", LogDisplayLocalizer.Message("Tool worker ready", "zh-CN"));
Assert.AreEqual("下载宿主进程已启动", LogDisplayLocalizer.Message("Download host process started", "zh-CN"));
Assert.AreEqual("打开插件页面:sample/main", LogDisplayLocalizer.Message("Open plugin surface: sample/main", "zh-CN"));
Assert.AreEqual("打开 JSON formatter", LogDisplayLocalizer.Message("Open JSON formatter", "zh-CN"));
Assert.AreEqual("\u66f4\u65b0\u901a\u77e5\u68c0\u67e5\u5931\u8d25", LogDisplayLocalizer.Message("Update notice check failed", "zh-CN"));
Assert.AreEqual("\u542f\u52a8\u81ea\u68c0\u7ed3\u679c\u9884\u52a0\u8f7d\u5931\u8d25", LogDisplayLocalizer.Message("Startup check result preload failed", "zh-CN"));
Assert.AreEqual("\u8bbe\u7f6e\u9875\u52a0\u8f7d\u5931\u8d25", LogDisplayLocalizer.Message("Settings page load failed", "zh-CN"));
Assert.AreEqual("\u5df2\u8bb0\u4f4f\u5173\u95ed\u786e\u8ba4\u9009\u62e9", LogDisplayLocalizer.Message("Close confirmation remembered", "zh-CN"));
}
[TestMethod]
public void LogDisplayLocalizerTranslatesCommonDetailKeysInChineseMode()
{
var detail = LogDisplayLocalizer.Detail("result=success; operation=launch; elapsedMs=12; root=C:\\Tools; count=8", "zh-CN");
StringAssert.Contains(detail, "结果=成功");
StringAssert.Contains(detail, "操作=启动");
StringAssert.Contains(detail, "耗时毫秒=12");
StringAssert.Contains(detail, "根目录=C:\\Tools");
StringAssert.Contains(detail, "数量=8");
var windowDetail = LogDisplayLocalizer.Detail("source=tray; behavior=minimize_to_tray; result=primary; dialog=com_exception", "zh-CN");
StringAssert.Contains(windowDetail, "来源=托盘");
StringAssert.Contains(windowDetail, "行为=最小化到托盘");
StringAssert.Contains(windowDetail, "结果=主按钮");
StringAssert.Contains(windowDetail, "弹窗=COM 异常");
}
[TestMethod]
public async Task ClearFilteredTodayDeletesOnlyMatchingRows()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
try
{
var service = new SqliteLogService(AppPaths.ForCurrentUser(tempRoot));
await service.WriteAsync("Information", "tool", "Open tool: JSON formatter");
await service.WriteAsync("Warning", "network", "Proxy diagnostic failed");
await service.ClearFilteredTodayAsync("Information", "JSON");
var entries = await service.ReadAsync(take: 10);
Assert.HasCount(1, entries);
Assert.AreEqual("network", entries[0].Category);
}
finally
{
SqliteConnection.ClearAllPools();
Directory.Delete(tempRoot, recursive: true);
}
}
[TestMethod]
public async Task ReadByDateReturnsAllMatchingRowsForSelectedDay()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
try
{
var service = new SqliteLogService(AppPaths.ForCurrentUser(tempRoot));
await service.WriteAsync("Information", "tool", "Open JSON formatter");
await service.WriteAsync("Information", "tool", "Open URL codec");
await service.WriteAsync("Warning", "network", "Proxy diagnostic failed");
var allToday = await service.ReadByDateAsync(DateOnly.FromDateTime(DateTime.Now));
var filtered = await service.ReadByDateAsync(DateOnly.FromDateTime(DateTime.Now), "Information", "Open");
Assert.HasCount(3, allToday);
Assert.HasCount(2, filtered);
}
finally
{
SqliteConnection.ClearAllPools();
Directory.Delete(tempRoot, recursive: true);
}
}
[TestMethod]
public async Task ReadByDatePageReturnsStablePages()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
try
{
var service = new SqliteLogService(AppPaths.ForCurrentUser(tempRoot));
for (var index = 0; index < 25; index++)
{
await service.WriteAsync("Information", "stream", $"Row {index:00}");
}
var today = DateOnly.FromDateTime(DateTime.Now);
var first = await service.ReadByDatePageAsync(today, "Information", "Row", skip: 0, take: 20);
var second = await service.ReadByDatePageAsync(today, "Information", "Row", skip: 20, take: 20);
Assert.HasCount(20, first);
Assert.HasCount(5, second);
CollectionAssert.AreNotEquivalent(
first.Select(entry => entry.Message).ToArray(),
second.Select(entry => entry.Message).ToArray());
}
finally
{
SqliteConnection.ClearAllPools();
Directory.Delete(tempRoot, recursive: true);
}
}
}
@@ -0,0 +1,39 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.Media;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class MediaVolumeModelTests
{
[TestMethod]
public void FromPercentClampsAndMapsOutputModes()
{
var silent = MediaVolumeModel.FromPercent(-5);
Assert.AreEqual(0, silent.Percent);
Assert.AreEqual(0, silent.PlatformVolume, 0.001);
Assert.AreEqual(0, silent.AudioGain, 0.001);
Assert.AreEqual(MediaVolumeOutputMode.Silent, silent.Mode);
var normal = MediaVolumeModel.FromPercent(80);
Assert.AreEqual(80, normal.Percent);
Assert.AreEqual(0.8, normal.PlatformVolume, 0.001);
Assert.AreEqual(0.8, normal.AudioGain, 0.001);
Assert.AreEqual(MediaVolumeOutputMode.Normal, normal.Mode);
var boosted = MediaVolumeModel.FromPercent(120);
Assert.AreEqual(120, boosted.Percent);
Assert.AreEqual(1.0, boosted.PlatformVolume, 0.001);
Assert.AreEqual(1.2, boosted.AudioGain, 0.001);
Assert.AreEqual(MediaVolumeOutputMode.Boosted, boosted.Mode);
}
[TestMethod]
public void FormatPercentNamesMutedAndBoostedStates()
{
Assert.AreEqual("0%(静音)", MediaVolumeModel.FormatPercent(0, "zh-CN"));
Assert.AreEqual("120%(增强音量)", MediaVolumeModel.FormatPercent(120, "zh-CN"));
Assert.AreEqual("0% (Muted)", MediaVolumeModel.FormatPercent(0, "en-US"));
Assert.AreEqual("120% (Boosted)", MediaVolumeModel.FormatPercent(120, "en-US"));
}
}
+359
View File
@@ -0,0 +1,359 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Data.Sqlite;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Plugins;
using YMhut.Box.Core.Settings;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class PluginTests
{
[TestMethod]
public async Task PluginRegistryLoadsValidManifestAndToolModule()
{
using var workspace = TempWorkspace();
var pluginRoot = CreatePlugin(workspace.Path, "hello-tools", """
{
"id": "hello-tools",
"name": "Hello Tools",
"version": "1.0.0",
"author": "tester",
"description": "Test plugin",
"entry": "index.html",
"permissions": ["Input", "Output", "Log", "Storage"],
"surfaces": [
{ "kind": "ToolboxTool", "id": "hello", "name": "Hello", "description": "Hello tool", "entry": "index.html", "category": "dev" },
{ "kind": "NavPage", "id": "home", "name": "Home", "description": "Home page", "entry": "index.html" }
],
"resources": ["index.html"]
}
""");
File.WriteAllText(Path.Combine(pluginRoot, "index.html"), "<h1>Hello</h1>");
var paths = AppPaths.ForCurrentUser(workspace.Path);
var stateStore = new PluginStateStore(paths);
await stateStore.SetEnabledAsync("hello-tools", true);
var registry = new PluginRegistryService(paths, stateStore);
var plugins = await registry.LoadPluginsAsync();
var tools = await registry.LoadEnabledToolModulesAsync();
Assert.HasCount(1, plugins);
Assert.IsTrue(plugins[0].IsValid, string.Join(", ", plugins[0].Errors));
Assert.HasCount(1, tools);
Assert.AreEqual("plugin:hello-tools:hello", tools[0].Id);
}
[TestMethod]
public async Task PluginRegistryRejectsInvalidAndCoreAssetOverride()
{
using var workspace = TempWorkspace();
var pluginRoot = CreatePlugin(workspace.Path, "bad", """
{
"id": "plugin:bad",
"name": "Bad",
"version": "1.0.0",
"author": "tester",
"description": "Bad plugin",
"entry": "index.html",
"permissions": [],
"surfaces": [
{ "kind": "ToolboxTool", "id": "json_formatter", "name": "Override", "description": "conflict", "entry": "index.html" }
],
"resources": ["Assets/icons/app_icon.ico"]
}
""");
File.WriteAllText(Path.Combine(pluginRoot, "index.html"), "<h1>Bad</h1>");
var paths = AppPaths.ForCurrentUser(workspace.Path);
var registry = new PluginRegistryService(paths, new PluginStateStore(paths));
var plugin = (await registry.LoadPluginsAsync()).Single();
Assert.IsFalse(plugin.IsValid);
Assert.IsTrue(plugin.Errors.Any(error => error.Contains("plugin:", StringComparison.OrdinalIgnoreCase)));
Assert.IsTrue(plugin.Errors.Any(error => error.Contains("built-in", StringComparison.OrdinalIgnoreCase)));
Assert.IsTrue(plugin.Errors.Any(error => error.Contains("Core asset", StringComparison.OrdinalIgnoreCase)));
}
[TestMethod]
public async Task PluginStateStorePersistsStatePermissionsSurfacesAndKv()
{
using var workspace = TempWorkspace();
var paths = AppPaths.ForCurrentUser(workspace.Path);
var store = new PluginStateStore(paths);
await store.SetEnabledAsync("demo", true);
await store.SetPermissionAsync("demo", PluginPermission.Log, true);
await store.SetSurfaceMountedAsync("demo", "tool", true);
await store.SetValueAsync("demo", "name", "value");
var secondStore = new PluginStateStore(paths);
var state = await secondStore.GetStateAsync("demo");
var values = await secondStore.ListValuesAsync("demo");
Assert.IsTrue(state.Enabled);
CollectionAssert.Contains(state.GrantedPermissions.ToList(), PluginPermission.Log);
CollectionAssert.Contains(state.MountedSurfaceIds.ToList(), "tool");
Assert.AreEqual("value", values["name"]);
}
[TestMethod]
public async Task PluginToolsMergeIntoToolCatalogWhenEnabled()
{
using var workspace = TempWorkspace();
var pluginRoot = CreatePlugin(workspace.Path, "merge-demo", """
{
"id": "merge-demo",
"name": "Merge Demo",
"version": "1.0.0",
"author": "tester",
"description": "Merge plugin",
"entry": "index.html",
"permissions": [],
"surfaces": [
{ "kind": "ToolboxTool", "id": "tool", "name": "Merged Tool", "description": "Merged", "entry": "index.html" }
],
"resources": ["index.html"]
}
""");
File.WriteAllText(Path.Combine(pluginRoot, "index.html"), "<h1>Merged</h1>");
var paths = AppPaths.ForCurrentUser(workspace.Path);
var store = new PluginStateStore(paths);
await store.SetEnabledAsync("merge-demo", true);
var registry = new PluginRegistryService(paths, store);
var catalog = new ToolCatalog(ToolCatalog.DefaultModules().Concat(await registry.LoadEnabledToolModulesAsync()));
Assert.IsNotNull(catalog.GetById("plugin:merge-demo:tool"));
}
[TestMethod]
public async Task PluginRegistryHonorsSettingsEnabledAndCustomRoot()
{
using var workspace = TempWorkspace();
var paths = AppPaths.ForCurrentUser(workspace.Path);
var settings = new AppSettingsService(new AppSettingsStore(paths.Root));
await settings.LoadAsync();
var store = new PluginStateStore(paths);
var defaultPluginRoot = CreatePlugin(workspace.Path, "default-demo", BasicManifest("default-demo"));
File.WriteAllText(Path.Combine(defaultPluginRoot, "index.html"), "<h1>Default</h1>");
await store.SetEnabledAsync("default-demo", true);
var registry = new PluginRegistryService(paths, store, settingsService: settings);
Assert.IsFalse(settings.Current.PluginsEnabled);
Assert.HasCount(0, await registry.LoadEnabledToolModulesAsync());
var customPluginsRoot = Path.Combine(workspace.Path, "ExternalPlugins");
var customPluginRoot = CreatePluginAtRoot(customPluginsRoot, "custom-demo", BasicManifest("custom-demo"));
File.WriteAllText(Path.Combine(customPluginRoot, "index.html"), "<h1>Custom</h1>");
await store.SetEnabledAsync("custom-demo", true);
await settings.UpdateAsync(value =>
{
value.PluginsEnabled = true;
value.PluginRootPath = customPluginsRoot;
});
Assert.AreEqual(Path.GetFullPath(customPluginsRoot), registry.PluginsRoot);
var tools = await registry.LoadEnabledToolModulesAsync();
Assert.HasCount(1, tools);
Assert.AreEqual("plugin:custom-demo:tool", tools[0].Id);
}
[TestMethod]
public async Task PluginRegistryRequiresReadmeFile()
{
using var workspace = TempWorkspace();
var pluginRoot = CreatePlugin(workspace.Path, "no-readme", BasicManifest("no-readme"));
File.WriteAllText(Path.Combine(pluginRoot, "index.html"), "<h1>No readme</h1>");
File.Delete(Path.Combine(pluginRoot, "README.md"));
var paths = AppPaths.ForCurrentUser(workspace.Path);
var registry = new PluginRegistryService(paths, new PluginStateStore(paths));
var plugin = (await registry.LoadPluginsAsync()).Single();
Assert.IsFalse(plugin.IsValid);
Assert.IsTrue(plugin.Errors.Any(error => error.Contains("README", StringComparison.OrdinalIgnoreCase)));
}
[TestMethod]
public async Task PluginSnapshotRoundTripsEnabledToolModules()
{
using var workspace = TempWorkspace();
var pluginRoot = CreatePlugin(workspace.Path, "snapshot-demo", BasicManifest("snapshot-demo"));
File.WriteAllText(Path.Combine(pluginRoot, "index.html"), "<h1>Snapshot</h1>");
var paths = AppPaths.ForCurrentUser(workspace.Path);
var store = new PluginStateStore(paths);
await store.SetEnabledAsync("snapshot-demo", true);
var registry = new PluginRegistryService(paths, store);
var plugin = (await registry.LoadPluginsAsync()).Single();
var tool = (await registry.LoadEnabledToolModulesAsync()).Single();
var snapshot = new PluginSnapshot(
true,
registry.PluginsRoot,
[LoadedPluginDto.FromLoadedPlugin(plugin)],
[PluginToolDto.FromPluginToolModule(tool)],
DateTimeOffset.Now);
var message = new PluginHostMessage(PluginHostProtocol.SnapshotChanged, Snapshot: snapshot);
var roundTrip = PluginHostProtocol.Deserialize(PluginHostProtocol.Serialize(message));
Assert.IsNotNull(roundTrip?.Snapshot);
Assert.HasCount(1, roundTrip.Snapshot.EnabledTools);
Assert.AreEqual("plugin:snapshot-demo:tool", roundTrip.Snapshot.EnabledTools[0].ToToolModule().Id);
}
[TestMethod]
public async Task BuiltInPluginInstallerCopiesMissingPluginOnly()
{
using var workspace = TempWorkspace();
var paths = AppPaths.ForCurrentUser(Path.Combine(workspace.Path, "App"));
var installer = new BuiltInPluginInstallerService(paths);
await installer.EnsureInstalledAsync();
var installed = Path.Combine(paths.Root, "Plugins", "ipcheck-demo", "index.html");
var installedReadme = Path.Combine(paths.Root, "Plugins", "ipcheck-demo", "README.md");
var installedManifest = Path.Combine(paths.Root, "Plugins", "ipcheck-demo", PluginManifest.FileName);
Assert.IsTrue(File.Exists(installed));
Assert.IsTrue(File.Exists(installedReadme));
Assert.IsTrue(File.Exists(installedManifest));
Assert.IsTrue(File.ReadAllText(installed).Contains("IPCheck 网络工具箱", StringComparison.OrdinalIgnoreCase));
var manifestText = File.ReadAllText(installedManifest);
Assert.IsTrue(manifestText.Contains("\"Http\"", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(manifestText.Contains("\"OpenExternal\"", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(manifestText.Contains("\"README.md\"", StringComparison.OrdinalIgnoreCase));
File.WriteAllText(installed, "<h1>User version</h1>");
await installer.EnsureInstalledAsync();
Assert.AreEqual("<h1>User version</h1>", File.ReadAllText(installed));
}
[TestMethod]
public async Task BuiltInSamplePluginIsValidAndDocumentsBridgeCapabilities()
{
using var workspace = TempWorkspace();
var paths = AppPaths.ForCurrentUser(Path.Combine(workspace.Path, "App"));
var installer = new BuiltInPluginInstallerService(paths);
await installer.EnsureInstalledAsync();
var stateStore = new PluginStateStore(paths);
var registry = new PluginRegistryService(paths, stateStore);
var plugin = (await registry.LoadPluginsAsync()).Single(item => item.Manifest.Id == "ipcheck-demo");
Assert.IsTrue(plugin.IsValid, string.Join("; ", plugin.Errors));
CollectionAssert.Contains(plugin.Manifest.Permissions.ToList(), PluginPermission.Storage);
CollectionAssert.Contains(plugin.Manifest.Permissions.ToList(), PluginPermission.Output);
CollectionAssert.Contains(plugin.Manifest.Permissions.ToList(), PluginPermission.OpenExternal);
CollectionAssert.Contains(plugin.Manifest.Resources.ToList(), "README.md");
var readme = File.ReadAllText(Path.Combine(plugin.RootPath, "README.md"));
StringAssert.Contains(readme, "Bridge 能力示例");
StringAssert.Contains(readme, "安全浏览器");
StringAssert.Contains(readme, "AI 实现提示");
}
[TestMethod]
public void PluginDocsAndToolResultAssetsExposeRequiredSections()
{
var root = FindRepositoryRoot();
var pluginDocs = File.ReadAllText(Path.Combine(root, "docs", "plugins", "README.md"));
var aiDocs = File.ReadAllText(Path.Combine(root, "docs", "plugins", "AI-INTEGRATION.md"));
var helpPage = File.ReadAllText(Path.Combine(root, "src", "box-winUI", "Views", "PluginDocsPage.cs"));
var resultJs = File.ReadAllText(Path.Combine(root, "src", "box-winUI", "Assets", "tool-results", "result.js"));
var resultCss = File.ReadAllText(Path.Combine(root, "src", "box-winUI", "Assets", "tool-results", "result.css"));
var pageJs = File.ReadAllText(Path.Combine(root, "src", "box-winUI", "Assets", "tool-pages", "tool-page.js"));
var pageCss = File.ReadAllText(Path.Combine(root, "src", "box-winUI", "Assets", "tool-pages", "tool-page.css"));
StringAssert.Contains(pluginDocs, "权限");
StringAssert.Contains(pluginDocs, "安全边界");
StringAssert.Contains(pluginDocs, "独立窗口");
StringAssert.Contains(aiDocs, "Acceptance Checklist");
StringAssert.Contains(helpPage, "输出区");
StringAssert.Contains(helpPage, "常见问题");
StringAssert.Contains(resultJs, "openSystemBrowser");
StringAssert.Contains(pageJs, "openSystemBrowser");
StringAssert.Contains(resultCss, "rank-1");
StringAssert.Contains(pageCss, "rank-1");
StringAssert.Contains(resultCss, "prefers-reduced-motion");
StringAssert.Contains(pageCss, "prefers-reduced-motion");
}
private static string CreatePlugin(string root, string folder, string manifest)
{
var pluginRoot = Path.Combine(root, "Plugins", folder);
Directory.CreateDirectory(pluginRoot);
File.WriteAllText(Path.Combine(pluginRoot, PluginManifest.FileName), manifest);
File.WriteAllText(Path.Combine(pluginRoot, "README.md"), "# Test plugin");
return pluginRoot;
}
private static string CreatePluginAtRoot(string pluginsRoot, string folder, string manifest)
{
var pluginRoot = Path.Combine(pluginsRoot, folder);
Directory.CreateDirectory(pluginRoot);
File.WriteAllText(Path.Combine(pluginRoot, PluginManifest.FileName), manifest);
File.WriteAllText(Path.Combine(pluginRoot, "README.md"), "# Test plugin");
return pluginRoot;
}
private static string BasicManifest(string id) => $$"""
{
"id": "{{id}}",
"name": "{{id}}",
"version": "1.0.0",
"author": "tester",
"description": "Test plugin",
"entry": "index.html",
"permissions": [],
"surfaces": [
{ "kind": "ToolboxTool", "id": "tool", "name": "Tool", "description": "Tool", "entry": "index.html" }
],
"resources": ["index.html"]
}
""";
private static TempDirectory TempWorkspace()
{
return new TempDirectory(Path.Combine(Path.GetTempPath(), "ymhut-plugin-tests", Guid.NewGuid().ToString("N")));
}
private static string FindRepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
if (Directory.Exists(Path.Combine(directory.FullName, "src")) &&
Directory.Exists(Path.Combine(directory.FullName, "docs")) &&
File.Exists(Path.Combine(directory.FullName, "YMhut.Box.Native.sln")))
{
return directory.FullName;
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not locate repository root from test output path.");
}
private sealed class TempDirectory : IDisposable
{
public TempDirectory(string path)
{
Path = path;
Directory.CreateDirectory(path);
}
public string Path { get; }
public void Dispose()
{
SqliteConnection.ClearAllPools();
if (Directory.Exists(Path))
{
Directory.Delete(Path, recursive: true);
}
}
}
}
+314
View File
@@ -0,0 +1,314 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
[DoNotParallelize]
public sealed class ReferenceToolsTests
{
[TestMethod]
public async Task ExternalToolCatalogScansLaunchablesFromToolsDirectory()
{
var category = "YMhutTestTools_" + Guid.NewGuid().ToString("N");
var toolsRoot = Path.Combine(AppContext.BaseDirectory, "Tools");
var categoryRoot = Path.Combine(toolsRoot, category);
var toolRoot = Path.Combine(categoryRoot, "SampleTool");
Directory.CreateDirectory(toolRoot);
var launchPath = Path.Combine(toolRoot, "SampleTool.cmd");
await File.WriteAllTextAsync(launchPath, "@echo off\r\necho YMhut");
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-reference-tests", Guid.NewGuid().ToString("N"));
try
{
var service = new ExternalToolCatalogService(AppPaths.ForCurrentUser(tempRoot));
var modules = await service.GetModulesAsync();
var module = modules.FirstOrDefault(item => item.Tool.CategoryName == category);
Assert.IsNotNull(module);
Assert.AreEqual("SampleTool", module.Tool.Name);
Assert.AreEqual("CMD", module.Tool.Extension);
Assert.AreEqual(ReferenceToolRiskLevel.High, module.Tool.RiskLevel);
Assert.IsTrue(module.Id.StartsWith(ExternalToolModule.IdPrefix, StringComparison.OrdinalIgnoreCase));
}
finally
{
if (Directory.Exists(categoryRoot))
{
Directory.Delete(categoryRoot, recursive: true);
}
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public async Task ExternalToolCatalogUsesInstallRootForToolsAndMetadata()
{
var category = "InstallRootCategory_" + Guid.NewGuid().ToString("N");
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-install-tool-tests", Guid.NewGuid().ToString("N"));
var installRoot = Path.Combine(tempRoot, "install");
var toolRoot = Path.Combine(installRoot, "Tools", category, "MetaTool");
var metadataRoot = Path.Combine(installRoot, "Metadata");
var appRoot = Path.Combine(tempRoot, "appdata");
var launchPath = Path.Combine(toolRoot, "MetaTool.cmd");
var oldInstallRoot = Environment.GetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable);
var oldArchivedRoot = Environment.GetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable);
try
{
Directory.CreateDirectory(toolRoot);
Directory.CreateDirectory(metadataRoot);
await File.WriteAllTextAsync(launchPath, "@echo off\r\necho install-root");
await File.WriteAllTextAsync(
Path.Combine(metadataRoot, "tools.json"),
"""
{
"tools": [
{
"match": "MetaTool",
"description": "Install root metadata description",
"publisher": "YMhut Tests",
"launchTarget": "MetaTool.cmd",
"tags": [ "install-root" ]
}
]
}
""");
Environment.SetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
Environment.SetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, null);
var service = new ExternalToolCatalogService(AppPaths.ForCurrentUser(appRoot));
var modules = await service.GetModulesAsync();
var module = modules.FirstOrDefault(item => item.Tool.CategoryName == category);
Assert.AreEqual(Path.Combine(installRoot, "Tools"), service.ResolveToolsRoot());
Assert.IsNotNull(module);
Assert.AreEqual("MetaTool", module.Tool.Name);
Assert.AreEqual("Install root metadata description", module.Tool.Description);
Assert.AreEqual("YMhut Tests", module.Tool.Publisher);
Assert.Contains("install-root", module.Tool.Tags);
Assert.IsTrue(module.Tool.LaunchPath.StartsWith(installRoot, StringComparison.OrdinalIgnoreCase));
}
finally
{
Environment.SetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable, oldInstallRoot);
Environment.SetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, oldArchivedRoot);
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public async Task RiskConfirmationStorePersistsAndResetsChoices()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-risk-tests", Guid.NewGuid().ToString("N"));
try
{
var paths = AppPaths.ForCurrentUser(tempRoot);
var store = new RiskConfirmationStore(paths);
await store.RememberAsync("builtin:hosts-editor", "execute");
var secondStore = new RiskConfirmationStore(paths);
Assert.IsTrue(await secondStore.IsRememberedAsync("builtin:hosts-editor", "execute"));
await secondStore.ResetAsync();
Assert.IsFalse(await secondStore.IsRememberedAsync("builtin:hosts-editor", "execute"));
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public async Task OpenSourceReferenceServiceLoadsBundledToolNoticesFromInstallRoot()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-install-reference-tests", Guid.NewGuid().ToString("N"));
var installRoot = Path.Combine(tempRoot, "install");
var noticePath = Path.Combine(installRoot, "Tools", "Category", "VendorTool", "LICENSE.txt");
var appRoot = Path.Combine(tempRoot, "appdata");
var oldInstallRoot = Environment.GetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable);
var oldArchivedRoot = Environment.GetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable);
try
{
Directory.CreateDirectory(Path.GetDirectoryName(noticePath)!);
await File.WriteAllTextAsync(noticePath, "Vendor license text");
Environment.SetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
Environment.SetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, null);
var service = new OpenSourceReferenceService(AppPaths.ForCurrentUser(appRoot));
var references = await service.GetReferencesAsync();
Assert.IsTrue(references.Any(item =>
item.Kind == "Third-party tool notice" &&
string.Equals(item.Path, noticePath, StringComparison.OrdinalIgnoreCase)));
}
finally
{
Environment.SetEnvironmentVariable(InstallLayoutPaths.InstallRootEnvironmentVariable, oldInstallRoot);
Environment.SetEnvironmentVariable(InstallLayoutPaths.ArchivedLayoutEnvironmentVariable, oldArchivedRoot);
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public void BuiltinReferenceCatalogRegistersExpectedToolsAndRiskLevels()
{
var catalog = new BuiltinReferenceToolCatalog();
var modules = catalog.GetModules();
var ids = modules.Select(module => module.Definition.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var id in new[]
{
"cert-block",
"port-viewer",
"hosts-editor",
"keyboard-test",
"junk-cleaner",
"bsod-analysis",
"winget-installer",
"battery-report",
"speed-test",
"wifi-password",
"disk-space-analyzer",
"lite-monitor",
"windows-activation",
"defender-control",
"cpu-ranking",
"gpu-ranking",
"context-menu-mgr"
})
{
Assert.Contains(id, ids);
}
Assert.AreEqual(ReferenceToolRiskLevel.High, catalog.GetByModuleId("builtin:hosts-editor")?.RiskLevel);
Assert.AreEqual(ReferenceToolRiskLevel.None, catalog.GetByModuleId("builtin:windows-activation")?.RiskLevel);
Assert.HasCount(17, modules);
}
[TestMethod]
public async Task ResilientLogServiceFallsBackToJsonlWhenPrimaryFails()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-log-fallback-tests", Guid.NewGuid().ToString("N"));
try
{
var service = new ResilientLogService(new ThrowingLogService(), AppPaths.ForCurrentUser(tempRoot));
await service.WriteAsync("Information", "tool-run", "External tool success", "toolId=external:test");
var entries = await service.ReadAsync(take: 10);
Assert.HasCount(1, entries);
Assert.AreEqual("tool-run", entries[0].Category);
Assert.IsTrue(File.Exists(Path.Combine(tempRoot, "Logs", "app-log-fallback.jsonl")));
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
[TestMethod]
public async Task OpenSourceReferenceServiceLoadsPackagesAndBundledToolNotices()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-reference-attribution-tests", Guid.NewGuid().ToString("N"));
var toolsRoot = Path.Combine(AppContext.BaseDirectory, "Tools");
var noticeCategory = "YMhutTestNotice_" + Guid.NewGuid().ToString("N");
var noticeCategoryRoot = Path.Combine(toolsRoot, noticeCategory);
var noticePath = Path.Combine(noticeCategoryRoot, "SampleVendor", "LICENSE.txt");
try
{
var projectRoot = Path.Combine(tempRoot, "src", "FakeProject");
Directory.CreateDirectory(projectRoot);
await File.WriteAllTextAsync(
Path.Combine(projectRoot, "FakeProject.csproj"),
"""
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Sample.Reference.Package" Version="1.2.3" />
</ItemGroup>
</Project>
""");
Directory.CreateDirectory(Path.GetDirectoryName(noticePath)!);
await File.WriteAllTextAsync(noticePath, "Sample vendor license");
var service = new OpenSourceReferenceService(AppPaths.ForCurrentUser(tempRoot));
var references = await service.GetReferencesAsync();
Assert.IsTrue(references.Any(item => item.Kind == "Reference project attribution" && item.Name.Contains("tubatool", StringComparison.OrdinalIgnoreCase)));
Assert.IsTrue(references.Any(item => item.Kind == "NuGet package" && item.Name == "Sample.Reference.Package" && item.Version == "1.2.3"));
Assert.IsTrue(references.Any(item => item.Kind == "Third-party tool notice" && item.Path == noticePath));
}
finally
{
if (Directory.Exists(noticeCategoryRoot))
{
Directory.Delete(noticeCategoryRoot, recursive: true);
}
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
private sealed class ThrowingLogService : ILogService
{
public event EventHandler<LogEntry>? EntryWritten;
public string LogPath => "throwing";
public Task WriteAsync(string level, string category, string message, string? detail = null, CancellationToken cancellationToken = default)
{
EntryWritten?.Invoke(this, new LogEntry(DateTimeOffset.Now, level, category, message, detail));
throw new IOException("primary failed");
}
public Task<IReadOnlyList<LogEntry>> ReadAsync(string? level = null, string? category = null, int take = 500, CancellationToken cancellationToken = default)
=> throw new IOException("primary failed");
public Task<IReadOnlyList<LogEntry>> ReadByDateAsync(DateOnly date, string? level = null, string? query = null, int take = 0, CancellationToken cancellationToken = default)
=> throw new IOException("primary failed");
public Task<IReadOnlyList<LogEntry>> ReadByDatePageAsync(DateOnly date, string? level = null, string? query = null, int skip = 0, int take = 100, CancellationToken cancellationToken = default)
=> throw new IOException("primary failed");
public Task CleanupAsync(int retentionCount, CancellationToken cancellationToken = default)
=> throw new IOException("primary failed");
public Task ClearAllAsync(CancellationToken cancellationToken = default)
=> throw new IOException("primary failed");
public Task ClearTodayAsync(CancellationToken cancellationToken = default)
=> throw new IOException("primary failed");
public Task ClearFilteredTodayAsync(string? level = null, string? query = null, CancellationToken cancellationToken = default)
=> throw new IOException("primary failed");
}
}
@@ -0,0 +1,167 @@
using YMhut.Box.Core.Api;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Media;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class RemoteMediaCatalogTests
{
[TestMethod]
public void ParsesCurrentMediaTypesSnapshot()
{
var catalog = RemoteMediaCatalogParser.Parse(ReadRepoFile("server", "update", "public", "media-types.json"));
Assert.AreEqual("1.0.6", catalog.LayoutVersion);
Assert.AreEqual("grid", catalog.UiConfig.DefaultView);
Assert.IsTrue(catalog.Categories.Count >= 2);
var image = catalog.Categories.Single(category => category.Id == "image");
Assert.IsTrue(image.Enabled);
Assert.AreEqual(RemoteMediaKind.Image, image.Kind);
Assert.IsTrue(image.Layout.ShowPreview);
CollectionAssert.Contains(image.Sources.First(source => source.Id == "xjj").SupportedFormats.ToArray(), "jpg");
Assert.AreEqual(30, image.Sources.First(source => source.Id == "xjj").RefreshIntervalSeconds);
var video = catalog.Categories.Single(category => category.Id == "video");
Assert.AreEqual(RemoteMediaKind.Video, video.Kind);
Assert.IsFalse(video.Layout.AutoPlay);
CollectionAssert.Contains(video.Sources.First().SupportedFormats.ToArray(), "mp4");
}
[TestMethod]
public void ParsesLegacyMediaTypesSnapshot()
{
var catalog = RemoteMediaCatalogParser.Parse(ReadRepoFile("box-old", "server", "media-types.json"));
Assert.AreEqual("1.0.8", catalog.LayoutVersion);
Assert.IsTrue(catalog.Categories.Count >= 2);
Assert.IsTrue(catalog.Categories.Any(category => category.Id == "image" && category.Sources.Count >= 7));
Assert.IsTrue(catalog.Categories.Any(category => category.Id == "video" && category.Sources.Any(source => source.Id == "radom_xjj_mv")));
}
[TestMethod]
public void AppliesDefaultsWhenOptionalFieldsAreMissing()
{
const string minimal = """
{
"categories": [
{
"id": "video",
"subcategories": [
{
"id": "demo",
"api_url": "https://example.test/media"
}
]
}
]
}
""";
var catalog = RemoteMediaCatalogParser.Parse(minimal);
var category = catalog.Categories.Single();
var source = category.Sources.Single();
Assert.IsTrue(category.Enabled);
Assert.AreEqual(RemoteMediaKind.Video, category.Kind);
Assert.AreEqual(1, category.Layout.Columns);
Assert.AreEqual("16:9", category.Layout.AspectRatio);
Assert.IsTrue(category.Layout.ShowPreview);
Assert.IsFalse(category.Layout.AutoPlay);
Assert.IsTrue(source.Downloadable);
Assert.AreEqual(60, source.RefreshIntervalSeconds);
CollectionAssert.AreEqual(new[] { "mp4", "webm" }, source.SupportedFormats.ToArray());
Assert.AreEqual("https://example.test/media", source.ThumbnailUrl);
}
[TestMethod]
public async Task ServiceWritesReadsFallsBackAndClearsCache()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-remote-media-" + Guid.NewGuid().ToString("N"));
try
{
var paths = new AppPaths(root);
paths.EnsureCreated();
var content = ReadRepoFile("server", "update", "public", "media-types.json");
var api = new FakeApiManager(content);
var service = new RemoteMediaCatalogService(paths, api);
var remote = await service.LoadAsync(forceRefresh: true);
Assert.AreEqual(RemoteMediaCatalogLoadSource.Remote, remote.Source);
Assert.IsTrue(api.LastUri?.Query.Contains("_=", StringComparison.Ordinal) == true);
Assert.IsTrue(File.Exists(Path.Combine(paths.Cache, "remote-media", "media-types.json")));
var fallback = new RemoteMediaCatalogService(paths, new FakeApiManager(string.Empty, success: false));
var cached = await fallback.LoadAsync();
Assert.AreEqual(RemoteMediaCatalogLoadSource.Cache, cached.Source);
Assert.IsFalse(string.IsNullOrWhiteSpace(cached.Warning));
await fallback.ClearCacheAsync();
Assert.IsFalse(Directory.Exists(Path.Combine(paths.Cache, "remote-media")));
Assert.IsTrue(Directory.Exists(paths.Cache));
}
finally
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
}
private static string ReadRepoFile(params string[] segments)
{
var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (directory is not null)
{
var candidate = Path.Combine(new[] { directory.FullName }.Concat(segments).ToArray());
if (File.Exists(candidate))
{
return File.ReadAllText(candidate);
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Unable to locate repository sample file.");
}
private sealed class FakeApiManager(string content, bool success = true) : IApiManager
{
public Uri? LastUri { get; private set; }
public Task<ApiResponse> FetchAsync(string endpointId, string input = "", CancellationToken cancellationToken = default)
{
LastUri = RemoteMediaCatalogService.PrimaryConfigUri;
return Task.FromResult(new ApiResponse(
endpointId,
LastUri,
success,
success ? content : string.Empty,
success ? null : "offline",
DateTimeOffset.Now,
success ? 200 : 0));
}
public Task<ApiResponse> FetchUriAsync(string endpointId, Uri uri, string input = "", CancellationToken cancellationToken = default)
{
LastUri = uri;
return Task.FromResult(new ApiResponse(
endpointId,
uri,
success,
success ? content : string.Empty,
success ? null : "offline",
DateTimeOffset.Now,
success ? 200 : 0));
}
public Task<ApiHealthStatus> CheckHealthAsync(string endpointId, string input = "", CancellationToken cancellationToken = default)
{
return Task.FromResult(success ? ApiHealthStatus.Healthy : ApiHealthStatus.Unhealthy);
}
}
}
@@ -0,0 +1,129 @@
using System.Net;
using YMhut.Box.Core.Media;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class RemoteMediaResolverTests
{
[TestMethod]
public async Task ResolvesRedirectToDirectMedia()
{
var resolver = CreateResolver(request =>
{
if (request.RequestUri?.AbsolutePath == "/api")
{
return Redirect("/media/demo.mp4");
}
return Text(HttpStatusCode.OK, string.Empty, "video/mp4");
});
var result = await resolver.ResolveMediaAsync("https://example.test/api", RemoteMediaKind.Video, cacheBust: false);
Assert.AreEqual("https://example.test/media/demo.mp4", result.Uri.AbsoluteUri);
Assert.IsTrue(result.IsDirectMedia);
Assert.AreEqual("video/mp4", result.ContentType);
Assert.AreEqual(".mp4", result.SuggestedExtension);
}
[TestMethod]
public async Task ExtractsPlainTextUrl()
{
var resolver = CreateResolver(request =>
{
if (request.RequestUri?.Host == "cdn.test")
{
return Text(HttpStatusCode.OK, string.Empty, "video/mp4");
}
return Text(HttpStatusCode.OK, "https://cdn.test/random/clip.mp4", "text/plain");
});
var result = await resolver.ResolveMediaAsync("https://example.test/api", RemoteMediaKind.Video, cacheBust: false);
Assert.AreEqual("https://cdn.test/random/clip.mp4", result.Uri.AbsoluteUri);
Assert.IsTrue(result.IsDirectMedia);
Assert.AreEqual(".mp4", result.SuggestedExtension);
}
[TestMethod]
public async Task ExtractsNestedJsonUrl()
{
var resolver = CreateResolver(request =>
{
if (request.RequestUri?.Host == "cdn.test")
{
return Text(HttpStatusCode.OK, string.Empty, "image/webp");
}
return Text(HttpStatusCode.OK, """
{
"data": {
"url": "https://cdn.test/images/pic.webp"
}
}
""", "application/json");
});
var result = await resolver.ResolveMediaAsync("https://example.test/api", RemoteMediaKind.Image, cacheBust: false);
Assert.AreEqual("https://cdn.test/images/pic.webp", result.Uri.AbsoluteUri);
Assert.IsTrue(result.IsDirectMedia);
Assert.AreEqual(".webp", result.SuggestedExtension);
}
[TestMethod]
public async Task KeepsHtmlWithoutMediaUrlAsNonDirect()
{
var resolver = CreateResolver(_ => Text(HttpStatusCode.OK, "<html><body>No media here</body></html>", "text/html"));
var result = await resolver.ResolveMediaAsync("https://example.test/page", RemoteMediaKind.Video, cacheBust: false);
Assert.AreEqual("https://example.test/page", result.Uri.AbsoluteUri);
Assert.IsFalse(result.IsDirectMedia);
Assert.AreEqual("text/html", result.ContentType);
}
[TestMethod]
public async Task TreatsDirectMediaContentTypeAsPlayable()
{
var resolver = CreateResolver(_ => Text(HttpStatusCode.OK, string.Empty, "audio/mpeg"));
var result = await resolver.ResolveMediaAsync("https://example.test/random", RemoteMediaKind.Audio, cacheBust: false);
Assert.AreEqual("https://example.test/random", result.Uri.AbsoluteUri);
Assert.IsTrue(result.IsDirectMedia);
Assert.AreEqual(".mp3", result.SuggestedExtension);
}
private static RemoteMediaResolver CreateResolver(Func<HttpRequestMessage, HttpResponseMessage> responseFactory)
{
return new RemoteMediaResolver(() => new StubHttpHandler(responseFactory));
}
private static HttpResponseMessage Redirect(string location)
{
var response = new HttpResponseMessage(HttpStatusCode.Redirect);
response.Headers.Location = new Uri(location, UriKind.RelativeOrAbsolute);
return response;
}
private static HttpResponseMessage Text(HttpStatusCode statusCode, string content, string contentType)
{
return new HttpResponseMessage(statusCode)
{
Content = new StringContent(content, System.Text.Encoding.UTF8, contentType)
};
}
private sealed class StubHttpHandler(Func<HttpRequestMessage, HttpResponseMessage> responseFactory) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = responseFactory(request);
response.RequestMessage = request;
return Task.FromResult(response);
}
}
}
+22
View File
@@ -0,0 +1,22 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class SensitiveTextTests
{
[TestMethod]
public void SanitizeRemovesInternalRemoteDetails()
{
var sanitized = SensitiveText.Sanitize("failed https://update.ymhut.cn/update-info.json?token=abc media-types.json download_url api_url");
Assert.DoesNotContain("https://", sanitized);
Assert.DoesNotContain("update.ymhut.cn", sanitized);
Assert.DoesNotContain("update-info.json", sanitized);
Assert.DoesNotContain("media-types.json", sanitized);
Assert.DoesNotContain("download_url", sanitized);
Assert.DoesNotContain("api_url", sanitized);
Assert.Contains("远程服务", sanitized);
}
}
+129
View File
@@ -0,0 +1,129 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Api;
using YMhut.Box.Core.Data;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Settings;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class ServiceLayerTests
{
[TestMethod]
public async Task SettingsServiceBroadcastsAndPersistsChanges()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
var service = new AppSettingsService(new AppSettingsStore(root));
var changed = false;
service.PropertyChanged += (_, args) => changed |= args.PropertyName == nameof(ISettingsService.Current);
await service.LoadAsync();
await service.UpdateAsync(settings => settings.Theme = "Dark");
var reloaded = await new AppSettingsStore(root).LoadAsync();
Assert.IsTrue(changed);
Assert.AreEqual("Dark", reloaded.Theme);
}
[TestMethod]
public async Task SettingsStorePersistsAndNormalizesFeedbackDefaults()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
var store = new AppSettingsStore(root);
await store.SaveAsync(new AppSettings
{
FeedbackDefaultContact = "dev@example.com",
FeedbackDefaultType = "bad-type",
FeedbackDefaultSeverity = "bad-severity",
FeedbackIncludeTodayLogsByDefault = true,
FeedbackIncludeToolStatusByDefault = true,
FeedbackIncludeSystemSummaryByDefault = true,
FeedbackRememberDefaults = false
});
var settings = await store.LoadAsync();
Assert.AreEqual("dev@example.com", settings.FeedbackDefaultContact);
Assert.AreEqual("issue", settings.FeedbackDefaultType);
Assert.AreEqual("normal", settings.FeedbackDefaultSeverity);
Assert.IsTrue(settings.FeedbackIncludeTodayLogsByDefault);
Assert.IsTrue(settings.FeedbackIncludeToolStatusByDefault);
Assert.IsTrue(settings.FeedbackIncludeSystemSummaryByDefault);
Assert.IsFalse(settings.FeedbackRememberDefaults);
}
[TestMethod]
public async Task SettingsStorePersistsToolboxAndRiskSettings()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
var store = new AppSettingsStore(root);
await store.SaveAsync(new AppSettings
{
ToolDisplayMode = "compact",
ToolboxDefaultScope = "plugins",
ToolboxCompactCards = true,
ToolboxShowRecentFirst = false,
ShowHardwareBrandLogo = false,
RequireRiskConfirmation = false,
LogRetentionCount = 90,
PluginsEnabled = true
});
var settings = await store.LoadAsync();
Assert.AreEqual("list", settings.ToolDisplayMode);
Assert.AreEqual("plugin", settings.ToolboxDefaultScope);
Assert.IsTrue(settings.ToolboxCompactCards);
Assert.IsFalse(settings.ToolboxShowRecentFirst);
Assert.IsFalse(settings.ShowHardwareBrandLogo);
Assert.IsFalse(settings.RequireRiskConfirmation);
Assert.AreEqual(90, settings.LogRetentionCount);
Assert.IsTrue(settings.PluginsEnabled);
}
[TestMethod]
public async Task LogServiceWritesReadsAndCleansUp()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
var service = new JsonFileLogService(AppPaths.ForCurrentUser(root));
await service.WriteAsync("Information", "test", "first");
await service.WriteAsync("Error", "test", "second");
var errors = await service.ReadAsync("Error");
Assert.HasCount(1, errors);
Assert.AreEqual("second", errors[0].Message);
await service.CleanupAsync(1);
var all = await service.ReadAsync();
Assert.HasCount(1, all);
}
[TestMethod]
public void ApiEndpointsResolveWeather()
{
var ok = ApiEndpoints.TryResolve("weather", "Shanghai", out var endpoint, out var uri);
Assert.IsTrue(ok);
Assert.AreEqual("weather", endpoint.Id);
StringAssert.Contains(endpoint.SourceName, "Open-Meteo");
StringAssert.Contains(uri.Host, "open-meteo.com");
}
[TestMethod]
public void ReferenceDataServiceValidatesRequiredAssets()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
var assets = Path.Combine(root, "Assets");
Directory.CreateDirectory(Path.Combine(assets, "data", "reference"));
File.WriteAllText(Path.Combine(assets, "data", "reference", "ymhut_reference_data.json"), "{}");
var service = new ReferenceDataService(AppPaths.ForCurrentUser(root, assets));
var result = service.ValidateRequiredAssets("data/reference/ymhut_reference_data.json", "data/missing.json");
Assert.IsFalse(result.IsHealthy);
Assert.HasCount(1, result.PresentPaths);
Assert.HasCount(1, result.MissingPaths);
}
}
@@ -0,0 +1,134 @@
using System.Net;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Net;
using YMhut.Box.Core.SolarSystem;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class SolarEphemerisServiceTests
{
[TestMethod]
public void MiriadeParserReadsRectangularPxPyPzPayload()
{
var json = """
{
"data": [
{
"Date": 0.24611925E+7,
"px": -0.32062933285171752E+0,
"py": 0.14714396284300654E+0,
"pz": 0.41432383444950593E-1
}
]
}
""";
var parsed = SolarEphemerisService.TryParseMiriadePayload(json, out var vector);
Assert.IsTrue(parsed);
Assert.AreEqual(-0.32062933285171752, vector.X, 1e-12);
Assert.AreEqual(0.14714396284300654, vector.Y, 1e-12);
Assert.AreEqual(0.041432383445, vector.Z, 1e-12);
}
[TestMethod]
public async Task CurrentEphemerisReturnsApproximatePayloadWhenPreciseCacheMissing()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-solar-tests", Guid.NewGuid().ToString("N"));
try
{
var service = new SolarEphemerisService(
AppPaths.ForCurrentUser(root),
new FakeHttpService(_ => Task.FromException<string>(new InvalidOperationException())));
var payload = await service.GetCurrentAsync();
Assert.AreEqual("ephemeris:sync", payload.Type);
Assert.AreEqual("China Ephemeris Approx", payload.Source);
Assert.HasCount(8, payload.Planets);
}
finally
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
}
[TestMethod]
public async Task PreciseEphemerisRequestsMiriadePlanetsConcurrently()
{
var root = Path.Combine(Path.GetTempPath(), "ymhut-solar-tests", Guid.NewGuid().ToString("N"));
var requestTimes = new List<DateTimeOffset>();
try
{
var service = new SolarEphemerisService(
AppPaths.ForCurrentUser(root),
new FakeHttpService(async uri =>
{
lock (requestTimes)
{
requestTimes.Add(DateTimeOffset.UtcNow);
}
await Task.Delay(80);
if (uri.Host.Contains("ssp.imcce.fr", StringComparison.OrdinalIgnoreCase))
{
return """
{
"data": [
{ "px": 1.0, "py": 2.0, "pz": 3.0 }
]
}
""";
}
throw new InvalidOperationException("Unexpected endpoint");
}));
var payload = await service.GetPreciseCurrentAsync();
Assert.IsNotNull(payload);
Assert.AreEqual("IMCCE Miriade", payload.Source);
Assert.HasCount(8, payload.Planets);
Assert.HasCount(8, requestTimes);
Assert.IsLessThan(70, (requestTimes.Max() - requestTimes.Min()).TotalMilliseconds);
}
finally
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
}
private sealed class FakeHttpService(Func<Uri, Task<string>> handler) : IHttpService
{
public async Task<HttpServiceResult> GetAsync(Uri uri, CancellationToken cancellationToken = default)
{
var content = await GetStringAsync(uri, cancellationToken);
return new HttpServiceResult(HttpStatusCode.OK, content, new Dictionary<string, string[]>(), TimeSpan.FromMilliseconds(1));
}
public Task<string> GetStringAsync(Uri uri, CancellationToken cancellationToken = default)
{
return handler(uri);
}
public Task<HttpServiceResult> SendAsync(
Uri uri,
string method = "GET",
string? body = null,
IReadOnlyDictionary<string, string>? headers = null,
bool ensureSuccess = true,
HttpRequestPolicy? policy = null,
CancellationToken cancellationToken = default)
{
return GetAsync(uri, cancellationToken);
}
}
}
+647
View File
@@ -0,0 +1,647 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Data.Sqlite;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Startup;
namespace YMhut.Box.Tests;
[TestClass]
[DoNotParallelize]
public sealed class StartupCheckTests
{
[TestMethod]
public async Task FastPreflightPassesForPureLangInstallLayout()
{
using var workspace = TestWorkspace.Create("ymhut-startup-fast");
var installRoot = CreateHealthyInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var store = new StartupCheckStore(paths);
var service = new InstallIntegrityCheckService(paths, store);
var report = await service.RunFastPreflightAsync();
Assert.AreEqual(StartupCheckKind.FastPreflight, report.Kind);
Assert.AreEqual(0, report.CriticalIssueCount);
Assert.IsTrue(report.Items.Any(item => item.Id == "lang:zh-CN" && item.Status == StartupCheckStatus.Passed));
Assert.IsTrue(report.Items.Any(item => item.Id == "lang:en-US" && item.Status == StartupCheckStatus.Passed));
Assert.IsFalse(Directory.Exists(Path.Combine(appRoot, "Runtime")));
StringAssert.StartsWith(store.DatabasePath, Path.Combine(appRoot, "Logs"));
}
[TestMethod]
public async Task FastPreflightDoesNotBlockDevelopmentRootCultureLayout()
{
using var workspace = TestWorkspace.Create("ymhut-startup-dev-layout");
var installRoot = CreateDevelopmentInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
var report = await service.RunFastPreflightAsync();
var resourcesPri = report.Items.FirstOrDefault(item => item.Id == "file:resources.pri");
var zhCn = report.Items.FirstOrDefault(item => item.Id == "lang:zh-CN");
var enUs = report.Items.FirstOrDefault(item => item.Id == "lang:en-US");
Assert.AreEqual(0, report.CriticalIssueCount);
Assert.IsNotNull(resourcesPri);
Assert.AreEqual(StartupCheckStatus.Missing, resourcesPri.Status);
Assert.AreEqual(StartupCheckSeverity.Warning, resourcesPri.Severity);
Assert.IsNotNull(zhCn);
Assert.AreEqual(StartupCheckStatus.Passed, zhCn.Status);
Assert.IsNotNull(enUs);
Assert.AreEqual(StartupCheckStatus.Passed, enUs.Status);
Assert.IsFalse(report.Items.Any(item => item.Id.StartsWith("lang:root:", StringComparison.OrdinalIgnoreCase)));
}
[TestMethod]
public async Task FastPreflightAcceptsWinUiDevelopmentPriAndRootCultureSatellites()
{
using var workspace = TestWorkspace.Create("ymhut-startup-dev-pri");
var installRoot = CreateDevelopmentInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
workspace.CreateFile(["install", "YMhutBox.pri"], string.Empty);
workspace.CreateFile(["install", "fr-FR", "Microsoft.ui.xaml.dll.mui"], string.Empty);
workspace.CreateFile(["install", "ja-JP", "Microsoft.ui.xaml.dll.mui"], string.Empty);
workspace.CreateFile(["install", "Tools", "Sample", "tool.exe"], string.Empty);
workspace.CreateFile(["install", "Metadata", "tools.json"], """{ "tools": [] }""");
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
var report = await service.RunFastPreflightAsync();
Assert.AreEqual(0, report.VisibleIssueCount);
Assert.IsTrue(report.Items.Any(item => item.Id == "manifest" && item.Status == StartupCheckStatus.Skipped));
Assert.IsTrue(report.Items.Any(item => item.Id == "file:resources.pri" && item.Status == StartupCheckStatus.Passed));
Assert.IsTrue(report.Items.Any(item => item.Id == "lang:root-culture-summary" && item.Status == StartupCheckStatus.Skipped));
Assert.IsFalse(report.Items.Any(item => item.Id.StartsWith("lang:root:", StringComparison.OrdinalIgnoreCase)));
}
[TestMethod]
public async Task CurrentHistoryNormalizesLegacyDevelopmentLayoutFalsePositives()
{
using var workspace = TestWorkspace.Create("ymhut-startup-legacy-false-positive");
var installRoot = CreateDevelopmentInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
workspace.CreateFile(["install", "YMhutBox.pri"], string.Empty);
workspace.CreateFile(["install", "fr-FR", "Microsoft.ui.xaml.dll.mui"], string.Empty);
workspace.CreateFile(["install", "Tools", "Sample", "tool.exe"], string.Empty);
workspace.CreateFile(["install", "Metadata", "tools.json"], """{ "tools": [] }""");
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var store = new StartupCheckStore(paths);
var manifestIdentity = InstallRootContext.BuildManifestIdentity(installRoot);
var oldReport = StartupCheckReport.Create(
StartupCheckKind.FastPreflight,
new DateTimeOffset(2026, 6, 1, 8, 0, 0, TimeSpan.Zero),
installRoot,
manifestIdentity,
InstallRootContext.BuildInstallIdentity(installRoot, manifestIdentity),
$"installRoot={installRoot}; manifest={manifestIdentity}",
[
new StartupCheckItem("manifest", "安装清单", StartupCheckSeverity.Warning, StartupCheckStatus.Missing, "文件缺失", Path.Combine(installRoot, "config", "install-manifest.ini")),
new StartupCheckItem("file:resources.pri", "resources.pri", StartupCheckSeverity.Warning, StartupCheckStatus.Missing, "文件缺失", Path.Combine(installRoot, "resources.pri")),
new StartupCheckItem("lang:zh-CN", "语言资源 zh-CN", StartupCheckSeverity.Warning, StartupCheckStatus.Missing, "语言目录缺失", Path.Combine(installRoot, "lang", "zh-CN")),
new StartupCheckItem("lang:root:fr-FR", "根目录语言资源 fr-FR", StartupCheckSeverity.Warning, StartupCheckStatus.Failed, "旧版误判", Path.Combine(installRoot, "fr-FR"))
]);
await store.SaveAsync(oldReport);
var service = new InstallIntegrityCheckService(paths, store);
var latest = await service.GetLatestCurrentReportAsync(StartupCheckKind.FastPreflight);
var history = await service.GetCurrentHistoryAsync(10);
Assert.IsNotNull(latest);
Assert.AreEqual(0, latest.VisibleIssueCount);
Assert.AreEqual(StartupCheckStatus.Skipped, latest.Items.First(item => item.Id == "manifest").Status);
Assert.AreEqual(StartupCheckStatus.Passed, latest.Items.First(item => item.Id == "file:resources.pri").Status);
Assert.AreEqual(StartupCheckStatus.Passed, latest.Items.First(item => item.Id == "lang:zh-CN").Status);
Assert.AreEqual(StartupCheckStatus.Skipped, latest.Items.First(item => item.Id == "lang:root:fr-FR").Status);
Assert.AreEqual(0, history[0].VisibleIssueCount);
}
[TestMethod]
public async Task MonthlyIntegrityUsesRecentReportUntilManifestIdentityChanges()
{
using var workspace = TestWorkspace.Create("ymhut-startup-monthly");
var installRoot = CreateHealthyInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
var now = new DateTimeOffset(2026, 6, 14, 9, 0, 0, TimeSpan.Zero);
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var store = new StartupCheckStore(paths);
var service = new InstallIntegrityCheckService(paths, store, clock: () => now);
var first = await service.RunMonthlyIntegrityCheckAsync();
now = now.AddDays(5);
var cached = await service.RunMonthlyIntegrityCheckAsync();
Assert.AreEqual(first.Id, cached.Id);
File.AppendAllText(Path.Combine(installRoot, InstallManifest.RelativePath.Replace('/', Path.DirectorySeparatorChar)), Environment.NewLine + "; changed");
now = now.AddDays(1);
var changed = await service.RunMonthlyIntegrityCheckAsync();
Assert.AreNotEqual(first.Id, changed.Id);
}
[TestMethod]
public async Task MonthlyIntegrityDoesNotReuseReportFromDifferentInstallRoot()
{
using var workspace = TestWorkspace.Create("ymhut-startup-monthly-root");
var installRoot = CreateHealthyInstall(workspace);
var secondRoot = workspace.CreateDirectory("install2");
CopyDirectory(installRoot, secondRoot);
var appRoot = workspace.CreateDirectory("user");
var now = new DateTimeOffset(2026, 6, 14, 9, 0, 0, TimeSpan.Zero);
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var store = new StartupCheckStore(paths);
var service = new InstallIntegrityCheckService(paths, store, clock: () => now);
var first = await service.RunMonthlyIntegrityCheckAsync();
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, secondRoot);
now = now.AddDays(1);
var second = await service.RunMonthlyIntegrityCheckAsync();
Assert.AreNotEqual(first.Id, second.Id);
Assert.AreNotEqual(first.InstallIdentity, second.InstallIdentity);
}
[TestMethod]
public async Task IntegrityReportsMissingExternalToolLaunchableAsWarning()
{
using var workspace = TestWorkspace.Create("ymhut-startup-tool-missing");
var installRoot = CreateHealthyInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
Directory.CreateDirectory(Path.Combine(installRoot, "Tools", "Sample"));
File.WriteAllText(Path.Combine(installRoot, "Tools", "Sample", "present.exe"), string.Empty);
File.AppendAllText(
Path.Combine(installRoot, InstallManifest.RelativePath.Replace('/', Path.DirectorySeparatorChar)),
Environment.NewLine + "Tools/Sample/missing.exe");
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
var report = await service.RunManualIntegrityCheckAsync();
var missing = report.Items.FirstOrDefault(item => item.Id == "file:Tools/Sample/missing.exe");
Assert.IsNotNull(missing);
Assert.AreEqual(StartupCheckStatus.Missing, missing.Status);
Assert.AreEqual(StartupCheckSeverity.Warning, missing.Severity);
}
[TestMethod]
public async Task UserDataRuntimePayloadIsRemovedBeforePreflight()
{
using var workspace = TestWorkspace.Create("ymhut-startup-userdata");
var installRoot = CreateHealthyInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
var runtimeRoot = Path.Combine(appRoot, "Runtime");
var runtimesRoot = Path.Combine(appRoot, "runtimes");
Directory.CreateDirectory(runtimeRoot);
Directory.CreateDirectory(runtimesRoot);
File.WriteAllText(Path.Combine(runtimeRoot, "YMhutBox.exe"), string.Empty);
File.WriteAllText(Path.Combine(runtimesRoot, "Microsoft.ui.xaml.dll"), string.Empty);
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
Assert.IsFalse(Directory.Exists(runtimeRoot));
Assert.IsFalse(Directory.Exists(runtimesRoot));
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
var report = await service.RunFastPreflightAsync();
var runtime = report.Items.FirstOrDefault(item => item.Id == "userdata:runtime");
Assert.IsNotNull(runtime);
Assert.AreEqual(StartupCheckStatus.Passed, runtime.Status);
Assert.IsFalse(Directory.Exists(runtimeRoot));
Assert.IsFalse(Directory.Exists(runtimesRoot));
}
[TestMethod]
public async Task UserDataToolsAndMetadataPayloadsAreRemovedBeforePreflight()
{
using var workspace = TestWorkspace.Create("ymhut-startup-userdata-tools");
var installRoot = CreateHealthyInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var toolsRoot = Path.Combine(appRoot, "Tools");
var metadataRoot = Path.Combine(appRoot, "Metadata");
Directory.CreateDirectory(toolsRoot);
Directory.CreateDirectory(metadataRoot);
File.WriteAllText(Path.Combine(toolsRoot, "YMhutBox.exe"), string.Empty);
File.WriteAllText(Path.Combine(metadataRoot, "tools.json"), "{}");
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
var report = await service.RunFastPreflightAsync();
var tools = report.Items.FirstOrDefault(item => item.Id == "userdata:tools");
var metadata = report.Items.FirstOrDefault(item => item.Id == "userdata:metadata");
Assert.IsNotNull(tools);
Assert.IsNotNull(metadata);
Assert.AreEqual(StartupCheckStatus.Passed, tools.Status);
Assert.AreEqual(StartupCheckStatus.Passed, metadata.Status);
Assert.IsFalse(Directory.Exists(toolsRoot));
Assert.IsFalse(Directory.Exists(metadataRoot));
Assert.AreEqual(0, report.VisibleIssueCount);
}
[TestMethod]
public async Task CurrentHistoryFiltersOutHistoricalInstallReports()
{
using var workspace = TestWorkspace.Create("ymhut-startup-current-history");
var installRoot = CreateHealthyInstall(workspace);
var secondRoot = workspace.CreateDirectory("install2");
CopyDirectory(installRoot, secondRoot);
var appRoot = workspace.CreateDirectory("user");
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var store = new StartupCheckStore(paths);
var service = new InstallIntegrityCheckService(paths, store);
var oldReport = await service.RunManualIntegrityCheckAsync();
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, secondRoot);
var currentReport = await service.RunManualIntegrityCheckAsync();
var all = await service.GetHistorySummariesAsync();
var current = await service.GetCurrentHistoryAsync();
Assert.IsTrue(all.Any(summary => summary.Id == oldReport.Id && !summary.IsCurrentInstall));
Assert.IsTrue(current.All(summary => summary.IsCurrentInstall));
Assert.IsTrue(current.Any(summary => summary.Id == currentReport.Id));
Assert.IsFalse(current.Any(summary => summary.Id == oldReport.Id));
}
[TestMethod]
public async Task RecheckItemReportsCurrentPathPassesAfterFileRestored()
{
using var workspace = TestWorkspace.Create("ymhut-startup-recheck");
var installRoot = CreateHealthyInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
var executable = Path.Combine(installRoot, "YMhutBox.exe");
File.Delete(executable);
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
var report = await service.RunManualIntegrityCheckAsync();
var missing = report.Items.First(item => item.Id == "file:YMhutBox.exe");
File.WriteAllText(executable, string.Empty);
var rechecked = await service.RecheckItemAsync(report.Id, missing.Id);
Assert.IsNotNull(rechecked);
Assert.AreEqual(StartupCheckStatus.Passed, rechecked.Status);
StringAssert.Contains(rechecked.Detail, "old record");
}
[TestMethod]
public async Task IntegrityDoesNotTreatNormalRootFoldersAsCultureFolders()
{
using var workspace = TestWorkspace.Create("ymhut-startup-root-folders");
var installRoot = CreateHealthyInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
Directory.CreateDirectory(Path.Combine(installRoot, "Assets"));
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var service = new InstallIntegrityCheckService(paths, new StartupCheckStore(paths));
var report = await service.RunManualIntegrityCheckAsync();
Assert.IsFalse(report.Items.Any(item => string.Equals(item.Id, "lang:root:Assets", StringComparison.OrdinalIgnoreCase)));
Assert.IsFalse(report.Items.Any(item => string.Equals(item.Id, "lang:root:Tools", StringComparison.OrdinalIgnoreCase)));
}
[TestMethod]
public async Task StartupCheckStorePersistsHistoryAndLatestReport()
{
using var workspace = TestWorkspace.Create("ymhut-startup-store");
var installRoot = CreateHealthyInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var store = new StartupCheckStore(paths);
var service = new InstallIntegrityCheckService(paths, store);
var report = await service.RunManualIntegrityCheckAsync();
var latest = await store.GetLatestReportAsync();
var history = await store.GetHistoryAsync();
var summaries = await store.GetHistorySummariesAsync();
var loaded = await store.GetReportAsync(report.Id);
Assert.IsNotNull(latest);
Assert.AreEqual(report.Id, latest.Id);
Assert.IsGreaterThanOrEqualTo(history.Count, 1);
Assert.IsNotEmpty(history[0].Items);
Assert.IsGreaterThanOrEqualTo(summaries.Count, 1);
Assert.AreEqual(report.Id, summaries[0].Id);
Assert.AreEqual(report.Items.Count, summaries[0].ItemCount);
Assert.AreEqual(report.IssueCount, summaries[0].IssueCount);
Assert.IsNotNull(loaded);
Assert.AreEqual(report.Id, loaded.Id);
Assert.IsNotEmpty(loaded.Items);
}
[TestMethod]
public async Task StartupCheckStoreMigratesLegacyDatabaseIntoMainSqlite()
{
using var workspace = TestWorkspace.Create("ymhut-startup-store-migrate");
var installRoot = CreateHealthyInstall(workspace);
var appRoot = workspace.CreateDirectory("user");
using var environment = EnvironmentScope.Capture(InstallLayoutPaths.InstallRootEnvironmentVariable);
environment.Set(InstallLayoutPaths.InstallRootEnvironmentVariable, installRoot);
var paths = AppPaths.ForCurrentUser(appRoot);
var legacyPath = Path.Combine(paths.Data, "startup-checks.db");
Directory.CreateDirectory(Path.GetDirectoryName(legacyPath)!);
await using (var connection = new SqliteConnection($"Data Source={legacyPath};Pooling=False"))
{
connection.Open();
await using var command = connection.CreateCommand();
command.CommandText = """
CREATE TABLE startup_check_reports (
id TEXT PRIMARY KEY,
kind TEXT NOT NULL,
started_at TEXT NOT NULL,
completed_at TEXT NOT NULL,
install_root TEXT NOT NULL,
manifest_identity TEXT NOT NULL,
issue_count INTEGER NOT NULL,
critical_issue_count INTEGER NOT NULL,
warning_issue_count INTEGER NOT NULL
);
CREATE TABLE startup_check_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
report_id TEXT NOT NULL,
item_id TEXT NOT NULL,
name TEXT NOT NULL,
severity TEXT NOT NULL,
status TEXT NOT NULL,
detail TEXT NOT NULL,
path TEXT NULL
);
INSERT INTO startup_check_reports(
id, kind, started_at, completed_at, install_root, manifest_identity,
issue_count, critical_issue_count, warning_issue_count)
VALUES(
'11111111111111111111111111111111',
'FastPreflight',
'2026-06-01T00:00:00.0000000+00:00',
'2026-06-01T00:00:01.0000000+00:00',
'C:\Legacy',
'legacy',
1,
0,
1);
INSERT INTO startup_check_items(report_id, item_id, name, severity, status, detail, path)
VALUES(
'11111111111111111111111111111111',
'legacy:item',
'',
'Warning',
'Failed',
'',
NULL);
""";
await command.ExecuteNonQueryAsync();
}
var store = new StartupCheckStore(paths);
var latest = await store.GetLatestReportAsync();
Assert.IsNotNull(latest);
Assert.AreEqual("legacy", latest.ManifestIdentity);
Assert.AreEqual(1, latest.Items.Count);
Assert.IsFalse(File.Exists(legacyPath));
Assert.AreEqual(Path.Combine(paths.Logs, AppDatabasePaths.MainDatabaseFileName), store.DatabasePath);
}
[TestMethod]
public async Task StartupInitializationPipelineReportsMonotonicWeightedProgress()
{
var progress = new List<StartupInitializationStageProgress>();
var pipeline = new StartupInitializationPipeline([
new StartupInitializationStage("first", "First", 1, true, (context, token) =>
{
context.Report(0.5, "Half");
return Task.CompletedTask;
}),
new StartupInitializationStage("second", "Second", 3, true, (context, token) =>
{
context.Report(0.25, "Quarter");
context.Report(0.75, "Almost");
return Task.CompletedTask;
})
]);
await pipeline.RunAsync(new InlineProgress<StartupInitializationStageProgress>(progress.Add));
Assert.IsTrue(progress.Count > 0);
Assert.AreEqual(100, progress[^1].Progress);
for (var index = 1; index < progress.Count; index++)
{
Assert.IsTrue(
progress[index].Progress >= progress[index - 1].Progress,
$"Progress moved backwards at {index}: {progress[index - 1].Progress} -> {progress[index].Progress}");
}
CollectionAssert.AreEqual(new[] { "first", "second" }, pipeline.Stages.Select(stage => stage.Id).ToArray());
}
[TestMethod]
public void StartupSplashMessageSerializesStableContract()
{
var message = new StartupSplashMessage(
"progress",
"正在检查",
42,
"fast-preflight",
"快速预检",
8,
10,
"检查关键文件",
"warning",
IsRepairStep: true,
Theme: "dark",
ReducedMotion: true);
var json = message.ToJson();
StringAssert.Contains(json, "\"type\": \"progress\"");
StringAssert.Contains(json, "\"stageId\": \"fast-preflight\"");
StringAssert.Contains(json, "\"isRepairStep\": true");
StringAssert.Contains(json, "\"reducedMotion\": true");
}
private static string CreateHealthyInstall(TestWorkspace workspace)
{
var installRoot = workspace.CreateDirectory("install");
workspace.CreateFile(["install", "YMhutBox.exe"], string.Empty);
workspace.CreateFile(["install", "YMhutBox.dll"], string.Empty);
workspace.CreateFile(["install", "resources.pri"], string.Empty);
workspace.CreateFile(["install", "lang", "zh-CN", "Microsoft.ui.xaml.dll.mui"], string.Empty);
workspace.CreateFile(["install", "lang", "en-US", "Microsoft.ui.xaml.dll.mui"], string.Empty);
workspace.CreateFile(["install", "Tools", "Sample", "tool.exe"], string.Empty);
workspace.CreateFile(["install", "Metadata", "tools.json"], """{ "tools": [] }""");
workspace.CreateFile(["install", "config", "install-manifest.ini"],
"""
[Release]
Version=2.0.6
Build=1
Channel=stable
PackageVersion=2.0.6.1
[RequiredFiles]
YMhutBox.exe
YMhutBox.dll
resources.pri
[Files]
YMhutBox.exe
YMhutBox.dll
resources.pri
lang/zh-CN/Microsoft.ui.xaml.dll.mui
lang/en-US/Microsoft.ui.xaml.dll.mui
Tools/Sample/tool.exe
Metadata/tools.json
""");
return installRoot;
}
private static string CreateDevelopmentInstall(TestWorkspace workspace)
{
var installRoot = workspace.CreateDirectory("install");
workspace.CreateFile(["install", "YMhutBox.exe"], string.Empty);
workspace.CreateFile(["install", "YMhutBox.dll"], string.Empty);
workspace.CreateFile(["install", "zh-CN", "Microsoft.ui.xaml.dll.mui"], string.Empty);
workspace.CreateFile(["install", "en-US", "Microsoft.ui.xaml.dll.mui"], string.Empty);
return installRoot;
}
private static void CopyDirectory(string source, string destination)
{
Directory.CreateDirectory(destination);
foreach (var directory in Directory.EnumerateDirectories(source, "*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(Path.Combine(destination, Path.GetRelativePath(source, directory)));
}
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
{
var target = Path.Combine(destination, Path.GetRelativePath(source, file));
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
File.Copy(file, target, overwrite: true);
}
}
private sealed class EnvironmentScope : IDisposable
{
private readonly Dictionary<string, string?> _snapshot;
private EnvironmentScope(IEnumerable<string> names)
{
_snapshot = names.ToDictionary(name => name, Environment.GetEnvironmentVariable, StringComparer.Ordinal);
}
public static EnvironmentScope Capture(params string[] names) => new(names);
public void Set(string name, string? value) => Environment.SetEnvironmentVariable(name, value);
public void Dispose()
{
foreach (var item in _snapshot)
{
Environment.SetEnvironmentVariable(item.Key, item.Value);
}
}
}
private sealed class InlineProgress<T>(Action<T> report) : IProgress<T>
{
public void Report(T value) => report(value);
}
private sealed class TestWorkspace : IDisposable
{
private TestWorkspace(string root)
{
Root = root;
}
public string Root { get; }
public static TestWorkspace Create(string prefix)
{
var root = Path.Combine(Path.GetTempPath(), prefix, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
return new TestWorkspace(root);
}
public string CreateDirectory(params string[] parts)
{
var directory = Path.Combine([Root, .. parts]);
Directory.CreateDirectory(directory);
return directory;
}
public string CreateFile(string[] parts, string contents)
{
var path = Path.Combine([Root, .. parts]);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, contents);
return path;
}
public void Dispose()
{
if (Directory.Exists(Root))
{
Directory.Delete(Root, recursive: true);
}
}
}
}
+298
View File
@@ -0,0 +1,298 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class ToolCatalogTests
{
[TestMethod]
public void DefaultCatalogContainsRequiredNativePages()
{
var catalog = new ToolCatalog();
Assert.IsNotNull(catalog.GetById("safe_browser"));
Assert.IsNotNull(catalog.GetById("media_player"));
Assert.IsNotNull(catalog.GetById("text_splitter"));
Assert.IsNotNull(catalog.GetById("loan_emi_calculator"));
Assert.IsNotNull(catalog.GetById("number_base_converter"));
Assert.IsNotNull(catalog.GetById("ymhut_uutool_suite"));
Assert.IsNotNull(catalog.GetById("html_js_playground"));
Assert.IsNotNull(catalog.GetById("timezone_abbr_lookup"));
Assert.IsNotNull(catalog.GetById("percentage_change_calculator"));
Assert.IsNotNull(catalog.GetById("dev_environment_config"));
Assert.IsNull(catalog.GetById("mmd_model_studio"));
foreach (var id in new[]
{
"compression_codec",
"totp_generator",
"color_contrast_checker",
"url_redirect_trace",
"image_metadata_inspector",
"markdown_preview"
})
{
Assert.IsNotNull(catalog.GetById(id), id);
}
Assert.IsGreaterThanOrEqualTo(160, catalog.Modules.Count);
}
[TestMethod]
public void SearchFiltersByQueryAndCategory()
{
var catalog = new ToolCatalog();
var results = catalog.Search("browser", ToolCategory.Network).ToList();
Assert.HasCount(1, results);
Assert.AreEqual("safe_browser", results[0].Id);
}
[TestMethod]
public void ToolViewModelStartsWithInlinePreviewHidden()
{
var module = new ToolCatalog().GetById("json_formatter")!;
var viewModel = (ToolModuleViewModel)module.CreateViewModel();
Assert.IsFalse(viewModel.InlinePreviewLoaded);
}
[TestMethod]
public void EveryCatalogToolHasNativeWinUiPageSpec()
{
var catalog = new ToolCatalog();
var specs = ToolPageSpecCatalog.CreateFor(catalog);
Assert.HasCount(catalog.Modules.Count, specs);
foreach (var module in catalog.Modules)
{
Assert.IsTrue(specs.ContainsKey(module.Id), module.Id);
var spec = specs[module.Id];
Assert.AreEqual(module.Id, spec.ToolId);
Assert.IsTrue(Enum.IsDefined(spec.Layout), module.Id);
Assert.IsNotEmpty(spec.ParameterControls, module.Id);
Assert.IsTrue(Enum.IsDefined(spec.AssistPanelMode), module.Id);
Assert.IsTrue(Enum.IsDefined(spec.InteractionMode), module.Id);
Assert.IsTrue(Enum.IsDefined(spec.PageExperience), module.Id);
Assert.IsTrue(Enum.IsDefined(spec.PageDensity), module.Id);
Assert.IsTrue(Enum.IsDefined(spec.ResultPriority), module.Id);
Assert.IsFalse(string.IsNullOrWhiteSpace(spec.DefaultFocusTarget), module.Id);
if (spec.PageExperience != ToolPageExperienceKind.LivePreview)
{
Assert.IsFalse(string.IsNullOrWhiteSpace(spec.PrimaryActionLabel), module.Id);
}
if (spec.PrimaryInput != ToolPrimaryInputKind.None)
{
Assert.IsNotEmpty(spec.Parameters, module.Id);
}
Assert.IsFalse(string.IsNullOrWhiteSpace(spec.EmptyState), module.Id);
Assert.IsFalse(string.IsNullOrWhiteSpace(spec.ErrorHint), module.Id);
}
}
[TestMethod]
public void RepresentativeToolSpecsUseGalleryNativeControls()
{
var catalog = new ToolCatalog();
Assert.AreEqual(ToolPrimaryInputKind.MultilineText, ToolPageSpecCatalog.For(catalog.GetById("json_formatter")!).PrimaryInput);
Assert.AreEqual(ToolResultKind.JsonTree, ToolPageSpecCatalog.For(catalog.GetById("json_formatter")!).Result);
Assert.AreEqual(ToolPrimaryInputKind.FixedOptions, ToolPageSpecCatalog.For(catalog.GetById("system_tool")!).PrimaryInput);
Assert.AreEqual(ToolLayoutKind.SystemLauncher, ToolPageSpecCatalog.For(catalog.GetById("system_tool")!).Layout);
Assert.AreEqual(ToolPrimaryInputKind.None, ToolPageSpecCatalog.For(catalog.GetById("baidu_hot")!).PrimaryInput);
Assert.IsTrue(ToolPageSpecCatalog.For(catalog.GetById("archive_tool")!).RequiresFilePicker);
Assert.IsTrue(ToolPageSpecCatalog.For(catalog.GetById("image_metadata_inspector")!).RequiresFilePicker);
Assert.AreEqual(ToolLayoutKind.CodecWorkbench, ToolPageSpecCatalog.For(catalog.GetById("compression_codec")!).Layout);
Assert.AreEqual(ToolLayoutKind.SecurityWorkbench, ToolPageSpecCatalog.For(catalog.GetById("totp_generator")!).Layout);
Assert.AreEqual(ToolLayoutKind.TextWorkbench, ToolPageSpecCatalog.For(catalog.GetById("color_contrast_checker")!).Layout);
Assert.AreEqual(ToolLayoutKind.QueryDashboard, ToolPageSpecCatalog.For(catalog.GetById("url_redirect_trace")!).Layout);
Assert.AreEqual(ToolLayoutKind.FormatterWorkbench, ToolPageSpecCatalog.For(catalog.GetById("markdown_preview")!).Layout);
Assert.AreEqual(ToolLayoutKind.RandomCinema, ToolPageSpecCatalog.For(catalog.GetById("random_cinema")!).Layout);
Assert.AreEqual(ToolCategory.Image, catalog.GetById("random_cinema")!.Metadata.Category);
}
[TestMethod]
public void ModelStudioToolIsRemovedFromCatalogAndSpecs()
{
var catalog = new ToolCatalog();
Assert.IsNull(catalog.GetById("mmd_model_studio"));
Assert.IsFalse(ToolPageSpecCatalog.CreateFor(catalog).ContainsKey("mmd_model_studio"));
}
[TestMethod]
public void ToolAssistAndInteractionModesMatchRepresentativeExperience()
{
var catalog = new ToolCatalog();
Assert.AreEqual(ToolAssistPanelMode.Custom, ToolPageSpecCatalog.For(catalog.GetById("color_picker")!).AssistPanelMode);
Assert.AreEqual(ToolInteractionMode.LivePreview, ToolPageSpecCatalog.For(catalog.GetById("color_picker")!).InteractionMode);
Assert.AreEqual(ToolPageExperienceKind.LivePreview, ToolPageSpecCatalog.For(catalog.GetById("color_picker")!).PageExperience);
Assert.AreEqual(ToolAssistPanelMode.Hidden, ToolPageSpecCatalog.For(catalog.GetById("system_info")!).AssistPanelMode);
Assert.AreEqual(ToolAssistPanelMode.Hidden, ToolPageSpecCatalog.For(catalog.GetById("baidu_hot")!).AssistPanelMode);
Assert.AreEqual(ToolAssistPanelMode.RulesOnly, ToolPageSpecCatalog.For(catalog.GetById("json_formatter")!).AssistPanelMode);
Assert.AreEqual(ToolAssistPanelMode.RulesOnly, ToolPageSpecCatalog.For(catalog.GetById("base64_codec")!).AssistPanelMode);
Assert.AreEqual(ToolAssistPanelMode.SourceOnly, ToolPageSpecCatalog.For(catalog.GetById("weather")!).AssistPanelMode);
Assert.AreEqual(ToolInteractionMode.ReferenceBrowser, ToolPageSpecCatalog.For(catalog.GetById("mime_lookup")!).InteractionMode);
Assert.AreEqual(ToolPageExperienceKind.RemoteDashboard, ToolPageSpecCatalog.For(catalog.GetById("baidu_hot")!).PageExperience);
Assert.AreEqual(ToolPageExperienceKind.FormCalculator, ToolPageSpecCatalog.For(catalog.GetById("bmi_calculator")!).PageExperience);
Assert.AreEqual(ToolPageExperienceKind.FileWorkflow, ToolPageSpecCatalog.For(catalog.GetById("archive_tool")!).PageExperience);
}
[TestMethod]
public void RuleDrivenToolsHaveRuleSpecsAndSourceOnlyCopyIsSanitized()
{
var catalog = new ToolCatalog();
var specs = ToolPageSpecCatalog.CreateFor(catalog);
foreach (var spec in specs.Values.Where(spec => spec.AssistPanelMode == ToolAssistPanelMode.RulesOnly))
{
Assert.IsTrue(spec.Rules.Count > 0 || spec.RequiresFilePicker || spec.ParameterControls.Contains(ToolParameterControlKind.NumberBox), spec.ToolId);
}
foreach (var spec in specs.Values.Where(spec => spec.AssistPanelMode == ToolAssistPanelMode.SourceOnly))
{
var uiCopy = $"{spec.EmptyState} {spec.ErrorHint}";
Assert.IsFalse(uiCopy.Contains("http://", StringComparison.OrdinalIgnoreCase), spec.ToolId);
Assert.IsFalse(uiCopy.Contains("https://", StringComparison.OrdinalIgnoreCase), spec.ToolId);
Assert.IsFalse(uiCopy.Contains(".json", StringComparison.OrdinalIgnoreCase), spec.ToolId);
}
}
[TestMethod]
public void RuleAndParameterSpecsCarryBilingualText()
{
var catalog = new ToolCatalog();
var json = ToolPageSpecCatalog.For(catalog.GetById("json_formatter")!);
var port = ToolPageSpecCatalog.For(catalog.GetById("port_lookup")!);
Assert.AreEqual("模式", json.Rules[0].LocalizedLabel?.ForLanguage("zh-CN"));
Assert.AreEqual("Mode", json.Rules[0].LocalizedLabel?.ForLanguage("en-US"));
Assert.AreEqual("格式化", json.Rules[0].Options[0].LocalizedLabel?.ForLanguage("zh-CN"));
Assert.AreEqual("Format", json.Rules[0].Options[0].LocalizedLabel?.ForLanguage("en-US"));
Assert.AreEqual("来源", port.Rules[0].LocalizedLabel?.ForLanguage("zh-CN"));
Assert.AreEqual("Source", port.Rules[0].LocalizedLabel?.ForLanguage("en-US"));
Assert.AreEqual("预设", port.Parameters[0].LocalizedLabel?.ForLanguage("zh-CN"));
Assert.AreEqual("Preset", port.Parameters[0].LocalizedLabel?.ForLanguage("en-US"));
}
[TestMethod]
public void PageExperienceRulesKeepToolPagesCompact()
{
var catalog = new ToolCatalog();
var color = ToolPageSpecCatalog.For(catalog.GetById("color_picker")!);
Assert.AreEqual(ToolPageExperienceKind.LivePreview, color.PageExperience);
Assert.AreEqual(string.Empty, color.PrimaryActionLabel);
var hot = ToolPageSpecCatalog.For(catalog.GetById("hotboard")!);
Assert.AreEqual(ToolPageExperienceKind.RemoteDashboard, hot.PageExperience);
Assert.IsFalse(hot.AutoRunOnOpen);
Assert.AreEqual("查询", hot.PrimaryActionLabel);
Assert.IsTrue(hot.ShowInputHeader);
var bmi = ToolPageSpecCatalog.For(catalog.GetById("bmi_calculator")!);
Assert.AreEqual(ToolPageExperienceKind.FormCalculator, bmi.PageExperience);
Assert.AreEqual("计算", bmi.PrimaryActionLabel);
Assert.AreNotEqual(ToolPrimaryInputKind.MultilineText, bmi.PrimaryInput);
var json = ToolPageSpecCatalog.For(catalog.GetById("json_formatter")!);
Assert.AreEqual(ToolPageExperienceKind.OneShotTransform, json.PageExperience);
Assert.AreEqual("格式化", json.PrimaryActionLabel);
}
[TestMethod]
public void CalculatorToolSpecsUseNativeFormInputsByArity()
{
var catalog = new ToolCatalog();
Assert.AreEqual(ToolPrimaryInputKind.NumberPair, ToolPageSpecCatalog.For(catalog.GetById("bmi_calculator")!).PrimaryInput);
Assert.AreEqual(ToolPrimaryInputKind.NumberTriple, ToolPageSpecCatalog.For(catalog.GetById("loan_emi_calculator")!).PrimaryInput);
Assert.AreEqual(ToolPrimaryInputKind.NumberQuad, ToolPageSpecCatalog.For(catalog.GetById("compound_interest_calculator")!).PrimaryInput);
Assert.AreEqual(ToolPrimaryInputKind.DateRange, ToolPageSpecCatalog.For(catalog.GetById("shelf_life")!).PrimaryInput);
Assert.IsTrue(ToolPageSpecCatalog.For(catalog.GetById("shelf_life")!).UsesNumberBox);
}
[TestMethod]
public void SpecsCoverEveryStructuredResultKind()
{
var catalog = new ToolCatalog();
var resultKinds = catalog.Modules
.Select(module => ToolPageSpecCatalog.For(module).Result)
.Distinct()
.ToHashSet();
foreach (ToolResultKind kind in Enum.GetValues<ToolResultKind>())
{
Assert.Contains(kind, resultKinds, kind.ToString());
}
}
[TestMethod]
public async Task ToolExecutorReturnsStructuredDocuments()
{
var catalog = new ToolCatalog();
var samples = new Dictionary<string, string>
{
["json_formatter"] = "{\"name\":\"YMhut Box\"}",
["base64_codec"] = "YMhut Box",
["bmi_calculator"] = "70 1.75",
["text_diff"] = "WinUI\n---\nWinUI 3",
["color_picker"] = "#3399ff"
};
foreach (var sample in samples)
{
var module = catalog.GetById(sample.Key)!;
var result = await ToolExecutor.ExecuteAsync(module, sample.Value);
Assert.IsTrue(result.Ok, sample.Key);
Assert.IsNotNull(result.Document, sample.Key);
Assert.AreEqual(module.Id, result.Document.ToolId);
Assert.IsNotEmpty(result.Document.Blocks, sample.Key);
Assert.AreEqual(result.Output, result.Document.RawText);
}
}
[TestMethod]
public void EveryResultKindBuildsStructuredDocumentBlocks()
{
var catalog = new ToolCatalog();
var samples = new Dictionary<string, string>
{
["ascii_art"] = "YMhut",
["xml_formatter"] = "<root><name>YMhut</name></root>",
["json_formatter"] = "{\"name\":\"YMhut\"}",
["base64_codec"] = "Encode:\nWU1odXQ=",
["regex_tool"] = "match | value\n1 | YMhut",
["hotboard"] = "1. YMhut 热点",
["cctv_news"] = "1. 新闻标题",
["qr_generator"] = "https://example.com/qr.png",
["smart_search"] = "Bing - https://www.bing.com/search?q=YMhut",
["archive_tool"] = "已创建归档:sample.zip",
["text_diff"] = "- old\n+ new",
["hash_manifest_verify"] = "OK hello",
["color_picker"] = "HEX: #336699\nRGB: 51, 102, 153",
["number_base"] = "DEC: 255\nHEX: 0xFF",
["system_tool"] = "OS: Windows\nProcessors: 8",
["random_cinema"] = "https://example.com/media.mp4",
["port_lookup"] = "数据源:YMhut Reference Data / IANA\n80 - HTTP: 网页服务"
};
foreach (ToolResultKind kind in Enum.GetValues<ToolResultKind>())
{
var module = catalog.Modules.FirstOrDefault(module => ToolPageSpecCatalog.For(module).Result == kind);
Assert.IsNotNull(module, kind.ToString());
var output = samples.TryGetValue(module.Id, out var sample) ? sample : "Key: Value\nItem: Result";
var document = ToolResultBuilder.FromOutput(module, output, "zh-CN");
Assert.AreEqual(module.Id, document.ToolId, kind.ToString());
Assert.AreEqual(kind, document.ResultKind, kind.ToString());
Assert.IsNotEmpty(document.Blocks, kind.ToString());
Assert.AreEqual("ok", document.Status, kind.ToString());
}
}
}
+966
View File
@@ -0,0 +1,966 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.IO.Compression;
using System.Text.Json;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Api;
using YMhut.Box.Core.Data;
using YMhut.Box.Core.Net;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class ToolExecutorTests
{
[TestMethod]
public async Task JsonFormatterFormatsInput()
{
var module = new ToolCatalog().GetById("json_formatter")!;
var result = await ToolExecutor.ExecuteAsync(module, "{\"name\":\"YMhut\"}");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "\"name\": \"YMhut\"");
}
[TestMethod]
public async Task NumberBaseSupportsCatalogId()
{
var module = new ToolCatalog().GetById("number_base")!;
var result = await ToolExecutor.ExecuteAsync(module, "255");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "HEX: 0xFF");
}
[TestMethod]
public async Task NumberBaseConverterSupportsPlanId()
{
var module = new ToolCatalog().GetById("number_base_converter")!;
var result = await ToolExecutor.ExecuteAsync(module, "255");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "HEX: 0xFF");
}
[TestMethod]
public async Task UuToolSuiteListsNativeTools()
{
var module = new ToolCatalog().GetById("ymhut_uutool_suite")!;
var result = await ToolExecutor.ExecuteAsync(module, "json");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "YMhut UU Tool Suite");
StringAssert.Contains(result.Output, "json_formatter");
}
[TestMethod]
public async Task ApiToolUsesApiManagerEndpoint()
{
var module = new ToolCatalog().GetById("dns_query")!;
var manager = new ApiManager(new FakeHttpService("""
{
"Status": 0,
"Question": [{ "name": "example.com.", "type": 1 }],
"Answer": [{ "name": "example.com.", "type": 1, "TTL": 300, "data": "1.2.3.4" }]
}
"""));
var result = await ToolExecutor.ExecuteAsync(module, "example.com", apiManager: manager);
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "Google Public DNS");
StringAssert.Contains(result.Output, "1.2.3.4");
Assert.IsNotNull(result.Document);
Assert.IsTrue(result.Document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Table));
}
[TestMethod]
public async Task QqProfileUsesNewUserInfoSourceAndDisplaysReturnedAvatar()
{
var module = new ToolCatalog().GetById("qq_avatar")!;
var manager = new ApiManager(new FakeHttpService("""
{
"qq": "10001",
"nickname": "Xiao Ming",
"long_nick": "Good day",
"avatar_url": "http://q.qlogo.cn/g?b=qq&nk=10001&s=640",
"age": 25,
"sex": "male",
"qid": "xiaoming2024",
"qq_level": 64,
"location": "Guangdong Shenzhen",
"email": "10001@qq.com",
"is_vip": true,
"is_years_vip": true,
"is_svip": false,
"is_big_club": true,
"vip_level": 7,
"reg_time": "2008-03-15T10:30:00Z",
"last_updated": "2024-08-14T15:45:30Z"
}
"""));
var result = await ToolExecutor.ExecuteAsync(module, "10001", apiManager: manager, language: "zh-CN");
Assert.IsTrue(result.Ok);
Assert.IsNotNull(result.Document);
Assert.AreEqual("Uapis QQ User Info", result.Document.SourceName);
Assert.IsTrue(ApiEndpoints.TryGet("qq_avatar", out var endpoint));
Assert.IsTrue(endpoint.ShouldHideEndpoint);
Assert.IsFalse(result.Document.Metadata.ContainsKey("sourceUrl"));
var avatar = result.Document.Blocks.FirstOrDefault(block => block.Kind == ToolResultBlockKind.Image);
Assert.IsNotNull(avatar);
Assert.AreEqual("http://q.qlogo.cn/g?b=qq&nk=10001&s=640", avatar.Uri);
var pairs = result.Document.Blocks.SelectMany(block => block.Pairs).ToArray();
Assert.IsTrue(pairs.Any(pair => pair.Value == "64"));
Assert.IsTrue(pairs.Any(pair => pair.Key.Contains("VIP", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(pair.Value)));
Assert.IsFalse(result.Output.Contains("uapis.cn/api/v1/social/qq/userinfo", StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public async Task QrGeneratorOutputsSvg()
{
var module = new ToolCatalog().GetById("qr_generator")!;
var result = await ToolExecutor.ExecuteAsync(module, "YMhut Box");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "<svg");
Assert.IsNotNull(result.Document);
Assert.IsTrue(result.Document.Blocks.Any(block =>
block.Kind == ToolResultBlockKind.Image &&
block.Metadata is not null &&
block.Metadata.TryGetValue("contentType", out var contentType) &&
contentType == "image/svg+xml"));
Assert.IsTrue(result.Document.Blocks.Any(block =>
block.Kind == ToolResultBlockKind.Image &&
block.Metadata is not null &&
block.Metadata.TryGetValue("pngBase64", out var pngBase64) &&
Convert.TryFromBase64String(pngBase64, new byte[pngBase64.Length], out _)));
Assert.AreEqual(ToolResultKind.ImagePreview, result.Document.ResultKind);
StringAssert.Contains(result.Document.RawText, "<svg");
}
[TestMethod]
public async Task TextDiffMarksAddedAndRemovedLines()
{
var module = new ToolCatalog().GetById("text_diff")!;
var result = await ToolExecutor.ExecuteAsync(module, "alpha\nbeta\n---\nalpha\ngamma");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "- beta");
StringAssert.Contains(result.Output, "+ gamma");
}
[TestMethod]
public async Task ColorPickerConvertsHexToRgb()
{
var module = new ToolCatalog().GetById("color_picker")!;
var result = await ToolExecutor.ExecuteAsync(module, "#336699");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "RGB: 51, 102, 153");
}
[TestMethod]
public async Task AsciiArtRendersBlockText()
{
var module = new ToolCatalog().GetById("ascii_art")!;
var result = await ToolExecutor.ExecuteAsync(module, "YM");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "#");
}
[TestMethod]
public async Task ProfanitySupportsCustomTerms()
{
var module = new ToolCatalog().GetById("profanity_check")!;
var result = await ToolExecutor.ExecuteAsync(module, "secret\n---\nthis is a secret token");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "Potential matches: secret");
StringAssert.Contains(result.Output, "******");
}
[TestMethod]
public async Task ArchiveCreatesZipForFile()
{
var tempRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
try
{
var file = Path.Combine(tempRoot, "sample.txt");
File.WriteAllText(file, "hello");
var module = new ToolCatalog().GetById("archive_tool")!;
var result = await ToolExecutor.ExecuteAsync(module, file);
Assert.IsTrue(result.Ok);
Assert.HasCount(1, Directory.GetFiles(tempRoot, "*.zip"));
}
finally
{
Directory.Delete(tempRoot, recursive: true);
}
}
[TestMethod]
public async Task QqAvatarBuildsStaticUrls()
{
var module = new ToolCatalog().GetById("qq_avatar")!;
var result = await ToolExecutor.ExecuteAsync(module, "10000");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "qlogo.cn");
Assert.IsNotNull(result.Document);
Assert.IsTrue(result.Document.Blocks.Any(block =>
block.Kind == ToolResultBlockKind.Image &&
block.Metadata is not null &&
block.Metadata.TryGetValue("displayMode", out var displayMode) &&
displayMode == "preview"));
Assert.IsTrue(result.Document.Blocks.Any(block =>
block.Kind is ToolResultBlockKind.KeyValue or ToolResultBlockKind.Metric &&
block.Pairs.Any(pair => pair.Key.Equals("QQ", StringComparison.OrdinalIgnoreCase) && pair.Value == "10000")));
}
[TestMethod]
public async Task MarkdownTableNormalizerAlignsColumns()
{
var module = new ToolCatalog().GetById("markdown_table_normalizer")!;
var result = await ToolExecutor.ExecuteAsync(module, "| a | long |\n|---|---|\n| 1 | 2 |");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "| a | long |");
StringAssert.Contains(result.Output, "| --- | ---- |");
}
[TestMethod]
public async Task CompressionCodecRoundTripsGzipBase64()
{
var module = new ToolCatalog().GetById("compression_codec")!;
var compressed = await ToolExecutor.ExecuteAsync(module, "gzip compress\nYMhut Box");
var base64 = compressed.Output
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Single(line => line.StartsWith("Base64:", StringComparison.OrdinalIgnoreCase))
.Replace("Base64:", string.Empty, StringComparison.OrdinalIgnoreCase)
.Trim();
var decoded = await ToolExecutor.ExecuteAsync(module, $"gzip decode\n{base64}");
Assert.IsTrue(compressed.Ok);
StringAssert.Contains(compressed.Output, "Algorithm: gzip");
Assert.IsTrue(decoded.Ok);
Assert.AreEqual("YMhut Box", decoded.Output);
}
[TestMethod]
public async Task TotpGeneratorMatchesRfc6238Sample()
{
var module = new ToolCatalog().GetById("totp_generator")!;
var result = await ToolExecutor.ExecuteAsync(module, "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\ntime=59\ndigits=8");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "Code: 94287082");
StringAssert.Contains(result.Output, "Counter: 1");
}
[TestMethod]
public async Task ColorContrastCheckerReportsWcagPasses()
{
var module = new ToolCatalog().GetById("color_contrast_checker")!;
var result = await ToolExecutor.ExecuteAsync(module, "#000000 #ffffff");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "Contrast: 21.00:1");
StringAssert.Contains(result.Output, "AA normal text: Pass");
StringAssert.Contains(result.Output, "AAA normal text: Pass");
}
[TestMethod]
public async Task MarkdownPreviewBuildsSummaryAndNormalizedText()
{
var module = new ToolCatalog().GetById("markdown_preview")!;
var result = await ToolExecutor.ExecuteAsync(module, "# Title\n\n* item\n\n[YMhut](https://example.com)");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "Headings: 1");
StringAssert.Contains(result.Output, "Links: 1");
StringAssert.Contains(result.Output, "Plain preview:");
StringAssert.Contains(result.Output, "Normalized:");
StringAssert.Contains(result.Output, "- item");
}
[TestMethod]
public async Task ImageMetadataInspectorFailsForInvalidPath()
{
var module = new ToolCatalog().GetById("image_metadata_inspector")!;
var result = await ToolExecutor.ExecuteAsync(module, @"Z:\missing\not-an-image.png");
Assert.IsFalse(result.Ok);
StringAssert.Contains(result.Error, "本地图片路径");
}
[TestMethod]
public async Task UrlRedirectTraceRejectsInvalidUrlWithoutNetwork()
{
var module = new ToolCatalog().GetById("url_redirect_trace")!;
var result = await ToolExecutor.ExecuteAsync(module, "not a url", language: "zh-CN");
Assert.IsFalse(result.Ok);
StringAssert.Contains(result.Error, "http 或 https URL");
}
[TestMethod]
public async Task JsonPathHelperListsLeafPaths()
{
var module = new ToolCatalog().GetById("json_path_helper")!;
var result = await ToolExecutor.ExecuteAsync(module, "{\"items\":[{\"name\":\"YMhut\"}]}");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "$.items[0].name = \"YMhut\"");
}
[TestMethod]
public async Task HashManifestBuilderAndVerifierSupportTextRows()
{
var builder = new ToolCatalog().GetById("hash_manifest_builder")!;
var verifier = new ToolCatalog().GetById("hash_manifest_verify")!;
var manifest = await ToolExecutor.ExecuteAsync(builder, "hello");
var result = await ToolExecutor.ExecuteAsync(verifier, manifest.Output);
Assert.IsTrue(manifest.Ok);
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "OK hello");
}
[TestMethod]
public async Task RenamePreviewSupportsIndexedNames()
{
var module = new ToolCatalog().GetById("indexed_rename_preview")!;
var result = await ToolExecutor.ExecuteAsync(module, "prefix=photo_\na.jpg\nb.png");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "a.jpg -> photo_001.jpg");
StringAssert.Contains(result.Output, "b.png -> photo_002.png");
}
[TestMethod]
public async Task ReferenceLookupUsesBundledAuthoritativeData()
{
var module = new ToolCatalog().GetById("mime_lookup")!;
var reference = new ReferenceDataService(AppPaths.ForCurrentUser(
Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N")),
FindAssetsRoot()));
var result = await ToolExecutor.ExecuteAsync(module, "json", referenceDataService: reference);
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "YMhut Reference Data");
StringAssert.Contains(result.Output, "application/json");
}
[TestMethod]
public async Task PortLookupLocalizesDescriptionsAndHidesSourceUrls()
{
var module = new ToolCatalog().GetById("port_lookup")!;
var reference = new ReferenceDataService(AppPaths.ForCurrentUser(
Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N")),
FindAssetsRoot()));
var result = await ToolExecutor.ExecuteAsync(module, "443", referenceDataService: reference, language: "zh-CN");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "HTTPS 加密网页服务");
StringAssert.Contains(result.Output, "数据源:YMhut Reference Data / IANA");
Assert.IsFalse(result.Output.Contains("https://", StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public async Task SmartSearchUsesOnlySelectedChannel()
{
var module = new ToolCatalog().GetById("smart_search")!;
var result = await ToolExecutor.ExecuteAsync(module, "bing\nYMhut Box", language: "zh-CN");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "搜索渠道:Bing");
StringAssert.Contains(result.Output, "https://www.bing.com/search");
Assert.IsFalse(result.Output.Contains("baidu.com", StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public async Task WeatherPrefersChinaWeatherForKnownChineseCity()
{
var module = new ToolCatalog().GetById("weather")!;
var manager = new ApiManager(new FakeHttpService("""
{"weatherinfo":{"city":"北京","weather":"晴","temp1":"10℃","temp2":"20℃","ptime":"11:00"}}
"""));
var result = await ToolExecutor.ExecuteAsync(module, "北京", apiManager: manager, language: "en-US");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "Data source: China Weather");
StringAssert.Contains(result.Output, "City: 北京");
Assert.IsFalse(result.Output.Contains("weather.com.cn", StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public async Task MarkdownConverterRendersJsonObjectAndLanguageLabels()
{
var module = new ToolCatalog().GetById("json_formatter")!;
var result = await ToolExecutor.ExecuteAsync(module, "{\"name\":\"YMhut\",\"count\":2}");
var markdown = ToolMarkdownConverter.FromResult(module, result, "en-US");
Assert.IsTrue(result.Ok);
StringAssert.Contains(markdown.MarkdownText, "## JSON Result");
StringAssert.Contains(markdown.MarkdownText, "| Field | Value |");
StringAssert.Contains(markdown.MarkdownText, "```json");
Assert.IsFalse(markdown.MarkdownText.Contains("字段", StringComparison.Ordinal));
}
[TestMethod]
public async Task MarkdownConverterRendersJsonArrayAsTable()
{
var module = new ToolCatalog().GetById("json_formatter")!;
var result = await ToolExecutor.ExecuteAsync(module, "[{\"name\":\"YMhut\",\"meta\":{\"kind\":\"tool\"}},{\"name\":\"Box\",\"meta\":{\"kind\":\"app\"}}]");
var markdown = ToolMarkdownConverter.FromResult(module, result, "zh-CN");
Assert.IsTrue(result.Ok);
StringAssert.Contains(markdown.MarkdownText, "**项目数**: 2");
StringAssert.Contains(markdown.MarkdownText, "| name | meta |");
StringAssert.Contains(markdown.MarkdownText, "{\"kind\":\"tool\"}");
}
[TestMethod]
public void MarkdownConverterKeepsInvalidJsonInCodeBlock()
{
var module = new ToolCatalog().GetById("json_formatter")!;
var result = ToolExecutionResult.Success("{not-json");
var markdown = ToolMarkdownConverter.FromResult(module, result, "zh-CN");
StringAssert.Contains(markdown.MarkdownText, "JSON 解析失败");
StringAssert.Contains(markdown.MarkdownText, "```json");
StringAssert.Contains(markdown.MarkdownText, "{not-json");
}
[TestMethod]
public async Task MarkdownConverterRendersStructuredResultKinds()
{
var catalog = new ToolCatalog();
var diffModule = catalog.GetById("text_diff")!;
var diff = await ToolExecutor.ExecuteAsync(diffModule, "alpha\n---\nbeta");
var diffMarkdown = ToolMarkdownConverter.FromResult(diffModule, diff, "zh-CN");
StringAssert.Contains(diffMarkdown.MarkdownText, "```diff");
var tableModule = catalog.GetById("markdown_table_normalizer")!;
var table = await ToolExecutor.ExecuteAsync(tableModule, "| a | b |\n|---|---|\n| 1 | 2 |");
var tableMarkdown = ToolMarkdownConverter.FromResult(tableModule, table, "zh-CN");
StringAssert.Contains(tableMarkdown.MarkdownText, "| a | b |");
var systemModule = catalog.GetById("system_tool")!;
var system = await ToolExecutor.ExecuteAsync(systemModule, "status");
var systemMarkdown = ToolMarkdownConverter.FromResult(systemModule, system, "zh-CN");
StringAssert.Contains(systemMarkdown.MarkdownText, "| 字段 | 值 |");
}
[TestMethod]
public async Task MarkdownConverterPreservesSanitizedReferenceSources()
{
var module = new ToolCatalog().GetById("port_lookup")!;
var reference = new ReferenceDataService(AppPaths.ForCurrentUser(
Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N")),
FindAssetsRoot()));
var result = await ToolExecutor.ExecuteAsync(module, "443", referenceDataService: reference, language: "zh-CN");
var markdown = ToolMarkdownConverter.FromResult(module, result, "zh-CN");
Assert.IsTrue(result.Ok);
StringAssert.Contains(markdown.MarkdownText, "YMhut Reference Data / IANA");
Assert.IsFalse(markdown.MarkdownText.Contains("https://", StringComparison.OrdinalIgnoreCase));
Assert.IsNotNull(result.Document);
Assert.IsTrue(result.Document.Blocks.Any(block => block.Kind is ToolResultBlockKind.Metric or ToolResultBlockKind.Table));
Assert.IsFalse(result.Document.Blocks.All(block => block.Kind == ToolResultBlockKind.Text));
}
[TestMethod]
public void RemoteResultBuilderAddsSanitizedSourceAndCustomBlocks()
{
Assert.IsTrue(ApiEndpoints.TryGet("baidu_hot", out var endpoint));
var response = new ApiResponse(
endpoint.Id,
new Uri("https://top.baidu.com/api/board?secret=hidden"),
true,
"{}",
null,
DateTimeOffset.Parse("2026-05-27T10:00:00+08:00"));
var output = """
?
О? ?026-05-27 10:00:00 +08:00
1. YMhut Box ?00
樿
""";
var document = ToolResultBuilder.FromRemote(endpoint, response, output, "zh-CN");
Assert.AreEqual("PearAPI DailyHot", document.SourceName);
Assert.AreEqual(ToolResultBlockKind.RankedList, document.Blocks[0].Kind);
Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.KeyValue));
Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.RankedList));
Assert.AreEqual("source", document.Blocks[^1].Metadata?["displayMode"]);
Assert.IsFalse(document.Metadata.ContainsKey("sourceUrl"));
Assert.IsFalse(document.Blocks.Any(block =>
block.Metadata is not null &&
block.Metadata.ContainsKey("sourceUrl")));
Assert.IsFalse(document.Blocks.SelectMany(block => block.Pairs).Any(pair =>
pair.Key.Contains("鎺ュ彛鍦板潃", StringComparison.OrdinalIgnoreCase) ||
pair.Key.Contains("Endpoint", StringComparison.OrdinalIgnoreCase)));
Assert.IsFalse(document.Blocks.SelectMany(block => block.Pairs).Any(pair => pair.Value.Contains("secret=hidden", StringComparison.OrdinalIgnoreCase)));
Assert.IsFalse(document.RawText.Contains("secret=hidden", StringComparison.OrdinalIgnoreCase));
}
[TestMethod]
public void RemoteFailureBuildsRenderableDocumentInsteadOfEmptyFailure()
{
Assert.IsTrue(ApiEndpoints.TryGet("bili_hot", out var endpoint));
var response = new ApiResponse(
endpoint.Id,
new Uri("https://api.bilibili.com/x/web-interface/popular"),
false,
"Forbidden",
"HTTP 403 Forbidden",
DateTimeOffset.Parse("2026-05-27T10:00:00+08:00"),
403);
var result = RemoteToolFormatter.FormatFailure(endpoint, response, string.Empty, "zh-CN");
Assert.IsTrue(result.Ok);
Assert.IsNotNull(result.Document);
Assert.AreEqual("error", result.Document.Status);
Assert.IsTrue(result.Document.Blocks.Count >= 2);
StringAssert.Contains(result.Output, "源站拒绝");
Assert.IsFalse(result.Output.Contains(endpoint.SourceUrl, StringComparison.OrdinalIgnoreCase));
Assert.IsFalse(result.Output.Contains("接口地址", StringComparison.OrdinalIgnoreCase));
Assert.IsFalse(result.Output.Contains("Endpoint", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(result.Document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Text));
Assert.AreEqual("source", result.Document.Blocks[^1].Metadata?["displayMode"]);
}
[TestMethod]
public void RemoteRankingFormatterRecoversCardsFromAlternativeJson()
{
Assert.IsTrue(ApiEndpoints.TryGet("bili_hot", out var endpoint));
var response = new ApiResponse(
endpoint.Id,
new Uri("https://api.bilibili.com/x/web-interface/popular"),
true,
"""
{
"data": {
"items": [
{ "title": "First video", "owner": { "name": "UP" }, "stat": { "view": 123456 } },
{ "title": "Second video", "stat": { "view": 888 } }
]
}
}
""",
null,
DateTimeOffset.Parse("2026-05-27T10:00:00+08:00"),
200);
var result = RemoteToolFormatter.Format(endpoint, response, string.Empty, "zh-CN");
Assert.IsTrue(result.Ok);
Assert.IsNotNull(result.Document);
var cards = result.Document.Blocks.FirstOrDefault(block => block.Kind == ToolResultBlockKind.RankedList);
Assert.IsNotNull(cards);
Assert.AreEqual("1", cards.Items[0].Leading);
StringAssert.Contains(cards.Items[0].Title, "First video");
StringAssert.Contains(cards.Items[0].Title + cards.Items[0].Subtitle, "123,456");
}
[TestMethod]
public void RemoteRankingFormatterRecoversCardsFromEmbeddedHtml()
{
Assert.IsTrue(ApiEndpoints.TryGet("zhihu_hot", out var endpoint));
var response = new ApiResponse(
endpoint.Id,
new Uri("https://www.zhihu.com/hot"),
true,
"""
<html><body>
<script id="js-initialData" type="application/json">
{"initialState":{"topstory":{"hotList":[{"target":{"title":"Hot title one"}},{"target":{"title":"Hot title two"}}]}}}
</script>
</body></html>
""",
null,
DateTimeOffset.Parse("2026-05-27T10:00:00+08:00"),
200);
var result = RemoteToolFormatter.Format(endpoint, response, string.Empty, "zh-CN");
Assert.IsTrue(result.Ok);
Assert.IsNotNull(result.Document);
var cards = result.Document.Blocks.FirstOrDefault(block => block.Kind == ToolResultBlockKind.RankedList);
Assert.IsNotNull(cards);
Assert.AreEqual(2, cards.Items.Count);
Assert.AreEqual("1", cards.Items[0].Leading);
StringAssert.Contains(cards.Items[0].Title, "Hot title one");
}
[TestMethod]
public void RemoteRankingResultIgnoresSanitizedSourceNoticeInCards()
{
Assert.IsTrue(ApiEndpoints.TryGet("hotboard", out var endpoint));
var response = new ApiResponse(
endpoint.Id,
new Uri("https://top.baidu.com/api/board?secret=hidden"),
true,
"{}",
null,
DateTimeOffset.Parse("2026-05-27T10:00:00+08:00"));
var output = string.Join(Environment.NewLine, new[]
{
"\u6570\u636e\u6e90\uff1aBaidu Hot\uff08official\uff09",
"\u6765\u6e90\u8bf4\u660e\uff1a\u5df2\u9690\u85cf\u8fdc\u7a0b\u5730\u5740\uff0c\u4ec5\u5c55\u793a\u8131\u654f\u6765\u6e90\u540d\u79f0\u3002",
"\u83b7\u53d6\u65f6\u95f4\uff1a2026-05-27 10:00:00 +08:00",
"",
"1. YMhut Box Hot",
" Summary line",
"2. WinUI Tools",
" Another summary"
});
var document = ToolResultBuilder.FromRemote(endpoint, response, output, "zh-CN");
var cards = document.Blocks.FirstOrDefault(block => block.Kind == ToolResultBlockKind.RankedList);
Assert.IsNotNull(cards);
Assert.IsGreaterThanOrEqualTo(cards.Items.Count, 2);
Assert.AreEqual("1", cards.Items[0].Leading);
StringAssert.Contains(cards.Items[0].Title, "YMhut Box Hot");
Assert.IsFalse(cards.Items.Any(item =>
item.Title.Contains("\u5df2\u9690\u85cf\u8fdc\u7a0b\u5730\u5740", StringComparison.OrdinalIgnoreCase) ||
item.Subtitle.Contains("\u5df2\u9690\u85cf\u8fdc\u7a0b\u5730\u5740", StringComparison.OrdinalIgnoreCase) ||
item.Title.Contains("remote address is hidden", StringComparison.OrdinalIgnoreCase) ||
item.Subtitle.Contains("remote address is hidden", StringComparison.OrdinalIgnoreCase)));
Assert.IsFalse(document.Metadata.ContainsKey("sourceUrl"));
}
[TestMethod]
public void GoldPriceRemoteResultBuildsTrendChart()
{
Assert.IsTrue(ApiEndpoints.TryGet("gold_price", out var endpoint));
var response = new ApiResponse(
endpoint.Id,
new Uri("https://prices.lbma.org.uk/json/gold_pm.json"),
true,
"{}",
null,
DateTimeOffset.Parse("2026-06-14T10:00:00+08:00"));
var output = string.Join(Environment.NewLine, new[]
{
"LBMA Gold PM Fixing",
"日期:2026-06-12",
"USD/oz3362.15",
"GBP/oz2491.22",
"",
"Date | USD/oz | GBP/oz",
"2026-06-10 | 3320.1 | 2460.4",
"2026-06-11 | 3340.8 | 2475.2",
"2026-06-12 | 3362.15 | 2491.22"
});
var document = ToolResultBuilder.FromRemote(endpoint, response, output, "zh-CN");
Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Metric));
Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Table));
var chart = document.Blocks.FirstOrDefault(block => block.Kind == ToolResultBlockKind.LineChart);
Assert.IsNotNull(chart);
Assert.IsGreaterThanOrEqualTo(chart.Rows.Count, 4);
Assert.AreEqual("USD/oz", chart.Metadata?["chartUnit"]);
}
[TestMethod]
public void UpdateNoticeJsonKeepsPlainTextAndAddsMarkdown()
{
var repoRoot = Directory.GetParent(FindAssetsRoot())!.FullName;
var noticePath = Path.Combine(repoRoot, "update-notice", "2.0.6.3.json");
var totalPath = Path.Combine(repoRoot, "update-notice", "total.json");
using var notice = JsonDocument.Parse(File.ReadAllText(noticePath));
var noticeRoot = notice.RootElement;
Assert.AreEqual("2.0.6.3", noticeRoot.GetProperty("app_version").GetString());
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("message").GetString()));
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("release_notes").GetString()));
StringAssert.Contains(noticeRoot.GetProperty("message_md").GetString(), "YMhut Box 2.0.6.3");
StringAssert.Contains(noticeRoot.GetProperty("release_notes_md").GetString(), "QQ 信息");
StringAssert.Contains(noticeRoot.GetProperty("release_notes_md").GetString(), "安全浏览器");
using var total = JsonDocument.Parse(File.ReadAllText(totalPath));
var totalRoot = total.RootElement;
Assert.AreEqual("2.0.6.3", totalRoot.GetProperty("latest_version").GetString());
var latest = totalRoot.GetProperty("latest");
Assert.AreEqual("2.0.6.3", latest.GetProperty("version").GetString());
StringAssert.Contains(latest.GetProperty("release_notes_md").GetString(), "QQ 信息");
Assert.AreEqual("2.0.6.3", totalRoot.GetProperty("versions")[0].GetProperty("version").GetString());
StringAssert.Contains(totalRoot.GetProperty("versions")[0].GetProperty("summary").GetString(), "QQ 信息");
}
[TestMethod]
public void WeatherRemoteResultBuildsSummaryForecastAndSourceMetadata()
{
Assert.IsTrue(ApiEndpoints.TryGet("weather", out var endpoint));
var response = new ApiResponse(endpoint.Id, new Uri("https://api.open-meteo.com/v1/forecast"), true, "{}", null, DateTimeOffset.Now);
var output = """
Data source: Open-Meteo (public trusted source)
Fetched at: 2026-05-28 10:00:00 +08:00
Location: Beijing China
Current temperature: 22.5 C
Apparent temperature: 23 C
Relative humidity: 56%
Wind speed: 12 km/h
Next three days:
2026-05-28: 18 - 27 C
2026-05-29: 19 - 28 C
""";
var document = ToolResultBuilder.FromRemote(endpoint, response, output, "zh-CN");
Assert.IsTrue(document.Metadata.TryGetValue("sourceVisibility", out var visibility));
Assert.AreEqual(ApiSourceVisibility.PublicTrusted.ToString(), visibility);
Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Metric));
Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Table));
}
[TestMethod]
public void HistoryRemoteResultBuildsTimelineInsteadOfPlainText()
{
Assert.IsTrue(ApiEndpoints.TryGet("history_today", out var endpoint));
var response = new ApiResponse(endpoint.Id, new Uri("https://api.wikimedia.org/feed/v1/wikipedia/zh/onthisday/all/05/28"), true, "{}", null, DateTimeOffset.Now);
var output = """
EVENTS
- 585: Historical event
- 1926: Another event
BIRTHS
- 1908: Person born
""";
var document = ToolResultBuilder.FromRemote(endpoint, response, output, "zh-CN");
Assert.IsTrue(document.Blocks.Any(block => block.Kind == ToolResultBlockKind.Timeline));
Assert.IsFalse(document.Blocks.All(block => block.Kind == ToolResultBlockKind.Text));
}
[TestMethod]
public async Task LocalToolFamiliesReturnSemanticDocumentBlocks()
{
var catalog = new ToolCatalog();
var hash = await ToolExecutor.ExecuteAsync(catalog.GetById("hash_generator")!, "YMhut");
Assert.IsTrue(hash.Document!.Blocks.Any(block => block.Kind == ToolResultBlockKind.Table));
var base64 = await ToolExecutor.ExecuteAsync(catalog.GetById("base64_codec")!, "YMhut");
Assert.IsTrue(base64.Document!.Blocks.Any(block => block.Kind == ToolResultBlockKind.KeyValue));
var calculator = await ToolExecutor.ExecuteAsync(catalog.GetById("number_base")!, "255");
Assert.IsTrue(calculator.Document!.Blocks.Any(block => block.Kind == ToolResultBlockKind.Metric));
var diff = await ToolExecutor.ExecuteAsync(catalog.GetById("text_diff")!, "a\n---\nb");
Assert.IsTrue(diff.Document!.Blocks.Any(block => block.Kind == ToolResultBlockKind.Diff));
}
[TestMethod]
public async Task DataSetToolsReadBundledAssets()
{
var catalog = new ToolCatalog();
var reference = new ReferenceDataService(AppPaths.ForCurrentUser(
Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N")),
FindAssetsRoot()));
var car = await ToolExecutor.ExecuteAsync(catalog.GetById("car_info")!, "京A12345", referenceDataService: reference);
var skin = await ToolExecutor.ExecuteAsync(catalog.GetById("sanguosha_skin")!, "刘备", referenceDataService: reference);
Assert.IsTrue(car.Ok);
Assert.IsTrue(skin.Ok);
StringAssert.Contains(car.Output, "北京");
StringAssert.Contains(skin.Output, "刘备");
}
[TestMethod]
public async Task ReferenceDataServiceReadsDatPackageWhenJsonIsPacked()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
var assetsRoot = Path.Combine(tempRoot, "Assets");
var dataRoot = Path.Combine(assetsRoot, "data");
Directory.CreateDirectory(dataRoot);
var datPath = Path.Combine(dataRoot, "reference-data.dat");
using (var archive = ZipFile.Open(datPath, ZipArchiveMode.Create))
{
var entry = archive.CreateEntry("data/reference/ymhut_reference_data.json");
await using var stream = entry.Open();
await using var writer = new StreamWriter(stream);
await writer.WriteAsync("{\"source\":\"packed\"}");
}
try
{
var service = new ReferenceDataService(AppPaths.ForCurrentUser(tempRoot, assetsRoot));
var text = await service.ReadTextAsync("data/reference/ymhut_reference_data.json");
Assert.IsTrue(service.Exists("data/reference/ymhut_reference_data.json"));
Assert.AreEqual("{\"source\":\"packed\"}", text);
}
finally
{
Directory.Delete(tempRoot, recursive: true);
}
}
[TestMethod]
public async Task SystemToolProvidesNativeStatus()
{
var module = new ToolCatalog().GetById("system_tool")!;
var result = await ToolExecutor.ExecuteAsync(module, "status");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, ".NET");
StringAssert.Contains(result.Output, "calculator");
}
[TestMethod]
public void OfficialSourceHidesPublicSourceUrlInDocumentMetadata()
{
var endpoint = new ApiEndpoint(
"dns_query",
"DNS",
"https://dns.google/resolve?name={value}",
"Google Public DNS",
"https://developers.google.com/speed/public-dns/docs/doh/json");
var response = new ApiResponse(endpoint.Id, new Uri("https://dns.google/resolve?name=example.com"), true, "Status: 0", null, DateTimeOffset.Now);
var document = ToolResultBuilder.FromRemote(endpoint, response, "Status: 0", "zh-CN");
Assert.IsFalse(document.Metadata.ContainsKey("sourceUrl"));
Assert.IsFalse(document.Blocks.Any(block =>
block.Metadata is not null &&
block.Metadata.ContainsKey("sourceUrl")));
Assert.IsTrue(document.Blocks.Any(block =>
block.Metadata is not null &&
block.Metadata.TryGetValue("sourceVisibility", out var visibility) &&
visibility == ApiSourceVisibility.PublicOfficial.ToString()));
}
[TestMethod]
public void SensitivePrivateSourceHidesEndpointInDocumentMetadata()
{
var endpoint = new ApiEndpoint(
"media_types",
"Media config",
"https://update.ymhut.cn/media-types.json",
"YMhut remote config",
"https://update.ymhut.cn/media-types.json",
false,
ApiSourceVisibility.SensitivePrivate);
var response = new ApiResponse(endpoint.Id, new Uri(endpoint.SourceUrl), true, "{}", null, DateTimeOffset.Now);
var document = ToolResultBuilder.FromRemote(endpoint, response, "{}", "zh-CN");
Assert.IsFalse(document.Metadata.Values.Any(value => value.Contains("update.ymhut.cn", StringComparison.OrdinalIgnoreCase)));
Assert.IsTrue(document.Blocks.Any(block =>
block.Metadata is not null &&
block.Metadata.TryGetValue("sourceSensitivity", out var sensitivity) &&
sensitivity == "sensitive"));
Assert.IsFalse(document.Blocks.Any(block =>
block.Metadata is not null &&
block.Metadata.Values.Any(value => value.Contains("update.ymhut.cn", StringComparison.OrdinalIgnoreCase))));
}
[TestMethod]
public void HttpDiagnosticHidesDynamicTargetAsEndpoint()
{
Assert.IsTrue(ApiEndpoints.TryGet("http_diagnostic", out var endpoint));
var response = new ApiResponse(endpoint.Id, new Uri("https://example.com/private/path?token=secret"), true, "<title>Example</title>", null, DateTimeOffset.Now);
var document = ToolResultBuilder.FromRemote(endpoint, response, "Example\n1. Status page", "zh-CN");
Assert.IsTrue(endpoint.ShouldHideEndpoint);
Assert.IsFalse(document.Metadata.ContainsKey("sourceUrl"));
Assert.IsFalse(document.Blocks.Any(block =>
block.Metadata is not null &&
block.Metadata.TryGetValue("sourceUrl", out var sourceUrl) &&
sourceUrl.Contains("example.com", StringComparison.OrdinalIgnoreCase)));
}
private sealed class FakeHttpService(string content) : IHttpService
{
public Task<HttpServiceResult> GetAsync(Uri uri, CancellationToken cancellationToken = default)
{
return Task.FromResult(new HttpServiceResult(System.Net.HttpStatusCode.OK, content, new Dictionary<string, string[]>(), TimeSpan.FromMilliseconds(1)));
}
public Task<string> GetStringAsync(Uri uri, CancellationToken cancellationToken = default)
{
return Task.FromResult(content);
}
public Task<HttpServiceResult> SendAsync(
Uri uri,
string method = "GET",
string? body = null,
IReadOnlyDictionary<string, string>? headers = null,
bool ensureSuccess = true,
HttpRequestPolicy? policy = null,
CancellationToken cancellationToken = default)
{
return GetAsync(uri, cancellationToken);
}
}
private static string FindAssetsRoot()
{
var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (directory is not null)
{
var candidate = Path.Combine(directory.FullName, "assets");
if (File.Exists(Path.Combine(candidate, "data", "reference", "ymhut_reference_data.json")))
{
return candidate;
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Unable to locate repository assets directory.");
}
}
@@ -0,0 +1,60 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.Api;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class ToolRemoteRankingTests
{
[TestMethod]
public void HotboardUsesPlatformSelectionBeforeRunning()
{
var module = new ToolCatalog().GetById("hotboard")!;
var spec = ToolPageSpecCatalog.For(module);
Assert.AreEqual(ToolPrimaryInputKind.FixedOptions, spec.PrimaryInput);
Assert.IsFalse(spec.AutoRunOnOpen);
Assert.IsTrue(spec.Parameters.Any(parameter =>
parameter.Key == "preset" &&
parameter.Options.Any(option => option.Value == "百度")));
}
[TestMethod]
public void HotboardPlatformResolvesDailyHotTitleParameter()
{
var ok = ApiEndpoints.TryResolve("hotboard", "百度", out var endpoint, out var uri);
Assert.IsTrue(ok);
Assert.AreEqual("hotboard", endpoint.Id);
StringAssert.Contains(Uri.UnescapeDataString(uri.Query), "title=百度");
}
[TestMethod]
public void DailyHotRankingKeepsPublicUriOutOfVisibleTitle()
{
Assert.IsTrue(ApiEndpoints.TryGet("hotboard", out var endpoint));
var uri = new Uri("https://example.invalid/api/dailyhot?title=test");
var response = new ApiResponse(
endpoint.Id,
uri,
Success: true,
Content: string.Empty,
Error: null,
FetchedAt: DateTimeOffset.UnixEpoch);
var document = ToolResultBuilder.FromRemote(
endpoint,
response,
"""
1. YMhut / 999 / https://example.com/news?id=1
2. https://example.com/news?id=2
""");
var ranked = document.Blocks.First(block => block.Kind == ToolResultBlockKind.RankedList);
var first = ranked.Items[0];
Assert.AreEqual("https://example.com/news?id=1", first.Uri);
Assert.IsFalse(first.Title.Contains("https://", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(ranked.Items.All(item => !item.Title.Contains("https://", StringComparison.OrdinalIgnoreCase)));
}
}
@@ -0,0 +1,64 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class ToolResultExperienceCatalogTests
{
[TestMethod]
public void EveryDefaultToolHasUniqueResultExperience()
{
var catalog = new ToolCatalog(ToolCatalog.DefaultModules());
var experiences = new ToolResultExperienceCatalog(catalog);
Assert.AreEqual(catalog.Modules.Count, experiences.Experiences.Count);
var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var module in catalog.Modules)
{
var experience = experiences.GetRequired(module.Id);
Assert.AreEqual(module.Id, experience.ToolId);
Assert.IsFalse(string.IsNullOrWhiteSpace(experience.ExperienceId), module.Id);
Assert.IsFalse(string.IsNullOrWhiteSpace(experience.Layout), module.Id);
Assert.IsFalse(string.IsNullOrWhiteSpace(experience.PrimaryPrimitive), module.Id);
Assert.IsTrue(ids.Add(experience.ExperienceId), $"Duplicate experienceId: {experience.ExperienceId}");
}
}
[TestMethod]
public void EveryDefaultToolHasUniquePageExperience()
{
var catalog = new ToolCatalog(ToolCatalog.DefaultModules());
var experiences = new ToolResultExperienceCatalog(catalog);
Assert.AreEqual(catalog.Modules.Count, experiences.PageExperiences.Count);
var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var module in catalog.Modules)
{
var experience = experiences.GetRequiredPage(module.Id);
Assert.AreEqual(module.Id, experience.ToolId);
Assert.IsFalse(string.IsNullOrWhiteSpace(experience.ExperienceId), module.Id);
Assert.IsFalse(string.IsNullOrWhiteSpace(experience.InputLayout), module.Id);
Assert.IsFalse(string.IsNullOrWhiteSpace(experience.ResultLayout), module.Id);
Assert.IsTrue(experience.InputPrimitives.Count > 0, module.Id);
Assert.IsTrue(experience.ResultPrimitives.Count > 0, module.Id);
Assert.IsTrue(ids.Add(experience.ExperienceId), $"Duplicate page experienceId: {experience.ExperienceId}");
}
}
[TestMethod]
public void PrivacySanitizerOnlyRedactsYmHutAndApiRequestUrls()
{
const string input =
"public https://example.com/path?q=1; ymhut https://update.ymhut.cn/update-info.json; api_url=https://api.example.net/v1/items";
var sanitized = ToolResultPrivacySanitizer.Redact(input, "en-US");
StringAssert.Contains(sanitized, "https://example.com/path?q=1");
Assert.IsFalse(sanitized.Contains("update.ymhut.cn", StringComparison.OrdinalIgnoreCase), sanitized);
Assert.IsFalse(sanitized.Contains("api.example.net", StringComparison.OrdinalIgnoreCase), sanitized);
StringAssert.Contains(sanitized, "[YMhut endpoint hidden]");
}
}
@@ -0,0 +1,105 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class ToolWebPayloadSerializationTests
{
private static readonly JsonSerializerOptions Options = CreateOptions();
[TestMethod]
public void ToolPagePayloadSerializesRulesParametersAndActions()
{
var catalog = new ToolCatalog();
var module = catalog.GetById("json_formatter")!;
var spec = ToolPageSpecCatalog.For(module);
var experiences = new ToolResultExperienceCatalog(catalog);
Assert.IsNotEmpty(spec.Rules);
var payload = new ToolPageWebPayload(
module.Id,
module.Metadata.Name,
module.Metadata.Description,
spec,
experiences.GetRequiredPage(module.Id),
new ToolInputState(
"""{"name":"YMhut"}""",
[new ToolInputField("input", "Input", "textarea", """{"name":"YMhut"}""")],
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["mode"] = "format" },
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["hint"] = "Enter values or choose options, then run the tool." }),
"Light",
"zh-CN",
new ToolResultPrivacyPolicy(["update.ymhut.cn"]),
new ToolPageRuntimeMetadata(DateTimeOffset.UnixEpoch, AutoRun: false, OfflineCapable: true, "Dev", "\uE943"),
["run", "copyResult", "showRaw"]);
var json = JsonSerializer.Serialize(payload, Options);
var roundTrip = JsonSerializer.Deserialize<ToolPageWebPayload>(json, Options);
Assert.IsNotNull(roundTrip);
Assert.AreEqual(module.Id, roundTrip.ToolId);
Assert.IsNotEmpty(roundTrip.Spec.Rules);
Assert.IsNotEmpty(roundTrip.Spec.Parameters);
Assert.AreEqual("format", roundTrip.Input.Rules["mode"]);
CollectionAssert.Contains(roundTrip.AvailableActions.ToList(), "copyResult");
}
[TestMethod]
public void ToolResultPayloadSerializesRepresentativeBlockKinds()
{
var catalog = new ToolCatalog();
var module = catalog.GetById("hotboard")!;
var experiences = new ToolResultExperienceCatalog(catalog);
var blocks = new[]
{
ToolResultBlock.KeyValue("Details", [new("Key", "Value")]),
ToolResultBlock.Table("Table", [new[] { "Name", "Value" }, new[] { "YMhut", "Box" }]),
ToolResultBlock.List(ToolResultBlockKind.RankedList, "Ranking", [new("1", "First", "Trend", "ok", "https://example.com")]),
ToolResultBlock.List(ToolResultBlockKind.NewsList, "News", [new("1", "Headline", "Source", "info", "https://example.com/news")]),
ToolResultBlock.File("File", "report.txt", @"C:\Temp\report.txt"),
ToolResultBlock.Link("Link", "YMhut", "https://example.com"),
ToolResultBlock.Media(ToolResultBlockKind.Media, "Media", "video", "https://example.com/video.mp4"),
ToolResultBlock.Json("JSON", """{"ok":true}"""),
ToolResultBlock.List(ToolResultBlockKind.Status, "Status", [new("OK", "Ready", "", "ok", "")])
};
var document = new ToolResultDocument(
module.Id,
ToolResultKind.RankedList,
"raw output",
blocks,
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["displayProfile"] = "cards" },
"raw output",
"test",
"ok");
var payload = new ToolResultWebPayload(
module.Id,
module.Metadata.Name,
document,
experiences.GetRequired(module.Id).ExperienceId,
"Dark",
"en-US",
new ToolResultPrivacyPolicy(["update.ymhut.cn"]),
new ToolResultRuntimeMetadata(42, DateTimeOffset.UnixEpoch, Cached: false, "test", new Dictionary<string, string>()),
experiences.GetRequired(module.Id));
var json = JsonSerializer.Serialize(payload, Options);
var roundTrip = JsonSerializer.Deserialize<ToolResultWebPayload>(json, Options);
Assert.IsNotNull(roundTrip);
Assert.HasCount(blocks.Length, roundTrip.ResultDocument.Blocks);
CollectionAssert.Contains(roundTrip.ResultDocument.Blocks.Select(block => block.Kind).ToList(), ToolResultBlockKind.RankedList);
CollectionAssert.Contains(roundTrip.ResultDocument.Blocks.Select(block => block.Kind).ToList(), ToolResultBlockKind.Media);
StringAssert.Contains(json, nameof(ToolResultBlockKind.RankedList));
}
private static JsonSerializerOptions CreateOptions()
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.Converters.Add(new JsonStringEnumConverter());
return options;
}
}
+68
View File
@@ -0,0 +1,68 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class ToolWorkerTests
{
[TestMethod]
public void WorkerProtocolRoundTripsExecuteMessage()
{
var message = new ToolWorkerMessage(
ToolWorkerProtocol.ExecuteTool,
"req-1",
ToolId: "json_formatter",
Input: "{\"name\":\"YMhut\"}",
TimeoutMs: 120000,
Language: "en-US");
var line = ToolWorkerProtocol.Serialize(message);
var parsed = ToolWorkerProtocol.Deserialize(line);
Assert.IsNotNull(parsed);
Assert.AreEqual(ToolWorkerProtocol.ExecuteTool, parsed.Type);
Assert.AreEqual("req-1", parsed.RequestId);
Assert.AreEqual("json_formatter", parsed.ToolId);
Assert.AreEqual("{\"name\":\"YMhut\"}", parsed.Input);
Assert.AreEqual(120000, parsed.TimeoutMs);
Assert.AreEqual("en-US", parsed.Language);
}
[TestMethod]
public async Task InProcessWorkerExecutesToolByModule()
{
var module = new ToolCatalog().GetById("json_formatter")!;
var worker = new ToolWorkerService();
var result = await worker.ExecuteToolAsync(module, "{\"name\":\"YMhut\"}");
Assert.IsTrue(result.Ok);
StringAssert.Contains(result.Output, "\"name\": \"YMhut\"");
}
[TestMethod]
public async Task InProcessWorkerHonorsCancellation()
{
var worker = new ToolWorkerService();
using var source = new CancellationTokenSource();
await source.CancelAsync();
try
{
await worker.RunAsync(
async token =>
{
await Task.Delay(TimeSpan.FromSeconds(5), token);
return true;
},
source.Token);
}
catch (TaskCanceledException)
{
return;
}
Assert.Fail("Expected TaskCanceledException.");
}
}
+41
View File
@@ -0,0 +1,41 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.Tools;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class ToolboxLayoutTests
{
[TestMethod]
[DataRow(480, 1)]
[DataRow(760, 2)]
[DataRow(1120, 3)]
[DataRow(1440, 4)]
[DataRow(1720, 5)]
[DataRow(2200, 6)]
public void ToolboxLayoutUsesStableBreakpoints(double width, int expectedColumns)
{
var layout = ToolboxLayoutCalculator.Calculate(width);
Assert.AreEqual(expectedColumns, layout.Columns);
Assert.IsGreaterThanOrEqualTo(ToolboxLayoutCalculator.MinCardWidth, layout.CardWidth);
Assert.IsLessThanOrEqualTo(ToolboxLayoutCalculator.MaxCardWidth, layout.CardWidth);
}
[TestMethod]
public void ToolboxLayoutKeepsCategoryRailOutOfNarrowWidths()
{
Assert.IsFalse(ToolboxLayoutCalculator.Calculate(760).ShowCategoryRail);
Assert.IsTrue(ToolboxLayoutCalculator.Calculate(1180).ShowCategoryRail);
}
[TestMethod]
public void ToolboxLayoutHasSafeFallbackForUnknownWidth()
{
var layout = ToolboxLayoutCalculator.Calculate(0);
Assert.AreEqual(3, layout.Columns);
Assert.AreEqual(318, layout.CardWidth);
Assert.IsTrue(layout.ShowCategoryRail);
}
}
@@ -0,0 +1,41 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YMhut.Box.Core.Updates;
namespace YMhut.Box.Tests;
[TestClass]
public sealed class UpdateVersionComparerTests
{
[TestMethod]
public void NormalizeVersionDoesNotAppendBuildWhenVersionAlreadyHasRevision()
{
Assert.AreEqual("2.0.6.2", UpdateVersionComparer.NormalizeVersion("2.0.6.2", "2"));
}
[TestMethod]
public void NormalizeVersionAppendsBuildOnlyToShortVersion()
{
Assert.AreEqual("2.0.6.2", UpdateVersionComparer.NormalizeVersion("2.0.6", "2"));
}
[TestMethod]
public void SameVersionWithBuildIsNotNewer()
{
Assert.IsFalse(UpdateVersionComparer.IsRemoteNewer("2.0.6.2", "2", "2.0.6.2"));
Assert.AreEqual(0, UpdateVersionComparer.Compare("2.0.6.2", "2", "2.0.6.2"));
}
[TestMethod]
public void HigherRemoteVersionIsNewer()
{
Assert.IsTrue(UpdateVersionComparer.IsRemoteNewer("2.0.6.3", "", "2.0.6.2"));
Assert.IsTrue(UpdateVersionComparer.IsRemoteNewer("2.0.7", "", "2.0.6.2"));
}
[TestMethod]
public void LowerRemoteVersionIsNotNewer()
{
Assert.IsFalse(UpdateVersionComparer.IsRemoteNewer("2.0.6.1", "", "2.0.6.2"));
Assert.IsFalse(UpdateVersionComparer.IsRemoteNewer("2.0.5", "", "2.0.6.2"));
}
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\YMhut.Box.Core\YMhut.Box.Core.csproj" />
</ItemGroup>
</Project>