feat(mosstank): add VTank-style automation PoC

This commit is contained in:
Erik 2026-08-27 18:57:21 +02:00
parent f6fe0f2a4f
commit 4e6e9bc9d9
212 changed files with 49462 additions and 416 deletions

View file

@ -8,6 +8,56 @@ namespace AcDream.Core.Tests.Plugins;
public sealed class PluginSessionTests
{
[Fact]
public void ScopedHostPrefixesStorageWithAuthenticatedManifestId()
{
var storage = new MemoryStorage();
using var alpha = new ScopedPluginHost(
new StubHost(storage),
"acdream.alpha",
"Alpha");
using var beta = new ScopedPluginHost(
new StubHost(storage),
"acdream.beta",
"Beta");
alpha.Storage.WriteText("profile.json", "alpha");
beta.Storage.WriteText("profile.json", "beta");
alpha.Storage.WriteText("imports/route.nav", "nav");
Assert.Equal("alpha", storage.Text[Path.Combine(
"acdream.alpha", "profile.json")]);
Assert.Equal("beta", storage.Text[Path.Combine(
"acdream.beta", "profile.json")]);
Assert.Equal(["imports/route.nav"], alpha.Storage.List("imports"));
Assert.Empty(beta.Storage.List("imports"));
Assert.Throws<ArgumentException>(() =>
alpha.Storage.WriteText("../escape.json", "bad"));
}
[Fact]
public void ScopedHostNamespacesAndUnregistersLootClassifierOnDispose()
{
var global = new PluginLootClassifierRegistry();
var scope = new ScopedPluginHost(
new StubHost(lootClassifiers: global),
"acdream.looter",
"Looter");
scope.LootClassifiers.Register(
"main",
"My loot rules",
new KeepClassifier());
PluginLootClassifierInfo registered = Assert.Single(global.Available);
Assert.Equal("acdream.looter/main", registered.Id);
Assert.Equal("My loot rules", registered.DisplayName);
scope.Dispose();
Assert.Empty(global.Available);
}
[Fact]
public void AbsentAllowListLoadsEveryDiscoveredPlugin()
{
@ -282,6 +332,11 @@ public sealed class PluginSessionTests
private static string FixturePluginPath()
{
string fileName = "AcDream.Core.Tests.Fixtures.HelloPlugin.dll";
string colocated = Path.Combine(AppContext.BaseDirectory, fileName);
if (File.Exists(colocated))
return colocated;
string configuration = new DirectoryInfo(AppContext.BaseDirectory)
.Parent!.Name;
string root = FindRepoRoot(AppContext.BaseDirectory);
@ -292,7 +347,7 @@ public sealed class PluginSessionTests
"bin",
configuration,
"net10.0",
"AcDream.Core.Tests.Fixtures.HelloPlugin.dll");
fileName);
}
private static string FindRepoRoot(string start)
@ -307,7 +362,9 @@ public sealed class PluginSessionTests
?? throw new InvalidOperationException("Repository root not found.");
}
private sealed class StubHost : IPluginHost
private sealed class StubHost(
IPluginStorage? storage = null,
IPluginLootClassifierRegistry? lootClassifiers = null) : IPluginHost
{
public bool HasUi => false;
public IPluginLogger Log { get; } = new StubLogger();
@ -316,6 +373,34 @@ public sealed class PluginSessionTests
public ISelectionService Selection { get; } = new SelectionState();
public IUiRegistry Ui => NoOpUiRegistry.Instance;
public IAutomationSurface Automation => NoOpAutomationSurface.Instance;
public IPluginStorage Storage { get; } =
storage ?? NoOpPluginStorage.Instance;
public IPluginLootClassifierRegistry LootClassifiers { get; } =
lootClassifiers ?? NoOpPluginLootClassifierRegistry.Instance;
}
private sealed class KeepClassifier : IPluginLootClassifier
{
public PluginLootClassification Classify(
in PluginLootClassificationContext context) => new(
true,
PluginLootAction.Keep);
}
private sealed class MemoryStorage : IPluginStorage
{
public Dictionary<string, string> Text { get; } =
new(StringComparer.Ordinal);
public bool IsAvailable => true;
public string? ReadText(string key) =>
Text.TryGetValue(key, out string? value) ? value : null;
public IReadOnlyList<string> List(string prefix) => Text.Keys
.Where(key => key.StartsWith(prefix + Path.DirectorySeparatorChar,
StringComparison.Ordinal))
.OrderBy(static key => key, StringComparer.Ordinal)
.ToArray();
public void WriteText(string key, string content) => Text[key] = content;
public bool Delete(string key) => Text.Remove(key);
}
private sealed class StubLogger : IPluginLogger