// 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);
}
}