feat(plugins): enforce apiVersion; launcher plugins default ON with "none" opt-out
Two gaps from the MossTank shipment review. **apiVersion was declared in every manifest and checked by nothing.** The loader now refuses an unsupported contract BEFORE loading any code from the plugin — checking after the fact is not equivalent, because by then the assembly is in a collectible context and the mismatch surfaces as a type-load or missing-member failure from inside the plugin, which reads like the plugin is broken rather than built for a different host. PluginApi (Current / MinimumSupported) lives in Plugin.Abstractions beside the contract it versions, and the refusal is a distinct PluginApiVersionException so callers can tell "update the client or the plugin" from "this plugin is broken". The tests pin the ordering too: a manifest with a future apiVersion AND a missing dll must fail on the version, a supported one on the dll. **A launcher-launched client loaded no plugins until the user typed ids.** LA5 distinguishes an omitted allow-list (load all) from an explicit empty one (load none); a fresh character profile's list is empty, so it composed to load-none. Direct launches pass null and load everything -- which is why the gap never showed in development: the two launch paths disagreed and the launcher was the one users get. This REVERSES the LA5 default deliberately: "nothing configured" now composes to the omitted list, so plugins are on by default, including ones installed later. The opt-out is kept -- losing it would be a real regression for stripped sessions -- respelled as the literal id "none", and the launcher's plugin box says so. The cross-host shared fixture composes its explicit-load-none case through the new spelling, keeping the reader-side contract tests (App and Headless both preserve an explicit empty list) exactly as they were. Complete Release suite: 14,469 tests pass on the standard hermetic lane filter, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7e75be23d1
commit
cd6eefd0ba
8 changed files with 194 additions and 7 deletions
|
|
@ -174,8 +174,20 @@ public sealed class SessionConfigComposerTests
|
|||
Assert.Equal("+Acdream", (string?)session["character"]!["name"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A character nobody has configured loads every discovered plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This REVERSES the original LA5 mapping, deliberately. LA5's
|
||||
/// omitted-vs-empty distinction is kept intact at the session-config
|
||||
/// level; what changed is which one an unconfigured character maps to. A
|
||||
/// new profile starts with an empty list, so a client that ships plugins
|
||||
/// used to load none of them until the user typed an id — and nothing
|
||||
/// reported it, the panel simply never appeared. The opt-out lives on as
|
||||
/// the literal id "none" (see the next test).
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void EmptyPluginsRemainAnExplicitLoadNoneAllowListWhileLoginCommandsAreOmitted()
|
||||
public void UnconfiguredPluginsOmitTheAllowListSoEveryPluginLoads()
|
||||
{
|
||||
CharacterProfile character = Character(LaunchMode.Gui);
|
||||
character.Plugins = [];
|
||||
|
|
@ -189,10 +201,48 @@ public sealed class SessionConfigComposerTests
|
|||
Paths,
|
||||
sessionId: "session-empty-lists");
|
||||
|
||||
JsonObject session = SingleSession(composed);
|
||||
Assert.False(session.ContainsKey("plugins"));
|
||||
Assert.False(session.ContainsKey("loginCommands"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThePluginIdNoneEmitsAnExplicitLoadNoneAllowList()
|
||||
{
|
||||
CharacterProfile character = Character(LaunchMode.Gui);
|
||||
character.Plugins = ["none"];
|
||||
|
||||
ComposedSessionConfig composed = SessionConfigComposer.Compose(
|
||||
Server(),
|
||||
Account(),
|
||||
character,
|
||||
Install,
|
||||
Paths,
|
||||
sessionId: "session-no-plugins");
|
||||
|
||||
JsonObject session = SingleSession(composed);
|
||||
Assert.True(session.ContainsKey("plugins"));
|
||||
Assert.Empty(session["plugins"]!.AsArray());
|
||||
Assert.False(session.ContainsKey("loginCommands"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfiguredPluginIdsArePassedThroughUnchanged()
|
||||
{
|
||||
CharacterProfile character = Character(LaunchMode.Gui);
|
||||
character.Plugins = ["acdream.mosstank"];
|
||||
|
||||
ComposedSessionConfig composed = SessionConfigComposer.Compose(
|
||||
Server(),
|
||||
Account(),
|
||||
character,
|
||||
Install,
|
||||
Paths,
|
||||
sessionId: "session-one-plugin");
|
||||
|
||||
JsonObject session = SingleSession(composed);
|
||||
Assert.Equal(
|
||||
["acdream.mosstank"],
|
||||
session["plugins"]!.AsArray().Select(node => (string?)node));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue