using System.Text.Json; using System.Runtime.CompilerServices; using AcDream.App.Plugins; using AcDream.Core.Plugins; using AcDream.Core.Selection; using AcDream.Platform; using AcDream.Plugin.Abstractions; using AcDream.Runtime.Session; namespace AcDream.App.Tests.Plugins; public sealed class GraphicalPluginSessionTests { private const string FixtureId = "acdream.test.host-fixture"; [Fact] public void ConfiguredSetLoadsOnlyAllowedPluginAndReportsBothOutcomes() { using var temporary = new TemporaryDirectory(); ApplicationPathSet paths = Paths(temporary.Path); InstallFixture(paths.PluginsDirectory, FixtureId); string statusPath = Path.Combine(temporary.Path, "status.jsonl"); var logger = new CapturingLogger(); var state = new WorldGameState(); var events = new WorldEvents(); var selection = new SelectionState(); var ui = new BufferedUiRegistry(); var host = new AppPluginHost(logger, state, events, selection, ui); using GraphicalPluginSession plugins = GraphicalPluginSession.Start( paths, [FixtureId.ToUpperInvariant(), "acdream.test.missing"], "gui-session", host, new SessionStatusWriter(statusPath)); Assert.Equal(1, plugins.LoadedCount); Assert.True(host.HasUi); AssertPanelWasRegisteredAndReleaseBinding(ui); Assert.Contains( logger.Messages, message => message.Contains("fixture-enabled:hasUi=True", StringComparison.Ordinal)); JsonElement[] statuses = ReadStatuses(statusPath); Assert.Equal(["pluginLoaded", "pluginFailed"], EventNames(statuses)); Assert.Equal(FixtureId, statuses[0].GetProperty("plugin").GetString()); Assert.Equal( "acdream.test.missing", statuses[1].GetProperty("plugin").GetString()); Assert.Contains( "not found", statuses[1].GetProperty("error").GetString(), StringComparison.OrdinalIgnoreCase); WeakReference context = Assert.Single( plugins.CaptureLoadContextWeakReferences()); plugins.Dispose(); Collect(context); Assert.False(context.IsAlive); } [Fact] public void ExplicitEmptyConfiguredSetLoadsNone() { using var temporary = new TemporaryDirectory(); ApplicationPathSet paths = Paths(temporary.Path); InstallFixture(paths.PluginsDirectory, FixtureId); string statusPath = Path.Combine(temporary.Path, "status.jsonl"); var ui = new BufferedUiRegistry(); var host = new AppPluginHost( new CapturingLogger(), new WorldGameState(), new WorldEvents(), new SelectionState(), ui); using GraphicalPluginSession plugins = GraphicalPluginSession.Start( paths, [], "gui-session", host, new SessionStatusWriter(statusPath)); Assert.Equal(0, plugins.LoadedCount); Assert.Empty(ui.Drain()); Assert.False(File.Exists(statusPath)); } private static ApplicationPathSet Paths(string root) => new( Path.Combine(root, "config"), Path.Combine(root, "data"), Path.Combine(root, "cache"), LegacyConfigDirectory: null); [MethodImpl(MethodImplOptions.NoInlining)] private static void AssertPanelWasRegisteredAndReleaseBinding( BufferedUiRegistry ui) { BufferedUiRegistry.Pending panel = Assert.Single(ui.Drain()); Assert.EndsWith( "fixture-panel.xml", panel.MarkupPath, StringComparison.Ordinal); Assert.Equal( "AcDream.Plugin.Tests.Fixtures.HostPlugin", panel.Binding.GetType().Assembly.GetName().Name); } private static JsonElement[] ReadStatuses(string path) => File.ReadAllLines(path) .Select(static line => JsonDocument.Parse(line).RootElement.Clone()) .ToArray(); private static string[] EventNames(IEnumerable events) => events.Select(static item => item.GetProperty("e").GetString()!).ToArray(); private static void InstallFixture(string root, string id) { string source = FixtureAssemblyPath(); Assert.True(File.Exists(source), $"fixture DLL not found: {source}"); string pluginDirectory = Path.Combine(root, "host-fixture"); Directory.CreateDirectory(pluginDirectory); string fileName = Path.GetFileName(source); File.Copy(source, Path.Combine(pluginDirectory, fileName)); File.WriteAllText( Path.Combine(pluginDirectory, "plugin.json"), JsonSerializer.Serialize(new { id, displayName = "Host fixture", version = "1.0.0", entryDll = fileName, apiVersion = 1, })); } private static string FixtureAssemblyPath() { string configuration = new DirectoryInfo(AppContext.BaseDirectory) .Parent!.Name; string root = FindRepoRoot(AppContext.BaseDirectory); return Path.Combine( root, "tests", "AcDream.Plugin.Tests.Fixtures.HostPlugin", "bin", configuration, "net10.0", "AcDream.Plugin.Tests.Fixtures.HostPlugin.dll"); } private static string FindRepoRoot(string start) { DirectoryInfo? directory = new(start); while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) { directory = directory.Parent; } return directory?.FullName ?? throw new InvalidOperationException("Repository root not found."); } private static void Collect(WeakReference reference) { for (int attempt = 0; attempt < 10 && reference.IsAlive; attempt++) { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } } private sealed class CapturingLogger : IPluginLogger { internal List Messages { get; } = []; public void Info(string message) => Messages.Add(message); public void Warn(string message) => Messages.Add(message); public void Error(string message, Exception? exception = null) => Messages.Add(exception is null ? message : $"{message}: {exception.Message}"); } private sealed class TemporaryDirectory : IDisposable { internal TemporaryDirectory() { Path = System.IO.Path.Combine( System.IO.Path.GetTempPath(), $"acdream-graphical-plugins-{Guid.NewGuid():N}"); Directory.CreateDirectory(Path); } internal string Path { get; } public void Dispose() { if (Directory.Exists(Path)) Directory.Delete(Path, recursive: true); } } }