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>
123 lines
4.1 KiB
C#
123 lines
4.1 KiB
C#
using AcDream.Launcher.Core.Launching;
|
|
using AcDream.Launcher.Core.Profiles;
|
|
using AcDream.Platform;
|
|
|
|
namespace AcDream.Tests.Fixtures.CampaignLa;
|
|
|
|
/// <summary>
|
|
/// Produces one real Launcher.Core session document that is compiled into
|
|
/// both host test suites. Keeping composition in one linked fixture makes the
|
|
/// LA1/LA3 anti-drift gate prove that App and Headless accept the identical
|
|
/// composer output rather than two hand-maintained lookalikes.
|
|
/// </summary>
|
|
internal static class LauncherCoreSessionConfigFixture
|
|
{
|
|
internal const string Password = "must-not-be-serialized";
|
|
|
|
internal static string Compose()
|
|
{
|
|
var server = new ServerProfile
|
|
{
|
|
Name = "Composer Server",
|
|
Host = "composer.example",
|
|
Port = 9010,
|
|
};
|
|
var account = new AccountProfile
|
|
{
|
|
Account = "composer-account",
|
|
Password = Password,
|
|
};
|
|
var character = new CharacterProfile
|
|
{
|
|
Name = "Composer Character",
|
|
Id = "0x50000001",
|
|
LaunchMode = LaunchMode.Headless,
|
|
Plugins = ["ComposerPlugin"],
|
|
LoginCommands = ["/composer command"],
|
|
};
|
|
var install = new LauncherInstallRecord(
|
|
"composer-dats",
|
|
"composer-dats/acdream.pak");
|
|
var paths = new ApplicationPathSet(
|
|
Path.Combine(Path.GetTempPath(), "composer-config"),
|
|
Path.Combine(Path.GetTempPath(), "composer-data"),
|
|
Path.Combine(Path.GetTempPath(), "composer-cache"),
|
|
LegacyConfigDirectory: null);
|
|
|
|
ComposedSessionConfig composed = SessionConfigComposer.Compose(
|
|
server,
|
|
account,
|
|
character,
|
|
install,
|
|
paths,
|
|
"composer-contract",
|
|
loginCommandDelayMs: 625);
|
|
|
|
return SessionConfigComposer.Serialize(composed.Document);
|
|
}
|
|
|
|
internal static string ComposeEmptyPlugins()
|
|
{
|
|
(ServerProfile server, AccountProfile account,
|
|
LauncherInstallRecord install, ApplicationPathSet paths) = Inputs();
|
|
var character = new CharacterProfile
|
|
{
|
|
Name = "Composer Character",
|
|
Id = "0x50000001",
|
|
LaunchMode = LaunchMode.Headless,
|
|
// "none" is how a profile now spells the explicit load-none
|
|
// allow-list; an EMPTY list means unconfigured and composes to an
|
|
// OMITTED allow-list (= load all) since plugins became default-on.
|
|
Plugins = ["none"],
|
|
LoginCommands = [],
|
|
};
|
|
|
|
ComposedSessionConfig composed = SessionConfigComposer.Compose(
|
|
server,
|
|
account,
|
|
character,
|
|
install,
|
|
paths,
|
|
"composer-empty-plugins");
|
|
return SessionConfigComposer.Serialize(composed.Document);
|
|
}
|
|
|
|
internal static string ComposeProbe()
|
|
{
|
|
(ServerProfile server, AccountProfile account,
|
|
LauncherInstallRecord install, ApplicationPathSet paths) = Inputs();
|
|
ComposedSessionConfig composed = SessionConfigComposer.ComposeProbe(
|
|
server,
|
|
account,
|
|
install,
|
|
paths,
|
|
"composer-probe");
|
|
return SessionConfigComposer.Serialize(composed.Document);
|
|
}
|
|
|
|
private static (
|
|
ServerProfile Server,
|
|
AccountProfile Account,
|
|
LauncherInstallRecord Install,
|
|
ApplicationPathSet Paths) Inputs() =>
|
|
(
|
|
new ServerProfile
|
|
{
|
|
Name = "Composer Server",
|
|
Host = "composer.example",
|
|
Port = 9010,
|
|
},
|
|
new AccountProfile
|
|
{
|
|
Account = "composer-account",
|
|
Password = Password,
|
|
},
|
|
new LauncherInstallRecord(
|
|
"composer-dats",
|
|
"composer-dats/acdream.pak"),
|
|
new ApplicationPathSet(
|
|
Path.Combine(Path.GetTempPath(), "composer-config"),
|
|
Path.Combine(Path.GetTempPath(), "composer-data"),
|
|
Path.Combine(Path.GetTempPath(), "composer-cache"),
|
|
LegacyConfigDirectory: null));
|
|
}
|