Add WinUI and core source
build-winui / winui (push) Has been cancelled
build-winui / winui (push) Has been cancelled
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user