// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Text.Json; namespace autoShell.Tests; /// /// End-to-end tests that launch autoShell.exe as a child process and communicate /// via the stdin/stdout JSON protocol. Tests the full pipeline including process /// startup, JSON parsing, command dispatch, and response serialization. /// [Trait("Category", "E2E")] public sealed class EndToEndTests : IDisposable { private readonly AutoShellProcess _process; public EndToEndTests() { _process = AutoShellProcess.StartInteractive(); } public void Dispose() { _process.Dispose(); } // --- Query commands (assert JSON stdout) --- /// /// Verifies that ListAppNames returns a valid JSON array of app names via stdout. /// [Fact] public async Task ListAppNames_ReturnsJsonArray() { _process.SendCommand("""{"actionName":"ListAppNames","parameters":{}}"""); string? response = await _process.ReadLineAsync(); Assert.NotNull(response); var result = JsonDocument.Parse(response).RootElement; Assert.True(result.GetProperty("success").GetBoolean()); var data = result.GetProperty("data"); Assert.True(data.GetArrayLength() > 0); } /// /// Verifies that ListThemes returns a valid JSON array of theme file paths via stdout. /// [Fact] public async Task ListThemes_ReturnsJsonArray() { _process.SendCommand("""{"actionName":"ListThemes","parameters":{}}"""); // Theme scanning involves disk I/O; allow extra time string? response = await _process.ReadLineAsync(10000); Assert.NotNull(response); var result = JsonDocument.Parse(response).RootElement; Assert.True(result.GetProperty("success").GetBoolean()); var data = result.GetProperty("data"); Assert.True(data.GetArrayLength() > 0); } /// /// Verifies that multiple sequential query commands each return a separate response. /// [Fact] public async Task MultipleQueries_EachReturnsResponse() { _process.SendCommand("""{"actionName":"ListAppNames","parameters":{}}"""); string? response1 = await _process.ReadLineAsync(); _process.SendCommand("""{"actionName":"ListThemes","parameters":{}}"""); string? response2 = await _process.ReadLineAsync(); Assert.NotNull(response1); Assert.NotNull(response2); _ = JsonDocument.Parse(response1); _ = JsonDocument.Parse(response2); } /// [Fact] public async Task ListResolutions_ReturnsResponse() { _process.SendCommand("""{"actionName":"ListResolutions","parameters":{}}"""); string? response = await _process.ReadLineAsync(10000); Assert.NotNull(response); Assert.NotEmpty(response); } /// /// Verifies that ListWifiNetworks returns a response via stdout. /// [Fact] public async Task ListWifiNetworks_ReturnsResponse() { _process.SendCommand("""{"actionName":"ListWifiNetworks","parameters":{}}"""); string? response = await _process.ReadLineAsync(10000); Assert.NotNull(response); Assert.NotEmpty(response); } /// /// Verifies that SetScreenResolution with an invalid value produces a response without crashing. /// Uses an intentionally invalid resolution to avoid changing the actual display. /// [Fact] public async Task SetScreenResolution_InvalidValue_ReturnsResponse() { _process.SendCommand("""{"actionName":"SetScreenResolution","parameters":{"width":99999,"height":99999}}"""); // May produce an error message or status — just verify the process survives // and we can still send commands await _process.ReadLineAsync(); _process.SendCommand("""{"actionName":"ListAppNames","parameters":{}}"""); string? response = await _process.ReadLineAsync(); Assert.False(_process.HasExited); Assert.NotNull(response); } // --- Protocol edge cases --- /// /// Verifies that two separate commands each produce a response. /// [Fact] public async Task MultiCommandObject_ProducesMultipleResponses() { _process.SendCommand("""{"actionName":"ListAppNames","parameters":{}}"""); _process.SendCommand("""{"actionName":"ListThemes","parameters":{}}"""); string? response1 = await _process.ReadLineAsync(); string? response2 = await _process.ReadLineAsync(10000); Assert.NotNull(response1); Assert.NotNull(response2); _ = JsonDocument.Parse(response1); _ = JsonDocument.Parse(response2); } /// /// Verifies that sending a quit command causes the process to exit. /// [Fact] public async Task Quit_StopsMidBatch() { _process.SendCommand("""{"actionName":"ListAppNames","parameters":{}}"""); string? response1 = await _process.ReadLineAsync(); Assert.NotNull(response1); _ = JsonDocument.Parse(response1); _process.SendCommand("""{"actionName":"quit","parameters":{}}"""); _process.WaitForExit(10000); Assert.True(_process.HasExited); } /// /// Verifies that sending {"quit":""} causes the process to exit cleanly. /// [Fact] public void Quit_ProcessExits() { _process.SendQuit(); Assert.True(_process.HasExited); } /// /// Verifies that malformed JSON does not crash the process. /// Sends invalid JSON followed by a valid query to confirm the process is still alive. /// [Fact] public async Task MalformedJson_ProcessSurvives() { _process.SendCommand("this is not json"); // Process should still be alive — send a valid command to verify _process.SendCommand("""{"actionName":"ListAppNames","parameters":{}}"""); string? response = await _process.ReadLineAsync(); Assert.False(_process.HasExited); Assert.NotNull(response); } /// /// Verifies that an empty line does not crash the process. /// The error message goes to stdout (known protocol limitation), so we /// consume it before verifying the process is still responsive. /// [Fact] public async Task EmptyLine_ProcessSurvives() { _process.SendCommand(""); // Consume the error/status message that goes to stdout. // Allow extra time since empty-line handling may be slow. string? errorLine = await _process.ReadLineAsync(); // Ensure the first read completed before starting a second one Assert.False(_process.HasExited); _process.SendCommand("""{"actionName":"ListAppNames","parameters":{}}"""); string? response = await _process.ReadLineAsync(); Assert.False(_process.HasExited); Assert.NotNull(response); } /// /// Verifies that an unknown command does not crash the process. /// [Fact] public async Task UnknownCommand_ProcessSurvives() { _process.SendCommand("""{"actionName":"NonExistentCommand","parameters":{}}"""); _process.SendCommand("""{"actionName":"ListAppNames","parameters":{}}"""); string? response = await _process.ReadLineAsync(); Assert.False(_process.HasExited); Assert.NotNull(response); } /// /// Verifies that closing stdin (EOF) causes the process to exit cleanly. /// [Fact] public void StdinClosed_ProcessExits() { _process.CloseStdin(); _process.WaitForExit(5000); Assert.True(_process.HasExited); } // --- Command-line mode --- /// /// Verifies that passing a single JSON command as a command-line argument /// executes it and exits (non-interactive mode). /// [Fact] public void CommandLineMode_SingleObject_ExecutesAndExits() { var (output, exitCode) = AutoShellProcess.RunWithArgs( """{"actionName":"ListAppNames","parameters":{}}"""); Assert.Equal(0, exitCode); Assert.NotEmpty(output); _ = JsonDocument.Parse(output.Trim()); } /// /// Verifies that passing a JSON array of commands as command-line arguments /// executes all of them and exits. /// [Fact] public void CommandLineMode_JsonArray_ExecutesAllAndExits() { var (output, exitCode) = AutoShellProcess.RunWithArgs( """[{"actionName":"ListAppNames","parameters":{}},{"actionName":"ListThemes","parameters":{}}]"""); Assert.Equal(0, exitCode); Assert.NotEmpty(output); } }