// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Text.Json; using autoShell.Logging; using autoShell.Services; using Moq; using static autoShell.Services.Interop.SpiConstants; namespace autoShell.Tests; /// /// Integration tests that exercise the full → handler → service pipeline /// using mock services. These verify that wiring is correct. /// public class ActionDispatcherIntegrationTests { private readonly Mock _registryMock = new(); private readonly Mock _systemParamsMock = new(); private readonly Mock _processMock = new(); private readonly Mock _audioMock = new(); private readonly Mock _appRegistryMock = new(); private readonly Mock _debuggerMock = new(); private readonly Mock _brightnessMock = new(); private readonly Mock _displayMock = new(); private readonly Mock _windowMock = new(); private readonly Mock _networkMock = new(); private readonly Mock _virtualDesktopMock = new(); private readonly Mock _loggerMock = new(); private readonly ActionDispatcher _dispatcher; public ActionDispatcherIntegrationTests() { _dispatcher = ActionDispatcher.Create( _loggerMock.Object, _registryMock.Object, _systemParamsMock.Object, _processMock.Object, _audioMock.Object, _appRegistryMock.Object, _debuggerMock.Object, _brightnessMock.Object, _displayMock.Object, _windowMock.Object, _networkMock.Object, _virtualDesktopMock.Object); } /// /// Verifies that a Volume command dispatched through reaches the audio service. /// [Fact] public void Dispatch_Volume_ReachesAudioService() { _audioMock.Setup(a => a.GetVolume()).Returns(50); Dispatch("""{"actionName":"Volume","parameters":{"targetVolume":75}}"""); _audioMock.Verify(a => a.SetVolume(75), Times.Once); } /// /// Verifies that a Mute command dispatched through reaches the audio service. /// [Fact] public void Dispatch_Mute_ReachesAudioService() { Dispatch("""{"actionName":"Mute","parameters":{"on":true}}"""); _audioMock.Verify(a => a.SetMute(true), Times.Once); } /// /// Verifies that a LaunchProgram command dispatched through reaches the process service. /// [Fact] public void Dispatch_LaunchProgram_ReachesProcessService() { _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns([]); _appRegistryMock.Setup(a => a.GetExecutablePath("notepad")).Returns("notepad.exe"); Dispatch("""{"actionName":"LaunchProgram","parameters":{"name":"notepad"}}"""); _processMock.Verify(p => p.Start(It.IsAny()), Times.Once); } /// /// Verifies that a SetWallpaper command dispatched through reaches the system parameters service. /// [Fact] public void Dispatch_SetWallpaper_ReachesSystemParamsService() { Dispatch("""{"actionName":"SetWallpaper","parameters":{"filePath":"C:\\wallpaper.jpg"}}"""); _systemParamsMock.Verify(s => s.SetParameter(SPI_SETDESKWALLPAPER, 0, @"C:\wallpaper.jpg", SPIF_UPDATEINIFILE_SENDCHANGE), Times.Once); } /// /// Verifies that a ConnectWifi command dispatched through reaches the network service. /// [Fact] public void Dispatch_ConnectWifi_ReachesNetworkService() { Dispatch("""{"actionName":"ConnectWifi","parameters":{"ssid":"MyNetwork","password":"pass123"}}"""); _networkMock.Verify(n => n.ConnectToWifi(It.IsAny(), It.IsAny()), Times.Once); } /// /// Verifies that a NextDesktop command dispatched through reaches the virtual desktop service. /// [Fact] public void Dispatch_NextDesktop_ReachesVirtualDesktopService() { Dispatch("""{"actionName":"NextDesktop","parameters":{}}"""); _virtualDesktopMock.Verify(v => v.NextDesktop(), Times.Once); } /// /// Verifies that a SetThemeMode command dispatched through reaches the registry service. /// [Fact] public void Dispatch_SetThemeMode_ReachesRegistryService() { Dispatch("""{"actionName":"SetThemeMode","parameters":{"mode":"dark"}}"""); _registryMock.Verify(r => r.SetValue( It.IsAny(), "AppsUseLightTheme", 0, It.IsAny()), Times.Once); } /// /// Verifies that an unknown command does not throw and logs a debug message. /// [Fact] public void Dispatch_UnknownCommand_DoesNotThrow() { var ex = Record.Exception(() => Dispatch("""{"actionName":"NonExistentCommand","parameters":{}}""")); Assert.Null(ex); } /// /// Verifies that multiple commands dispatched separately all reach their services. /// [Fact] public void Dispatch_MultipleCommands_AllReachServices() { _audioMock.Setup(a => a.GetVolume()).Returns(50); Dispatch("""{"actionName":"Volume","parameters":{"targetVolume":80}}"""); Dispatch("""{"actionName":"Mute","parameters":{"on":false}}"""); _audioMock.Verify(a => a.SetVolume(80), Times.Once); _audioMock.Verify(a => a.SetMute(false), Times.Once); } /// /// Verifies that quit stops processing and returns null. /// [Fact] public void Dispatch_Quit_ReturnsQuitResult() { ActionResult result = _dispatcher.Dispatch(JsonDocument.Parse("""{"actionName":"quit","parameters":{}}""").RootElement); Assert.NotNull(result); Assert.True(result.Success); Assert.True(result.IsQuit); } private void Dispatch(string json) { _dispatcher.Dispatch(JsonDocument.Parse(json).RootElement); } // --- Schema wiring validation --- /// /// Verifies that every action defined in the .pas.json schemas has a registered C# handler. /// This test fails when a new action is added to a TypeScript schema but not wired in C#. /// [Fact] public void AllSchemaActions_HaveRegisteredHandlers() { var schemaActions = LoadRealSchemaActions(); Assert.True(schemaActions.Count > 0, "No schema actions loaded — .pas.json files must be present after build"); var (missingHandlers, _) = SchemaValidator.FindMismatches(schemaActions, _dispatcher.RegisteredActions); Assert.True( missingHandlers.Count == 0, $"Schema actions without C# handlers: {string.Join(", ", missingHandlers)}"); } /// /// Actions registered in C# handlers that intentionally have no .pas.json schema definition /// (query/utility actions that take no parameters from the LLM). /// private static readonly System.Collections.Generic.HashSet SchemalessActions = new() { "ListAppNames", "ListThemes", "ListWifiNetworks", "ListResolutions", "DisplayResolutionAndAspectRatio", }; /// /// Verifies that every registered C# handler action has a matching .pas.json schema definition. /// Known schemaless actions (query/utility) are excluded from the check. /// [Fact] public void AllRegisteredHandlers_HaveSchemaDefinitions() { var schemaActions = LoadRealSchemaActions(); Assert.True(schemaActions.Count > 0, "No schema actions loaded — .pas.json files must be present after build"); var (_, missingSchemas) = SchemaValidator.FindMismatches(schemaActions, _dispatcher.RegisteredActions); missingSchemas.RemoveAll(a => SchemalessActions.Contains(a)); Assert.True( missingSchemas.Count == 0, $"Handler actions without schema definitions: {string.Join(", ", missingSchemas)}"); } private static System.Collections.Generic.HashSet LoadRealSchemaActions() { var validator = new SchemaValidator(new Logging.NullLogger()); // From test output (autoShell.Tests/bin/Debug/net8.0-windows/) we need 5 levels up to repo root var schemaDir = System.IO.Path.Combine( AppContext.BaseDirectory, "..", "..", "..", "..", "..", "ts", "packages", "agents", "desktop", "dist"); return validator.LoadActionNames(schemaDir); } }