changed.更了好多

This commit is contained in:
lincube
2026-05-12 16:46:49 +08:00
parent 563f12caa1
commit 33c264f6dd
127 changed files with 5257 additions and 10534 deletions

View File

@@ -1,4 +1,5 @@
using LanMountainDesktop.Launcher.Services;
using LanMountainDesktop.Shared.Contracts.Launcher;
using Xunit;
namespace LanMountainDesktop.Tests;
@@ -88,4 +89,54 @@ public sealed class HostAppSettingsOobeMergerTests
Directory.Delete(dir, recursive: true);
}
}
[Theory]
[InlineData("RestartApp", MultiInstanceLaunchBehavior.RestartApp)]
[InlineData("OpenDesktopSilently", MultiInstanceLaunchBehavior.OpenDesktopSilently)]
[InlineData("PromptOnly", MultiInstanceLaunchBehavior.PromptOnly)]
[InlineData("NotifyAndOpenDesktop", MultiInstanceLaunchBehavior.NotifyAndOpenDesktop)]
public void LoadMultiInstanceLaunchBehavior_ReadsStringValues(
string value,
MultiInstanceLaunchBehavior expected)
{
var dir = Path.Combine(Path.GetTempPath(), "LMD.MultiInstanceSettings", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
var path = Path.Combine(dir, "settings.json");
File.WriteAllText(path, $$"""
{
"MultiInstanceLaunchBehavior": "{{value}}"
}
""");
try
{
Assert.Equal(expected, HostAppSettingsOobeMerger.LoadMultiInstanceLaunchBehavior(path));
}
finally
{
Directory.Delete(dir, recursive: true);
}
}
[Theory]
[InlineData("{}")]
[InlineData("{ \"MultiInstanceLaunchBehavior\": \"Unknown\" }")]
public void LoadMultiInstanceLaunchBehavior_FallsBackToNotifyAndOpenDesktop(string json)
{
var dir = Path.Combine(Path.GetTempPath(), "LMD.MultiInstanceSettings", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
var path = Path.Combine(dir, "settings.json");
File.WriteAllText(path, json);
try
{
Assert.Equal(
MultiInstanceLaunchBehavior.NotifyAndOpenDesktop,
HostAppSettingsOobeMerger.LoadMultiInstanceLaunchBehavior(path));
}
finally
{
Directory.Delete(dir, recursive: true);
}
}
}

View File

@@ -0,0 +1,123 @@
using LanMountainDesktop.Launcher;
using LanMountainDesktop.Launcher.Services;
using LanMountainDesktop.Models;
using LanMountainDesktop.Shared.Contracts.Launcher;
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class LauncherMultiInstancePolicyTests
{
[Fact]
public void AppSettingsSnapshot_DefaultsToNotifyAndOpenDesktop()
{
Assert.Equal(
MultiInstanceLaunchBehavior.NotifyAndOpenDesktop,
new AppSettingsSnapshot().MultiInstanceLaunchBehavior);
}
[Fact]
public void ShouldProbeExistingHostBeforeLaunch_ReturnsTrue_ForNormalLaunch()
{
var context = CommandContext.FromArgs(["launch"]);
Assert.True(LauncherFlowCoordinator.ShouldProbeExistingHostBeforeLaunch(context));
}
[Fact]
public void ShouldProbeExistingHostBeforeLaunch_ReturnsFalse_ForRestartLaunch()
{
var context = CommandContext.FromArgs([
"launch",
$"--{LauncherIpcConstants.LaunchSourceOptionName}=restart"
]);
Assert.False(LauncherFlowCoordinator.ShouldProbeExistingHostBeforeLaunch(context));
}
[Fact]
public void ActivationExitCodes_AreClassifiedSeparatelyFromEarlyHostExit()
{
Assert.True(LauncherFlowCoordinator.IsSuccessfulActivationExitCode(HostExitCodes.SecondaryActivationSucceeded));
Assert.True(LauncherFlowCoordinator.IsFailedActivationExitCode(HostExitCodes.SecondaryActivationFailed));
Assert.True(LauncherFlowCoordinator.IsFailedActivationExitCode(HostExitCodes.RestartLockNotAcquired));
Assert.False(LauncherFlowCoordinator.IsFailedActivationExitCode(1));
}
[Fact]
public void IsRecoverableActivationFailure_ReturnsTrue_WhenPublicIpcIsReadyButShellIsPending()
{
var activation = new PublicShellActivationResult(
false,
"shell_not_ready",
"Desktop shell is still initializing.",
CreateShellStatus(
publicIpcReady: true,
mainWindowOpened: false,
desktopVisible: false));
Assert.True(LauncherFlowCoordinator.IsRecoverableActivationFailure(activation));
}
[Fact]
public void IsRecoverableActivationFailure_ReturnsFalse_WhenShutdownIsInProgress()
{
var activation = new PublicShellActivationResult(
false,
"shutdown_in_progress",
"Desktop is shutting down.",
CreateShellStatus(
publicIpcReady: true,
mainWindowOpened: false,
desktopVisible: false));
Assert.False(LauncherFlowCoordinator.IsRecoverableActivationFailure(activation));
}
[Fact]
public void IsExistingHostReadyForLauncherDecision_RequiresPublicIpcReady()
{
Assert.False(LauncherFlowCoordinator.IsExistingHostReadyForLauncherDecision(null));
Assert.False(LauncherFlowCoordinator.IsExistingHostReadyForLauncherDecision(CreateShellStatus(
publicIpcReady: false,
mainWindowOpened: true,
desktopVisible: true)));
Assert.True(LauncherFlowCoordinator.IsExistingHostReadyForLauncherDecision(CreateShellStatus(
publicIpcReady: true,
mainWindowOpened: true,
desktopVisible: true)));
}
private static PublicShellStatus CreateShellStatus(
bool publicIpcReady,
bool mainWindowOpened,
bool desktopVisible)
{
return new PublicShellStatus(
ProcessId: Environment.ProcessId,
StartedAtUtc: DateTimeOffset.UtcNow,
LaunchSource: "normal",
ShellState: mainWindowOpened ? "opened" : "initializing",
MainWindowCreated: mainWindowOpened,
MainWindowVisible: desktopVisible,
MainWindowOpened: mainWindowOpened,
DesktopVisible: desktopVisible,
PublicIpcReady: publicIpcReady,
Tray: new PublicTrayStatus(
State: "Unavailable",
IsReady: false,
HasIcon: false,
HasMenu: false,
IsVisible: false,
ConsecutiveRecoveryFailures: 0),
Taskbar: new PublicTaskbarStatus(
RequestedBySettings: false,
MainWindowExists: mainWindowOpened,
MainWindowShowInTaskbar: mainWindowOpened,
MainWindowVisible: desktopVisible,
MainWindowMinimized: false,
IsUsable: mainWindowOpened));
}
}

View File

@@ -0,0 +1,106 @@
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class NotificationListenerServiceTests
{
[Fact]
public void AddNotification_DeduplicatesByPlatformAndSourceId()
{
var settings = new FakeSettingsService();
var service = new NotificationListenerService(settings);
service.AddNotification(new NotificationItem
{
Platform = "Windows",
SourceNotificationId = "42",
AppId = "mail",
AppName = "Mail",
Title = "First"
});
service.AddNotification(new NotificationItem
{
Platform = "Windows",
SourceNotificationId = "42",
AppId = "mail",
AppName = "Mail",
Title = "Updated"
});
var notification = Assert.Single(service.GetNotifications());
Assert.Equal("Updated", notification.Title);
}
[Fact]
public void AddNotification_RespectsBlockedApps()
{
var settings = new FakeSettingsService();
settings.Snapshot.NotificationBoxBlockedApps.Add("blocked-app");
var service = new NotificationListenerService(settings);
service.AddNotification(new NotificationItem
{
AppId = "blocked-app",
AppName = "Blocked",
Title = "Hidden"
});
Assert.Empty(service.GetNotifications());
}
[Fact]
public void AddNotification_TrimsToMaxStoredCount()
{
var settings = new FakeSettingsService();
settings.Snapshot.NotificationBoxMaxStoredCount = 2;
var service = new NotificationListenerService(settings);
service.AddNotification(new NotificationItem { AppId = "a", AppName = "A", Title = "1" });
service.AddNotification(new NotificationItem { AppId = "b", AppName = "B", Title = "2" });
service.AddNotification(new NotificationItem { AppId = "c", AppName = "C", Title = "3" });
var notifications = service.GetNotifications();
Assert.Equal(2, notifications.Count);
Assert.DoesNotContain(notifications, n => n.Title == "1");
}
private sealed class FakeSettingsService : ISettingsService
{
public AppSettingsSnapshot Snapshot { get; } = new();
public event EventHandler<SettingsChangedEvent>? Changed;
public T LoadSnapshot<T>(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new()
=> typeof(T) == typeof(AppSettingsSnapshot)
? (T)(object)Snapshot
: new T();
public void SaveSnapshot<T>(SettingsScope scope, T snapshot, string? subjectId = null, string? placementId = null, string? sectionId = null, IReadOnlyCollection<string>? changedKeys = null)
{
}
public T LoadSection<T>(SettingsScope scope, string subjectId, string sectionId, string? placementId = null) where T : new()
=> new();
public void SaveSection<T>(SettingsScope scope, string subjectId, string sectionId, T section, string? placementId = null, IReadOnlyCollection<string>? changedKeys = null)
{
}
public void DeleteSection(SettingsScope scope, string subjectId, string sectionId, string? placementId = null)
{
}
public T? GetValue<T>(SettingsScope scope, string key, string? subjectId = null, string? placementId = null, string? sectionId = null)
=> default;
public void SetValue<T>(SettingsScope scope, string key, T value, string? subjectId = null, string? placementId = null, string? sectionId = null, IReadOnlyCollection<string>? changedKeys = null)
{
}
public IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId)
=> throw new NotSupportedException();
}
}

View File

@@ -1,102 +0,0 @@
using System;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using LanMountainDesktop.Services;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class SingleInstanceServiceTests
{
[Fact]
public async Task TryNotifyPrimaryInstance_ReturnsTrue_WhenPrimaryAcknowledges()
{
var mutexName = $"Local\\LanMountainDesktop.Tests.SingleInstance.{Guid.NewGuid():N}";
var pipeName = $"LanMountainDesktop.Tests.Activate.{Guid.NewGuid():N}";
using var primary = CreateService(mutexName, pipeName);
using var secondary = CreateSecondaryService(mutexName, pipeName);
Assert.True(primary.IsPrimaryInstance);
MarkAsSecondaryForTest(secondary);
var activated = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
primary.StartActivationListener(() => activated.TrySetResult());
var acknowledged = secondary.TryNotifyPrimaryInstance(TimeSpan.FromSeconds(2), out var failureReason);
Assert.True(acknowledged);
Assert.Null(failureReason);
var completed = await Task.WhenAny(activated.Task, Task.Delay(TimeSpan.FromSeconds(2)));
Assert.Same(activated.Task, completed);
}
[Fact]
public void TryNotifyPrimaryInstance_ReturnsFalse_WhenListenerIsNotRunning()
{
var mutexName = $"Local\\LanMountainDesktop.Tests.SingleInstance.{Guid.NewGuid():N}";
var pipeName = $"LanMountainDesktop.Tests.Activate.{Guid.NewGuid():N}";
using var primary = CreateService(mutexName, pipeName);
using var secondary = CreateSecondaryService(mutexName, pipeName);
Assert.True(primary.IsPrimaryInstance);
MarkAsSecondaryForTest(secondary);
var acknowledged = secondary.TryNotifyPrimaryInstance(TimeSpan.FromMilliseconds(300), out var failureReason);
Assert.False(acknowledged);
Assert.False(string.IsNullOrWhiteSpace(failureReason));
}
private static SingleInstanceService CreateService(string mutexName, string pipeName)
{
var ctor = typeof(SingleInstanceService).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
binder: null,
[typeof(string), typeof(string)],
modifiers: null);
Assert.NotNull(ctor);
return (SingleInstanceService)ctor!.Invoke([mutexName, pipeName]);
}
private static SingleInstanceService CreateSecondaryService(string mutexName, string pipeName)
{
SingleInstanceService? created = null;
Exception? creationError = null;
var thread = new Thread(() =>
{
try
{
created = CreateService(mutexName, pipeName);
}
catch (Exception ex)
{
creationError = ex;
}
});
thread.IsBackground = true;
thread.Start();
thread.Join();
if (creationError is not null)
{
throw new InvalidOperationException("Failed to create secondary SingleInstanceService.", creationError);
}
Assert.NotNull(created);
return created!;
}
private static void MarkAsSecondaryForTest(SingleInstanceService service)
{
var ownsMutexField = typeof(SingleInstanceService).GetField(
"_ownsMutex",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.NotNull(ownsMutexField);
ownsMutexField!.SetValue(service, false);
Assert.False(service.IsPrimaryInstance);
}
}