diff --git a/src/Aspire.Cli/Commands/DoCommand.cs b/src/Aspire.Cli/Commands/DoCommand.cs index 51922787000..ce14ce8b2a5 100644 --- a/src/Aspire.Cli/Commands/DoCommand.cs +++ b/src/Aspire.Cli/Commands/DoCommand.cs @@ -35,9 +35,29 @@ public DoCommand(IDotNetCliRunner runner, IInteractionService interactionService { var step = result.GetValue(_stepArgument); var listSteps = result.GetValue(s_listStepsOption); - if (string.IsNullOrEmpty(step) && !listSteps && !ExtensionHelper.IsExtensionHost(interactionService, out _, out _)) + if (!string.IsNullOrEmpty(step)) { - result.AddError("The 'step' argument is required when not using --list-steps."); + return; + } + + if (listSteps) + { + // `aspire do --list-steps` with no step has no meaningful scope: the listing for + // `do` is always relative to a target step. Surface a friendly error pointing at + // common starting steps and the docs rather than launching the AppHost and + // crashing mid-pipeline (see https://github.com/microsoft/aspire/issues/17526). + // This applies in the extension host too because `--list-steps` does not flow + // through the interactive step prompt in GetRunArgumentsAsync, so without this + // error the extension would still hit the original crash path. + result.AddError(DoCommandStrings.ListStepsRequiresStep); + return; + } + + // For a plain `aspire do` invocation, the extension host prompts the user for a step + // later in GetRunArgumentsAsync, so don't add a validation error there. + if (!ExtensionHelper.IsExtensionHost(interactionService, out _, out _)) + { + result.AddError(DoCommandStrings.StepArgumentRequired); } }); } diff --git a/src/Aspire.Cli/Resources/DoCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/DoCommandStrings.Designer.cs index c7224cb9a1c..ab8fbd526f5 100644 --- a/src/Aspire.Cli/Resources/DoCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/DoCommandStrings.Designer.cs @@ -122,5 +122,23 @@ public static string StepArgumentDescription { return ResourceManager.GetString("StepArgumentDescription", resourceCulture); } } + + /// + /// Looks up a localized string similar to The 'step' argument is required.. + /// + public static string StepArgumentRequired { + get { + return ResourceManager.GetString("StepArgumentRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps.. + /// + public static string ListStepsRequiresStep { + get { + return ResourceManager.GetString("ListStepsRequiresStep", resourceCulture); + } + } } } diff --git a/src/Aspire.Cli/Resources/DoCommandStrings.resx b/src/Aspire.Cli/Resources/DoCommandStrings.resx index 2db6a8643cd..1e5862df55b 100644 --- a/src/Aspire.Cli/Resources/DoCommandStrings.resx +++ b/src/Aspire.Cli/Resources/DoCommandStrings.resx @@ -138,4 +138,10 @@ The name of the step to execute + + The 'step' argument is required. + + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.cs.xlf index cded75496fa..1050dc23952 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.cs.xlf @@ -12,6 +12,11 @@ Provádí se kanál... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. Operace byla zrušena. @@ -37,6 +42,11 @@ Název kroku, který se má provést + + The 'step' argument is required. + The 'step' argument is required. + + - + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.de.xlf index 86ce4dee647..b0ce2d267f9 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.de.xlf @@ -12,6 +12,11 @@ Pipeline wird ausgeführt … + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. Der Vorgang wurde abgebrochen. @@ -37,6 +42,11 @@ Der Name des auszuführenden Schritts. + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.es.xlf index 5a2abd0fc26..12c5af2811a 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.es.xlf @@ -12,6 +12,11 @@ Ejecutando canalización... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. Operación cancelada. @@ -37,6 +42,11 @@ Nombre del paso que se va a ejecutar. + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.fr.xlf index f1fdc9a1809..4bffb1c76a5 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.fr.xlf @@ -12,6 +12,11 @@ Exécution du pipeline... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. L'opération a été annulée. @@ -37,6 +42,11 @@ Nom de l’étape à exécuter. + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.it.xlf index f59033ec917..19daad8d54c 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.it.xlf @@ -12,6 +12,11 @@ Esecuzione della pipeline in corso... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. L'operazione è stata annullata. @@ -37,6 +42,11 @@ Nome del passaggio da eseguire. + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ja.xlf index 66a8f27a628..687dba75e06 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ja.xlf @@ -12,6 +12,11 @@ パイプラインを実行しています... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. 操作は取り消されました。 @@ -37,6 +42,11 @@ 実行するステップの名前です。 + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ko.xlf index 27f033fa266..9e341bfd91c 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ko.xlf @@ -12,6 +12,11 @@ 파이프라인을 실행하는 중... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. 작업이 취소되었습니다. @@ -37,6 +42,11 @@ 실행할 단계의 이름입니다. + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.pl.xlf index 6a4c038846f..40869b4afe5 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.pl.xlf @@ -12,6 +12,11 @@ Wykonywanie potoku... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. Operacja została anulowana. @@ -37,6 +42,11 @@ Nazwa kroku do wykonania. + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.pt-BR.xlf index 88c144bf453..c3688ae13a0 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.pt-BR.xlf @@ -12,6 +12,11 @@ Executando pipeline... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. A operação foi cancelada. @@ -37,6 +42,11 @@ O nome da etapa a ser executada. + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ru.xlf index 84495e31d97..7787f742110 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.ru.xlf @@ -12,6 +12,11 @@ Выполнение конвейера... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. Операция была отменена. @@ -37,6 +42,11 @@ Имя выполняемого шага. + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.tr.xlf index f9d7324b6ee..392907d6050 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.tr.xlf @@ -12,6 +12,11 @@ İşlem hattı yürütülüyor... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. İşlem iptal edildi. @@ -37,6 +42,11 @@ Yürütülecek adımın adı. + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.zh-Hans.xlf index 19934085624..44cdbd985d3 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.zh-Hans.xlf @@ -12,6 +12,11 @@ 正在执行管道... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. 该操作已取消。 @@ -37,6 +42,11 @@ 要执行的步骤名称。 + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.zh-Hant.xlf index 81f8d9ba37b..ca03fadae07 100644 --- a/src/Aspire.Cli/Resources/xlf/DoCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/DoCommandStrings.zh-Hant.xlf @@ -12,6 +12,11 @@ 正在執行管線... + + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + The 'step' argument is required when using --list-steps. Example: 'aspire do deploy --list-steps'. Common starting steps are 'build', 'publish' and 'deploy'. See https://aspire.dev/reference/cli/commands/aspire-do/ for the full list of pipeline steps. + + The operation was canceled. 已取消作業。 @@ -37,6 +42,11 @@ 要執行的步驟名稱。 + + The 'step' argument is required. + The 'step' argument is required. + + \ No newline at end of file diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ListStepsTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ListStepsTests.cs index 63343a555aa..831248ddb38 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/ListStepsTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/ListStepsTests.cs @@ -9,13 +9,15 @@ namespace Aspire.Cli.EndToEnd.Tests; /// -/// End-to-end tests for the aspire do --list-steps feature. -/// Verifies that the CLI can list pipeline steps without executing them. +/// End-to-end tests for the --list-steps feature on the aspire do, publish, and deploy commands. +/// Verifies that the CLI can list pipeline steps without executing them, and that +/// invalid combinations (such as `aspire do --list-steps` without a step argument) +/// surface a friendly error instead of crashing. /// public sealed class ListStepsTests(ITestOutputHelper output) { [Fact] - public async Task DoListStepsShowsPipelineSteps() + public async Task DoPublishAndDeployListStepsWork() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var strategy = CliInstallStrategy.Detect(output.WriteLine); @@ -40,17 +42,57 @@ public async Task DoListStepsShowsPipelineSteps() await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); - // Run aspire do deploy --list-steps + // 1. Regression for https://github.com/microsoft/aspire/issues/17526: + // `aspire do --list-steps` with no step argument should surface a friendly error + // pointing at concrete examples rather than crashing with + // 'Sequence contains more than one matching element'. + await auto.TypeAsync("aspire do --list-steps"); + await auto.EnterAsync(); + + await auto.WaitUntilAsync(s => + { + if (s.ContainsText("Sequence contains more than one matching element")) + { + throw new InvalidOperationException( + "aspire do --list-steps regressed: pipeline executed and crashed instead of surfacing the friendly validation error."); + } + // Match short fragments that are unlikely to straddle a wrap boundary in a narrow + // terminal. The full error message is a single long sentence, so asserting on the + // raw URL (or any 30+ char run) is flaky because the screen buffer inserts wrap + // newlines that defeat ContainsText's literal substring match. + return s.ContainsText("required when using --list-steps") + && s.ContainsText("aspire.dev/"); + }, timeout: TimeSpan.FromMinutes(2), + description: "waiting for friendly error with example and docs link"); + + // The validation error returns a non-zero exit code, but the shell prompt should come back. + await auto.WaitForAnyPromptAsync(counter); + + // 2. `aspire do --list-steps` lists pipeline steps for that step. await auto.TypeAsync("aspire do deploy --list-steps"); await auto.EnterAsync(); + await auto.WaitUntilAsync(s => + s.ContainsText("Depends on:") || s.ContainsText("No dependencies"), + timeout: TimeSpan.FromMinutes(3), + description: "waiting for aspire do deploy --list-steps output"); + await auto.WaitForSuccessPromptAsync(counter); - // Wait for the output to contain step information - // The output should contain numbered steps with dependencies + // 3. `aspire publish --list-steps` lists steps for the publish target. + await auto.TypeAsync("aspire publish --list-steps"); + await auto.EnterAsync(); await auto.WaitUntilAsync(s => s.ContainsText("Depends on:") || s.ContainsText("No dependencies"), timeout: TimeSpan.FromMinutes(3), - description: "waiting for --list-steps output with step dependency information"); + description: "waiting for aspire publish --list-steps output"); + await auto.WaitForSuccessPromptAsync(counter); + // 4. `aspire deploy --list-steps` lists steps for the deploy target. + await auto.TypeAsync("aspire deploy --list-steps"); + await auto.EnterAsync(); + await auto.WaitUntilAsync(s => + s.ContainsText("Depends on:") || s.ContainsText("No dependencies"), + timeout: TimeSpan.FromMinutes(3), + description: "waiting for aspire deploy --list-steps output"); await auto.WaitForSuccessPromptAsync(counter); // Exit the terminal diff --git a/tests/Aspire.Cli.Tests/Commands/DoCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/DoCommandTests.cs index 7356d6fd67f..84254f5246b 100644 --- a/tests/Aspire.Cli.Tests/Commands/DoCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/DoCommandTests.cs @@ -7,6 +7,7 @@ using Aspire.Cli.Backchannel; using Microsoft.Extensions.DependencyInjection; using Aspire.Cli.Utils; +using Aspire.Hosting; using Microsoft.AspNetCore.InternalTesting; namespace Aspire.Cli.Tests.Commands; @@ -325,8 +326,8 @@ public async Task DoCommandWithListStepsReturnsZero() using var provider = services.BuildServiceProvider(); var command = provider.GetRequiredService(); - // Act - no step argument needed with --list-steps - var result = command.Parse("do --list-steps"); + // Act - step argument is required, even with --list-steps + var result = command.Parse("do deploy --list-steps"); var exitCode = await result.InvokeAsync().DefaultTimeout(); // Assert @@ -335,6 +336,59 @@ public async Task DoCommandWithListStepsReturnsZero() Assert.True(requestStopCalled.Task.IsCompleted, "RequestStopAsync should have been called"); } + [Fact] + public async Task DoCommandWithListStepsAndNoStepArgumentShowsFriendlyError() + { + // Regression for https://github.com/microsoft/aspire/issues/17526: + // `aspire do --list-steps` with no step argument used to launch the AppHost + // and crash mid-pipeline. It should now fail validation with a friendly + // error pointing at concrete examples. + using var tempRepo = TemporaryWorkspace.Create(outputHelper); + + var services = CliTestHelper.CreateServiceCollection(tempRepo, outputHelper); + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + + var result = command.Parse("do --list-steps"); + + Assert.NotEmpty(result.Errors); + var combined = string.Join("\n", result.Errors.Select(e => e.Message)); + Assert.Contains("--list-steps", combined); + Assert.Contains("aspire do deploy --list-steps", combined); + Assert.Contains("build", combined); + Assert.Contains("publish", combined); + Assert.Contains("https://aspire.dev/reference/cli/commands/aspire-do/", combined); + } + + [Fact] + public async Task DoCommandWithListStepsAndNoStepArgumentInExtensionHostShowsFriendlyError() + { + // The extension host bypasses the plain `aspire do` step requirement because + // GetRunArgumentsAsync prompts the user interactively. But `--list-steps` does + // not flow through that prompt, so without the validator firing the extension + // would still hit the original crash from https://github.com/microsoft/aspire/issues/17526. + using var tempRepo = TemporaryWorkspace.Create(outputHelper); + + var services = CliTestHelper.CreateServiceCollection(tempRepo, outputHelper, options => + { + options.ExtensionBackchannelFactory = _ => new TestExtensionBackchannel(); + options.InteractionServiceFactory = sp => new TestExtensionInteractionService(sp); + options.ConfigurationCallback += config => + { + config[KnownConfigNames.ExtensionDebugSessionId] = "test-session-id"; + }; + }); + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + + var result = command.Parse("do --list-steps"); + + Assert.NotEmpty(result.Errors); + var combined = string.Join("\n", result.Errors.Select(e => e.Message)); + Assert.Contains("--list-steps", combined); + Assert.Contains("https://aspire.dev/reference/cli/commands/aspire-do/", combined); + } + [Fact] public async Task DoCommandWithListStepsAndStepArgumentReturnsZero() { @@ -431,7 +485,7 @@ public async Task DoCommandWithListStepsDoesNotExecutePipeline() using var provider = services.BuildServiceProvider(); var command = provider.GetRequiredService(); - var result = command.Parse("do --list-steps"); + var result = command.Parse("do deploy --list-steps"); var exitCode = await result.InvokeAsync().DefaultTimeout(); // Assert - pipeline should NOT have been executed @@ -489,7 +543,7 @@ public async Task DoCommandListStepsDisplaysCustomSteps() using var provider = services.BuildServiceProvider(); var command = provider.GetRequiredService(); - var result = command.Parse("do --list-steps"); + var result = command.Parse("do deploy --list-steps"); var exitCode = await result.InvokeAsync().DefaultTimeout(); Assert.Equal(0, exitCode);