// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.ComponentModel; using System.Diagnostics; using System.Text.Json; using autoShell.Handlers; using autoShell.Logging; using autoShell.Services; using Moq; namespace autoShell.Tests; public class AppActionHandlerTests { private readonly Mock _appRegistryMock = new(); private readonly Mock _processMock = new(); private readonly Mock _windowMock = new(); private readonly Mock _loggerMock = new(); private readonly AppActionHandler _handler; public AppActionHandlerTests() { _handler = new AppActionHandler(_appRegistryMock.Object, _processMock.Object, _windowMock.Object, _loggerMock.Object); } /// /// Verifies that launching a non-running app starts it using its executable path. /// [Fact] public void LaunchProgram_AppNotRunning_StartsViaPath() { _appRegistryMock.Setup(a => a.ResolveProcessName("chrome")).Returns("chrome"); _processMock.Setup(p => p.GetProcessesByName("chrome")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("chrome")).Returns("chrome.exe"); _handler.Handle("LaunchProgram", JsonDocument.Parse("""{"name":"chrome"}""").RootElement); _processMock.Verify(p => p.Start(It.Is( psi => psi.FileName == "chrome.exe" && psi.UseShellExecute == true)), Times.Once); } /// /// Verifies that launching an app with a configured working directory env var sets the working directory. /// [Fact] public void LaunchProgram_WithWorkingDir_SetsWorkingDirectory() { _appRegistryMock.Setup(a => a.ResolveProcessName("github copilot")).Returns("github copilot"); _processMock.Setup(p => p.GetProcessesByName("github copilot")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("github copilot")).Returns("copilot.exe"); _appRegistryMock.Setup(a => a.GetWorkingDirectoryEnvVar("github copilot")).Returns("GITHUB_COPILOT_ROOT_DIR"); _handler.Handle("LaunchProgram", JsonDocument.Parse("""{"name":"github copilot"}""").RootElement); _processMock.Verify(p => p.Start(It.Is( psi => psi.WorkingDirectory != "")), Times.Once); } /// /// Verifies that launching an app with configured arguments passes them to the process start info. /// [Fact] public void LaunchProgram_WithArguments_SetsArguments() { _appRegistryMock.Setup(a => a.ResolveProcessName("github copilot")).Returns("github copilot"); _processMock.Setup(p => p.GetProcessesByName("github copilot")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("github copilot")).Returns("copilot.exe"); _appRegistryMock.Setup(a => a.GetArguments("github copilot")).Returns("--allow-all-tools"); _handler.Handle("LaunchProgram", JsonDocument.Parse("""{"name":"github copilot"}""").RootElement); _processMock.Verify(p => p.Start(It.Is( psi => psi.Arguments == "--allow-all-tools")), Times.Once); } /// /// Verifies that when no executable path is available, the app is launched via its AppUserModelId through explorer.exe. /// [Fact] public void LaunchProgram_NoPath_UsesAppUserModelId() { _appRegistryMock.Setup(a => a.ResolveProcessName("calculator")).Returns("calculator"); _processMock.Setup(p => p.GetProcessesByName("calculator")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("calculator")).Returns((string)null!); _appRegistryMock.Setup(a => a.GetAppUserModelId("calculator")).Returns("Microsoft.WindowsCalculator"); _handler.Handle("LaunchProgram", JsonDocument.Parse("""{"name":"calculator"}""").RootElement); _processMock.Verify(p => p.Start(It.Is( psi => psi.FileName == "explorer.exe")), Times.Once); } /// /// Verifies that closing a program resolves its process name and looks up running processes. /// Note: the actual call path cannot be unit-tested because /// is not virtual and cannot be mocked. /// [Fact] public void CloseProgram_ResolvesProcessNameAndLooksUpProcesses() { _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); // Return a real (albeit useless in test) empty array to avoid null ref; // We cannot easily mock Process objects, so we verify the lookup was attempted. _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns([]); _handler.Handle("CloseProgram", JsonDocument.Parse("""{"name":"notepad"}""").RootElement); _processMock.Verify(p => p.GetProcessesByName("notepad"), Times.Once); } /// /// Verifies that closing a program that is not running does not throw an exception. /// [Fact] public void CloseProgram_NotRunning_DoesNothing() { _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns([]); var ex = Record.Exception(() => _handler.Handle("CloseProgram", JsonDocument.Parse("""{"name":"notepad"}""").RootElement)); Assert.Null(ex); } /// /// Verifies that the ListAppNames command invokes on the app registry. /// [Fact] public void ListAppNames_CallsGetAllAppNames() { _appRegistryMock.Setup(a => a.GetAllAppNames()).Returns(["notepad", "chrome"]); _handler.Handle("ListAppNames", JsonDocument.Parse("""{"name":""}""").RootElement); _appRegistryMock.Verify(a => a.GetAllAppNames(), Times.Once); } /// /// Verifies that launching an already-running app raises its window instead of starting a new process. /// [Fact] public void LaunchProgram_AlreadyRunning_RaisesWindow() { _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns([Process.GetCurrentProcess()]); _appRegistryMock.Setup(a => a.GetExecutablePath("notepad")).Returns("notepad.exe"); _handler.Handle("LaunchProgram", JsonDocument.Parse("""{"name":"notepad"}""").RootElement); _windowMock.Verify(w => w.RaiseWindow("notepad", "notepad.exe"), Times.Once); _processMock.Verify(p => p.Start(It.IsAny()), Times.Never); } /// /// Verifies that a on first launch attempt triggers a fallback retry using the friendly name. /// [Fact] public void LaunchProgram_Win32Exception_FallsBackToFriendlyName() { _appRegistryMock.Setup(a => a.ResolveProcessName("myapp")).Returns("myapp"); _processMock.Setup(p => p.GetProcessesByName("myapp")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("myapp")).Returns("myapp.exe"); _processMock.SetupSequence(p => p.Start(It.IsAny())) .Throws(new Win32Exception("not found")) .Returns(Process.GetCurrentProcess()); _handler.Handle("LaunchProgram", JsonDocument.Parse("""{"name":"myapp"}""").RootElement); _processMock.Verify(p => p.Start(It.IsAny()), Times.Exactly(2)); } /// /// Verifies that launching an app with no path and no AppUserModelId does not start any process. /// [Fact] public void LaunchProgram_NoPathNoAppModelId_DoesNothing() { _appRegistryMock.Setup(a => a.ResolveProcessName("unknown")).Returns("unknown"); _processMock.Setup(p => p.GetProcessesByName("unknown")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("unknown")).Returns((string)null!); _appRegistryMock.Setup(a => a.GetAppUserModelId("unknown")).Returns((string)null!); _handler.Handle("LaunchProgram", JsonDocument.Parse("""{"name":"unknown"}""").RootElement); _processMock.Verify(p => p.Start(It.IsAny()), Times.Never); } }