diff --git a/.github/skills/cli-e2e-testing/SKILL.md b/.github/skills/cli-e2e-testing/SKILL.md index a9ae8dbe883..15a5ba8b778 100644 --- a/.github/skills/cli-e2e-testing/SKILL.md +++ b/.github/skills/cli-e2e-testing/SKILL.md @@ -45,29 +45,53 @@ public sealed class SmokeTests(ITestOutputHelper output) [Fact] public async Task MyCliTest() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var strategy = CliInstallStrategy.Detect(output.WriteLine); var workspace = TemporaryWorkspace.Create(output); - var installMode = CliE2ETestHelpers.DetectDockerInstallMode(); - using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); - await auto.InstallAspireCliInDockerAsync(installMode, counter); + await auto.InstallAspireCliAsync(strategy, counter); await auto.TypeAsync("aspire --version"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; } } ``` +### TerminalRun Pattern + +**Always use `CliE2ETestHelpers.StartRun`** to wrap the terminal run. This returns a `TerminalRun` (implements `IAsyncDisposable`) that automatically: +1. Captures Aspire diagnostics via `CaptureAspireDiagnosticsAsync` (best effort) +2. Types `exit` and presses Enter to close the terminal +3. Awaits the pending run task + +This eliminates the need for manual `exit`/`await pendingRun` at the end of every test and ensures diagnostics are always captured, even when tests fail. + +```csharp +// DO: Use StartRun for consistent diagnostics capture and cleanup +using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); + +var counter = new SequenceCounter(); +var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); +await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, TestContext.Current.CancellationToken); + +// ... test body — no exit/pendingRun needed at the end + +// DON'T: Manually handle exit and pendingRun +var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); +// ... test body ... +await auto.TypeAsync("exit"); +await auto.EnterAsync(); +await pendingRun; +``` + ## Running Tests Locally CLI E2E tests run inside Docker containers on Linux. The workflow is: build a portable archive with `localhive`, then point the tests at it. This is the primary way to iterate on E2E tests during development. @@ -246,10 +270,10 @@ await auto.WaitUntilAsync( | Method | Description | |--------|-------------| -| `WaitForSuccessPromptAsync(counter, timeout?)` | Waits for `[N OK] $ ` prompt and increments counter | +| `WaitForSuccessPromptAsync(counter, timeout?)` | Waits for `[N OK] $ ` prompt, fails immediately if error prompt appears, and increments counter | | `WaitForAnyPromptAsync(counter, timeout?)` | Waits for any prompt (`OK` or `ERR`) and increments counter | | `WaitForErrorPromptAsync(counter, timeout?)` | Waits for `[N ERR:code] $ ` prompt and increments counter | -| `WaitForSuccessPromptFailFastAsync(counter, timeout?)` | Waits for success prompt, fails immediately if error prompt appears | +| `RunCommandAsync(command, counter, timeout?)` | Types a command, presses Enter, and waits for success prompt (fails fast on error) | | `DeclineAgentInitPromptAsync()` | Declines the `aspire agent init` prompt if it appears | | `AspireNewAsync(projectName, counter, template?, useRedisCache?)` | Runs `aspire new` interactively, handling template selection, project name, output path, URLs, Redis, and test project prompts | @@ -277,8 +301,7 @@ The following extensions on `Hex1bTerminalInputSequenceBuilder` are still availa |--------|-------------| | `WaitForSuccessPrompt(counter, timeout?)` | *(legacy)* Waits for `[N OK] $ ` prompt and increments counter | | `PrepareEnvironment(workspace, counter)` | *(legacy)* Sets up custom prompt with command tracking | -| `InstallAspireCliFromPullRequest(prNumber, counter)` | *(legacy)* Downloads and installs CLI from PR artifacts | -| `SourceAspireCliEnvironment(counter)` | *(legacy)* Adds `~/.aspire/bin` to PATH | +| `SourceAspireBundleEnvironment(counter)` | *(legacy)* Sources bundle PATH environment variables | ## DO: Use CellPatternSearcher for Output Detection diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index 57eda7152fb..6399edd8d0a 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -306,7 +306,7 @@ protected override async Task ExecuteAsync(ParseResult parseResul catch (TimeoutException) { runActivity?.SetTag(TelemetryConstants.Tags.ErrorType, "startup_timeout"); - await CancelAppHostStartupAsync(runCancellationTokenSource, pendingRun).ConfigureAwait(false); + await CancelAppHostStartupAsync(runCancellationTokenSource, pendingRun, cancellationToken).ConfigureAwait(false); return CreateStartupTimeoutResult(timeoutSeconds); } @@ -351,7 +351,7 @@ protected override async Task ExecuteAsync(ParseResult parseResul catch (TimeoutException) { runActivity?.SetTag(TelemetryConstants.Tags.ErrorType, "startup_timeout"); - await CancelAppHostStartupAsync(runCancellationTokenSource, pendingRun).ConfigureAwait(false); + await CancelAppHostStartupAsync(runCancellationTokenSource, pendingRun, cancellationToken).ConfigureAwait(false); return CreateStartupTimeoutResult(timeoutSeconds); } @@ -1124,15 +1124,18 @@ private TimeSpan GetRemainingStartupTimeout(long startupStartTimestamp, TimeSpan return elapsed >= startupTimeout ? TimeSpan.Zero : startupTimeout - elapsed; } - private async Task CancelAppHostStartupAsync(CancellationTokenSource runCancellationTokenSource, Task pendingRun) + private async Task CancelAppHostStartupAsync(CancellationTokenSource runCancellationTokenSource, Task pendingRun, CancellationToken cancellationToken) { runCancellationTokenSource.Cancel(); try { - await pendingRun.WaitAsync(s_appHostStartupCancellationTimeout, _timeProvider).ConfigureAwait(false); + // The timeout is a safety net for the startup-timeout path (no Ctrl+C). When the user + // presses Ctrl+C, cancellationToken fires and WaitAsync exits immediately via the token + // rather than waiting for the full timeout duration. + await pendingRun.WaitAsync(s_appHostStartupCancellationTimeout, _timeProvider, cancellationToken).ConfigureAwait(false); } - catch (OperationCanceledException) when (runCancellationTokenSource.IsCancellationRequested) + catch (OperationCanceledException) when (runCancellationTokenSource.IsCancellationRequested || cancellationToken.IsCancellationRequested) { } catch (TimeoutException ex) diff --git a/src/Aspire.Cli/ConsoleCancellationManager.cs b/src/Aspire.Cli/ConsoleCancellationManager.cs index d4588f3a550..74a6c3d16b0 100644 --- a/src/Aspire.Cli/ConsoleCancellationManager.cs +++ b/src/Aspire.Cli/ConsoleCancellationManager.cs @@ -1,18 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Aspire.Cli; /// /// Manages Ctrl+C, SIGINT, and SIGTERM signal handling with a shared CancellationTokenSource. -/// After cancellation is requested, waits up to processTerminationTimeout for the running -/// handler to complete before signaling forced termination via . +/// After cancellation is requested, schedules an asynchronous timeout for the running handler +/// to complete before signaling forced termination via . +/// A second signal forces immediate termination without waiting for the timeout. /// Disposing this instance unregisters all signal handlers and disposes the token source. /// internal sealed class ConsoleCancellationManager : IDisposable { + // Standard Unix exit codes: 128 + signal number (SIGINT=2, SIGTERM=15). + // SigIntExitCode (130): used when the user presses Ctrl+C (SIGINT) or Ctrl+Break/SIGQUIT. + // SigTermExitCode (143): used when the process receives SIGTERM (e.g. container stop, ProcessExit). private const int SigIntExitCode = 130; private const int SigTermExitCode = 143; @@ -20,7 +27,9 @@ internal sealed class ConsoleCancellationManager : IDisposable private readonly TimeSpan _processTerminationTimeout; private readonly PosixSignalRegistration? _sigIntRegistration; private readonly PosixSignalRegistration? _sigTermRegistration; + private readonly PosixSignalRegistration? _sigQuitRegistration; private readonly CancellationToken _token; + private ILogger _logger; private Task? _startedHandler; private int _cancelCalled; @@ -38,9 +47,16 @@ internal sealed class ConsoleCancellationManager : IDisposable /// internal void SetStartedHandler(Task handler) => Volatile.Write(ref _startedHandler, handler); + /// + /// Sets the logger instance used for diagnostic messages during signal handling. + /// Call this once the logging infrastructure is available. + /// + internal void SetLogger(ILogger logger) => Volatile.Write(ref _logger, logger); + public ConsoleCancellationManager(TimeSpan processTerminationTimeout) { _processTerminationTimeout = processTerminationTimeout; + _logger = NullLogger.Instance; // Set to a field so getting the token doesn't error after dispose. _token = _cts.Token; @@ -56,9 +72,22 @@ public ConsoleCancellationManager(TimeSpan processTerminationTimeout) { _sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, OnPosixSignal); _sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, OnPosixSignal); + + // SIGQUIT maps to CTRL_BREAK_EVENT on Windows. Register it to maintain parity with + // Console.CancelKeyPress which handled both Ctrl+C and Ctrl+Break. + // On Linux/macOS, SIGQUIT's default action produces a core dump which is useful for + // debugging hung processes — don't intercept it there. + if (OperatingSystem.IsWindows()) + { + _sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, OnPosixSignal); + } + } + else + { + // Fall back to Console.CancelKeyPress on platforms that don't support PosixSignalRegistration. + Console.CancelKeyPress += OnCancelKeyPress; } - Console.CancelKeyPress += OnCancelKeyPress; AppDomain.CurrentDomain.ProcessExit += OnProcessExit; } @@ -69,7 +98,13 @@ public ConsoleCancellationManager(TimeSpan processTerminationTimeout) private void OnPosixSignal(PosixSignalContext context) { context.Cancel = true; - Cancel(context.Signal == PosixSignal.SIGINT ? SigIntExitCode : SigTermExitCode); + var exitCode = context.Signal switch + { + PosixSignal.SIGINT => SigIntExitCode, + PosixSignal.SIGQUIT => SigIntExitCode, + _ => SigTermExitCode + }; + Cancel(exitCode); } private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e) @@ -80,40 +115,73 @@ private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e) private void OnProcessExit(object? sender, EventArgs e) => Cancel(SigTermExitCode); - private void Cancel(int forcedTerminationExitCode) + internal void Cancel(int forcedTerminationExitCode) { - // Ensure only the first signal triggers cancellation logic; subsequent signals are no-ops. - if (Interlocked.CompareExchange(ref _cancelCalled, 1, 0) != 0) - { - return; - } + var signalCount = Interlocked.Increment(ref _cancelCalled); - // Request cancellation so cooperative listeners can begin shutting down. - try + if (signalCount == 1) { - _cts.Cancel(); + // First signal: request cooperative cancellation and schedule an async timeout + // that will force-terminate if the handler doesn't complete in time. + _logger.LogInformation("Termination signal received, requesting cancellation."); + + try + { + _cts.Cancel(); + } + catch (ObjectDisposedException) + { + // A signal can race with process shutdown after cancellation resources are disposed. + return; + } + + // Schedule the forced-completion timeout asynchronously so the signal handler + // returns immediately. This allows Program.Main's Task.WhenAny to observe + // handlerTask completion without being blocked by the signal handler thread. + _ = ForceTerminationAfterTimeoutAsync(forcedTerminationExitCode); } - catch (ObjectDisposedException) + else { - // A signal can race with process shutdown after cancellation resources are disposed. - return; + // Second (or subsequent) signal: force immediate termination without waiting. + _logger.LogWarning("Second termination signal received, forcing immediate exit."); + _processTerminationCompletionSource.TrySetResult(forcedTerminationExitCode); } + } + private async Task ForceTerminationAfterTimeoutAsync(int forcedTerminationExitCode) + { try { + // When a debugger is attached, don't force-terminate — the developer needs + // unlimited time to step through cancellation/cleanup logic. + if (Debugger.IsAttached) + { + return; + } + var startedHandler = Volatile.Read(ref _startedHandler); - // Wait for the configured interval to allow graceful shutdown. - if (startedHandler is null || !startedHandler.Wait(_processTerminationTimeout)) + if (startedHandler is not null) { - // If the handler does not finish within configured time, use the completion - // source to signal forced completion (preserving native exit code). - _processTerminationCompletionSource.TrySetResult(forcedTerminationExitCode); + // Give the handler a chance to finish gracefully within the configured timeout. + // Task.WhenAny completes when either the handler or the delay finishes first, + // without propagating exceptions from the losing task. + // It's ok that this delay isn't cancellable. The process is ending. + var timeoutTask = Task.Delay(_processTerminationTimeout); + if (await Task.WhenAny(startedHandler, timeoutTask).ConfigureAwait(false) == startedHandler) + { + // Handler finished within the timeout; no forced termination needed. + return; + } } + + _logger.LogWarning("Handler did not complete within {Timeout}s, forcing termination.", _processTerminationTimeout.TotalSeconds); + _processTerminationCompletionSource.TrySetResult(forcedTerminationExitCode); } - catch (AggregateException) + catch (Exception) { - // The task was cancelled or an exception was thrown during task execution. + // Any failure in the timeout path should still force termination rather than hang. + _processTerminationCompletionSource.TrySetResult(forcedTerminationExitCode); } } @@ -121,6 +189,7 @@ public void Dispose() { _sigIntRegistration?.Dispose(); _sigTermRegistration?.Dispose(); + _sigQuitRegistration?.Dispose(); Console.CancelKeyPress -= OnCancelKeyPress; AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs index 3a78e399b7b..4cb97e53d8c 100644 --- a/src/Aspire.Cli/Program.cs +++ b/src/Aspire.Cli/Program.cs @@ -801,6 +801,7 @@ public static async Task Main(string[] args) var logBufferContext = new ConsoleLogBufferContext(); var (loggerFactory, fileLoggerProvider) = CreateLoggerFactory(args, loggingOptions, errorWriter, logBufferContext); var logger = loggerFactory.CreateLogger(RootLoggerName); + cancellationManager.SetLogger(logger); using var startupContext = new CliStartupContext(loggingOptions, errorWriter, loggerFactory, fileLoggerProvider, logBufferContext, logger); logger.LogInformation("Aspire CLI version: {Version}", AspireCliTelemetry.GetCliVersion()); @@ -899,9 +900,10 @@ public static async Task Main(string[] args) var firstCompletedTask = await Task.WhenAny(handlerTask, cancellationManager.ProcessTerminationCompletionSource.Task); if (firstCompletedTask != handlerTask) { - // The termination signal triggered cancellation and the timeout has completed. Kill the process. + // ProcessTerminationCompletionSource was signaled — either the graceful-shutdown + // timeout elapsed, or a second signal forced immediate termination. // handlerTask is not awaited because the process is shutting down and we assume the task is hung. - logger.LogWarning("Timeout waiting for cancellation from termination signal."); + logger.LogWarning("Termination signal forced process exit."); } exitCode = await firstCompletedTask; // return the result or propagate the exception diff --git a/src/Aspire.Cli/Projects/ProcessGuestLauncher.cs b/src/Aspire.Cli/Projects/ProcessGuestLauncher.cs index fdf137100f7..3df478d333a 100644 --- a/src/Aspire.Cli/Projects/ProcessGuestLauncher.cs +++ b/src/Aspire.Cli/Projects/ProcessGuestLauncher.cs @@ -123,6 +123,7 @@ public ProcessGuestLauncher(string language, ILogger logger, FileLoggerProvider? AddEvent(activity, ProfilingTelemetry.Events.GuestProcessStart); process.Start(); + _logger.LogDebug("{Language} guest process {ProcessId} started: {Command}", _language, process.Id, resolvedCommandPath); activity?.SetTag(TelemetryConstants.Tags.ProcessPid, process.Id); AddEvent(activity, ProfilingTelemetry.Events.GuestProcessStarted, TelemetryConstants.Tags.ProcessPid, process.Id); if (afterLaunchAsync is not null) @@ -135,7 +136,12 @@ public ProcessGuestLauncher(string language, ILogger logger, FileLoggerProvider? try { - await process.WaitForExitAsync(cancellationToken); + var waitForExitTask = process.WaitForExitAsync(cancellationToken); + + using var _ = cancellationToken.Register(() => + _logger.LogInformation("Cancellation requested while waiting for {Language} guest process {ProcessId} to exit", _language, process.Id)); + + await waitForExitTask.ConfigureAwait(false); } catch (OperationCanceledException) { @@ -152,6 +158,7 @@ public ProcessGuestLauncher(string language, ILogger logger, FileLoggerProvider? // the redirected output streams have time to drain. if (!process.HasExited) { + _logger.LogInformation("Killing {Language} guest process tree {ProcessId}", _language, process.Id); try { process.Kill(entireProcessTree: true); @@ -161,10 +168,17 @@ public ProcessGuestLauncher(string language, ILogger logger, FileLoggerProvider? _logger.LogDebug(killEx, "Failed to kill guest process {ProcessId} after cancellation", process.Id); } } + else + { + _logger.LogDebug("{Language} guest process {ProcessId} already exited before kill", _language, process.Id); + } + _logger.LogDebug("Waiting for {Language} guest process {ProcessId} to exit after kill", _language, process.Id); await process.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false); } + _logger.LogDebug("{Language} guest process {ProcessId} exited with code {ExitCode}", _language, process.Id, process.ExitCode); + activity?.SetTag(TelemetryConstants.Tags.ProcessExitCode, process.ExitCode); AddEvent(activity, ProfilingTelemetry.Events.GuestProcessExited, TelemetryConstants.Tags.ProcessExitCode, process.ExitCode); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs index 98c153aed56..917c86c5d67 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs @@ -31,10 +31,9 @@ public async Task AgentCommands_AllHelpOutputs_AreCorrect() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -73,11 +72,6 @@ await auto.WaitUntilAsync( await auto.EnterAsync(); await auto.WaitUntilTextAsync("aspire mcp tools [options]", timeout: TimeSpan.FromSeconds(30)); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// @@ -93,8 +87,6 @@ public async Task AgentInitCommand_MigratesDeprecatedConfig() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - // Use .mcp.json (Claude Code format) for simpler testing // This is the same format used by the doctor test that passes var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json"); @@ -102,6 +94,7 @@ public async Task AgentInitCommand_MigratesDeprecatedConfig() var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -129,7 +122,7 @@ public async Task AgentInitCommand_MigratesDeprecatedConfig() await auto.TypeAsync("aspire agent init --workspace-root . --skill-locations none --skills none"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("configuration complete", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitForSuccessPromptFailFastAsync(counter); + await auto.WaitForSuccessPromptAsync(counter); // Step 3: Verify config was updated to new format // The updated config should contain "agent" and "mcp" but not "start" @@ -137,11 +130,6 @@ public async Task AgentInitCommand_MigratesDeprecatedConfig() Assert.Contains("\"agent\"", fileContent); Assert.Contains("\"mcp\"", fileContent); Assert.DoesNotContain("\"start\"", fileContent); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// @@ -156,12 +144,11 @@ public async Task DoctorCommand_DetectsDeprecatedAgentConfig() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json"); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -175,11 +162,6 @@ await auto.WaitUntilAsync( s => s.ContainsText("dev-certs") && s.ContainsText("deprecated") && s.ContainsText("aspire agent init"), timeout: TimeSpan.FromSeconds(60), description: "doctor output with deprecated warning and fix suggestion"); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// @@ -196,13 +178,12 @@ public async Task AgentInitCommand_DefaultSelection_InstallsDefaultSkills() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - // Set up .vscode folder so VS Code scanner detects it var vscodePath = Path.Combine(workspace.WorkspaceRoot.FullName, ".vscode"); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -230,7 +211,7 @@ await auto.WaitUntilAsync( // the default Aspire skills from the seeded bundle. await auto.EnterAsync(); await auto.WaitUntilTextAsync("configuration complete", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitForSuccessPromptFailFastAsync(counter); + await auto.WaitForSuccessPromptAsync(counter); // Verify skill files were created (skills are now installed at .agents/skills/ by StandardLocationAgentEnvironmentScanner) var skillFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, ".agents", "skills", "aspire", "SKILL.md"); @@ -239,11 +220,6 @@ await auto.WaitUntilAsync( var deploymentSkillFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, ".agents", "skills", "aspire-deployment", "SKILL.md"); var deploymentFileContent = File.ReadAllText(deploymentSkillFilePath); Assert.Contains("Aspire Deployment", deploymentFileContent); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } private static async Task SeedAspireSkillsBundleCacheAsync(Hex1bTerminalAutomator auto, TemporaryWorkspace workspace, SequenceCounter counter) diff --git a/tests/Aspire.Cli.EndToEnd.Tests/AgentMcpLogsTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/AgentMcpLogsTests.cs index 53e2d3677b6..01f7b0ece04 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/AgentMcpLogsTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/AgentMcpLogsTests.cs @@ -38,10 +38,9 @@ private async Task AgentMcpListStructuredLogsFromStarterAppCore(bool isolated, b using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -68,11 +67,5 @@ private async Task AgentMcpListStructuredLogsFromStarterAppCore(bool isolated, b // Stop the AppHost await auto.AspireStopAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/AppHostSyntaxErrorOutputTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/AppHostSyntaxErrorOutputTests.cs index b2bc70249e0..010ef85f315 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/AppHostSyntaxErrorOutputTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/AppHostSyntaxErrorOutputTests.cs @@ -94,51 +94,25 @@ private async Task RunSyntaxErrorScenarioAsync( workspace: workspace, testName: testName); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); - var testBodyFailed = false; - - try - { - await auto.PrepareDockerEnvironmentAsync(counter, workspace); - await auto.InstallAspireCliAsync(strategy, counter); - - await auto.AspireNewAsync(projectName, counter, template: template); - configureProject(Path.Combine(workspace.WorkspaceRoot.FullName, projectName)); - - await AssertAspireCommandOutputAsync( - auto, - counter, - projectName, - command, - expectedExitCode, - outputExpectation, - recordingPath, - timeout); - } - catch - { - testBodyFailed = true; - throw; - } - finally - { - try - { - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; - } - catch - { - if (!testBodyFailed) - { - throw; - } - } - } + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); + + await auto.PrepareDockerEnvironmentAsync(counter, workspace); + await auto.InstallAspireCliAsync(strategy, counter); + + await auto.AspireNewAsync(projectName, counter, template: template); + configureProject(Path.Combine(workspace.WorkspaceRoot.FullName, projectName)); + + await AssertAspireCommandOutputAsync( + auto, + counter, + projectName, + command, + expectedExitCode, + outputExpectation, + recordingPath, + timeout); } private static async Task AssertAspireCommandOutputAsync( diff --git a/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs index 24ef5b2a85d..849bcfed550 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs @@ -24,10 +24,9 @@ public async Task Banner_DisplayedOnFirstRun() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -50,10 +49,6 @@ await auto.WaitUntilAsync( s => s.ContainsText(RootCommandStrings.BannerWelcomeText) && s.ContainsText("Telemetry"), timeout: TimeSpan.FromSeconds(30), description: "waiting for banner and telemetry notice on first run"); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -65,10 +60,9 @@ public async Task Banner_DisplayedWithExplicitFlag() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -82,10 +76,6 @@ await auto.WaitUntilAsync( s => s.ContainsText(RootCommandStrings.BannerWelcomeText) && s.ContainsText("CLI"), timeout: TimeSpan.FromSeconds(30), description: "waiting for banner with version info"); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -97,10 +87,9 @@ public async Task Banner_NotDisplayedWithNoLogoFlag() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -130,9 +119,5 @@ await auto.WaitUntilAsync(s => return s.ContainsText(HelpGroupStrings.HelpHint); }, timeout: TimeSpan.FromSeconds(30), description: "waiting for help output to complete"); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs index 5a72dd22291..fa5a69fa1d2 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs @@ -26,10 +26,9 @@ public async Task CreateAndRunAspireStarterProjectWithBundle() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -38,10 +37,5 @@ public async Task CreateAndRunAspireStarterProjectWithBundle() await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/CSharpInitTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/CSharpInitTests.cs index 702ef4c1fe2..7b7fd55c8f2 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/CSharpInitTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/CSharpInitTests.cs @@ -31,10 +31,9 @@ public async Task InteractiveCSharpInitCreatesExpectedFiles() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -73,10 +72,5 @@ await auto.WaitUntilAsync( var language = appHostNode["language"]?.GetValue(); Assert.NotNull(language); Assert.Contains("csharp", language, StringComparison.OrdinalIgnoreCase); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/CSharpProjectModeInitTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/CSharpProjectModeInitTests.cs index 0c31e632919..9700eb307e5 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/CSharpProjectModeInitTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/CSharpProjectModeInitTests.cs @@ -69,11 +69,9 @@ public async Task AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstCha File.WriteAllText(solutionPath, "Fake solution file"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -106,17 +104,12 @@ public async Task AspireInitWithSolutionFileGeneratesAppHostThatBuildsAgainstCha // `dotnet build` fails with `error MSB4236: The SDK 'Aspire.AppHost.Sdk/...' // could not be found.` 3 minutes is enough headroom for a cold restore + build on // CI; the cache-hit case (the template's `restore` post-action already populated - // ~/.nuget/packages during init) finishes well under 30 seconds. Using the - // fail-fast helper so a build failure surfaces immediately via the shell's - // numbered ERR prompt instead of timing out. - await auto.RunCommandFailFastAsync( + // ~/.nuget/packages during init) finishes well under 30 seconds. A build failure + // surfaces immediately via the shell's ERR prompt instead of timing out. + await auto.RunCommandAsync( "dotnet build Test.AppHost/Test.AppHost.csproj", counter, TimeSpan.FromMinutes(3)); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; } /// @@ -152,11 +145,9 @@ public async Task AspireInitWithExistingAppHostDirRecreatesMissingNuGetConfigAnd File.WriteAllText(leftoverPath, LeftoverContent); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -174,9 +165,5 @@ public async Task AspireInitWithExistingAppHostDirRecreatesMissingNuGetConfigAnd + "Directory.Exists(appHostDirPath) early return so reruns recover the missing config."); Assert.True(File.Exists(leftoverPath), $"Pre-existing AppHost file should be preserved: {leftoverPath}"); Assert.Equal(LeftoverContent, File.ReadAllText(leftoverPath)); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs index 779393d04ba..5b2eee07896 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs @@ -25,11 +25,9 @@ public async Task AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesP var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -114,10 +112,6 @@ public async Task AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesP await auto.TypeAsync("aspire config delete features.updateNotificationsEnabled -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -150,11 +144,9 @@ public async Task AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCu var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -277,10 +269,6 @@ public async Task AspireUpdateRemovesOrphanAppHostPackageVersionWhenSdkAlreadyCu await auto.TypeAsync("aspire config delete features.updateNotificationsEnabled -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -291,11 +279,9 @@ public async Task AspireAddPackageVersionToDirectoryPackagesProps() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -369,9 +355,5 @@ static IEnumerable FindRedisProperties(XDocument document, string prop await auto.TypeAsync($"dotnet restore \"{containerAppHostCsprojPath}\""); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(120)); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/CertificatesCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/CertificatesCommandTests.cs index c455dcb0f71..fba51c7ae2f 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/CertificatesCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/CertificatesCommandTests.cs @@ -23,10 +23,9 @@ public async Task CertificatesTrust_WithUntrustedCert_TrustsCertificate() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -52,10 +51,6 @@ public async Task CertificatesTrust_WithUntrustedCert_TrustsCertificate() await auto.EnterAsync(); await auto.WaitUntilTextAsync("certificate is trusted", timeout: TimeSpan.FromSeconds(60)); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -67,10 +62,9 @@ public async Task CertificatesClean_RemovesCertificates() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -95,10 +89,6 @@ public async Task CertificatesClean_RemovesCertificates() await auto.EnterAsync(); await auto.WaitUntilTextAsync("No HTTPS development certificate", timeout: TimeSpan.FromSeconds(60)); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -110,10 +100,9 @@ public async Task CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -134,9 +123,5 @@ public async Task CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate() await auto.EnterAsync(); await auto.WaitUntilTextAsync("certificate is trusted", timeout: TimeSpan.FromSeconds(60)); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ChannelUpdateWorkflowTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ChannelUpdateWorkflowTests.cs index bcb737db9c2..dae5f7d44a7 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/ChannelUpdateWorkflowTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/ChannelUpdateWorkflowTests.cs @@ -73,11 +73,9 @@ public async Task UpdateProjectChannelToStable_TypeScript_PreviewsStablePackages variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -118,7 +116,7 @@ public async Task UpdateProjectChannelToStable_TypeScript_PreviewsStablePackages ? "./.aspire/modules/aspire.mjs" : "./.aspire/modules/aspire.js"; - await auto.RunCommandFailFastAsync($"cd {projectName}", counter); + await auto.RunCommandAsync($"cd {projectName}", counter); // Step 3: Add the first package on the non-stable channel. Don't pass --non-interactive — the // helper handles both direct success and the "based on NuGet.config" version picker that @@ -210,11 +208,6 @@ await File.WriteAllTextAsync(appHostPath, { } } - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } // ---------------------------------------------------------------------------------- @@ -251,11 +244,9 @@ public async Task UpdateProjectChannelToStable_CSharpSingleFileInit_PreservesAsp repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.DotNet, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -265,7 +256,7 @@ public async Task UpdateProjectChannelToStable_CSharpSingleFileInit_PreservesAsp const string projectName = "ChannelUpdateCsharpInitApp"; var projectPath = Path.Combine(workspace.WorkspaceRoot.FullName, projectName); Directory.CreateDirectory(projectPath); - await auto.RunCommandFailFastAsync($"cd {projectName}", counter); + await auto.RunCommandAsync($"cd {projectName}", counter); await auto.AspireInitAsync(counter); @@ -275,10 +266,6 @@ public async Task UpdateProjectChannelToStable_CSharpSingleFileInit_PreservesAsp } await RunStableChannelUpdateAndAssertChannelPreservedAsync(auto, counter, Path.Combine(projectPath, "aspire.config.json")); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; } [Fact] @@ -300,11 +287,9 @@ public async Task UpdateProjectChannelToStable_CSharpEmptyAppHost_PreservesAspir repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.DotNet, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -319,12 +304,8 @@ public async Task UpdateProjectChannelToStable_CSharpEmptyAppHost_PreservesAspir CliE2ETestHelpers.WriteLocalChannelSettings(projectPath, localChannel.SdkVersion); } - await auto.RunCommandFailFastAsync($"cd {projectName}", counter); + await auto.RunCommandAsync($"cd {projectName}", counter); await RunStableChannelUpdateAndAssertChannelPreservedAsync(auto, counter, Path.Combine(projectPath, "aspire.config.json")); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; } [Fact] @@ -346,11 +327,9 @@ public async Task UpdateProjectChannelToStable_TypeScriptSingleFileInit_Preserve repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -360,7 +339,7 @@ public async Task UpdateProjectChannelToStable_TypeScriptSingleFileInit_Preserve const string projectName = "ChannelUpdateTsInitApp"; var projectPath = Path.Combine(workspace.WorkspaceRoot.FullName, projectName); Directory.CreateDirectory(projectPath); - await auto.RunCommandFailFastAsync($"cd {projectName}", counter); + await auto.RunCommandAsync($"cd {projectName}", counter); await auto.TypeAsync("aspire init --language typescript --non-interactive"); await auto.EnterAsync(); @@ -373,10 +352,6 @@ public async Task UpdateProjectChannelToStable_TypeScriptSingleFileInit_Preserve } await RunStableChannelUpdateAndAssertChannelPreservedAsync(auto, counter, Path.Combine(projectPath, "aspire.config.json")); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; } /// diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ConfigDiscoveryTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ConfigDiscoveryTests.cs index 3aef230bcfd..8fb01078aba 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/ConfigDiscoveryTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/ConfigDiscoveryTests.cs @@ -40,10 +40,9 @@ public async Task RunFromParentDirectory_UsesExistingConfigNearAppHost() mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -140,10 +139,5 @@ await auto.WaitUntilAsync(s => } Assert.True(hasApplicationUrl, $"No profile has 'applicationUrl'. Content:\n{currentContent}"); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ConfigHealingTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ConfigHealingTests.cs index fa617337cb9..cf77b30f89f 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/ConfigHealingTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/ConfigHealingTests.cs @@ -34,10 +34,9 @@ public async Task InvalidAppHostPathWithComments_IsHealedOnRun() mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -97,10 +96,5 @@ public async Task InvalidAppHostPathWithComments_IsHealedOnRun() throw new InvalidOperationException( $"Config file still contains invalid path after healing. Content:\n{content}"); } - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ConfigMigrationTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ConfigMigrationTests.cs index fc1b2d7417d..c814ab9ecc4 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/ConfigMigrationTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/ConfigMigrationTests.cs @@ -115,10 +115,9 @@ public async Task GlobalSettings_MigratedFromLegacyFormat() var (aspireHomeDir, terminal) = CreateMigrationTerminal(repoRoot, strategy, workspace); using var _ = terminal; - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -163,10 +162,6 @@ public async Task GlobalSettings_MigratedFromLegacyFormat() await auto.TypeAsync("aspire config delete features.polyglotSupportEnabled -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// @@ -182,10 +177,9 @@ public async Task GlobalMigration_SkipsWhenNewConfigExists() var (aspireHomeDir, terminal) = CreateMigrationTerminal(repoRoot, strategy, workspace); using var _ = terminal; - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -213,10 +207,6 @@ public async Task GlobalMigration_SkipsWhenNewConfigExists() await auto.TypeAsync("aspire config delete channel -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// @@ -232,10 +222,9 @@ public async Task GlobalMigration_HandlesMalformedLegacyJson() var (aspireHomeDir, terminal) = CreateMigrationTerminal(repoRoot, strategy, workspace); using var _ = terminal; - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -272,10 +261,6 @@ public async Task GlobalMigration_HandlesMalformedLegacyJson() await auto.TypeAsync("aspire config delete channel -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// @@ -293,10 +278,9 @@ public async Task GlobalMigration_HandlesCommentsAndTrailingCommas() var (aspireHomeDir, terminal) = CreateMigrationTerminal(repoRoot, strategy, workspace); using var _ = terminal; - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -348,10 +332,6 @@ public async Task GlobalMigration_HandlesCommentsAndTrailingCommas() await auto.TypeAsync("aspire config delete features.polyglotSupportEnabled -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// @@ -368,10 +348,9 @@ public async Task ConfigSetGet_CreatesNestedJsonFormat() var (aspireHomeDir, terminal) = CreateMigrationTerminal(repoRoot, strategy, workspace); using var _ = terminal; - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -432,10 +411,6 @@ public async Task ConfigSetGet_CreatesNestedJsonFormat() await auto.TypeAsync("aspire config delete features.stagingChannelEnabled -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// @@ -453,10 +428,9 @@ public async Task GlobalMigration_PreservesAllValueTypes() var (aspireHomeDir, terminal) = CreateMigrationTerminal(repoRoot, strategy, workspace); using var _ = terminal; - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -521,10 +495,6 @@ public async Task GlobalMigration_PreservesAllValueTypes() await auto.TypeAsync("aspire config delete packages -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// @@ -543,10 +513,9 @@ public async Task FullUpgrade_LegacyCliToNewCli_MigratesGlobalSettings() var (aspireHomeDir, terminal) = CreateMigrationTerminal(repoRoot, strategy, workspace); using var _ = terminal; - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -608,9 +577,5 @@ public async Task FullUpgrade_LegacyCliToNewCli_MigratesGlobalSettings() await auto.TypeAsync("aspire config delete features.polyglotSupportEnabled -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DashboardRunTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DashboardRunTests.cs index 952078bd794..b37e8db0417 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/DashboardRunTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/DashboardRunTests.cs @@ -37,10 +37,9 @@ private async Task DashboardRunWithOtelTracesReturnsNoTracesCore(string frontend using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -96,12 +95,6 @@ private async Task DashboardRunWithOtelTracesReturnsNoTracesCore(string frontend await auto.TypeAsync("kill -9 $DASHBOARD_PID 2>/dev/null; wait $DASHBOARD_PID 2>/dev/null; true"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -123,10 +116,9 @@ private async Task DashboardRunWithAgentMcpCore(string frontendUrl, string local using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -167,11 +159,5 @@ private async Task DashboardRunWithAgentMcpCore(string frontendUrl, string local await auto.TypeAsync("kill -9 $DASHBOARD_PID 2>/dev/null; wait $DASHBOARD_PID 2>/dev/null; true"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs index 3b2ad14b891..f51e8128265 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs @@ -25,10 +25,9 @@ public async Task DescribeCommandShowsRunningResources() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -77,12 +76,6 @@ public async Task DescribeCommandShowsRunningResources() await auto.EnterAsync(); await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -95,14 +88,13 @@ public async Task DescribeCommandResolvesReplicaNames() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - // Pattern for describe output showing a specific replica var waitForApiserviceReplicaName = new CellPatternSearcher() .FindPattern("apiservice-[a-z0-9]+"); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -202,11 +194,5 @@ public async Task DescribeCommandResolvesReplicaNames() await auto.EnterAsync(); await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DockerDeploymentTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DockerDeploymentTests.cs index 0994a341fe5..f256e4591d6 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/DockerDeploymentTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/DockerDeploymentTests.cs @@ -28,11 +28,9 @@ public async Task CreateAndDeployToDockerCompose() using var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -118,11 +116,6 @@ public async Task CreateAndDeployToDockerCompose() // Step 11: Clean up - destroy the deployment using aspire destroy await auto.AspireDestroyAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -135,11 +128,9 @@ public async Task CreateAndDeployToDockerComposeInteractive() using var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -226,10 +217,5 @@ public async Task CreateAndDeployToDockerComposeInteractive() // Step 11: Clean up - destroy the deployment using aspire destroy await auto.AspireDestroyAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DocsCommandE2ETests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DocsCommandE2ETests.cs index 09d475f2652..9c50dfdf9f5 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/DocsCommandE2ETests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/DocsCommandE2ETests.cs @@ -43,10 +43,10 @@ aspire docs get docs-smoke-test """); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -90,10 +90,5 @@ await auto.WaitUntilAsync(snapshot => && snapshot.ContainsText("Target Azure subscription"); }, timeout: TimeSpan.FromSeconds(60), description: "waiting for docs get rendered output"); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs index f6e65fcd297..aacd7ab4860 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs @@ -30,10 +30,9 @@ public async Task DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -54,10 +53,6 @@ await auto.WaitUntilAsync( s => s.ContainsText("dev-certs") && s.ContainsText("partially trusted"), timeout: TimeSpan.FromSeconds(60), description: "doctor to complete with partial trust warning"); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -69,10 +64,9 @@ public async Task DoctorCommand_WithSslCertDir_ShowsTrusted() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -101,10 +95,6 @@ await auto.WaitUntilAsync(s => return s.ContainsText("certificate is trusted"); }, timeout: TimeSpan.FromSeconds(60), description: "doctor to complete with trusted certificate"); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Theory] @@ -118,10 +108,9 @@ public async Task DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolcha using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -136,7 +125,7 @@ public async Task DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolcha TypeScriptAppHostToolchainTestHelpers.SetPackageManager(workspace.WorkspaceRoot.FullName, toolchain, cleanInstallState: true); if (TypeScriptAppHostToolchainTestHelpers.UsesCorepack(toolchain)) { - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( $"COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare {TypeScriptAppHostToolchainTestHelpers.GetPackageManager(toolchain)} --activate", counter, TimeSpan.FromMinutes(2)); @@ -160,9 +149,5 @@ await auto.WaitUntilAsync( timeout: TimeSpan.FromSeconds(60), description: $"doctor to report missing {toolchain} tooling"); await auto.WaitForAnyPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DotnetToolSmokeTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DotnetToolSmokeTests.cs index 4c56392bec8..a1242a6f971 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/DotnetToolSmokeTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/DotnetToolSmokeTests.cs @@ -37,10 +37,9 @@ public async Task CreateAndRunAspireStarterProject() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); // Prepare Docker environment (prompt counting, umask, env vars) await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -102,12 +101,6 @@ await auto.WaitUntilAsync(s => // Stop the running apphost with Ctrl+C await auto.Ctrl().KeyAsync(Hex1bKey.C); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// diff --git a/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs index 5c8703ffbe8..15fdae380a9 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs @@ -24,10 +24,9 @@ public async Task CreateAndRunEmptyAppHostProject() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -41,10 +40,5 @@ public async Task CreateAndRunEmptyAppHostProject() await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs index 579eee358b5..b2d86ca72e5 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs @@ -106,7 +106,7 @@ internal static async Task InstallAspireCliAsync( case CliInstallMode.PullRequest: var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( AspireCliShellCommandHelpers.GetPullRequestInstallCommand(prNumber, AspireCliShellCommandHelpers.DockerPullRequestInstallCommandPrefix), counter, TimeSpan.FromSeconds(300)); @@ -114,7 +114,7 @@ await auto.RunCommandFailFastAsync( break; case CliInstallMode.LocalArchive: - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( AspireCliShellCommandHelpers.GetLocalArchiveInstallCommand("/tmp/aspire-cli-archives", AspireCliShellCommandHelpers.DockerPullRequestInstallCommandPrefix), counter, TimeSpan.FromSeconds(120)); @@ -122,7 +122,7 @@ await auto.RunCommandFailFastAsync( break; case CliInstallMode.InstallScript: - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( AspireCliShellCommandHelpers.GetInstallScriptCommand(strategy, AspireCliShellCommandHelpers.DockerInstallScriptCommandPrefix), counter, TimeSpan.FromSeconds(120)); @@ -131,7 +131,7 @@ await auto.RunCommandFailFastAsync( case CliInstallMode.DotnetTool: await auto.SourceDotnetToolEnvironmentAsync(counter); - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( AspireCliShellCommandHelpers.GetDotnetToolInstallCommandInDocker(strategy), counter, TimeSpan.FromSeconds(120)); @@ -404,7 +404,7 @@ internal static async Task InstallAspireCliInShellAsync( case CliInstallMode.LocalArchive: var archiveDir = strategy.ArchiveDir ?? throw new InvalidOperationException("LocalArchive strategy is missing the archive directory."); var localDirPrScript = AspireCliShellCommandHelpers.QuoteBashArg(Path.Combine(CliE2ETestHelpers.GetRepoRoot(), "eng", "scripts", "get-aspire-cli-pr.sh")); - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( AspireCliShellCommandHelpers.GetLocalArchiveInstallCommand(archiveDir, $"bash {localDirPrScript}"), counter, TimeSpan.FromSeconds(120)); @@ -413,7 +413,7 @@ await auto.RunCommandFailFastAsync( case CliInstallMode.InstallScript: var getAspireCliScript = AspireCliShellCommandHelpers.QuoteBashArg(Path.Combine(CliE2ETestHelpers.GetRepoRoot(), "eng", "scripts", "get-aspire-cli.sh")); - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( AspireCliShellCommandHelpers.GetInstallScriptCommand(strategy, $"bash {getAspireCliScript}"), counter, TimeSpan.FromSeconds(120)); @@ -568,7 +568,7 @@ internal static async Task InstallAspireCliFromPullRequestAsync( SequenceCounter counter) { var command = AspireCliShellCommandHelpers.GetPullRequestInstallCommand(prNumber, AspireCliShellCommandHelpers.MainPullRequestInstallCommandPrefix); - await auto.RunCommandFailFastAsync(command, counter, TimeSpan.FromSeconds(300)); + await auto.RunCommandAsync(command, counter, TimeSpan.FromSeconds(300)); } /// @@ -705,7 +705,7 @@ internal static async Task InstallAspireCliVersionAsync( var command = AspireCliShellCommandHelpers.GetInstallScriptCommand( CliInstallStrategy.FromVersion(version), AspireCliShellCommandHelpers.MainInstallScriptCommandPrefix); - await auto.RunCommandFailFastAsync(command, counter, TimeSpan.FromSeconds(300)); + await auto.RunCommandAsync(command, counter, TimeSpan.FromSeconds(300)); } /// @@ -774,7 +774,7 @@ await auto.TypeAsync( throw new InvalidOperationException( workspacePath is null || !ShouldCaptureWorkspaceDiagnostics() ? "aspire start failed. Check terminal output for CLI logs." - : $"aspire start failed. Workspace: {workspacePath}. See _aspire-detach.log, _aspire-cli.log, .aspire-logs, and _aspire-start.json in the captured workspace."); + : $"aspire start failed. Workspace: {workspacePath}. See {DiagnosticsDirectoryName}/ in the captured workspace."); } await auto.TypeAsync( @@ -833,7 +833,7 @@ await auto.WaitUntilAsync(snapshot => throw new InvalidOperationException( workspacePath is null || !ShouldCaptureWorkspaceDiagnostics() ? "aspire start did not return a dashboard URL. Check terminal output for detached child and CLI logs." - : $"aspire start did not return a dashboard URL. Workspace: {workspacePath}. See _aspire-detach.log, _aspire-cli.log, .aspire-logs, and _aspire-start.json in the captured workspace."); + : $"aspire start did not return a dashboard URL. Workspace: {workspacePath}. See {DiagnosticsDirectoryName}/ in the captured workspace."); } // Check whether $DASHBOARD_URL was set using variable expansion so the marker @@ -995,22 +995,29 @@ await auto.TypeAsync( await auto.WaitForSuccessPromptAsync(counter); } + /// + /// The well-known subdirectory name under the workspace where diagnostics are captured. + /// Both the in-Docker bash capture and the host-side copy use this name. + /// + internal const string DiagnosticsDirectoryName = ".aspire-diagnostics"; + private static string BuildAspireDiagnosticsCaptureCommand(string destinationExpression) { // This returns a single bash fragment because it is reused from EXIT traps and failure paths where the helper // needs to inject one inline shell command rather than orchestrate several terminal round-trips. + // All diagnostics are placed under a single .aspire-diagnostics/ subdirectory so the host-side + // capture in TerminalRun can copy one directory instead of enumerating individual files. + var diag = $"{destinationExpression}/{DiagnosticsDirectoryName}"; return - $"mkdir -p \"{destinationExpression}\"; " + - $"rm -rf \"{destinationExpression}/.aspire-logs\" \"{destinationExpression}/.aspire-packages\" \"{destinationExpression}/.aspire-dcp-logs\"; " + - $"cp -r ~/.aspire/logs \"{destinationExpression}/.aspire-logs\" 2>/dev/null || true; " + - $"cp -r ~/.aspire/packages \"{destinationExpression}/.aspire-packages\" 2>/dev/null || true; " + - $"cp -r ~/.aspire/dcp-logs \"{destinationExpression}/.aspire-dcp-logs\" 2>/dev/null || true; " + - $"cp {AspireStartJsonFile} \"{destinationExpression}/_aspire-start.json\" 2>/dev/null || true; " + - "DETACH_LOG=$(ls -t ~/.aspire/logs/cli_*detach*.log 2>/dev/null | head -1); " + - $"[ -n \"$DETACH_LOG\" ] && cp \"$DETACH_LOG\" \"{destinationExpression}/_aspire-detach.log\" 2>/dev/null || true; " + - "CLI_LOG=$(ls -t ~/.aspire/logs/cli_*.log 2>/dev/null | grep -v 'detach' | head -1); " + - "if [ -z \"$CLI_LOG\" ]; then CLI_LOG=$(ls -t ~/.aspire/logs/cli_*.log 2>/dev/null | head -1); fi; " + - $"[ -n \"$CLI_LOG\" ] && cp \"$CLI_LOG\" \"{destinationExpression}/_aspire-cli.log\" 2>/dev/null || true; "; + $"mkdir -p \"{diag}\"; " + + $"rm -rf \"{diag}/logs\" \"{diag}/packages\" \"{diag}/dcp-logs\"; " + + $"cp -r ~/.aspire/logs \"{diag}/logs\" 2>/dev/null || true; " + + $"cp -r ~/.aspire/packages \"{diag}/packages\" 2>/dev/null || true; " + + $"cp -r ~/.aspire/dcp-logs \"{diag}/dcp-logs\" 2>/dev/null || true; " + + $"cp {AspireStartJsonFile} \"{diag}/aspire-start.json\" 2>/dev/null || true; " + + $"echo \"diagnostics: logs=$(find \"{diag}/logs\" -type f 2>/dev/null | wc -l) " + + $"packages=$(find \"{diag}/packages\" -type f 2>/dev/null | wc -l) " + + $"dcp-logs=$(find \"{diag}/dcp-logs\" -type f 2>/dev/null | wc -l)\"; "; } private static string? GetRegisteredWorkspacePath() diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs index 0b70b1db12f..da85d0f916f 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs @@ -8,6 +8,7 @@ using System.Xml.Linq; using Aspire.Cli.Tests.Utils; using Hex1b; +using Hex1b.Automation; using Xunit; namespace Aspire.Cli.EndToEnd.Tests.Helpers; @@ -127,6 +128,22 @@ internal static Hex1bTerminal CreateTestTerminal(int width = 160, int height = 4 .Build(); } + /// + /// Starts the terminal run and returns a that captures diagnostics + /// and exits the terminal on disposal. + /// + /// The Hex1b terminal to run. + /// The workspace for diagnostic capture. + /// The automator used to drive the terminal. + /// The sequence counter for prompt tracking. + /// Cancellation token passed to . + /// A that ensures diagnostics capture and clean exit on disposal. + internal static TerminalRun StartRun(Hex1bTerminal terminal, TemporaryWorkspace workspace, Hex1bTerminalAutomator automator, SequenceCounter counter, ITestOutputHelper output, CancellationToken cancellationToken) + { + var pendingRun = terminal.RunAsync(cancellationToken); + return new TerminalRun(pendingRun, automator, counter, workspace, output); + } + /// /// Specifies which Dockerfile variant to use for the test container. /// diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/TerminalRun.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/TerminalRun.cs new file mode 100644 index 00000000000..d8e25e951ef --- /dev/null +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/TerminalRun.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Tests.Utils; +using Hex1b.Automation; +using Xunit; + +namespace Aspire.Cli.EndToEnd.Tests.Helpers; + +/// +/// Wraps a terminal run session and ensures diagnostics are captured and the terminal is properly +/// exited on disposal. Use via to consistently capture +/// diagnostics at the end of every CLI E2E test. +/// +internal sealed class TerminalRun : IAsyncDisposable +{ + private readonly Task _pendingRun; + private readonly Hex1bTerminalAutomator _automator; + private readonly SequenceCounter _counter; + private readonly TemporaryWorkspace _workspace; + private readonly ITestOutputHelper _output; + + internal TerminalRun(Task pendingRun, Hex1bTerminalAutomator automator, SequenceCounter counter, TemporaryWorkspace workspace, ITestOutputHelper output) + { + _pendingRun = pendingRun; + _automator = automator; + _counter = counter; + _workspace = workspace; + _output = output; + } + + public async ValueTask DisposeAsync() + { + // Capture diagnostics (best effort) + try + { + await _automator.CaptureAspireDiagnosticsAsync(_counter, _workspace); + } + catch + { + // Best effort diagnostics capture — don't mask the original test failure. + } + + // Exit the terminal (best effort) + try + { + await _automator.TypeAsync("exit"); + await _automator.EnterAsync(); + } + catch + { + // Best effort exit — the terminal may already be closed. + } + + // Wait for the terminal process to finish + try + { + await _pendingRun; + } + catch + { + // Best effort — if the test body threw, we don't want to mask it. + } + + // Copy workspace diagnostics to the host-side testresults directory so they appear + // in CI artifacts. The in-Docker capture (CaptureAspireDiagnosticsAsync / EXIT trap) + // writes files to the workspace volume mount, but that temp directory is not in the + // CI-uploaded testresults/ path. This step bridges that gap. + try + { + CaptureWorkspaceDiagnosticsToTestResults(); + } + catch + { + // Best effort — don't mask the original test failure. + } + } + + /// + /// Copies the diagnostics directory from the workspace temp directory to the testresults path + /// that CI uploads as artifacts. The in-Docker capture writes everything under a single + /// subdirectory, so the host + /// side only needs to copy that one directory. + /// + private void CaptureWorkspaceDiagnosticsToTestResults() + { + var diagnosticsSource = Path.Combine(_workspace.WorkspaceRoot.FullName, CliE2EAutomatorHelpers.DiagnosticsDirectoryName); + if (!Directory.Exists(diagnosticsSource)) + { + WriteTestOutput($"[TerminalRun] No diagnostics directory found at: {diagnosticsSource}"); + return; + } + + var testName = TestContext.Current?.TestCase is { TestMethodName: { } methodName } + ? methodName + : "unknown"; + + var destDir = GetDiagnosticsCapturePath(testName); + CopyDirectoryIfExists(diagnosticsSource, destDir); + + WriteTestOutput($"[TerminalRun] Captured diagnostics to: {destDir}"); + WriteTestOutput($"[TerminalRun] Source workspace: {_workspace.WorkspaceRoot.FullName}"); + + // Report file counts per subdirectory so CI logs show what was actually captured. + foreach (var subDir in Directory.GetDirectories(destDir)) + { + var fileCount = Directory.GetFiles(subDir, "*", SearchOption.AllDirectories).Length; + WriteTestOutput($"[TerminalRun] {Path.GetFileName(subDir)}/: {fileCount} file(s)"); + } + + // Count top-level files (e.g. aspire-start.json) + var topLevelFiles = Directory.GetFiles(destDir); + if (topLevelFiles.Length > 0) + { + WriteTestOutput($"[TerminalRun] (root): {topLevelFiles.Length} file(s)"); + } + } + + private static string GetDiagnosticsCapturePath(string testName) + { + var githubWorkspace = Environment.GetEnvironmentVariable("GITHUB_WORKSPACE"); + + if (!string.IsNullOrEmpty(githubWorkspace)) + { + // CI environment — write to testresults/ so upload-artifact includes these files. + return Path.Combine(githubWorkspace, "testresults", "workspaces", testName); + } + + // Local development — keep diagnostics with other test output. + return Path.Combine(AppContext.BaseDirectory, "TestResults", "workspaces", testName); + } + + private static void CopyDirectoryIfExists(string source, string destination) + { + if (!Directory.Exists(source)) + { + return; + } + + Directory.CreateDirectory(destination); + + foreach (var file in Directory.GetFiles(source)) + { + File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), overwrite: true); + } + + foreach (var dir in Directory.GetDirectories(source)) + { + CopyDirectoryIfExists(dir, Path.Combine(destination, Path.GetFileName(dir))); + } + } + + private void WriteTestOutput(string message) + { + _output.WriteLine(message); + } +} diff --git a/tests/Aspire.Cli.EndToEnd.Tests/JavaCodegenValidationTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/JavaCodegenValidationTests.cs index 86642259236..3337b8eae0a 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/JavaCodegenValidationTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/JavaCodegenValidationTests.cs @@ -22,11 +22,9 @@ public async Task RestoreGeneratesSdkFiles() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.PolyglotJava, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -96,10 +94,5 @@ public async Task RestoreGeneratesSdkFiles() { throw new InvalidOperationException("IDistributedApplicationBuilder.java does not contain addSqlServer from Aspire.Hosting.SqlServer"); } - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/JavaEmptyAppHostTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/JavaEmptyAppHostTemplateTests.cs index 9c32d1a5355..462117918bc 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/JavaEmptyAppHostTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/JavaEmptyAppHostTemplateTests.cs @@ -24,11 +24,9 @@ public async Task CreateAndRunJavaEmptyAppHostProject() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.PolyglotJava, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -46,10 +44,5 @@ public async Task CreateAndRunJavaEmptyAppHostProject() await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/JavaPolyglotApphostDirectoryTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/JavaPolyglotApphostDirectoryTests.cs index a4225e9e3c2..9afd2c67471 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/JavaPolyglotApphostDirectoryTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/JavaPolyglotApphostDirectoryTests.cs @@ -32,11 +32,9 @@ public async Task StopJavaPolyglotAppHostUsingApphostDirectory() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.PolyglotJava, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -109,10 +107,5 @@ void main(String[] args) throws Exception { await auto.EnterAsync(); await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/JavaPolyglotTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/JavaPolyglotTests.cs index f8eb0bcaa7c..16a50de7505 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/JavaPolyglotTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/JavaPolyglotTests.cs @@ -22,11 +22,9 @@ public async Task CreateJavaAppHostWithViteApp() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.PolyglotJava, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -76,9 +74,5 @@ void main(String[] args) throws Exception { await auto.Ctrl().KeyAsync(Hex1b.Input.Hex1bKey.C); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/JavaScriptPublishTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/JavaScriptPublishTests.cs index 3a11453cfcc..f3badac9aae 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/JavaScriptPublishTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/JavaScriptPublishTests.cs @@ -31,9 +31,9 @@ public async Task AllPublishMethodsBuildDockerImages() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -85,10 +85,6 @@ public async Task AllPublishMethodsBuildDockerImages() await auto.TypeAsync("docker ps -q --filter label=com.docker.compose.project | xargs -r docker rm -f 2>/dev/null || true"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; } [Fact] @@ -103,17 +99,16 @@ public async Task JavaScriptHostingApisRunFromTypeScriptAppHost() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); - var testBodyFailed = false; + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); try { await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); - await auto.RunCommandFailFastAsync("aspire init --language typescript --non-interactive", counter, TimeSpan.FromMinutes(2)); + await auto.RunCommandAsync("aspire init --language typescript --non-interactive", counter, TimeSpan.FromMinutes(2)); if (localChannel is not null) { @@ -128,15 +123,10 @@ public async Task JavaScriptHostingApisRunFromTypeScriptAppHost() WriteRuntimeAppHost(workspace); WriteRuntimeVerificationScript(workspace); - await auto.RunCommandFailFastAsync("unset ASPIRE_PLAYGROUND", counter); + await auto.RunCommandAsync("unset ASPIRE_PLAYGROUND", counter); - await auto.RunCommandFailFastAsync("aspire run > aspire-run.log 2>&1 & echo $! > aspire-run.pid", counter); - await auto.RunCommandFailFastAsync("bash verify-runtime.sh", counter, TimeSpan.FromMinutes(2)); - } - catch - { - testBodyFailed = true; - throw; + await auto.RunCommandAsync("aspire run > aspire-run.log 2>&1 & echo $! > aspire-run.pid", counter); + await auto.RunCommandAsync("bash verify-runtime.sh", counter, TimeSpan.FromMinutes(2)); } finally { @@ -148,29 +138,6 @@ public async Task JavaScriptHostingApisRunFromTypeScriptAppHost() { // Best effort. A failure before aspire run writes its PID leaves no process to stop. } - - try - { - await auto.CaptureAspireDiagnosticsAsync(counter, workspace); - } - catch - { - // Best effort diagnostics capture. - } - - try - { - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; - } - catch - { - if (!testBodyFailed) - { - throw; - } - } } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs index d092a928b88..89e187dcc19 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs @@ -24,11 +24,9 @@ public async Task CreateAndRunJsReactProject() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -53,10 +51,5 @@ await auto.WaitUntilAsync(s => await auto.Ctrl().KeyAsync(Hex1bKey.C); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployBasicApiServiceTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployBasicApiServiceTests.cs index d533effc530..1621be4b058 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployBasicApiServiceTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployBasicApiServiceTests.cs @@ -30,10 +30,9 @@ public async Task DeployK8sBasicApiService() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); // Prepare environment await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -135,15 +134,10 @@ await auto.VerifyDeploymentAsync( // ===================================================================== await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs index 79ff1b79c47..2b8dd03ee0e 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployTypeScriptTests.cs @@ -31,10 +31,9 @@ public async Task DeployTypeScriptAppToKubernetes() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -136,15 +135,10 @@ await auto.AspireDeployInteractiveAsync( // ===================================================================== await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithGarnetTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithGarnetTests.cs index 07ffc696c85..1aad9a2d420 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithGarnetTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithGarnetTests.cs @@ -30,10 +30,9 @@ public async Task DeployK8sWithGarnet() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -129,15 +128,10 @@ await auto.VerifyDeploymentAsync( testPath: "/test-deployment"); await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithHelmChartTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithHelmChartTests.cs index 5f9dee01004..42647d84fd4 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithHelmChartTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithHelmChartTests.cs @@ -32,10 +32,9 @@ public async Task DeployK8sWithExternalHelmChart() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); // Prepare environment await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -162,15 +161,10 @@ await auto.TypeAsync( // ===================================================================== await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMongoDBTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMongoDBTests.cs index b83c7290738..d04293e73c4 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMongoDBTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMongoDBTests.cs @@ -30,10 +30,9 @@ public async Task DeployK8sWithMongoDB() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -132,15 +131,10 @@ await auto.VerifyDeploymentAsync( testPath: "/test-deployment"); await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMySqlTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMySqlTests.cs index f097f0036be..1969e908c90 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMySqlTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithMySqlTests.cs @@ -30,10 +30,9 @@ public async Task DeployK8sWithMySql() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -126,15 +125,10 @@ await auto.VerifyDeploymentAsync( testPath: "/test-deployment"); await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithNatsTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithNatsTests.cs index e9f751454c1..bef24caadfe 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithNatsTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithNatsTests.cs @@ -32,10 +32,9 @@ public async Task DeployK8sWithNats() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -125,15 +124,10 @@ await auto.VerifyDeploymentAsync( testPath: "/test-deployment"); await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPostgresTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPostgresTests.cs index c824c32e840..fe30d5644da 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPostgresTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithPostgresTests.cs @@ -30,10 +30,9 @@ public async Task DeployK8sWithPostgres() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -126,15 +125,10 @@ await auto.VerifyDeploymentAsync( testPath: "/test-deployment"); await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRabbitMQTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRabbitMQTests.cs index ed0498db66e..1d3a802a581 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRabbitMQTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRabbitMQTests.cs @@ -30,10 +30,9 @@ public async Task DeployK8sWithRabbitMQ() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -122,15 +121,10 @@ await auto.VerifyDeploymentAsync( testPath: "/test-deployment"); await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRedisTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRedisTests.cs index 3dda99453f0..edb3b6fc2b6 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRedisTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithRedisTests.cs @@ -30,10 +30,9 @@ public async Task DeployK8sWithRedis() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -136,15 +135,10 @@ await auto.VerifyDeploymentAsync( testPath: "/test-deployment"); await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithSqlServerTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithSqlServerTests.cs index c9c1a467019..241e71c5e61 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithSqlServerTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithSqlServerTests.cs @@ -30,10 +30,9 @@ public async Task DeployK8sWithSqlServer() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -126,15 +125,10 @@ await auto.VerifyDeploymentAsync( testPath: "/test-deployment"); await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithValkeyTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithValkeyTests.cs index fa6aadbce44..4063f2fd77c 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithValkeyTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesDeployWithValkeyTests.cs @@ -30,10 +30,9 @@ public async Task DeployK8sWithValkey() output.WriteLine($"Namespace: {k8sNamespace}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -129,15 +128,10 @@ await auto.VerifyDeploymentAsync( testPath: "/test-deployment"); await auto.CleanupKubernetesDeploymentAsync(counter, clusterName); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { await KubernetesDeployTestHelpers.CleanupKindClusterOutOfBandAsync(clusterName, output); } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesPublishRequiresExternalEndpointTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesPublishRequiresExternalEndpointTests.cs index 37155f272f4..15c539a16de 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesPublishRequiresExternalEndpointTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesPublishRequiresExternalEndpointTests.cs @@ -57,103 +57,77 @@ private async Task RunPublishFailureScenarioAsync(string appHostBodyExtension) using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); - var testBodyFailed = false; - - try - { - await auto.PrepareDockerEnvironmentAsync(counter, workspace); - await auto.InstallAspireCliAsync(strategy, counter); - - // The starter template gives us the conventional - // `{ProjectName}/{ProjectName}.AppHost/AppHost.cs` layout, matching - // KubernetesPublishTests so the AppHost-mutation logic below stays - // consistent across both tests. - await auto.AspireNewAsync(ProjectName, counter, useRedisCache: false); - - // cd into the project so subsequent `aspire add` and `aspire publish` - // commands resolve the AppHost via repo-root discovery. - await auto.TypeAsync($"cd {ProjectName}"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); - - // The Kubernetes hosting package is required to compile the AppHost code - // we're about to write. `aspire add` resolves the version against the - // same feed configuration the rest of the CLI uses (including PR builds). - await auto.TypeAsync("aspire add Aspire.Hosting.Kubernetes"); - await auto.EnterAsync(); - await auto.WaitForAspireAddCompletionAsync(counter, TimeSpan.FromSeconds(180)); - - // Patch AppHost.cs in-place. The Starter template's AppHost.cs ends - // with `builder.Build().Run();`; we insert the K8s wiring immediately - // before it. Failing to find the marker should surface as a clear - // test failure rather than a silently no-op publish. - var projectDir = Path.Combine(workspace.WorkspaceRoot.FullName, ProjectName); - var appHostDir = Path.Combine(projectDir, $"{ProjectName}.AppHost"); - var appHostFilePath = Path.Combine(appHostDir, "AppHost.cs"); - var content = File.ReadAllText(appHostFilePath); - const string buildRunPattern = "builder.Build().Run();"; - Assert.Contains(buildRunPattern, content); - content = content.Replace(buildRunPattern, appHostBodyExtension + Environment.NewLine + buildRunPattern); - File.WriteAllText(appHostFilePath, content); - - // ASPIRE_PLAYGROUND interferes with `--non-interactive`. See - // KubernetesPublishTests for full context. - await auto.TypeAsync("unset ASPIRE_PLAYGROUND"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); - - // Drive aspire publish. The validation throws an InvalidOperationException - // during model materialization, so publish should exit with a non-zero code - // and surface our guidance message verbatim in stderr/stdout. - await auto.TypeAsync("aspire publish -o helm-output --non-interactive"); - await auto.EnterAsync(); - - var expectedCounter = counter.Value; - // We don't pin to a specific exit code — the publish pipeline currently - // surfaces validation failures as exit 1, but treating any non-zero - // ERR:* prompt as the success condition keeps this test stable across - // future exit-code refactors. - var errorPromptSearcher = new CellPatternSearcher() - .FindPattern(expectedCounter.ToString(CultureInfo.InvariantCulture)) - .RightText(" ERR:"); - - await auto.WaitUntilAsync( - snapshot => errorPromptSearcher.Search(snapshot).Count > 0, - TimeSpan.FromMinutes(5), - description: "waiting for aspire publish to fail"); - counter.Increment(); - - // After the publish exits, scrape the screen for the guidance fragments. - // We use a generous WaitUntilTextAsync so any in-progress rendering - // settles before we assert. - await auto.WaitUntilTextAsync("WithExternalHttpEndpoints", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitUntilTextAsync("'api'", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitUntilTextAsync("'public'", timeout: TimeSpan.FromSeconds(30)); - } - catch - { - testBodyFailed = true; - throw; - } - finally - { - try - { - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; - } - catch - { - if (!testBodyFailed) - { - throw; - } - } - } + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); + + await auto.PrepareDockerEnvironmentAsync(counter, workspace); + await auto.InstallAspireCliAsync(strategy, counter); + + // The starter template gives us the conventional + // `{ProjectName}/{ProjectName}.AppHost/AppHost.cs` layout, matching + // KubernetesPublishTests so the AppHost-mutation logic below stays + // consistent across both tests. + await auto.AspireNewAsync(ProjectName, counter, useRedisCache: false); + + // cd into the project so subsequent `aspire add` and `aspire publish` + // commands resolve the AppHost via repo-root discovery. + await auto.TypeAsync($"cd {ProjectName}"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // The Kubernetes hosting package is required to compile the AppHost code + // we're about to write. `aspire add` resolves the version against the + // same feed configuration the rest of the CLI uses (including PR builds). + await auto.TypeAsync("aspire add Aspire.Hosting.Kubernetes"); + await auto.EnterAsync(); + await auto.WaitForAspireAddCompletionAsync(counter, TimeSpan.FromSeconds(180)); + + // Patch AppHost.cs in-place. The Starter template's AppHost.cs ends + // with `builder.Build().Run();`; we insert the K8s wiring immediately + // before it. Failing to find the marker should surface as a clear + // test failure rather than a silently no-op publish. + var projectDir = Path.Combine(workspace.WorkspaceRoot.FullName, ProjectName); + var appHostDir = Path.Combine(projectDir, $"{ProjectName}.AppHost"); + var appHostFilePath = Path.Combine(appHostDir, "AppHost.cs"); + var content = File.ReadAllText(appHostFilePath); + const string buildRunPattern = "builder.Build().Run();"; + Assert.Contains(buildRunPattern, content); + content = content.Replace(buildRunPattern, appHostBodyExtension + Environment.NewLine + buildRunPattern); + File.WriteAllText(appHostFilePath, content); + + // ASPIRE_PLAYGROUND interferes with `--non-interactive`. See + // KubernetesPublishTests for full context. + await auto.TypeAsync("unset ASPIRE_PLAYGROUND"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Drive aspire publish. The validation throws an InvalidOperationException + // during model materialization, so publish should exit with a non-zero code + // and surface our guidance message verbatim in stderr/stdout. + await auto.TypeAsync("aspire publish -o helm-output --non-interactive"); + await auto.EnterAsync(); + + var expectedCounter = counter.Value; + // We don't pin to a specific exit code — the publish pipeline currently + // surfaces validation failures as exit 1, but treating any non-zero + // ERR:* prompt as the success condition keeps this test stable across + // future exit-code refactors. + var errorPromptSearcher = new CellPatternSearcher() + .FindPattern(expectedCounter.ToString(CultureInfo.InvariantCulture)) + .RightText(" ERR:"); + + await auto.WaitUntilAsync( + snapshot => errorPromptSearcher.Search(snapshot).Count > 0, + TimeSpan.FromMinutes(5), + description: "waiting for aspire publish to fail"); + counter.Increment(); + + // After the publish exits, scrape the screen for the guidance fragments. + // We use a generous WaitUntilTextAsync so any in-progress rendering + // settles before we assert. + await auto.WaitUntilTextAsync("WithExternalHttpEndpoints", timeout: TimeSpan.FromSeconds(30)); + await auto.WaitUntilTextAsync("'api'", timeout: TimeSpan.FromSeconds(30)); + await auto.WaitUntilTextAsync("'public'", timeout: TimeSpan.FromSeconds(30)); } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesPublishTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesPublishTests.cs index 02960bd64a3..757c6b50a61 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/KubernetesPublishTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/KubernetesPublishTests.cs @@ -42,11 +42,9 @@ public async Task CreateAndPublishToKubernetes() output.WriteLine($"Using cluster name: {clusterName}"); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -291,9 +289,6 @@ await auto.TypeAsync("helm install aspire-app helm-output " + await auto.TypeAsync($"kind delete cluster --name={clusterName}"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); } finally { @@ -316,7 +311,5 @@ await auto.TypeAsync("helm install aspire-app helm-output " + output.WriteLine($"Cleanup: Failed to delete KinD cluster '{clusterName}': {ex.Message}"); } } - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ListStepsTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ListStepsTests.cs index 831248ddb38..65800cca286 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/ListStepsTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/ListStepsTests.cs @@ -25,11 +25,9 @@ public async Task DoPublishAndDeployListStepsWork() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -96,8 +94,5 @@ await auto.WaitUntilAsync(s => await auto.WaitForSuccessPromptAsync(counter); // Exit the terminal - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/LocalConfigMigrationTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/LocalConfigMigrationTests.cs index 0655bf36673..20ed5a162f7 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/LocalConfigMigrationTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/LocalConfigMigrationTests.cs @@ -47,11 +47,9 @@ public async Task LegacySettingsMigration_AdjustsRelativeAppHostPath() variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -107,11 +105,6 @@ public async Task LegacySettingsMigration_AdjustsRelativeAppHostPath() var content = File.ReadAllText(configPath); Assert.DoesNotContain("\"../apphost.mts\"", content); Assert.Contains("\"apphost.mts\"", content); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [CaptureWorkspaceOnFailure] @@ -127,11 +120,9 @@ public async Task AspireStartUpdatesStaleTypeScriptAppHostPath() variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -170,10 +161,5 @@ public async Task AspireStartUpdatesStaleTypeScriptAppHostPath() await auto.TypeAsync("aspire stop --apphost apphost.mts"); await auto.EnterAsync(); await auto.WaitForAnyPromptAsync(counter, timeout: TimeSpan.FromMinutes(1)); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/LogLevelTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/LogLevelTests.cs index 2f87ece2713..1db58abd5b2 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/LogLevelTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/LogLevelTests.cs @@ -25,82 +25,49 @@ public async Task LogLevelTrace_ProducesTraceEntriesInCliLogFile() using var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); - var testBodyFailed = false; + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); - try - { - await auto.PrepareDockerEnvironmentAsync(counter, workspace, enableDcpDiagnostics: true); - await auto.InstallAspireCliAsync(strategy, counter); + await auto.PrepareDockerEnvironmentAsync(counter, workspace, enableDcpDiagnostics: true); + await auto.InstallAspireCliAsync(strategy, counter); - // Create a new empty AppHost project - await auto.AspireNewCSharpEmptyAppHostAsync("LogLevelApp", counter); + // Create a new empty AppHost project + await auto.AspireNewCSharpEmptyAppHostAsync("LogLevelApp", counter); - // Navigate to the AppHost directory - await auto.TypeAsync("cd LogLevelApp"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); + // Navigate to the AppHost directory + await auto.TypeAsync("cd LogLevelApp"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); - // Start the AppHost with --log-level trace so both the CLI and the - // AppHost produce trace-level output in the CLI log file. - await auto.TypeAsync("aspire start --log-level trace"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter, timeout: TimeSpan.FromMinutes(3)); + // Start the AppHost with --log-level trace so both the CLI and the + // AppHost produce trace-level output in the CLI log file. + await auto.TypeAsync("aspire start --log-level trace"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, timeout: TimeSpan.FromMinutes(3)); - // Stop the AppHost so the log file is flushed and closed. - await auto.AspireStopAsync(counter); + // Stop the AppHost so the log file is flushed and closed. + await auto.AspireStopAsync(counter); - // Find the most recent CLI log file (the detached child writes its own log). - // The detached process log usually contains "detach" in the name. - await auto.TypeAsync( + // Find the most recent CLI log file (the detached child writes its own log). + // The detached process log usually contains "detach" in the name. + await auto.TypeAsync( "DETACH_LOG=$(ls -t ~/.aspire/logs/cli_*detach*.log 2>/dev/null | head -1); " + "if [ -z \"$DETACH_LOG\" ]; then DETACH_LOG=$(ls -t ~/.aspire/logs/cli_*.log 2>/dev/null | head -1); fi; " + "echo \"LOG_FILE:$DETACH_LOG\""); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); - // Check for trace-level AppHost log entry (format: [TRCE] [AppHost/...]) - await auto.RunCommandFailFastAsync( + // Check for trace-level AppHost log entry (format: [TRCE] [AppHost/...]) + await auto.RunCommandAsync( "test -n \"$DETACH_LOG\" && grep -q '\\[TRCE\\] \\[AppHost/' \"$DETACH_LOG\"", counter, TimeSpan.FromSeconds(10)); - // Check for trace-level CLI log entry from the Features category - await auto.RunCommandFailFastAsync( + // Check for trace-level CLI log entry from the Features category + await auto.RunCommandAsync( "test -n \"$DETACH_LOG\" && grep -q '\\[TRCE\\] \\[Features\\]' \"$DETACH_LOG\"", counter, TimeSpan.FromSeconds(10)); - } - catch - { - testBodyFailed = true; - throw; - } - finally - { - try - { - await auto.CaptureAspireDiagnosticsAsync(counter, workspace); - } - catch { } // Best effort - - try - { - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; - } - catch - { - if (!testBodyFailed) - { - throw; - } - } - } } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/LogsCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/LogsCommandTests.cs index 58b36fdc1a4..9034518c671 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/LogsCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/LogsCommandTests.cs @@ -24,11 +24,9 @@ public async Task LogsCommandShowsResourceLogs() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -85,11 +83,5 @@ public async Task LogsCommandShowsResourceLogs() await auto.EnterAsync(); await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs index 4966eadb5cd..5f2bb9b91e2 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs @@ -23,11 +23,9 @@ public async Task DetachFormatJsonProducesValidJson() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -71,10 +69,6 @@ public async Task DetachFormatJsonProducesValidJson() // Clean up: stop any running instances await auto.AspireStopAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -86,11 +80,9 @@ public async Task DetachFormatJsonProducesValidJsonWhenRestartingExistingInstanc var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -126,9 +118,5 @@ public async Task DetachFormatJsonProducesValidJsonWhenRestartingExistingInstanc await auto.TypeAsync("aspire stop --all 2>/dev/null || true"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/NewWithAgentInitTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/NewWithAgentInitTests.cs index e886e27db21..5682523299a 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/NewWithAgentInitTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/NewWithAgentInitTests.cs @@ -37,11 +37,9 @@ public async Task AspireNew_WithAgentInit_InstallsPlaywrightWithoutErrors() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -158,10 +156,5 @@ await auto.WaitUntilAsync(s => await auto.EnterAsync(); await auto.WaitUntilTextAsync("SKILL.md", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/OtelLogsTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/OtelLogsTests.cs index a0351924f3d..71bd820b9bd 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/OtelLogsTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/OtelLogsTests.cs @@ -33,11 +33,9 @@ private async Task OtelLogsReturnsStructuredLogsFromStarterAppCore(bool isolated using var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace, testName: testName); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -88,11 +86,5 @@ private async Task OtelLogsReturnsStructuredLogsFromStarterAppCore(bool isolated // Stop the AppHost await auto.AspireStopAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PlaywrightCliInstallTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PlaywrightCliInstallTests.cs index 7c9bec1b3ce..1093a54bbf6 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/PlaywrightCliInstallTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/PlaywrightCliInstallTests.cs @@ -32,11 +32,9 @@ public async Task AgentInit_InstallsPlaywrightCli_AndGeneratesSkillFiles() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -74,11 +72,6 @@ public async Task AgentInit_InstallsPlaywrightCli_AndGeneratesSkillFiles() await auto.EnterAsync(); await auto.WaitUntilTextAsync("SKILL.md", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// @@ -98,11 +91,9 @@ public async Task AgentInit_WhenCwdDiffersFromWorkspaceRoot_PlacesSkillFilesInWo var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -115,7 +106,7 @@ public async Task AgentInit_WhenCwdDiffersFromWorkspaceRoot_PlacesSkillFilesInWo // Crucially, do NOT cd into the project — stay in the parent directory. await auto.TypeAsync("mkdir -p TestProject/.claude"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter); + await auto.WaitForSuccessPromptAsync(counter); // Step 3: Run aspire agent init from the PARENT directory for Playwright // only. When provided as options, the workspace root and skill selection @@ -124,23 +115,18 @@ public async Task AgentInit_WhenCwdDiffersFromWorkspaceRoot_PlacesSkillFilesInWo await auto.EnterAsync(); await auto.WaitUntilTextAsync("configuration complete", timeout: TimeSpan.FromMinutes(3)); - await auto.WaitForSuccessPromptFailFastAsync(counter); + await auto.WaitForSuccessPromptAsync(counter); // Step 4: Verify skill file exists in the workspace root (project subdirectory). await auto.TypeAsync("ls TestProject/.claude/skills/playwright-cli/SKILL.md"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("SKILL.md", timeout: TimeSpan.FromSeconds(10)); - await auto.WaitForSuccessPromptFailFastAsync(counter); + await auto.WaitForSuccessPromptAsync(counter); // Step 5: Verify no stray skill files were created in the CWD (parent directory). await auto.TypeAsync("test -d .claude/skills/playwright-cli && echo 'STRAY_FILES_FOUND' || echo 'NO_STRAY_FILES'"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("NO_STRAY_FILES", timeout: TimeSpan.FromSeconds(10)); - await auto.WaitForSuccessPromptFailFastAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; + await auto.WaitForSuccessPromptAsync(counter); } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PodmanDeploymentTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PodmanDeploymentTests.cs index d5137e9432e..31ff35eed39 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/PodmanDeploymentTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/PodmanDeploymentTests.cs @@ -27,11 +27,9 @@ public async Task CreateAndDeployToDockerComposeWithPodman() var strategy = CliInstallStrategy.Detect(output.WriteLine); using var terminal = CliE2ETestHelpers.CreatePodmanDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -113,10 +111,5 @@ public async Task CreateAndDeployToDockerComposeWithPodman() // Step 11: Clean up - destroy the deployment using aspire destroy await auto.AspireDestroyAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ProjectReferenceTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ProjectReferenceTests.cs index 9abd2a3a6c3..49349afa229 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/ProjectReferenceTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/ProjectReferenceTests.cs @@ -27,11 +27,9 @@ public async Task TypeScriptAppHostWithProjectReferenceIntegration() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -185,9 +183,5 @@ await auto.WaitUntilAsync(s => await auto.TypeAsync("aspire stop"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PsCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PsCommandTests.cs index 00091c464cc..84bb0960557 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/PsCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/PsCommandTests.cs @@ -24,11 +24,9 @@ public async Task PsCommandListsRunningAppHost() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -77,12 +75,6 @@ public async Task PsCommandListsRunningAppHost() await auto.EnterAsync(); await auto.WaitUntilTextAsync(SharedCommandStrings.AppHostNotRunning, timeout: TimeSpan.FromSeconds(30)); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -94,11 +86,9 @@ public async Task PsFormatJsonOutputsOnlyJsonToStdout() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -119,11 +109,5 @@ public async Task PsFormatJsonOutputsOnlyJsonToStdout() // Verify the file contains only the expected JSON output (empty array). var content = File.ReadAllText(outputFilePath).Trim(); Assert.Equal("[]", content); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs index 9d16f783c7b..064a10d2772 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs @@ -23,11 +23,9 @@ public async Task CreateAndRunPythonReactProject() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -47,15 +45,10 @@ public async Task CreateAndRunPythonReactProject() await auto.WaitForSuccessPromptAsync(counter); // Step 3: Verify the generated TypeScript AppHost builds successfully. - await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2)); + await auto.RunCommandAsync("npm run build", counter, TimeSpan.FromMinutes(2)); // Step 4: Start and stop the project await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ResourceCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ResourceCommandTests.cs index b32fd53f209..19203418cf3 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/ResourceCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/ResourceCommandTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Cli.EndToEnd.Tests.Helpers; @@ -27,121 +27,84 @@ public async Task ResourceCommand_SetAndDeleteParameterUpdatesDescribeOutput() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); - var testBodyFailed = false; - - try - { - await auto.PrepareDockerEnvironmentAsync(counter, workspace); - await auto.InstallAspireCliAsync(strategy, counter); - await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.EmptyAppHost); - - await auto.TypeAsync($"cd {projectName}"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); - - var appHostFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, projectName, "apphost.cs"); - var content = File.ReadAllText(appHostFilePath); - var sdkLine = content.Split('\n', 2)[0].TrimEnd('\r'); - - var newContent = $$""" - {{sdkLine}} - - var builder = DistributedApplication.CreateBuilder(args); - - builder.AddParameter("greeting"); - - builder.Build().Run(); - """; - - File.WriteAllText(appHostFilePath, newContent); - - await auto.TypeAsync("aspire start"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3)); - await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("aspire describe greeting --format json > greeting-unset.json"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); - - await auto.TypeAsync("jq -er '.resources[0].state' greeting-unset.json"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("ValueMissing", timeout: TimeSpan.FromSeconds(10)); - await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("aspire resource greeting set-parameter --value 'Hello world'"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("Resource 'greeting' set successfully.", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); - - await auto.TypeAsync("aspire describe greeting --format json > greeting-set.json"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); - - await auto.TypeAsync("jq -er '.resources[0].state' greeting-set.json"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("Running", timeout: TimeSpan.FromSeconds(10)); - await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("jq -er '.resources[0].properties.Value' greeting-set.json"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("Hello world", timeout: TimeSpan.FromSeconds(10)); - await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("aspire resource greeting delete-parameter"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("Resource 'greeting' deleted successfully.", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); - - await auto.TypeAsync("aspire describe greeting --format json > greeting-deleted.json"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); - - await auto.TypeAsync("jq -er '.resources[0].state' greeting-deleted.json"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("ValueMissing", timeout: TimeSpan.FromSeconds(10)); - await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("aspire stop"); - await auto.EnterAsync(); - await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); - await auto.WaitForSuccessPromptAsync(counter); - } - catch - { - testBodyFailed = true; - throw; - } - finally - { - try - { - await auto.CaptureAspireDiagnosticsAsync(counter, workspace); - } - catch - { - // Best effort diagnostics capture. - } - - try - { - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; - } - catch - { - if (!testBodyFailed) - { - throw; - } - } - } + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); + await auto.PrepareDockerEnvironmentAsync(counter, workspace); + await auto.InstallAspireCliAsync(strategy, counter); + await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.EmptyAppHost); + + await auto.TypeAsync($"cd {projectName}"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + var appHostFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, projectName, "apphost.cs"); + var content = File.ReadAllText(appHostFilePath); + var sdkLine = content.Split('\n', 2)[0].TrimEnd('\r'); + + var newContent = $$""" + {{sdkLine}} + + var builder = DistributedApplication.CreateBuilder(args); + + builder.AddParameter("greeting"); + + builder.Build().Run(); + """; + + File.WriteAllText(appHostFilePath, newContent); + + await auto.TypeAsync("aspire start"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3)); + await auto.WaitForSuccessPromptAsync(counter); + + await auto.TypeAsync("aspire describe greeting --format json > greeting-unset.json"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); + + await auto.TypeAsync("jq -er '.resources[0].state' greeting-unset.json"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("ValueMissing", timeout: TimeSpan.FromSeconds(10)); + await auto.WaitForSuccessPromptAsync(counter); + + await auto.TypeAsync("aspire resource greeting set-parameter --value 'Hello world'"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("Resource 'greeting' set successfully.", timeout: TimeSpan.FromSeconds(30)); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); + + await auto.TypeAsync("aspire describe greeting --format json > greeting-set.json"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); + + await auto.TypeAsync("jq -er '.resources[0].state' greeting-set.json"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("Running", timeout: TimeSpan.FromSeconds(10)); + await auto.WaitForSuccessPromptAsync(counter); + + await auto.TypeAsync("jq -er '.resources[0].properties.Value' greeting-set.json"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("Hello world", timeout: TimeSpan.FromSeconds(10)); + await auto.WaitForSuccessPromptAsync(counter); + + await auto.TypeAsync("aspire resource greeting delete-parameter"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("Resource 'greeting' deleted successfully.", timeout: TimeSpan.FromSeconds(30)); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); + + await auto.TypeAsync("aspire describe greeting --format json > greeting-deleted.json"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(30)); + + await auto.TypeAsync("jq -er '.resources[0].state' greeting-deleted.json"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("ValueMissing", timeout: TimeSpan.FromSeconds(10)); + await auto.WaitForSuccessPromptAsync(counter); + + await auto.TypeAsync("aspire stop"); + await auto.EnterAsync(); + await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); + await auto.WaitForSuccessPromptAsync(counter); } [Fact] @@ -157,129 +120,92 @@ public async Task ResourceCommand_FailsWhenInteractionServiceIsRequired() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); - var testBodyFailed = false; + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); + await auto.PrepareDockerEnvironmentAsync(counter, workspace, enableDcpDiagnostics: true); + await auto.InstallAspireCliAsync(strategy, counter); + await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.EmptyAppHost); - try - { - await auto.PrepareDockerEnvironmentAsync(counter, workspace, enableDcpDiagnostics: true); - await auto.InstallAspireCliAsync(strategy, counter); - await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.EmptyAppHost); + await auto.TypeAsync($"cd {projectName}"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync($"cd {projectName}"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); + // Read the generated apphost.cs so we can extract the #:sdk line with the + // resolved version, then replace the entire file with a minimal AppHost + // that has a placeholder resource and a command that uses IInteractionService. + var appHostFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, projectName, "apphost.cs"); + var content = File.ReadAllText(appHostFilePath); - // Read the generated apphost.cs so we can extract the #:sdk line with the - // resolved version, then replace the entire file with a minimal AppHost - // that has a placeholder resource and a command that uses IInteractionService. - var appHostFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, projectName, "apphost.cs"); - var content = File.ReadAllText(appHostFilePath); + // Extract the first line (#:sdk directive) so the replacement uses the same SDK version. + var sdkLine = content.Split('\n', 2)[0].TrimEnd('\r'); - // Extract the first line (#:sdk directive) so the replacement uses the same SDK version. - var sdkLine = content.Split('\n', 2)[0].TrimEnd('\r'); + var newContent = $$""" + {{sdkLine}} - var newContent = $$""" - {{sdkLine}} + #pragma warning disable ASPIREINTERACTION001 - #pragma warning disable ASPIREINTERACTION001 + var builder = DistributedApplication.CreateBuilder(args); - var builder = DistributedApplication.CreateBuilder(args); + var cache = builder.AddContainer("cache", "redis"); - var cache = builder.AddContainer("cache", "redis"); + cache.WithCommand( + name: "needs-interaction", + displayName: "Needs interaction", + executeCommand: async context => + { + var interactionService = (IInteractionService)context.ServiceProvider.GetService(typeof(IInteractionService))!; - cache.WithCommand( - name: "needs-interaction", - displayName: "Needs interaction", - executeCommand: async context => + try { - var interactionService = (IInteractionService)context.ServiceProvider.GetService(typeof(IInteractionService))!; - - try - { - // This should throw because InteractionService is not available in non-interactive mode. - // Bound the wait to avoid hanging the E2E run if behavior regresses. - _ = await interactionService.PromptInputAsync( - title: "Prompt title", - message: "Prompt message", - inputLabel: "Name", - placeHolder: "placeholder").WaitAsync(TimeSpan.FromSeconds(10)); - - // We're looking for a failure. Treat a successful prompt completion as a test failure since that would - // indicate the interaction service is available when it shouldn't be. - return CommandResults.Success("Prompt unexpectedly completed without throwing."); - } - catch (TimeoutException) - { - // We're looking for a failure, and the most likely failure mode if the interaction service is incorrectly - // available would be that the prompt hangs waiting for input that will never come. - // Treat a timeout as a success since that indicates the interaction service is not available to show the prompt. - return CommandResults.Success("Prompt timed out after 10 seconds."); - } - }); - - builder.Build().Run(); - """; - - File.WriteAllText(appHostFilePath, newContent); - - await auto.TypeAsync("aspire start"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3)); - await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("aspire resource cache needs-interaction"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("Failed to execute command 'needs-interaction' on resource 'cache'", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitUntilTextAsync("InteractionService is not available", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitUntilTextAsync("See logs at", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitUntilTextAsync("See AppHost logs at", timeout: TimeSpan.FromSeconds(30)); - await auto.WaitForAnyPromptAsync(counter, timeout: TimeSpan.FromSeconds(30)); - - await auto.TypeAsync($"if [ $? -eq {CliExitCodes.FailedToExecuteResourceCommand} ]; then echo RESOURCE_CMD_EXIT_CODE_CORRECT; else echo RESOURCE_CMD_UNEXPECTED_EXIT_CODE; fi"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("RESOURCE_CMD_EXIT_CODE_CORRECT", timeout: TimeSpan.FromSeconds(10)); - await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("aspire stop"); - await auto.EnterAsync(); - await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); - await auto.WaitForSuccessPromptAsync(counter); - } - catch - { - testBodyFailed = true; - throw; - } - finally - { - try - { - await auto.CaptureAspireDiagnosticsAsync(counter, workspace); - } - catch - { - // Best effort diagnostics capture. - } - - try - { - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; - } - catch - { - if (!testBodyFailed) - { - throw; - } - } - } + // This should throw because InteractionService is not available in non-interactive mode. + // Bound the wait to avoid hanging the E2E run if behavior regresses. + _ = await interactionService.PromptInputAsync( + title: "Prompt title", + message: "Prompt message", + inputLabel: "Name", + placeHolder: "placeholder").WaitAsync(TimeSpan.FromSeconds(10)); + + // We're looking for a failure. Treat a successful prompt completion as a test failure since that would + // indicate the interaction service is available when it shouldn't be. + return CommandResults.Success("Prompt unexpectedly completed without throwing."); + } + catch (TimeoutException) + { + // We're looking for a failure, and the most likely failure mode if the interaction service is incorrectly + // available would be that the prompt hangs waiting for input that will never come. + // Treat a timeout as a success since that indicates the interaction service is not available to show the prompt. + return CommandResults.Success("Prompt timed out after 10 seconds."); + } + }); + + builder.Build().Run(); + """; + + File.WriteAllText(appHostFilePath, newContent); + + await auto.TypeAsync("aspire start"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3)); + await auto.WaitForSuccessPromptAsync(counter); + + await auto.TypeAsync("aspire resource cache needs-interaction"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("Failed to execute command 'needs-interaction' on resource 'cache'", timeout: TimeSpan.FromSeconds(30)); + await auto.WaitUntilTextAsync("InteractionService is not available", timeout: TimeSpan.FromSeconds(30)); + await auto.WaitUntilTextAsync("See logs at", timeout: TimeSpan.FromSeconds(30)); + await auto.WaitUntilTextAsync("See AppHost logs at", timeout: TimeSpan.FromSeconds(30)); + await auto.WaitForAnyPromptAsync(counter, timeout: TimeSpan.FromSeconds(30)); + + await auto.TypeAsync($"if [ $? -eq {CliExitCodes.FailedToExecuteResourceCommand} ]; then echo RESOURCE_CMD_EXIT_CODE_CORRECT; else echo RESOURCE_CMD_UNEXPECTED_EXIT_CODE; fi"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("RESOURCE_CMD_EXIT_CODE_CORRECT", timeout: TimeSpan.FromSeconds(10)); + await auto.WaitForSuccessPromptAsync(counter); + + await auto.TypeAsync("aspire stop"); + await auto.EnterAsync(); + await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); + await auto.WaitForSuccessPromptAsync(counter); } [Fact] @@ -294,136 +220,99 @@ public async Task ResourceCommand_FailedExecution_DisplaysAppHostLogPathAndLogCo var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); - var testBodyFailed = false; - - try - { - await auto.PrepareDockerEnvironmentAsync(counter, workspace, enableDcpDiagnostics: true); - await auto.InstallAspireCliAsync(strategy, counter); - await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.EmptyAppHost); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); + await auto.PrepareDockerEnvironmentAsync(counter, workspace, enableDcpDiagnostics: true); + await auto.InstallAspireCliAsync(strategy, counter); + await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.EmptyAppHost); - await auto.TypeAsync($"cd {projectName}"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); + await auto.TypeAsync($"cd {projectName}"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); - // Replace the generated apphost.cs with a minimal AppHost that has a - // command writing to context.Logger before returning a failure result. - var appHostFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, projectName, "apphost.cs"); - var content = File.ReadAllText(appHostFilePath); + // Replace the generated apphost.cs with a minimal AppHost that has a + // command writing to context.Logger before returning a failure result. + var appHostFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, projectName, "apphost.cs"); + var content = File.ReadAllText(appHostFilePath); - // Extract the first line (#:sdk directive) so the replacement uses the same SDK version. - var sdkLine = content.Split('\n', 2)[0].TrimEnd('\r'); + // Extract the first line (#:sdk directive) so the replacement uses the same SDK version. + var sdkLine = content.Split('\n', 2)[0].TrimEnd('\r'); - var newContent = $$""" - {{sdkLine}} + var newContent = $$""" + {{sdkLine}} - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; - var builder = DistributedApplication.CreateBuilder(args); + var builder = DistributedApplication.CreateBuilder(args); - var cache = builder.AddContainer("cache", "redis"); + var cache = builder.AddContainer("cache", "redis"); - cache.WithCommand( - name: "fail-with-log", - displayName: "Fail with log", - executeCommand: context => - { - var logger = context.ServiceProvider.GetRequiredService>(); - logger.LogInformation("CUSTOM_E2E_LOG_ENTRY_FOR_VERIFICATION"); - - return Task.FromResult(CommandResults.Failure("Command failed intentionally.")); - }); - - builder.Build().Run(); - """; - - File.WriteAllText(appHostFilePath, newContent); - - // Start the AppHost in detached mode. - await auto.TypeAsync("aspire start"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3)); - await auto.WaitForSuccessPromptAsync(counter); - - // Run the failing resource command, capturing stdout and stderr so we can extract the - // AppHost log path from the "See AppHost logs at " message. - await auto.TypeAsync("aspire resource cache fail-with-log > /tmp/resource-cmd-output.txt 2>&1"); - await auto.EnterAsync(); - await auto.WaitForAnyPromptAsync(counter, timeout: TimeSpan.FromSeconds(30)); - - await auto.TypeAsync($"if [ $? -eq {CliExitCodes.FailedToExecuteResourceCommand} ]; then echo RESOURCE_CMD_EXIT_CODE_CORRECT; else echo RESOURCE_CMD_UNEXPECTED_EXIT_CODE; fi"); - await auto.EnterAsync(); - await auto.WaitUntilTextAsync("RESOURCE_CMD_EXIT_CODE_CORRECT", timeout: TimeSpan.FromSeconds(10)); - await auto.WaitForSuccessPromptAsync(counter); - - // Extract the AppHost log file path from the captured output and verify - // it exists and is non-empty. - // Extract the AppHost log file path from the captured output. The CLI - // wraps paths in OSC 8 terminal hyperlinks (\e]8;...;\e\\path\e]8;;\e\\), - // so we must strip those escape sequences before extracting the path. - await auto.RunCommandAsync( - "APPHOST_LOG=$(sed 's/\\x1b\\][^\\x1b]*\\x1b\\\\//g' /tmp/resource-cmd-output.txt | grep 'See AppHost logs at' | sed 's/.*See AppHost logs at //')", - counter); - - // Debug: Show what was captured in stderr and the extracted APPHOST_LOG value. - // Run these in a single command with || true so they always complete even if one step fails. - await auto.RunCommandAsync( - "echo '=== Contents of /tmp/resource-cmd-output.txt ===' && cat /tmp/resource-cmd-output.txt && echo && echo '=== APPHOST_LOG value ===' && echo \"$APPHOST_LOG\" && echo '=== End debug output ===' || true", - counter); - - await auto.RunCommandFailFastAsync( - "test -n \"$APPHOST_LOG\" && test -s \"$APPHOST_LOG\"", - counter, - TimeSpan.FromSeconds(10)); - - // Verify the log file contains the custom log entry written by the - // command handler via ILogger before returning the failure result. - await auto.RunCommandFailFastAsync( - "grep -q 'CUSTOM_E2E_LOG_ENTRY_FOR_VERIFICATION' \"$APPHOST_LOG\"", - counter, - TimeSpan.FromSeconds(10)); - - // Stop the AppHost. - await auto.TypeAsync("aspire stop"); - await auto.EnterAsync(); - await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); - await auto.WaitForSuccessPromptAsync(counter); - } - catch - { - testBodyFailed = true; - throw; - } - finally - { - try - { - await auto.CaptureAspireDiagnosticsAsync(counter, workspace); - } - catch - { - // Best effort diagnostics capture. - } - - try - { - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; - } - catch - { - if (!testBodyFailed) + cache.WithCommand( + name: "fail-with-log", + displayName: "Fail with log", + executeCommand: context => { - throw; - } - } - } + var logger = context.ServiceProvider.GetRequiredService>(); + logger.LogInformation("CUSTOM_E2E_LOG_ENTRY_FOR_VERIFICATION"); + + return Task.FromResult(CommandResults.Failure("Command failed intentionally.")); + }); + + builder.Build().Run(); + """; + + File.WriteAllText(appHostFilePath, newContent); + + // Start the AppHost in detached mode. + await auto.TypeAsync("aspire start"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3)); + await auto.WaitForSuccessPromptAsync(counter); + + // Run the failing resource command, capturing stdout and stderr so we can extract the + // AppHost log path from the "See AppHost logs at " message. + await auto.TypeAsync("aspire resource cache fail-with-log > /tmp/resource-cmd-output.txt 2>&1"); + await auto.EnterAsync(); + await auto.WaitForAnyPromptAsync(counter, timeout: TimeSpan.FromSeconds(30)); + + await auto.TypeAsync($"if [ $? -eq {CliExitCodes.FailedToExecuteResourceCommand} ]; then echo RESOURCE_CMD_EXIT_CODE_CORRECT; else echo RESOURCE_CMD_UNEXPECTED_EXIT_CODE; fi"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("RESOURCE_CMD_EXIT_CODE_CORRECT", timeout: TimeSpan.FromSeconds(10)); + await auto.WaitForSuccessPromptAsync(counter); + + // Extract the AppHost log file path from the captured output and verify + // it exists and is non-empty. + // Extract the AppHost log file path from the captured output. The CLI + // wraps paths in OSC 8 terminal hyperlinks (\e]8;...;\e\\path\e]8;;\e\\), + // so we must strip those escape sequences before extracting the path. + await auto.RunCommandAsync( + "APPHOST_LOG=$(sed 's/\\x1b\\][^\\x1b]*\\x1b\\\\//g' /tmp/resource-cmd-output.txt | grep 'See AppHost logs at' | sed 's/.*See AppHost logs at //')", + counter); + + // Debug: Show what was captured in stderr and the extracted APPHOST_LOG value. + // Run these in a single command with || true so they always complete even if one step fails. + await auto.RunCommandAsync( + "echo '=== Contents of /tmp/resource-cmd-output.txt ===' && cat /tmp/resource-cmd-output.txt && echo && echo '=== APPHOST_LOG value ===' && echo \"$APPHOST_LOG\" && echo '=== End debug output ===' || true", + counter); + + await auto.RunCommandAsync( + "test -n \"$APPHOST_LOG\" && test -s \"$APPHOST_LOG\"", + counter, + TimeSpan.FromSeconds(10)); + + // Verify the log file contains the custom log entry written by the + // command handler via ILogger before returning the failure result. + await auto.RunCommandAsync( + "grep -q 'CUSTOM_E2E_LOG_ENTRY_FOR_VERIFICATION' \"$APPHOST_LOG\"", + counter, + TimeSpan.FromSeconds(10)); + + // Stop the AppHost. + await auto.TypeAsync("aspire stop"); + await auto.EnterAsync(); + await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); + await auto.WaitForSuccessPromptAsync(counter); } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/SecretDotNetAppHostTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/SecretDotNetAppHostTests.cs index 8c6f93abec8..ea65d377981 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/SecretDotNetAppHostTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/SecretDotNetAppHostTests.cs @@ -21,11 +21,9 @@ public async Task SecretCrudOnDotNetAppHost() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -73,10 +71,5 @@ public async Task SecretCrudOnDotNetAppHost() await auto.EnterAsync(); await auto.WaitUntilTextAsync("db-password", timeout: TimeSpan.FromSeconds(30)); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/SecretTypeScriptAppHostTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/SecretTypeScriptAppHostTests.cs index 2e315d3c0ef..ec0a9bd6ab1 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/SecretTypeScriptAppHostTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/SecretTypeScriptAppHostTests.cs @@ -21,11 +21,9 @@ public async Task SecretCrudOnTypeScriptAppHost() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -75,10 +73,5 @@ public async Task SecretCrudOnTypeScriptAppHost() await auto.EnterAsync(); await auto.WaitUntilTextAsync("ConnectionStrings:Db", timeout: TimeSpan.FromSeconds(30)); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/SingleFileAppHostInitDotnetRunTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/SingleFileAppHostInitDotnetRunTests.cs index 0f8ad468896..46da7adc2e4 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/SingleFileAppHostInitDotnetRunTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/SingleFileAppHostInitDotnetRunTests.cs @@ -40,11 +40,9 @@ public async Task AspireInitSingleFileAppHostRunsViaDotnetRunAppHost() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -107,10 +105,6 @@ await auto.WaitUntilTextAsync( // Stop the running AppHost with Ctrl+C and wait for the shell prompt. await auto.Ctrl().KeyAsync(Hex1bKey.C); await auto.WaitForAnyPromptAsync(counter, TimeSpan.FromMinutes(1)); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs index 94af249296f..8b9530cbad3 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs @@ -28,10 +28,9 @@ public async Task CreateAndRunAspireStarterProject() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); // Prepare Docker environment (prompt counting, umask, env vars) await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -63,12 +62,6 @@ await auto.WaitUntilAsync(s => // Stop the running apphost with Ctrl+C await auto.Ctrl().KeyAsync(Hex1bKey.C); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [CaptureWorkspaceOnFailure] @@ -82,10 +75,9 @@ public async Task LatestCliCanStartStableChannelAppHost() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -103,14 +95,9 @@ public async Task LatestCliCanStartStableChannelAppHost() output.WriteLine($"Stable AppHost SDK version: {appHostSdkVersion}"); - await auto.RunCommandFailFastAsync($"cd {projectName}", counter); + await auto.RunCommandAsync($"cd {projectName}", counter); await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [CaptureWorkspaceOnFailure] @@ -124,10 +111,9 @@ public async Task LatestCliCanStartStableChannelTypeScriptAppHost() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -146,14 +132,9 @@ public async Task LatestCliCanStartStableChannelTypeScriptAppHost() output.WriteLine("Stable TypeScript AppHost config verified."); - await auto.RunCommandFailFastAsync($"cd {projectName}", counter); + await auto.RunCommandAsync($"cd {projectName}", counter); await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } private static string GetAppHostSdkVersion(string appHostPath) diff --git a/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs index ec1bbae58e4..312a34f855c 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs @@ -23,11 +23,9 @@ public async Task StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -113,10 +111,5 @@ public async Task StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels() await auto.TypeAsync("aspire config delete channel -g"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/StartStopTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/StartStopTests.cs index 20801b43cf6..173362e700e 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/StartStopTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/StartStopTests.cs @@ -30,71 +30,39 @@ public async Task CreateStartAndStopAspireProject() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); - var testBodyFailed = false; + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); - try - { - // Prepare Docker environment (prompt counting, umask, env vars) - await auto.PrepareDockerEnvironmentAsync(counter, workspace, enableDcpDiagnostics: true); + // Prepare Docker environment (prompt counting, umask, env vars) + await auto.PrepareDockerEnvironmentAsync(counter, workspace, enableDcpDiagnostics: true); - // Install the Aspire CLI - await auto.InstallAspireCliAsync(strategy, counter); + // Install the Aspire CLI + await auto.InstallAspireCliAsync(strategy, counter); - // Create a new project using aspire new - await auto.AspireNewAsync(projectName, counter); + // Create a new project using aspire new + await auto.AspireNewAsync(projectName, counter); - // Navigate to the AppHost directory - await auto.TypeAsync($"cd {projectName}/{projectName}.AppHost"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); + // Navigate to the AppHost directory + await auto.TypeAsync($"cd {projectName}/{projectName}.AppHost"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); - // Start the AppHost in the background using aspire start - await auto.TypeAsync("aspire start"); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptAsync(counter); + // Start the AppHost in the background using aspire start + await auto.TypeAsync("aspire start"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); - // Stop the AppHost using aspire stop - await auto.TypeAsync("aspire stop"); - await auto.EnterAsync(); - await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); - await auto.WaitForSuccessPromptAsync(counter); + // Stop the AppHost using aspire stop + await auto.TypeAsync("aspire stop"); + await auto.EnterAsync(); + await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); + await auto.WaitForSuccessPromptAsync(counter); - await auto.ClearScreenAsync(counter); + await auto.ClearScreenAsync(counter); - // Docker network cleanup can lag behind aspire stop on contended CI runners. - await auto.ExecuteCommandUntilOutputAsync(counter, $"docker network ls --format json | grep -i -- '{projectName}' | wc -l", "0", timeout: TimeSpan.FromMinutes(5)); - } - catch - { - testBodyFailed = true; - throw; - } - finally - { - try - { - await auto.CaptureAspireDiagnosticsAsync(counter, workspace); - } - catch { } // Best effort - - try - { - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; - } - catch - { - if (!testBodyFailed) - { - throw; - } - } - } + // Docker network cleanup can lag behind aspire stop on contended CI runners. + await auto.ExecuteCommandUntilOutputAsync(counter, $"docker network ls --format json | grep -i -- '{projectName}' | wc -l", "0", timeout: TimeSpan.FromMinutes(5)); } [Fact] @@ -107,10 +75,9 @@ public async Task StopWithNoRunningAppHostExitsSuccessfully() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); // Prepare Docker environment (prompt counting, umask, env vars) await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -122,12 +89,6 @@ public async Task StopWithNoRunningAppHostExitsSuccessfully() await auto.TypeAsync("aspire stop"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -140,10 +101,9 @@ public async Task AddPackageWhileAppHostRunningDetached() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); // Prepare Docker environment (prompt counting, umask, env vars) await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -178,12 +138,6 @@ public async Task AddPackageWhileAppHostRunningDetached() await auto.TypeAsync("aspire stop"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter, timeout: TimeSpan.FromMinutes(1)); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -196,10 +150,9 @@ public async Task AddPackageInteractiveWhileAppHostRunningDetached() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); // Prepare Docker environment (prompt counting, umask, env vars) await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -249,11 +202,5 @@ await auto.WaitUntilAsync(snapshot => await auto.TypeAsync("aspire stop"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter, timeout: TimeSpan.FromMinutes(1)); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/StopNonInteractiveTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/StopNonInteractiveTests.cs index 3f2353f60e4..3d84dc1f57a 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/StopNonInteractiveTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/StopNonInteractiveTests.cs @@ -27,10 +27,9 @@ public async Task StopNonInteractiveSingleAppHost() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -72,12 +71,6 @@ public async Task StopNonInteractiveSingleAppHost() await auto.EnterAsync(); await auto.WaitUntilTextAsync(SharedCommandStrings.AppHostNotRunning, timeout: TimeSpan.FromSeconds(30)); await auto.WaitForAnyPromptAsync(counter, TimeSpan.FromSeconds(30)); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -89,11 +82,9 @@ public async Task StopAllAppHostsFromAppHostDirectory() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -139,12 +130,6 @@ public async Task StopAllAppHostsFromAppHostDirectory() await auto.EnterAsync(); await auto.WaitUntilTextAsync(SharedCommandStrings.AppHostNotRunning, timeout: TimeSpan.FromSeconds(30)); await auto.WaitForAnyPromptAsync(counter, TimeSpan.FromSeconds(30)); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -157,11 +142,9 @@ public async Task StopAllAppHostsFromUnrelatedDirectory() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -212,12 +195,6 @@ public async Task StopAllAppHostsFromUnrelatedDirectory() await auto.EnterAsync(); await auto.WaitUntilTextAsync(SharedCommandStrings.AppHostNotRunning, timeout: TimeSpan.FromSeconds(30)); await auto.WaitForAnyPromptAsync(counter, TimeSpan.FromSeconds(30)); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -230,11 +207,9 @@ public async Task StopNonInteractiveMultipleAppHostsShowsError() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -287,11 +262,5 @@ await auto.WaitUntilTextAsync( await auto.EnterAsync(); await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptCodegenValidationTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptCodegenValidationTests.cs index 1f693e69f9a..059268070f7 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptCodegenValidationTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptCodegenValidationTests.cs @@ -35,11 +35,9 @@ public async Task RestoreGeneratesSdkFiles_WithConfiguredToolchain(string toolch ["Aspire.Hosting.CodeGeneration.TypeScript.", "Aspire.Hosting.Redis.", "Aspire.Hosting.SqlServer."]); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.DotNet, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -90,7 +88,7 @@ public async Task RestoreGeneratesSdkFiles_WithConfiguredToolchain(string toolch await auto.TypeAsync(TypeScriptAppHostToolchainTestHelpers.GetTypeCheckCommand(toolchain, "tsconfig.apphost.json")); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2)); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); // Step 4: Verify generated SDK files exist. var modulesDir = Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "modules"); @@ -125,11 +123,6 @@ public async Task RestoreGeneratesSdkFiles_WithConfiguredToolchain(string toolch { throw new InvalidOperationException("aspire.mts does not contain addSqlServer from Aspire.Hosting.SqlServer"); } - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -150,11 +143,9 @@ public async Task RestoreRefreshesGeneratedSdkAfterAddingIntegration() variant: CliE2ETestHelpers.DockerfileVariant.DotNet, mountDockerSocket: false, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -256,12 +247,7 @@ public async Task RestoreRefreshesGeneratedSdkAfterAddingIntegration() await auto.TypeAsync("npx tsc --noEmit --project tsconfig.apphost.json"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2)); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); } [Fact] @@ -279,10 +265,9 @@ public async Task UnAwaitedChainsCompileWithAutoResolvePromises() variant: CliE2ETestHelpers.DockerfileVariant.DotNet, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -333,7 +318,7 @@ public async Task UnAwaitedChainsCompileWithAutoResolvePromises() // withReference(db) should accept PromiseLike from the un-awaited addDatabase(). await auto.TypeAsync("npx tsc --noEmit --project tsconfig.apphost.json"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2)); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); // Validate runtime behavior: aspire start launches the apphost, which calls // build() and triggers flushPendingPromises(). If the flush deadlocks (e.g. the @@ -346,12 +331,5 @@ public async Task UnAwaitedChainsCompileWithAutoResolvePromises() await auto.AssertResourcesExistAsync(counter, "postgres", "db", "consumer"); await auto.AspireStopAsync(counter); - - await auto.CaptureAspireDiagnosticsAsync(counter, workspace); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs index d76991579d5..1847a78b052 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptEmptyAppHostTemplateTests.cs @@ -24,11 +24,9 @@ public async Task CreateAndRunTypeScriptEmptyAppHostProject() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -44,14 +42,9 @@ public async Task CreateAndRunTypeScriptEmptyAppHostProject() await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2)); + await auto.RunCommandAsync("npm run build", counter, TimeSpan.FromMinutes(2)); await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptLegacyAppHostTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptLegacyAppHostTests.cs index 944b29872a2..ce06769005f 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptLegacyAppHostTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptLegacyAppHostTests.cs @@ -40,11 +40,9 @@ public async Task AspireAddAndStartWorkAgainstLegacyAppHostTs() variant: CliE2ETestHelpers.DockerfileVariant.DotNet, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -84,7 +82,7 @@ public async Task AspireAddAndStartWorkAgainstLegacyAppHostTs() // legacy `.modules/` folder — the contract the conversion enforces. await auto.TypeAsync("npx --no-install tsc --noEmit -p tsconfig.apphost.json"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2)); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); // Step 5: `aspire start` exercises apphost.ts at runtime — proving the generated // SDK is dynamically importable AND that addRedis (added via aspire add in step 2) @@ -93,11 +91,6 @@ public async Task AspireAddAndStartWorkAgainstLegacyAppHostTs() await auto.AspireStartAsync(counter); await auto.AssertResourcesExistAsync(counter, "cache"); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotApphostDirectoryTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotApphostDirectoryTests.cs index 7084c0c9e69..4c869c728d1 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotApphostDirectoryTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotApphostDirectoryTests.cs @@ -38,11 +38,9 @@ public async Task StopTypeScriptPolyglotAppHostUsingApphostDirectory() var channelArgument = localChannel is not null ? " --channel local" : string.Empty; using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -113,10 +111,5 @@ public async Task StopTypeScriptPolyglotAppHostUsingApphostDirectory() await auto.EnterAsync(); await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotTests.cs index 5efb23c8a96..2be2fb330ce 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotTests.cs @@ -43,11 +43,9 @@ public async Task CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain(str var channelArgument = localChannel is not null ? " --channel local" : string.Empty; using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -135,7 +133,7 @@ public async Task CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain(str await auto.TypeAsync(TypeScriptAppHostToolchainTestHelpers.GetTypeCheckCommand(toolchain, "tsconfig.apphost.json")); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2)); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); // Step 7: Run the apphost await auto.TypeAsync("aspire run"); @@ -155,10 +153,6 @@ await auto.WaitUntilAsync(s => // Step 8: Stop the apphost await auto.Ctrl().KeyAsync(Hex1bKey.C); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -176,11 +170,9 @@ public async Task CreateTypeScriptAppHostWithViteApp_AllowsGuestAppPackageManage var channelArgument = localChannel is not null ? " --channel local" : string.Empty; using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -247,12 +239,7 @@ public async Task CreateTypeScriptAppHostWithViteApp_AllowsGuestAppPackageManage await auto.TypeAsync(TypeScriptAppHostToolchainTestHelpers.GetTypeCheckCommand(appHostToolchain, "tsconfig.apphost.json")); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2)); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); } [Theory] @@ -269,11 +256,9 @@ public async Task GeneratedAspireDevScript_StartsWatchMode_WithConfiguredToolcha var channelArgument = localChannel is not null ? " --channel local" : string.Empty; using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -310,10 +295,6 @@ await auto.WaitUntilAsync( await auto.Ctrl().KeyAsync(Hex1bKey.C); await auto.WaitForAnyPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -332,9 +313,6 @@ public async Task InitTypeScriptAppHost_AugmentsExistingViteRepoInWorkspaceSubdi var channelArgument = localChannel is not null ? " --channel local" : string.Empty; using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.DotNet, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - string? originalDevScript = null; string? originalBuildScript = null; string? originalPreviewScript = null; @@ -343,6 +321,7 @@ public async Task InitTypeScriptAppHost_AugmentsExistingViteRepoInWorkspaceSubdi var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -485,9 +464,5 @@ await auto.WaitUntilAsync(s => await auto.Ctrl().KeyAsync(Hex1bKey.C); await auto.WaitForSuccessPromptAsync(counter); - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPublishTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPublishTests.cs index cc68058ebe4..2b9adcae0ef 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPublishTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPublishTests.cs @@ -26,11 +26,9 @@ public async Task PublishWithDockerComposeServiceCallbackSucceeds() using var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.DotNet, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -97,11 +95,6 @@ public async Task PublishWithDockerComposeServiceCallbackSucceeds() await auto.TypeAsync("grep -F \"postgres:\" artifacts/docker-compose.yaml"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -115,11 +108,9 @@ public async Task PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifac ["Aspire.Hosting.CodeGeneration.TypeScript.", "Aspire.Hosting.JavaScript.", "Aspire.Hosting.Docker."]); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -152,7 +143,7 @@ public async Task PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifac await auto.TypeAsync("aspire publish -o artifacts --non-interactive"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, timeout: TimeSpan.FromMinutes(5)); + await auto.WaitForSuccessPromptAsync(counter, timeout: TimeSpan.FromMinutes(5)); var artifactsPath = Path.Combine(workspace.WorkspaceRoot.FullName, "artifacts"); var composeContent = await File.ReadAllTextAsync(Path.Combine(artifactsPath, "docker-compose.yaml")); @@ -194,11 +185,6 @@ public async Task PublishJavaScriptPatternsGeneratesExpectedDockerComposeArtifac "COPY --from=build --chown=node:node /app/.next/static ./.next/static", "USER node", "ENTRYPOINT [\"node\",\"server.js\"]"); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -216,11 +202,9 @@ public async Task PublishWithoutOutputPathUsesAppHostDirectoryDefault() using var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.DotNet, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -278,18 +262,13 @@ public async Task PublishWithoutOutputPathUsesAppHostDirectoryDefault() await auto.TypeAsync("aspire publish --non-interactive"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, timeout: TimeSpan.FromMinutes(5)); + await auto.WaitForSuccessPromptAsync(counter, timeout: TimeSpan.FromMinutes(5)); var dockerComposePath = Path.Combine(workspace.WorkspaceRoot.FullName, "aspire-output", "docker-compose.yaml"); Assert.True(File.Exists(dockerComposePath), $"Expected docker-compose output at {dockerComposePath}"); var dockerComposeContent = await File.ReadAllTextAsync(dockerComposePath); Assert.Contains("postgres:", dockerComposeContent); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } [Fact] @@ -307,11 +286,9 @@ public async Task PublishWithConfigureEnvFileUpdatesEnvOutput() using var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -363,7 +340,7 @@ await compose.configureEnvFile(async (envVars) => { await auto.TypeAsync("aspire publish -o artifacts --non-interactive"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, timeout: TimeSpan.FromMinutes(5)); + await auto.WaitForSuccessPromptAsync(counter, timeout: TimeSpan.FromMinutes(5)); var envFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, "artifacts", ".env"); Assert.True(File.Exists(envFilePath), $"Expected env file at {envFilePath}"); @@ -371,11 +348,6 @@ await compose.configureEnvFile(async (envVars) => { var envFileContent = await File.ReadAllTextAsync(envFilePath); Assert.Contains("# Customized bind mount source", envFileContent); Assert.DoesNotContain("# Bind mount source for my-container:/container/data", envFileContent); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } private static void WriteJavaScriptPublishAppHost(TemporaryWorkspace workspace) diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptReusablePackageTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptReusablePackageTests.cs index 3d227ff7105..493b900e4fb 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptReusablePackageTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptReusablePackageTests.cs @@ -22,11 +22,9 @@ public async Task RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -75,7 +73,7 @@ public async Task RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes() await auto.TypeAsync("npx tsc --noEmit"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2)); + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); await auto.TypeAsync($"cd {CliE2ETestHelpers.ToContainerPath(appDirectory.FullName, workspace)}"); await auto.EnterAsync(); @@ -88,12 +86,7 @@ public async Task RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes() await auto.TypeAsync("npx tsc --noEmit"); await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, TimeSpan.FromMinutes(2)); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; + await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); } private static string GetSdkVersion(DirectoryInfo appDirectory) diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs index ac5c91f3f66..61b87688dc4 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs @@ -30,11 +30,9 @@ public async Task StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets() variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -74,10 +72,5 @@ public async Task StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets() await auto.WaitForSuccessPromptAsync(counter); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterSmokeTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterSmokeTests.cs index 42325df5e54..9e5c7629f1f 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterSmokeTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterSmokeTests.cs @@ -35,11 +35,9 @@ public async Task CreateAndRunTypeScriptStarterProject() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -86,15 +84,10 @@ public async Task CreateAndRunTypeScriptStarterProject() await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2)); + await auto.RunCommandAsync("npm run build", counter, TimeSpan.FromMinutes(2)); await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } /// diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs index 65fb09c1e9a..eb52b975719 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs @@ -24,11 +24,9 @@ public async Task CreateAndRunTypeScriptStarterProject() var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -57,14 +55,9 @@ public async Task CreateAndRunTypeScriptStarterProject() await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - await auto.RunCommandFailFastAsync("npm run build", counter, TimeSpan.FromMinutes(2)); + await auto.RunCommandAsync("npm run build", counter, TimeSpan.FromMinutes(2)); await auto.AspireStartAsync(counter); await auto.AspireStopAsync(counter); - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/UpdateChannelNuGetConfigOrderingTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/UpdateChannelNuGetConfigOrderingTests.cs index aa56a803dee..10acabbfe2f 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/UpdateChannelNuGetConfigOrderingTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/UpdateChannelNuGetConfigOrderingTests.cs @@ -72,11 +72,9 @@ public async Task AspireUpdateAppliesAllPackageEditsBeforeRestoringWhenNuGetConf using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal( repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); - - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); @@ -205,9 +203,5 @@ await File.WriteAllTextAsync(nugetConfigPath, catch { } - - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - await pendingRun; } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs index 025bf6d812d..47349a86ad5 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs @@ -26,10 +26,9 @@ public async Task CreateStartWaitAndStopAspireProject() using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: true, workspace: workspace); - var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); - var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); // Prepare Docker environment (prompt counting, umask, env vars) await auto.PrepareDockerEnvironmentAsync(counter, workspace); @@ -63,11 +62,5 @@ public async Task CreateStartWaitAndStopAspireProject() await auto.EnterAsync(); await auto.WaitUntilAppHostStoppedSuccessfullyAsync(timeout: TimeSpan.FromMinutes(1)); await auto.WaitForSuccessPromptAsync(counter); - - // Exit the shell - await auto.TypeAsync("exit"); - await auto.EnterAsync(); - - await pendingRun; } } diff --git a/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs index 2a95c68af13..d558f25b7d2 100644 --- a/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs @@ -130,6 +130,67 @@ public async Task RunCommand_WhenAppHostStartupTimesOut_DisplaysTimeoutGuidance( Assert.True(runCancellationObserved.Task.IsCompletedSuccessfully); } + [Fact] + public async Task RunCommand_WhenCancelledDuringStartupTimeout_ExitsWithoutWaitingForFullTimeout() + { + // Verifies that when Ctrl+C fires (cancellationToken) during startup, the command exits + // promptly rather than blocking for the 5-second CancelAppHostStartupAsync timeout. + using var workspace = TemporaryWorkspace.Create(outputHelper); + using var cts = new CancellationTokenSource(); + var interactionService = new TestInteractionService(); + var buildCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var appHostDir = workspace.WorkspaceRoot.CreateSubdirectory("AppHost"); + var appHostFile = new FileInfo(Path.Combine(appHostDir.FullName, "AppHost.csproj")); + await File.WriteAllTextAsync(appHostFile.FullName, ""); + + var projectLocator = new TestProjectLocator + { + UseOrFindAppHostProjectFileWithBehaviorAsyncCallback = (_, _, _, _) => + Task.FromResult(new AppHostProjectSearchResult(appHostFile, [appHostFile])) + }; + + var projectFactory = new TestAppHostProjectFactory + { + RunAsyncCallback = async (context, _) => + { + context.BuildCompletionSource?.TrySetResult(true); + buildCompleted.SetResult(); + + // Never signal BackchannelCompletionSource and ignore cancellation to + // simulate a hung AppHost process. + await Task.Delay(TimeSpan.FromSeconds(30), CancellationToken.None); + return 0; + } + }; + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => interactionService; + options.ProjectLocatorFactory = _ => projectLocator; + options.AppHostProjectFactory = _ => projectFactory; + }); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse($"run --apphost {appHostFile.FullName}"); + + var pendingRun = result.InvokeAsync(cancellationToken: cts.Token); + + // Cancel after build completes to simulate Ctrl+C during startup. + await buildCompleted.Task.DefaultTimeout(); + cts.Cancel(); + + var stopwatch = Stopwatch.StartNew(); + var exitCode = await pendingRun.DefaultTimeout(); + stopwatch.Stop(); + + // Without the cancellationToken plumbing, this would block for the full 5-second + // CancelAppHostStartupAsync timeout. With the fix, it exits promptly. + Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(3), $"Expected prompt exit after Ctrl+C, but took {stopwatch.Elapsed}."); + Assert.Equal(CliExitCodes.Success, exitCode); + } + [Fact] public async Task RunCommand_StartupTimeoutBudgetIncludesBuildAndBackchannelWaits() { diff --git a/tests/Aspire.Cli.Tests/ConsoleCancellationManagerTests.cs b/tests/Aspire.Cli.Tests/ConsoleCancellationManagerTests.cs new file mode 100644 index 00000000000..8555e99c39c --- /dev/null +++ b/tests/Aspire.Cli.Tests/ConsoleCancellationManagerTests.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.InternalTesting; + +namespace Aspire.Cli.Tests; + +public class ConsoleCancellationManagerTests +{ + [Fact] + public void FirstSignal_RequestsCancellation() + { + using var manager = new ConsoleCancellationManager(TimeSpan.FromSeconds(5)); + + Assert.False(manager.IsCancellationRequested); + + manager.Cancel(130); + + Assert.True(manager.IsCancellationRequested); + } + + [Fact] + public void FirstSignal_TokenIsCancelled() + { + using var manager = new ConsoleCancellationManager(TimeSpan.FromSeconds(5)); + var token = manager.Token; + + Assert.False(token.IsCancellationRequested); + + manager.Cancel(130); + + Assert.True(token.IsCancellationRequested); + } + + [Fact] + public async Task SecondSignal_ForcesImmediateTermination() + { + using var manager = new ConsoleCancellationManager(TimeSpan.FromSeconds(30)); + + // Set a handler that never completes so the first signal doesn't resolve ProcessTerminationCompletionSource + manager.SetStartedHandler(new TaskCompletionSource().Task); + + manager.Cancel(130); + manager.Cancel(130); + + var exitCode = await manager.ProcessTerminationCompletionSource.Task.DefaultTimeout(); + Assert.Equal(130, exitCode); + } + + [Fact] + public async Task FirstSignal_WithNoHandler_ForcesTerminationAfterTimeout() + { + using var manager = new ConsoleCancellationManager(TimeSpan.FromMilliseconds(50)); + + // No handler set, so ForceTerminationAfterTimeoutAsync should complete quickly + manager.Cancel(143); + + var exitCode = await manager.ProcessTerminationCompletionSource.Task.DefaultTimeout(); + Assert.Equal(143, exitCode); + } + + [Fact] + public async Task FirstSignal_HandlerCompletesWithinTimeout_DoesNotForceTermination() + { + using var manager = new ConsoleCancellationManager(TimeSpan.FromSeconds(5)); + + // Set a handler that completes immediately + manager.SetStartedHandler(Task.FromResult(0)); + + manager.Cancel(130); + + // Give the async timeout path time to evaluate + await Task.Delay(100); + + // ProcessTerminationCompletionSource should NOT be signaled because the handler completed in time + Assert.False(manager.ProcessTerminationCompletionSource.Task.IsCompleted); + } + + [Fact] + public async Task FirstSignal_HandlerExceedsTimeout_ForcesTermination() + { + using var manager = new ConsoleCancellationManager(TimeSpan.FromMilliseconds(50)); + + // Set a handler that never completes + manager.SetStartedHandler(new TaskCompletionSource().Task); + + manager.Cancel(143); + + var exitCode = await manager.ProcessTerminationCompletionSource.Task.DefaultTimeout(); + Assert.Equal(143, exitCode); + } + + [Fact] + public void Cancel_IsNonBlocking() + { + using var manager = new ConsoleCancellationManager(TimeSpan.FromSeconds(30)); + + // Set a handler that never completes + manager.SetStartedHandler(new TaskCompletionSource().Task); + + // Cancel should return immediately without blocking (this would hang if Cancel were synchronous) + var sw = System.Diagnostics.Stopwatch.StartNew(); + manager.Cancel(130); + sw.Stop(); + + // Cancel should complete in well under a second (it's non-blocking) + Assert.True(sw.ElapsedMilliseconds < 1000, $"Cancel took {sw.ElapsedMilliseconds}ms, expected < 1000ms"); + Assert.True(manager.IsCancellationRequested); + } + + [Fact] + public async Task MultipleSignals_OnlyFirstAndSecondHaveEffect() + { + using var manager = new ConsoleCancellationManager(TimeSpan.FromSeconds(30)); + + // Set a handler that never completes + manager.SetStartedHandler(new TaskCompletionSource().Task); + + // Third signal should not throw or cause issues + manager.Cancel(130); + manager.Cancel(130); + manager.Cancel(130); + + var exitCode = await manager.ProcessTerminationCompletionSource.Task.DefaultTimeout(); + Assert.Equal(130, exitCode); + } + + [Fact] + public void Dispose_AllowsSubsequentCancelWithoutException() + { + var manager = new ConsoleCancellationManager(TimeSpan.FromSeconds(5)); + manager.Dispose(); + + // Cancel after dispose should not throw (signal can race with shutdown) + manager.Cancel(130); + } + + [Fact] + public void Token_RemainsAccessibleAfterDispose() + { + var manager = new ConsoleCancellationManager(TimeSpan.FromSeconds(5)); + var token = manager.Token; + manager.Dispose(); + + // Token should still be accessible (stored in field before dispose) + Assert.False(token.IsCancellationRequested); + } +} diff --git a/tests/Aspire.Deployment.EndToEnd.Tests/AcaManagedRedisDeploymentTests.cs b/tests/Aspire.Deployment.EndToEnd.Tests/AcaManagedRedisDeploymentTests.cs index 50e88fa2e5f..f596e596d8b 100644 --- a/tests/Aspire.Deployment.EndToEnd.Tests/AcaManagedRedisDeploymentTests.cs +++ b/tests/Aspire.Deployment.EndToEnd.Tests/AcaManagedRedisDeploymentTests.cs @@ -177,7 +177,7 @@ await auto.RunCommandAsync( // Step 13: Verify deployed endpoints with retry // Retry each endpoint for up to 3 minutes (18 attempts * 10 seconds) output.WriteLine("Step 13: Verifying deployed endpoints..."); - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( $"RG_NAME=\"{resourceGroupName}\" && " + "echo \"Resource group: $RG_NAME\" && " + "if ! az group show -n \"$RG_NAME\" &>/dev/null; then echo \"❌ Resource group not found\"; exit 1; fi && " + @@ -200,7 +200,7 @@ await auto.RunCommandFailFastAsync( // Step 14: Verify /api/weatherforecast returns valid JSON (exercises Redis output cache) output.WriteLine("Step 14: Verifying /api/weatherforecast returns valid JSON..."); - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( $"RG_NAME=\"{resourceGroupName}\" && " + "SERVER_FQDN=$(az containerapp list -g \"$RG_NAME\" --query \"[?contains(name,'server')].properties.configuration.ingress.fqdn\" -o tsv 2>/dev/null | head -1) && " + "if [ -z \"$SERVER_FQDN\" ]; then echo \"❌ Server container app not found\"; exit 1; fi && " + diff --git a/tests/Aspire.Deployment.EndToEnd.Tests/Helpers/DeploymentE2EAutomatorHelpers.cs b/tests/Aspire.Deployment.EndToEnd.Tests/Helpers/DeploymentE2EAutomatorHelpers.cs index 7ac226532c2..7d2f249c758 100644 --- a/tests/Aspire.Deployment.EndToEnd.Tests/Helpers/DeploymentE2EAutomatorHelpers.cs +++ b/tests/Aspire.Deployment.EndToEnd.Tests/Helpers/DeploymentE2EAutomatorHelpers.cs @@ -54,14 +54,14 @@ internal static async Task InstallAspireCliAsync( if (includeBundlePath) { - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( AspireCliShellCommandHelpers.GetBundlePullRequestInstallCommand(prNumber), counter, TimeSpan.FromSeconds(300)); } else { - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( AspireCliShellCommandHelpers.GetPullRequestInstallCommand(prNumber, AspireCliShellCommandHelpers.MainPullRequestInstallCommandPrefix), counter, TimeSpan.FromSeconds(300)); @@ -72,7 +72,7 @@ await auto.RunCommandFailFastAsync( case CliInstallMode.LocalArchive: var archiveDir = strategy.ArchiveDir ?? throw new InvalidOperationException("LocalArchive strategy is missing the archive directory."); - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( AspireCliShellCommandHelpers.GetLocalArchiveInstallCommandFromCurrentRef(archiveDir), counter, TimeSpan.FromSeconds(120)); @@ -80,7 +80,7 @@ await auto.RunCommandFailFastAsync( break; case CliInstallMode.InstallScript: - await auto.RunCommandFailFastAsync( + await auto.RunCommandAsync( AspireCliShellCommandHelpers.GetInstallScriptCommand(strategy, AspireCliShellCommandHelpers.AkaMsInstallScriptCommandPrefix), counter, TimeSpan.FromSeconds(300)); diff --git a/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptAzureContainerAppJobDeploymentTests.cs b/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptAzureContainerAppJobDeploymentTests.cs index 2af5ceaa12a..57cecd137c6 100644 --- a/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptAzureContainerAppJobDeploymentTests.cs +++ b/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptAzureContainerAppJobDeploymentTests.cs @@ -64,19 +64,19 @@ private async Task DeployTypeScriptContainerAppJobsToAzureContainerAppsCore(Canc await auto.PrepareEnvironmentAsync(workspace, counter); await auto.InstallCurrentBuildAspireBundleAsync(counter, output); - await auto.RunCommandFailFastAsync("aspire init --language typescript --non-interactive", counter, TimeSpan.FromMinutes(2)); + await auto.RunCommandAsync("aspire init --language typescript --non-interactive", counter, TimeSpan.FromMinutes(2)); await AddPackageAsync(auto, counter, "Aspire.Hosting.Azure.AppContainers"); WriteContainerAppJobsAppHost(workspace); - await auto.RunCommandFailFastAsync($"unset ASPIRE_PLAYGROUND && export AZURE__LOCATION=westus3 && export AZURE__RESOURCEGROUP={resourceGroupName}", counter); + await auto.RunCommandAsync($"unset ASPIRE_PLAYGROUND && export AZURE__LOCATION=westus3 && export AZURE__RESOURCEGROUP={resourceGroupName}", counter); await auto.TypeAsync("aspire deploy --clear-cache"); await auto.EnterAsync(); await auto.WaitForPipelineSuccessAsync(timeout: TimeSpan.FromMinutes(25)); await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); - await auto.RunCommandFailFastAsync(BuildJobVerificationCommand(resourceGroupName), counter, TimeSpan.FromMinutes(5)); + await auto.RunCommandAsync(BuildJobVerificationCommand(resourceGroupName), counter, TimeSpan.FromMinutes(5)); await auto.TypeAsync("exit"); await auto.EnterAsync(); diff --git a/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptJavaScriptHostingDeploymentTests.cs b/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptJavaScriptHostingDeploymentTests.cs index ab80fd5dfe2..bed9c64c532 100644 --- a/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptJavaScriptHostingDeploymentTests.cs +++ b/tests/Aspire.Deployment.EndToEnd.Tests/TypeScriptJavaScriptHostingDeploymentTests.cs @@ -64,21 +64,21 @@ private async Task DeployTypeScriptStaticWebsiteWithNodeApiToAzureContainerAppsC await auto.PrepareEnvironmentAsync(workspace, counter); await auto.InstallCurrentBuildAspireBundleAsync(counter, output); - await auto.RunCommandFailFastAsync("aspire init --language typescript --non-interactive", counter, TimeSpan.FromMinutes(2)); + await auto.RunCommandAsync("aspire init --language typescript --non-interactive", counter, TimeSpan.FromMinutes(2)); await AddPackageAsync(auto, counter, "Aspire.Hosting.JavaScript"); await AddPackageAsync(auto, counter, "Aspire.Hosting.Azure.AppContainers"); WriteStaticWebsiteWithNodeApiAppHost(workspace); - await auto.RunCommandFailFastAsync($"unset ASPIRE_PLAYGROUND && export AZURE__LOCATION=westus3 && export AZURE__RESOURCEGROUP={resourceGroupName}", counter); + await auto.RunCommandAsync($"unset ASPIRE_PLAYGROUND && export AZURE__LOCATION=westus3 && export AZURE__RESOURCEGROUP={resourceGroupName}", counter); await auto.TypeAsync("aspire deploy --clear-cache"); await auto.EnterAsync(); await auto.WaitForPipelineSuccessAsync(timeout: TimeSpan.FromMinutes(30)); await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromMinutes(2)); - await auto.RunCommandFailFastAsync(BuildEndpointVerificationCommand(resourceGroupName), counter, TimeSpan.FromMinutes(10)); + await auto.RunCommandAsync(BuildEndpointVerificationCommand(resourceGroupName), counter, TimeSpan.FromMinutes(10)); await auto.TypeAsync("exit"); await auto.EnterAsync(); diff --git a/tests/Shared/Hex1bAutomatorTestHelpers.cs b/tests/Shared/Hex1bAutomatorTestHelpers.cs index 8fb6be3e3a9..ce3be0b01f6 100644 --- a/tests/Shared/Hex1bAutomatorTestHelpers.cs +++ b/tests/Shared/Hex1bAutomatorTestHelpers.cs @@ -14,29 +14,6 @@ namespace Aspire.Tests.Shared; /// internal static class Hex1bAutomatorTestHelpers { - /// - /// Waits for a shell success prompt matching the current sequence counter value, - /// then increments the counter. Looks for the pattern: [N OK] $ - /// - internal static async Task WaitForSuccessPromptAsync( - this Hex1bTerminalAutomator auto, - SequenceCounter counter, - TimeSpan? timeout = null) - { - var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(500); - - await auto.WaitUntilAsync(snapshot => - { - var successPromptSearcher = new CellPatternSearcher() - .FindPattern(counter.Value.ToString()) - .RightText(" OK] $ "); - - return successPromptSearcher.Search(snapshot).Count > 0; - }, timeout: effectiveTimeout, description: $"success prompt [{counter.Value} OK] $"); - - counter.Increment(); - } - /// /// Waits for any prompt (success or error) matching the current sequence counter. /// @@ -241,7 +218,7 @@ private static int FindCommandLineIndex(IHex1bTerminalRegion snapshot, string co /// /// Waits for a successful command prompt, but fails fast if an error prompt is detected. /// - internal static async Task WaitForSuccessPromptFailFastAsync( + internal static async Task WaitForSuccessPromptAsync( this Hex1bTerminalAutomator auto, SequenceCounter counter, TimeSpan? timeout = null) @@ -296,20 +273,6 @@ internal static async Task RunCommandAsync( await auto.WaitForSuccessPromptAsync(counter, timeout); } - /// - /// Types a shell command, waits for it to complete successfully, and fails immediately on a shell error prompt. - /// - internal static async Task RunCommandFailFastAsync( - this Hex1bTerminalAutomator auto, - string command, - SequenceCounter counter, - TimeSpan? timeout = null) - { - await auto.TypeAsync(command); - await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, timeout); - } - /// /// Configures a numbered bash prompt and changes into the provided workspace directory. /// @@ -435,12 +398,12 @@ await auto.WaitUntilAsync(s => if (!sawVersionPrompt) { - await auto.WaitForSuccessPromptFailFastAsync(counter, effectiveTimeout); + await auto.WaitForSuccessPromptAsync(counter, effectiveTimeout); return; } await auto.EnterAsync(); - await auto.WaitForSuccessPromptFailFastAsync(counter, effectiveTimeout); + await auto.WaitForSuccessPromptAsync(counter, effectiveTimeout); } /// @@ -503,7 +466,7 @@ await auto.WaitUntilAsync(s => // Enter and executes a phantom blank command, advancing CMDCOUNT and desyncing // the test counter from the shell counter. - await auto.WaitForSuccessPromptFailFastAsync(counter, effectiveTimeout); + await auto.WaitForSuccessPromptAsync(counter, effectiveTimeout); } /// diff --git a/tests/Shared/Hex1bTestHelpers.cs b/tests/Shared/Hex1bTestHelpers.cs index 2b958615030..3abbc0a848b 100644 --- a/tests/Shared/Hex1bTestHelpers.cs +++ b/tests/Shared/Hex1bTestHelpers.cs @@ -225,56 +225,6 @@ internal static Hex1bTerminalInputSequenceBuilder WaitForErrorPrompt( .IncrementSequence(counter); } - /// - /// Waits for a successful command prompt, but fails fast if an error prompt is detected. - /// Unlike , this method also watches for error prompts - /// (ERR:N pattern) and throws immediately instead of waiting for the full timeout. - /// Use this for commands that may fail due to transient errors (e.g., CLI downloads). - /// - internal static Hex1bTerminalInputSequenceBuilder WaitForSuccessPromptFailFast( - this Hex1bTerminalInputSequenceBuilder builder, - SequenceCounter counter, - TimeSpan? timeout = null) - { - var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(500); - var sawError = false; - - return builder.WaitUntil(snapshot => - { - var successSearcher = new CellPatternSearcher() - .FindPattern(counter.Value.ToString()) - .RightText(" OK] $ "); - - if (successSearcher.Search(snapshot).Count > 0) - { - return true; - } - - var errorSearcher = new CellPatternSearcher() - .FindPattern(counter.Value.ToString()) - .RightText(" ERR:"); - - if (errorSearcher.Search(snapshot).Count > 0) - { - sawError = true; - return true; - } - - return false; - }, effectiveTimeout) - .WaitUntil(_ => - { - if (sawError) - { - throw new InvalidOperationException( - $"Command failed with non-zero exit code (detected ERR prompt at sequence {counter.Value}). Check the terminal recording for details."); - } - - counter.Increment(); - return true; - }, TimeSpan.FromSeconds(1)); - } - /// /// Increments the sequence counter. /// @@ -564,47 +514,6 @@ internal static Hex1bTerminalInputSequenceBuilder AspireInit( .WaitForSuccessPrompt(counter, TimeSpan.FromMinutes(2)); } - /// - /// Installs the Aspire CLI Bundle from a specific pull request's artifacts. - /// The bundle is a self-contained distribution that includes: - /// - Native AOT Aspire CLI - /// - .NET runtime - /// - Dashboard, DCP, AppHost Server (for polyglot apps) - /// This is required for polyglot (TypeScript, Python) AppHost scenarios which - /// cannot use SDK-based fallback mode. - /// - /// The sequence builder. - /// The pull request number to download from. - /// The sequence counter for prompt detection. - /// The builder for chaining. - internal static Hex1bTerminalInputSequenceBuilder InstallAspireBundleFromPullRequest( - this Hex1bTerminalInputSequenceBuilder builder, - int prNumber, - SequenceCounter counter) - { - // The install script may not be on main yet, so we need to fetch it from the PR's branch. - // Use the PR head SHA (not branch ref) to avoid CDN caching on raw.githubusercontent.com - // which can serve stale script content for several minutes after a push. - string command; - if (OperatingSystem.IsWindows()) - { - // PowerShell: Get PR head SHA, then fetch and run install script from that SHA - command = $"$ref = (gh api repos/microsoft/aspire/pulls/{prNumber} --jq '.head.sha'); " + - $"iex \"& {{ $(irm https://raw.githubusercontent.com/microsoft/aspire/$ref/eng/scripts/get-aspire-cli-pr.ps1) }} {prNumber}\""; - } - else - { - // Bash: Get PR head SHA, then fetch and run install script from that SHA - command = $"ref=$(gh api repos/microsoft/aspire/pulls/{prNumber} --jq '.head.sha') && " + - $"curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/$ref/eng/scripts/get-aspire-cli-pr.sh | bash -s -- {prNumber}"; - } - - return builder - .Type(command) - .Enter() - .WaitForSuccessPromptFailFast(counter, TimeSpan.FromSeconds(300)); - } - /// /// Sources the Aspire Bundle environment after installation. /// Adds both the bundle's bin/ directory and root directory to PATH so the CLI