feat(core): add PluginDiscovery directory scan

This commit is contained in:
Erik 2026-04-10 09:35:58 +02:00
parent 99d2702c13
commit 91618682e2
2 changed files with 129 additions and 0 deletions

View file

@ -0,0 +1,38 @@
namespace AcDream.Core.Plugins;
public sealed record PluginDiscoveryResult(
string PluginDirectory,
PluginManifest? Manifest,
string? Error)
{
public bool Success => Manifest is not null && Error is null;
}
public static class PluginDiscovery
{
public static IReadOnlyList<PluginDiscoveryResult> Scan(string pluginsRootDirectory)
{
if (!Directory.Exists(pluginsRootDirectory))
return Array.Empty<PluginDiscoveryResult>();
var results = new List<PluginDiscoveryResult>();
foreach (var subdir in Directory.EnumerateDirectories(pluginsRootDirectory))
{
var manifestPath = Path.Combine(subdir, "plugin.json");
if (!File.Exists(manifestPath))
continue;
try
{
var json = File.ReadAllText(manifestPath);
var manifest = PluginManifest.Parse(json);
results.Add(new PluginDiscoveryResult(subdir, manifest, null));
}
catch (Exception ex)
{
results.Add(new PluginDiscoveryResult(subdir, null, ex.Message));
}
}
return results;
}
}

View file

@ -0,0 +1,91 @@
using AcDream.Core.Plugins;
namespace AcDream.Core.Tests.Plugins;
public class PluginDiscoveryTests : IDisposable
{
private readonly string _tempDir;
public PluginDiscoveryTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "acdream-test-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, recursive: true);
}
private string WriteManifest(string folder, string json)
{
var dir = Path.Combine(_tempDir, folder);
Directory.CreateDirectory(dir);
var path = Path.Combine(dir, "plugin.json");
File.WriteAllText(path, json);
return dir;
}
private const string ValidManifest = """
{
"id": "acdream.example",
"displayName": "Example",
"version": "0.1.0",
"entryDll": "example.dll",
"apiVersion": 1
}
""";
[Fact]
public void Scan_EmptyDirectory_ReturnsEmpty()
{
var results = PluginDiscovery.Scan(_tempDir);
Assert.Empty(results);
}
[Fact]
public void Scan_NonexistentDirectory_ReturnsEmpty()
{
var results = PluginDiscovery.Scan(Path.Combine(_tempDir, "does-not-exist"));
Assert.Empty(results);
}
[Fact]
public void Scan_OneValidPlugin_ReturnsOneSuccess()
{
var pluginDir = WriteManifest("example", ValidManifest);
var results = PluginDiscovery.Scan(_tempDir);
var single = Assert.Single(results);
Assert.True(single.Success);
Assert.NotNull(single.Manifest);
Assert.Equal("acdream.example", single.Manifest!.Id);
Assert.Equal(pluginDir, single.PluginDirectory);
}
[Fact]
public void Scan_SubdirWithoutManifest_IsSkipped()
{
Directory.CreateDirectory(Path.Combine(_tempDir, "noplugin"));
WriteManifest("real", ValidManifest);
var results = PluginDiscovery.Scan(_tempDir);
Assert.Single(results);
}
[Fact]
public void Scan_InvalidManifest_ReturnsFailureButKeepsGoing()
{
WriteManifest("broken", "{ not json");
WriteManifest("ok", ValidManifest);
var results = PluginDiscovery.Scan(_tempDir).ToList();
Assert.Equal(2, results.Count);
Assert.Contains(results, r => !r.Success && r.Error != null);
Assert.Contains(results, r => r.Success);
}
}