File: Commands\DoCommandTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// 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.Commands;
using Aspire.Cli.Interaction;
using Aspire.Cli.Tests.Utils;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Backchannel;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Aspire.Cli.Utils;
using Aspire.Hosting;
using Microsoft.AspNetCore.InternalTesting;
 
namespace Aspire.Cli.Tests.Commands;
 
[Collection(ConsoleOutputCollection.Name)]
public class DoCommandTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task DoCommandWithHelpArgumentReturnsZero()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("do --help");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DoCommand_WhenExtensionStepPromptIsCancelled_DoesNotDisplayError()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var displayedErrors = new List<string>();
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new TestProjectLocator();
            options.ExtensionBackchannelFactory = _ => new TestExtensionBackchannel();
            options.InteractionServiceFactory = sp => new TestExtensionInteractionService(sp)
            {
                DisplayErrorCallback = displayedErrors.Add,
                PromptForStringCallback = (_, _, _, _, _, _) => throw new ExtensionOperationCanceledException("Pipeline step selection was canceled.")
            };
            options.ConfigurationCallback += config => config[KnownConfigNames.ExtensionDebugSessionId] = "test-session-id";
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse("do");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Cancelled, exitCode);
        Assert.Empty(displayedErrors);
    }
 
    [Fact]
    public async Task DoCommandWithStepArgumentSucceeds()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        // Arrange
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    // Simulate a successful build
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    // Simulate a successful app host information retrieval
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    // Simulate apphost running successfully and establishing a backchannel
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        Assert.True(options.NoLaunchProfile);
 
                        // Verify that the custom step is passed
                        Assert.Contains("--step", args);
                        Assert.Contains("my-custom-step", args);
 
                        var completed = new TaskCompletionSource();
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = completed
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await completed.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        // Act
        var result = command.Parse("do my-custom-step");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        // Assert
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DoCommandWithDeployStepSucceeds()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        // Arrange
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        // Verify that --step deploy is passed
                        Assert.Contains("--step", args);
                        Assert.Contains("deploy", args);
 
                        var completed = new TaskCompletionSource();
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = completed
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await completed.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        // Act
        var result = command.Parse("do deploy");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        // Assert
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DoCommandWithPublishStepSucceeds()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        // Arrange
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        // Verify that --step publish is passed
                        Assert.Contains("--step", args);
                        Assert.Contains("publish", args);
 
                        var completed = new TaskCompletionSource();
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = completed
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await completed.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        // Act
        var result = command.Parse("do publish");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        // Assert
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DoCommandPassesOutputPathWhenSpecified()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        // Arrange
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        // Verify output path is included
                        Assert.Contains("--output-path", args);
                        
                        // Find the output path argument value
                        var outputPathIndex = Array.IndexOf(args, "--output-path");
                        Assert.True(outputPathIndex >= 0 && outputPathIndex < args.Length - 1);
                        var outputPath = args[outputPathIndex + 1];
                        Assert.EndsWith("test-output", outputPath);
 
                        var completed = new TaskCompletionSource();
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = completed
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await completed.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        // Act
        var result = command.Parse("do my-step --output-path test-output");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        // Assert
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DoCommandFailsWithInvalidProjectFile()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        // Arrange
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (1, false, null);
                    }
                };
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        // Act
        var result = command.Parse("do my-step --apphost invalid.csproj");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        // Assert
        Assert.Equal(CliExitCodes.FailedToFindProject, exitCode);
    }
 
    [Theory]
    [InlineData("do --list-steps --format=json", "do", null)]
    [InlineData("do --list-steps --format json deploy", "do", "deploy")]
    [InlineData("do --format json --list-steps deploy", "do", "deploy")]
    [InlineData("do deploy --list-steps --format json", "do", "deploy")]
    [InlineData("publish --list-steps --format=json --operation run", "publish", "publish")]
    [InlineData("deploy --list-steps --format=json --operation run", "deploy", "deploy")]
    public async Task PipelineCommandWithListStepsUsesInspectOperation(string commandLine, string commandName, string? expectedStep)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interactionService = new TestInteractionService();
 
        var requestStopCalled = new TaskCompletionSource();
        var getPipelineStepsCalled = new TaskCompletionSource();
        string? requestedStep = "not-called";
        string[]? capturedArgs = null;
        var publishingActivitiesRequested = false;
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interactionService;
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        capturedArgs = args;
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = requestStopCalled,
                            GetPipelineStepsAsyncCalled = getPipelineStepsCalled,
                            GetPipelineStepsAsyncCallback = (step, ct) =>
                            {
                                requestedStep = step;
                                return Task.FromResult(new GetPipelineStepsResponse
                                {
                                    Steps =
                                    [
                                        new PipelineStepInfo
                                        {
                                            Name = "deploy",
                                            Description = "Deploy the application",
                                            DependsOn = ["publish"],
                                            Tags = ["deployment"],
                                            ResourceName = "api"
                                        }
                                    ]
                                });
                            },
                            GetPublishingActivitiesAsyncCallback = ct =>
                            {
                                publishingActivitiesRequested = true;
                                return AsyncEnumerable.Empty<PublishingActivity>();
                            }
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await requestStopCalled.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse(commandLine);
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(0, exitCode);
        Assert.True(getPipelineStepsCalled.Task.IsCompleted, "GetPipelineStepsAsync should have been called");
        Assert.True(requestStopCalled.Task.IsCompleted, "RequestStopAsync should have been called");
        Assert.Equal(expectedStep, requestedStep);
        Assert.False(publishingActivitiesRequested);
        Assert.NotNull(capturedArgs);
        Assert.Equal(
            commandName switch
            {
                "do" when expectedStep is not null => ["--operation", "inspect", "--step", expectedStep, "--operation", "inspect", "--list-steps", "true"],
                "do" => ["--operation", "inspect", "--operation", "inspect", "--list-steps", "true"],
                "publish" => ["--operation", "publish", "--step", "publish", "--operation", "run", "--operation", "inspect", "--list-steps", "true"],
                "deploy" => ["--operation", "publish", "--step", "deploy", "--operation", "run", "--operation", "inspect", "--list-steps", "true"],
                _ => throw new InvalidOperationException()
            },
            capturedArgs);
        var output = Assert.Single(interactionService.DisplayedRawText);
        Assert.Equal(ConsoleOutput.Standard, output.ConsoleOverride);
        using var document = JsonDocument.Parse(output.Text);
        var step = Assert.Single(document.RootElement.EnumerateArray());
        Assert.Equal(5, step.EnumerateObject().Count());
        Assert.Equal("deploy", step.GetProperty("name").GetString());
        Assert.Equal("Deploy the application", step.GetProperty("description").GetString());
        Assert.Equal(["publish"], step.GetProperty("dependsOn").EnumerateArray().Select(value => value.GetString()));
        Assert.Equal(["deployment"], step.GetProperty("tags").EnumerateArray().Select(value => value.GetString()));
        Assert.Equal("api", step.GetProperty("resourceName").GetString());
    }
 
    [Fact]
    public async Task DoCommandWithListStepsPropagatesAppHostExitCode()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var requestStopCalled = new TaskCompletionSource();
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new TestProjectLocator();
            options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner
            {
                BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
                GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    (0, true, VersionHelper.GetDefaultTemplateVersion()),
                RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                {
                    backchannelCompletionSource?.SetResult(new TestAppHostBackchannel
                    {
                        RequestStopAsyncCalled = requestStopCalled,
                        GetPipelineStepsAsyncCallback = (step, ct) => Task.FromResult(new GetPipelineStepsResponse { Steps = [] })
                    });
                    await requestStopCalled.Task.DefaultTimeout();
                    return 42;
                }
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var exitCode = await command.Parse("do --list-steps --format json").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(42, exitCode);
    }
 
    [Fact]
    public async Task DoCommandWithListStepsJsonDoesNotWriteTerminalProgressToStandardOutput()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interactionService = new TestInteractionService();
        var requestStopCalled = new TaskCompletionSource();
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interactionService;
            options.ProjectLocatorFactory = _ => new TestProjectLocator();
            options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
            options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner
            {
                BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
                GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    (0, true, VersionHelper.GetDefaultTemplateVersion()),
                RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                {
                    backchannelCompletionSource?.SetResult(new TestAppHostBackchannel
                    {
                        RequestStopAsyncCalled = requestStopCalled,
                        GetPipelineStepsAsyncCallback = (step, ct) => Task.FromResult(new GetPipelineStepsResponse
                        {
                            Steps = [new PipelineStepInfo { Name = "deploy" }]
                        })
                    });
                    await requestStopCalled.Task.DefaultTimeout();
                    return 0;
                }
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        using var standardOutput = new StringWriter();
        var originalStandardOutput = Console.Out;
 
        try
        {
            Console.SetOut(standardOutput);
            var exitCode = await command.Parse("do --list-steps --format json").InvokeAsync().DefaultTimeout();
 
            Assert.Equal(0, exitCode);
        }
        finally
        {
            Console.SetOut(originalStandardOutput);
        }
 
        Assert.Empty(standardOutput.ToString());
        var output = Assert.Single(interactionService.DisplayedRawText);
        JsonDocument.Parse(output.Text).Dispose();
    }
 
    [Fact]
    public async Task DoCommandWithListStepsAndStepArgumentReturnsZero()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var requestStopCalled = new TaskCompletionSource();
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = requestStopCalled
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await requestStopCalled.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        // Act - step argument with --list-steps
        var result = command.Parse("do deploy --list-steps");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        // Assert
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DoCommandWithListStepsDoesNotExecutePipeline()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var publishingActivitiesRequested = false;
        var requestStopCalled = new TaskCompletionSource();
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = requestStopCalled,
                            GetPublishingActivitiesAsyncCallback = (ct) =>
                            {
                                publishingActivitiesRequested = true;
                                return AsyncEnumerable.Empty<PublishingActivity>();
                            }
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await requestStopCalled.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse("do deploy --list-steps");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        // Assert - pipeline should NOT have been executed
        Assert.Equal(0, exitCode);
        Assert.False(publishingActivitiesRequested, "Publishing activities should not be requested when --list-steps is used");
    }
 
    [Fact]
    public async Task DoCommandWithListStepsReturnsBuildFailureWhenCurrentAppHostExitsBeforeBackchannel()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new TestProjectLocator();
            options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner
            {
                BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
                GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    (0, true, VersionHelper.GetDefaultTemplateVersion()),
                RunAsyncCallback = (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    Task.FromResult(1)
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var exitCode = await command.Parse("do --list-steps --format json").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.FailedToBuildArtifacts, exitCode);
    }
 
    [Fact]
    public async Task DoCommandWithListStepsReturnsIncompatibleWhenLegacyAppHostRejectsInspectOperation()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new TestProjectLocator();
            options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner
            {
                BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
                GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    (0, true, "13.6.0-preview.1.25310.2"),
                RunAsyncCallback = (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                {
                    options.StandardErrorCallback?.Invoke("Unhandled exception. Aspire.Hosting.DistributedApplicationException: Invalid operation specified. Valid operations are 'publish' or 'run'.");
                    return Task.FromResult(1);
                }
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var exitCode = await command.Parse("do --list-steps --format json").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.AppHostIncompatible, exitCode);
    }
 
    [Fact]
    public async Task DoCommandWithListStepsReturnsIncompatibleWhenLegacyAppHostFailsBackchannelConnection()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var appHostExit = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new TestProjectLocator();
            options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner
            {
                BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
                GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    (0, true, "13.6.0-preview.1.25310.2"),
                RunAsyncCallback = (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                {
                    options.StandardErrorCallback?.Invoke("Unhandled exception. Aspire.Hosting.DistributedApplicationException: Invalid operation specified. Valid operations are 'publish' or 'run'.");
                    backchannelCompletionSource!.SetException(
                        new FailedToConnectBackchannelConnection("The AppHost process exited unexpectedly with exit code 1", new InvalidOperationException()));
                    return appHostExit.Task;
                }
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var exitCode = await command.Parse("do --list-steps --format json").InvokeAsync().DefaultTimeout();
        appHostExit.SetResult(1);
 
        Assert.Equal(CliExitCodes.AppHostIncompatible, exitCode);
    }
 
    [Fact]
    public async Task DoCommandWithListStepsReturnsIncompatibleWithoutLaunchingLegacyAppHost()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var runCalled = false;
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new TestProjectLocator();
            options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner
            {
                GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    (0, true, "13.5.0"),
                RunAsyncCallback = (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                {
                    runCalled = true;
                    return Task.FromResult(0);
                }
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var exitCode = await command.Parse("do --list-steps --format json").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.AppHostIncompatible, exitCode);
        Assert.False(runCalled);
    }
 
    [Fact]
    public async Task DoCommandWithListStepsStopsAppHostWhenCapabilityIsMissing()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var requestStopCalled = new TaskCompletionSource();
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new TestProjectLocator();
            options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner
            {
                BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
                GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    (0, true, VersionHelper.GetDefaultTemplateVersion()),
                RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                {
                    backchannelCompletionSource?.SetResult(new TestAppHostBackchannel
                    {
                        RequestStopAsyncCalled = requestStopCalled,
                        GetCapabilitiesAsyncCallback = _ => Task.FromResult<string[]>(["baseline.v2"])
                    });
                    await requestStopCalled.Task.DefaultTimeout();
                    return 0;
                }
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var exitCode = await command.Parse("do --list-steps --format json").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.AppHostIncompatible, exitCode);
        Assert.True(requestStopCalled.Task.IsCompleted);
    }
 
    [Fact]
    public async Task DoCommandListStepsDisplaysCustomSteps()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var requestStopCalled = new TaskCompletionSource();
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = requestStopCalled,
                            GetPipelineStepsAsyncCallback = (step, ct) => Task.FromResult(new GetPipelineStepsResponse
                            {
                                Steps = [
                                    new() { Name = "parameter-prompt" },
                                    new() { Name = "provision-redis-infra", DependsOn = ["parameter-prompt"], Tags = ["provision-infra"] },
                                    new() { Name = "build-webapi", DependsOn = ["parameter-prompt"], Tags = ["build-compute"] },
                                    new() { Name = "deploy-webapi", DependsOn = ["provision-redis-infra", "build-webapi"], Tags = ["deploy-compute"] }
                                ]
                            })
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await requestStopCalled.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse("do deploy --list-steps");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DoCommandWithHelpShowsListStepsOption()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("do --help");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DoCommandForwardsFormatWithoutListStepsToAppHost()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        string[]? capturedArgs = null;
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new TestProjectLocator();
            options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner
            {
                BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
                GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    (0, true, VersionHelper.GetDefaultTemplateVersion()),
                RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                {
                    capturedArgs = args;
                    var completed = new TaskCompletionSource();
                    backchannelCompletionSource?.SetResult(new TestAppHostBackchannel
                    {
                        RequestStopAsyncCalled = completed
                    });
                    await completed.Task.DefaultTimeout();
                    return 0;
                }
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var exitCode = await command.Parse("do deploy --format yaml").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(0, exitCode);
        Assert.NotNull(capturedArgs);
        Assert.Equal(["--operation", "publish", "--step", "deploy", "--format", "yaml"], capturedArgs);
    }
 
    [Theory]
    [InlineData("do --list-steps --format")]
    [InlineData("do --list-steps --format=")]
    [InlineData("do --list-steps --format yaml")]
    public async Task DoCommandWithListStepsReturnsInvalidCommandForInvalidFormat(string commandLine)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interactionService = new TestInteractionService();
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => interactionService;
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var exitCode = await command.Parse(commandLine).InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, exitCode);
        Assert.Single(interactionService.DisplayedErrors, "The --format option requires either 'table' or 'json'.");
    }
 
    [Fact]
    public async Task DoCommandForwardsPipelineLogLevelAsLogLevelToAppHost()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        string[]? capturedArgs = null;
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        capturedArgs = args;
 
                        var completed = new TaskCompletionSource();
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = completed
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await completed.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse("do my-step --pipeline-log-level debug");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(0, exitCode);
        Assert.NotNull(capturedArgs);
        var logLevelIndex = Array.IndexOf(capturedArgs, "--log-level");
        Assert.True(logLevelIndex >= 0, "Expected --log-level argument to be passed to AppHost");
        Assert.Equal("debug", capturedArgs[logLevelIndex + 1]);
        Assert.DoesNotContain("--pipeline-log-level", capturedArgs);
    }
 
    [Fact]
    public async Task DoCommandDoesNotForwardCliLogLevelToAppHost()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        string[]? capturedArgs = null;
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        capturedArgs = args;
 
                        var completed = new TaskCompletionSource();
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = completed
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await completed.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse("do my-step --log-level Warning");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(0, exitCode);
        Assert.NotNull(capturedArgs);
        Assert.DoesNotContain("--log-level", capturedArgs);
    }
}
 
[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class ConsoleOutputCollection
{
    public const string Name = nameof(ConsoleOutputCollection);
}