fix(launcher): close LA4 review findings

This commit is contained in:
Erik 2026-08-14 19:02:20 +02:00
parent d0a9c65d85
commit 10a712d66b
19 changed files with 1631 additions and 134 deletions

View file

@ -160,6 +160,28 @@ public sealed class LauncherWindowViewModelTests
Assert.Equal("Headless session started for +Acdream.", viewModel.OperationStatus);
}
[Fact]
public async Task AccountGuiSelectWorksWithoutAnyCachedCharacter()
{
using var orchestrator = new FakeLauncherOrchestrator
{
IncludeCharacter = false,
};
using var viewModel = CreateInitialized(orchestrator);
SelectAccount(viewModel);
Assert.Empty(viewModel.SelectedNode!.Children);
Assert.True(viewModel.CanLaunchAccountGuiSelect);
await viewModel.LaunchAccountGuiSelectCommand.ExecuteAsync();
Assert.Equal(
("Local ACE", "testaccount", (string?)null, LaunchMode.GuiSelect),
orchestrator.LaunchRequest);
Assert.Equal(
"Character-select session started for testaccount.",
viewModel.OperationStatus);
}
[Fact]
public async Task ProbeUsesTheSelectedAccountAndRunningAccountDisablesIt()
{
@ -201,6 +223,25 @@ public sealed class LauncherWindowViewModelTests
Assert.Contains("parked at L1", viewModel.GuiLaunchDisabledReason, StringComparison.Ordinal);
}
[Fact]
public void MissingCoDeployedHostReasonIsVisibleForCharacterActions()
{
using var orchestrator = new FakeLauncherOrchestrator
{
AccountLaunchCapability = LauncherCapability.Unavailable(
"The co-deployed host is missing; reinstall or update the client."),
};
using var viewModel = CreateInitialized(orchestrator);
SelectCharacter(viewModel);
Assert.False(viewModel.CanLaunchGui);
Assert.False(viewModel.CanLaunchHeadless);
Assert.True(viewModel.ShowGuiLaunchDisabledReason);
Assert.True(viewModel.ShowHeadlessLaunchDisabledReason);
Assert.Contains("missing", viewModel.GuiLaunchDisabledReason, StringComparison.Ordinal);
Assert.Contains("missing", viewModel.HeadlessLaunchDisabledReason, StringComparison.Ordinal);
}
[Fact]
public async Task LaunchErrorAndCancellationBecomeSafeVisibleState()
{
@ -254,6 +295,36 @@ public sealed class LauncherWindowViewModelTests
Assert.False(dialog.IsOpen);
}
[Fact]
public void ModalShellsBlockBackgroundCommandsAndAreMutuallyExclusive()
{
using var orchestrator = new FakeLauncherOrchestrator();
using var viewModel = CreateInitialized(orchestrator);
Assert.True(viewModel.AddServerCommand.CanExecute(null));
viewModel.FirstRunWizardShell.OpenCommand.Execute(null);
Assert.True(viewModel.IsModalOpen);
Assert.False(viewModel.AddServerCommand.CanExecute(null));
Assert.False(viewModel.UpdatePromptShell.OpenCommand.CanExecute(null));
Assert.False(Assert.Single(viewModel.Sessions).StopCommand.CanExecute(null));
// ICommand.Execute cannot bypass the modal gate.
viewModel.AddServerCommand.Execute(null);
Assert.False(viewModel.EditorDialog.IsOpen);
Assert.Null(orchestrator.AddedServer);
viewModel.CloseActiveModal();
Assert.False(viewModel.IsModalOpen);
Assert.True(viewModel.AddServerCommand.CanExecute(null));
viewModel.AddServerCommand.Execute(null);
Assert.True(viewModel.EditorDialog.IsOpen);
Assert.False(viewModel.FirstRunWizardShell.OpenCommand.CanExecute(null));
Assert.False(viewModel.ClearFinishedSessionsCommand.CanExecute(null));
viewModel.CloseActiveModal();
Assert.False(viewModel.EditorDialog.IsOpen);
}
[Fact]
public async Task ActiveSessionStopAndFinishedSessionClearUseCoreOwnership()
{
@ -320,6 +391,10 @@ public sealed class LauncherWindowViewModelTests
public LauncherCapability ProbeCapability { get; set; } = LauncherCapability.Available;
public LauncherCapability? AccountLaunchCapability { get; set; }
public bool IncludeCharacter { get; init; } = true;
public LauncherSessionSnapshot Session { get; set; } = CreateSession();
public Func<CancellationToken, Task<LauncherSessionSnapshot>>? LaunchHandler { get; set; }
@ -344,7 +419,7 @@ public sealed class LauncherWindowViewModelTests
public (LaunchMode Mode, IReadOnlyList<string> Plugins, IReadOnlyList<string> Commands)? SettingsUpdate { get; private set; }
public (string Server, string Account, string Character, LaunchMode Mode)? LaunchRequest { get; private set; }
public (string Server, string Account, string? Character, LaunchMode Mode)? LaunchRequest { get; private set; }
public (string Server, string Account)? ProbeRequest { get; private set; }
@ -362,6 +437,12 @@ public sealed class LauncherWindowViewModelTests
public LauncherCapability GetLaunchCapability(LaunchMode mode) =>
Platform.ForLaunchMode(mode);
public LauncherCapability GetAccountLaunchCapability(
string serverName,
string accountName,
LaunchMode mode) =>
AccountLaunchCapability ?? GetLaunchCapability(mode);
public LauncherCapability GetProbeCapability(string serverName, string accountName) =>
ProbeCapability;
@ -428,7 +509,7 @@ public sealed class LauncherWindowViewModelTests
public Task<LauncherSessionSnapshot> LaunchAsync(
string serverName,
string accountName,
string characterName,
string? characterName,
LaunchMode mode,
CancellationToken cancellationToken = default)
{
@ -481,7 +562,7 @@ public sealed class LauncherWindowViewModelTests
Error: null,
CreatedAt: DateTimeOffset.UnixEpoch);
private static LauncherServerSnapshot CreateServerSnapshot() => new(
private LauncherServerSnapshot CreateServerSnapshot() => new(
"Local ACE",
"127.0.0.1",
9000,
@ -489,18 +570,21 @@ public sealed class LauncherWindowViewModelTests
new LauncherAccountSnapshot(
"Local ACE",
"testaccount",
[
new LauncherCharacterSnapshot(
"Local ACE",
"testaccount",
"+Acdream",
"0x5000000A",
LaunchMode.GuiSelect,
["Existing.Plugin"],
["/tell someone, ready"],
HasRunningSession: true,
SessionStatus: "Connected."),
],
IncludeCharacter
?
[
new LauncherCharacterSnapshot(
"Local ACE",
"testaccount",
"+Acdream",
"0x5000000A",
LaunchMode.GuiSelect,
["Existing.Plugin"],
["/tell someone, ready"],
HasRunningSession: true,
SessionStatus: "Connected."),
]
: [],
HasRunningActivity: true,
ActivityStatus: "Connected."),
]);