microsoft/TypeAgent
Publicmirrored fromhttps://github.com/microsoft/TypeAgentAvailable
dotnet/autoShell.Tests/ActionDispatcherIntegrationTests.cs
236lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | using System.Text.Json; |
| 5 | using autoShell.Logging; |
| 6 | using autoShell.Services; |
| 7 | using Moq; |
| 8 | using static autoShell.Services.Interop.SpiConstants; |
| 9 | |
| 10 | namespace autoShell.Tests; |
| 11 | |
| 12 | /// <summary> |
| 13 | /// Integration tests that exercise the full <see cref="ActionDispatcher.Create"/> → <see cref="ActionDispatcher.Dispatch"/> → handler → service pipeline |
| 14 | /// using mock services. These verify that <see cref="ActionDispatcher"/> wiring is correct. |
| 15 | /// </summary> |
| 16 | public class ActionDispatcherIntegrationTests |
| 17 | { |
| 18 | private readonly Mock<IRegistryService> _registryMock = new(); |
| 19 | private readonly Mock<ISystemParametersService> _systemParamsMock = new(); |
| 20 | private readonly Mock<IProcessService> _processMock = new(); |
| 21 | private readonly Mock<IAudioService> _audioMock = new(); |
| 22 | private readonly Mock<IAppRegistry> _appRegistryMock = new(); |
| 23 | private readonly Mock<IDebuggerService> _debuggerMock = new(); |
| 24 | private readonly Mock<IBrightnessService> _brightnessMock = new(); |
| 25 | private readonly Mock<IDisplayService> _displayMock = new(); |
| 26 | private readonly Mock<IWindowService> _windowMock = new(); |
| 27 | private readonly Mock<INetworkService> _networkMock = new(); |
| 28 | private readonly Mock<IVirtualDesktopService> _virtualDesktopMock = new(); |
| 29 | private readonly Mock<ILogger> _loggerMock = new(); |
| 30 | private readonly ActionDispatcher _dispatcher; |
| 31 | |
| 32 | public ActionDispatcherIntegrationTests() |
| 33 | { |
| 34 | _dispatcher = ActionDispatcher.Create( |
| 35 | _loggerMock.Object, |
| 36 | _registryMock.Object, |
| 37 | _systemParamsMock.Object, |
| 38 | _processMock.Object, |
| 39 | _audioMock.Object, |
| 40 | _appRegistryMock.Object, |
| 41 | _debuggerMock.Object, |
| 42 | _brightnessMock.Object, |
| 43 | _displayMock.Object, |
| 44 | _windowMock.Object, |
| 45 | _networkMock.Object, |
| 46 | _virtualDesktopMock.Object); |
| 47 | } |
| 48 | |
| 49 | /// <summary> |
| 50 | /// Verifies that a Volume command dispatched through <see cref="ActionDispatcher.Create"/> reaches the audio service. |
| 51 | /// </summary> |
| 52 | [Fact] |
| 53 | public void Dispatch_Volume_ReachesAudioService() |
| 54 | { |
| 55 | _audioMock.Setup(a => a.GetVolume()).Returns(50); |
| 56 | |
| 57 | Dispatch("""{"actionName":"Volume","parameters":{"targetVolume":75}}"""); |
| 58 | |
| 59 | _audioMock.Verify(a => a.SetVolume(75), Times.Once); |
| 60 | } |
| 61 | |
| 62 | /// <summary> |
| 63 | /// Verifies that a Mute command dispatched through <see cref="ActionDispatcher.Create"/> reaches the audio service. |
| 64 | /// </summary> |
| 65 | [Fact] |
| 66 | public void Dispatch_Mute_ReachesAudioService() |
| 67 | { |
| 68 | Dispatch("""{"actionName":"Mute","parameters":{"on":true}}"""); |
| 69 | |
| 70 | _audioMock.Verify(a => a.SetMute(true), Times.Once); |
| 71 | } |
| 72 | |
| 73 | /// <summary> |
| 74 | /// Verifies that a LaunchProgram command dispatched through <see cref="ActionDispatcher.Create"/> reaches the process service. |
| 75 | /// </summary> |
| 76 | [Fact] |
| 77 | public void Dispatch_LaunchProgram_ReachesProcessService() |
| 78 | { |
| 79 | _appRegistryMock.Setup(a => a.ResolveProcessName("notepad")).Returns("notepad"); |
| 80 | _processMock.Setup(p => p.GetProcessesByName("notepad")).Returns([]); |
| 81 | _appRegistryMock.Setup(a => a.GetExecutablePath("notepad")).Returns("notepad.exe"); |
| 82 | |
| 83 | Dispatch("""{"actionName":"LaunchProgram","parameters":{"name":"notepad"}}"""); |
| 84 | |
| 85 | _processMock.Verify(p => p.Start(It.IsAny<System.Diagnostics.ProcessStartInfo>()), Times.Once); |
| 86 | } |
| 87 | |
| 88 | /// <summary> |
| 89 | /// Verifies that a SetWallpaper command dispatched through <see cref="ActionDispatcher.Create"/> reaches the system parameters service. |
| 90 | /// </summary> |
| 91 | [Fact] |
| 92 | public void Dispatch_SetWallpaper_ReachesSystemParamsService() |
| 93 | { |
| 94 | Dispatch("""{"actionName":"SetWallpaper","parameters":{"filePath":"C:\\wallpaper.jpg"}}"""); |
| 95 | |
| 96 | _systemParamsMock.Verify(s => s.SetParameter(SPI_SETDESKWALLPAPER, 0, @"C:\wallpaper.jpg", SPIF_UPDATEINIFILE_SENDCHANGE), Times.Once); |
| 97 | } |
| 98 | |
| 99 | /// <summary> |
| 100 | /// Verifies that a ConnectWifi command dispatched through <see cref="ActionDispatcher.Create"/> reaches the network service. |
| 101 | /// </summary> |
| 102 | [Fact] |
| 103 | public void Dispatch_ConnectWifi_ReachesNetworkService() |
| 104 | { |
| 105 | Dispatch("""{"actionName":"ConnectWifi","parameters":{"ssid":"MyNetwork","password":"pass123"}}"""); |
| 106 | |
| 107 | _networkMock.Verify(n => n.ConnectToWifi(It.IsAny<string>(), It.IsAny<string>()), Times.Once); |
| 108 | } |
| 109 | |
| 110 | /// <summary> |
| 111 | /// Verifies that a NextDesktop command dispatched through <see cref="ActionDispatcher.Create"/> reaches the virtual desktop service. |
| 112 | /// </summary> |
| 113 | [Fact] |
| 114 | public void Dispatch_NextDesktop_ReachesVirtualDesktopService() |
| 115 | { |
| 116 | Dispatch("""{"actionName":"NextDesktop","parameters":{}}"""); |
| 117 | |
| 118 | _virtualDesktopMock.Verify(v => v.NextDesktop(), Times.Once); |
| 119 | } |
| 120 | |
| 121 | /// <summary> |
| 122 | /// Verifies that a SetThemeMode command dispatched through <see cref="ActionDispatcher.Create"/> reaches the registry service. |
| 123 | /// </summary> |
| 124 | [Fact] |
| 125 | public void Dispatch_SetThemeMode_ReachesRegistryService() |
| 126 | { |
| 127 | Dispatch("""{"actionName":"SetThemeMode","parameters":{"mode":"dark"}}"""); |
| 128 | |
| 129 | _registryMock.Verify(r => r.SetValue( |
| 130 | It.IsAny<string>(), "AppsUseLightTheme", 0, It.IsAny<Microsoft.Win32.RegistryValueKind>()), Times.Once); |
| 131 | } |
| 132 | |
| 133 | /// <summary> |
| 134 | /// Verifies that an unknown command does not throw and logs a debug message. |
| 135 | /// </summary> |
| 136 | [Fact] |
| 137 | public void Dispatch_UnknownCommand_DoesNotThrow() |
| 138 | { |
| 139 | var ex = Record.Exception(() => Dispatch("""{"actionName":"NonExistentCommand","parameters":{}}""")); |
| 140 | |
| 141 | Assert.Null(ex); |
| 142 | } |
| 143 | |
| 144 | /// <summary> |
| 145 | /// Verifies that multiple commands dispatched separately all reach their services. |
| 146 | /// </summary> |
| 147 | [Fact] |
| 148 | public void Dispatch_MultipleCommands_AllReachServices() |
| 149 | { |
| 150 | _audioMock.Setup(a => a.GetVolume()).Returns(50); |
| 151 | |
| 152 | Dispatch("""{"actionName":"Volume","parameters":{"targetVolume":80}}"""); |
| 153 | Dispatch("""{"actionName":"Mute","parameters":{"on":false}}"""); |
| 154 | |
| 155 | _audioMock.Verify(a => a.SetVolume(80), Times.Once); |
| 156 | _audioMock.Verify(a => a.SetMute(false), Times.Once); |
| 157 | } |
| 158 | |
| 159 | /// <summary> |
| 160 | /// Verifies that quit stops processing and returns null. |
| 161 | /// </summary> |
| 162 | [Fact] |
| 163 | public void Dispatch_Quit_ReturnsQuitResult() |
| 164 | { |
| 165 | ActionResult result = _dispatcher.Dispatch(JsonDocument.Parse("""{"actionName":"quit","parameters":{}}""").RootElement); |
| 166 | |
| 167 | Assert.NotNull(result); |
| 168 | Assert.True(result.Success); |
| 169 | Assert.True(result.IsQuit); |
| 170 | } |
| 171 | |
| 172 | private void Dispatch(string json) |
| 173 | { |
| 174 | _dispatcher.Dispatch(JsonDocument.Parse(json).RootElement); |
| 175 | } |
| 176 | |
| 177 | // --- Schema wiring validation --- |
| 178 | |
| 179 | /// <summary> |
| 180 | /// Verifies that every action defined in the .pas.json schemas has a registered C# handler. |
| 181 | /// This test fails when a new action is added to a TypeScript schema but not wired in C#. |
| 182 | /// </summary> |
| 183 | [Fact] |
| 184 | public void AllSchemaActions_HaveRegisteredHandlers() |
| 185 | { |
| 186 | var schemaActions = LoadRealSchemaActions(); |
| 187 | Assert.True(schemaActions.Count > 0, "No schema actions loaded — .pas.json files must be present after build"); |
| 188 | |
| 189 | var (missingHandlers, _) = SchemaValidator.FindMismatches(schemaActions, _dispatcher.RegisteredActions); |
| 190 | |
| 191 | Assert.True( |
| 192 | missingHandlers.Count == 0, |
| 193 | $"Schema actions without C# handlers: {string.Join(", ", missingHandlers)}"); |
| 194 | } |
| 195 | |
| 196 | /// <summary> |
| 197 | /// Actions registered in C# handlers that intentionally have no .pas.json schema definition |
| 198 | /// (query/utility actions that take no parameters from the LLM). |
| 199 | /// </summary> |
| 200 | private static readonly System.Collections.Generic.HashSet<string> SchemalessActions = new() |
| 201 | { |
| 202 | "ListAppNames", |
| 203 | "ListThemes", |
| 204 | "ListWifiNetworks", |
| 205 | "ListResolutions", |
| 206 | "DisplayResolutionAndAspectRatio", |
| 207 | }; |
| 208 | |
| 209 | /// <summary> |
| 210 | /// Verifies that every registered C# handler action has a matching .pas.json schema definition. |
| 211 | /// Known schemaless actions (query/utility) are excluded from the check. |
| 212 | /// </summary> |
| 213 | [Fact] |
| 214 | public void AllRegisteredHandlers_HaveSchemaDefinitions() |
| 215 | { |
| 216 | var schemaActions = LoadRealSchemaActions(); |
| 217 | Assert.True(schemaActions.Count > 0, "No schema actions loaded — .pas.json files must be present after build"); |
| 218 | |
| 219 | var (_, missingSchemas) = SchemaValidator.FindMismatches(schemaActions, _dispatcher.RegisteredActions); |
| 220 | missingSchemas.RemoveAll(a => SchemalessActions.Contains(a)); |
| 221 | |
| 222 | Assert.True( |
| 223 | missingSchemas.Count == 0, |
| 224 | $"Handler actions without schema definitions: {string.Join(", ", missingSchemas)}"); |
| 225 | } |
| 226 | |
| 227 | private static System.Collections.Generic.HashSet<string> LoadRealSchemaActions() |
| 228 | { |
| 229 | var validator = new SchemaValidator(new Logging.NullLogger()); |
| 230 | // From test output (autoShell.Tests/bin/Debug/net8.0-windows/) we need 5 levels up to repo root |
| 231 | var schemaDir = System.IO.Path.Combine( |
| 232 | AppContext.BaseDirectory, "..", "..", "..", "..", "..", |
| 233 | "ts", "packages", "agents", "desktop", "dist"); |
| 234 | return validator.LoadActionNames(schemaDir); |
| 235 | } |
| 236 | } |
| 237 | |