File: Commands\DashboardRunCommandTests.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 System.Globalization;
using System.Text;
using Aspire.Cli.Commands;
using Aspire.Cli.DotNet;
using Aspire.Cli.Interaction;
using Aspire.Cli.Layout;
using Aspire.Cli.Resources;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Aspire.Cli.Utils;
using Aspire.Hosting;
using Aspire.Shared;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Spectre.Console;
 
namespace Aspire.Cli.Tests.Commands;
 
public class DashboardRunCommandTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task DashboardRunCommand_BundleNotAvailable_DisplaysError()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var testInteractionService = new TestInteractionService();
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.InteractionServiceFactory = _ => testInteractionService;
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.DashboardFailure, exitCode);
        var errorMessage = Assert.Single(testInteractionService.DisplayedErrors);
        Assert.Equal(DashboardCommandStrings.BundleLayoutNotFound, errorMessage);
    }
 
    [Fact]
    public async Task DashboardRunCommand_Help_ReturnsSuccess()
    {
        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("dashboard run --help");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
    }
 
    [Theory]
    [InlineData("--frontend-url http://localhost:5000")]
    [InlineData("--otlp-grpc-url http://localhost:4317")]
    [InlineData("--otlp-http-url http://localhost:4318")]
    [InlineData("--allow-anonymous")]
    [InlineData("--config-file-path /path/to/config.json")]
    public void DashboardRunCommand_ParsesOptionsWithoutErrors(string args)
    {
        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($"dashboard run {args}");
 
        Assert.Empty(result.Errors);
    }
 
    [Fact]
    public void DashboardRunCommand_ForwardsUnmatchedTokens()
    {
        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("dashboard run --ASPNETCORE_URLS=http://localhost:9999");
 
        Assert.Empty(result.Errors);
        Assert.Equal("--ASPNETCORE_URLS=http://localhost:9999", Assert.Single(result.UnmatchedTokens));
    }
 
    [Fact]
    public void DashboardRunCommand_SkipsDefaultWhenEnvVarIsSet()
    {
        var environment = CreateEnvironment(new Dictionary<string, string?>
        {
            ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://custom:9999"
        });
 
        var unmatchedTokens = Array.Empty<string>();
 
        Assert.True(DashboardRunCommand.ConfigSettingHasValue(unmatchedTokens, environment, "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"));
        Assert.False(DashboardRunCommand.ConfigSettingHasValue(unmatchedTokens, environment, "ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"));
    }
 
    [Fact]
    public void DashboardRunCommand_SkipsDefaultWhenUnmatchedTokenHasValue()
    {
        var environment = CreateEnvironment(new Dictionary<string, string?>());
 
        var unmatchedTokens = new[] { "--ASPNETCORE_URLS=http://localhost:9999" };
 
        Assert.True(DashboardRunCommand.ConfigSettingHasValue(unmatchedTokens, environment, KnownAspNetCoreConfigNames.Urls));
        Assert.False(DashboardRunCommand.ConfigSettingHasValue(unmatchedTokens, environment, "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"));
    }
 
    [Fact]
    public void DashboardRunCommand_SkipsDefaultWhenUnmatchedTokenHasSpaceSeparatedValue()
    {
        var environment = CreateEnvironment(new Dictionary<string, string?>());
 
        var unmatchedTokens = new[] { "--ASPNETCORE_URLS", "http://localhost:9999" };
 
        Assert.True(DashboardRunCommand.ConfigSettingHasValue(unmatchedTokens, environment, KnownAspNetCoreConfigNames.Urls));
    }
 
    [Fact]
    public async Task DashboardRunCommand_DefaultOptions_DoesNotEmitAllowAnonymous()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        string[]? capturedArgs = null;
        var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
        executionFactory.AssertionCallback = (args, _, _, _) => { capturedArgs = args; };
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.NotNull(capturedArgs);
        Assert.DoesNotContain(capturedArgs, arg => arg.Contains("ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS"));
    }
 
    [Fact]
    public async Task DashboardRunCommand_BundleAvailableWithinDelay_DoesNotDisplayBundleStatus()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var testInteractionService = new TestInteractionService();
        var (services, _, _) = CreateServicesWithLayout(workspace, interactionService: testInteractionService);
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.Equal([DashboardCommandStrings.StartingDashboard], testInteractionService.ShownStatuses);
    }
 
    [Fact]
    public async Task DashboardRunCommand_BundleUnavailableAfterDelay_DisplaysBundleStatus()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var releaseBundle = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var bundleStatusDisplayed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var testInteractionService = new TestInteractionService
        {
            ShowStatusCallback = status =>
            {
                if (status == DashboardCommandStrings.EnsuringDashboardBundle)
                {
                    bundleStatusDisplayed.TrySetResult();
                }
            }
        };
        var bundleService = new TestBundleService(isBundle: true)
        {
            EnsureExtractedAndAcquireLayoutAsyncCallback = cancellationToken => releaseBundle.Task.WaitAsync(cancellationToken)
        };
        var (services, _, _) = CreateServicesWithLayout(workspace, testInteractionService, bundleService);
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run");
 
        var pendingRun = result.InvokeAsync();
        await bundleStatusDisplayed.Task.DefaultTimeout();
        releaseBundle.TrySetResult();
        var exitCode = await pendingRun.DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.Equal(
            [DashboardCommandStrings.EnsuringDashboardBundle, DashboardCommandStrings.StartingDashboard],
            testInteractionService.ShownStatuses);
    }
 
    [Fact]
    public async Task DashboardRunCommand_DefaultOptions_PassesDefaultArgsToProcess()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        string[]? capturedArgs = null;
        var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
        executionFactory.AssertionCallback = (args, _, _, _) => { capturedArgs = args; };
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.NotNull(capturedArgs);
        Assert.Collection(capturedArgs,
            arg => Assert.Equal("dashboard", arg),
            arg => Assert.Equal("--ASPNETCORE_URLS=http://localhost:18888", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL=http://localhost:4317", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL=http://localhost:4318", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_API_ENABLED=true", arg));
    }
 
    [Theory]
    [InlineData("--frontend-url http://localhost:5000", "--ASPNETCORE_URLS=http://localhost:5000")]
    [InlineData("--otlp-grpc-url http://localhost:9317", "--ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL=http://localhost:9317")]
    [InlineData("--otlp-http-url http://localhost:9318", "--ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL=http://localhost:9318")]
    [InlineData("--allow-anonymous", "--ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true")]
    [InlineData("--config-file-path /path/to/config.json", "--ASPIRE_DASHBOARD_CONFIG_FILE_PATH=/path/to/config.json")]
    public async Task DashboardRunCommand_IndividualOption_PassesCorrectArgToProcess(string cliArgs, string expectedArg)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        string[]? capturedArgs = null;
        var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
        executionFactory.AssertionCallback = (args, _, _, _) => { capturedArgs = args; };
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse($"dashboard run {cliArgs}");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.NotNull(capturedArgs);
        Assert.Contains(expectedArg, capturedArgs);
    }
 
    [Fact]
    public async Task DashboardRunCommand_WithoutAllowAnonymous_SetsBrowserTokenEnvVar()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        IDictionary<string, string>? capturedEnv = null;
        var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
        executionFactory.AssertionCallback = (_, env, _, _) => { capturedEnv = env; };
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.NotNull(capturedEnv);
        Assert.True(capturedEnv.ContainsKey("DASHBOARD__FRONTEND__BROWSERTOKEN"));
        Assert.False(string.IsNullOrEmpty(capturedEnv["DASHBOARD__FRONTEND__BROWSERTOKEN"]));
        Assert.True(capturedEnv.ContainsKey("DASHBOARD__API__PRIMARYAPIKEY"));
        Assert.False(string.IsNullOrEmpty(capturedEnv["DASHBOARD__API__PRIMARYAPIKEY"]));
        Assert.Equal("ApiKey", capturedEnv["DASHBOARD__API__AUTHMODE"]);
    }
 
    [Fact]
    public async Task DashboardRunCommand_UnmatchedTokens_ForwardedToProcess()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        string[]? capturedArgs = null;
        var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
        executionFactory.AssertionCallback = (args, _, _, _) => { capturedArgs = args; };
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run --CUSTOM_SETTING=myvalue");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.NotNull(capturedArgs);
        Assert.Collection(capturedArgs,
            arg => Assert.Equal("dashboard", arg),
            arg => Assert.Equal("--ASPNETCORE_URLS=http://localhost:18888", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL=http://localhost:4317", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL=http://localhost:4318", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_API_ENABLED=true", arg),
            arg => Assert.Equal("--CUSTOM_SETTING=myvalue", arg));
    }
 
    [Fact]
    public async Task DashboardRunCommand_CombinedOptions_PassesAllArgsToProcess()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        string[]? capturedArgs = null;
        var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
        executionFactory.AssertionCallback = (args, _, _, _) => { capturedArgs = args; };
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run --frontend-url http://localhost:5000 --otlp-grpc-url http://localhost:9317 --otlp-http-url http://localhost:9318 --allow-anonymous --config-file-path /my/config.json");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.NotNull(capturedArgs);
        Assert.Collection(capturedArgs,
            arg => Assert.Equal("dashboard", arg),
            arg => Assert.Equal("--ASPNETCORE_URLS=http://localhost:5000", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL=http://localhost:9317", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL=http://localhost:9318", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_API_ENABLED=true", arg),
            arg => Assert.Equal("--ASPIRE_DASHBOARD_CONFIG_FILE_PATH=/my/config.json", arg));
    }
 
    [Fact]
    public async Task DashboardRunCommand_ProcessExitsWithError_ReturnsFailure()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var testInteractionService = new TestInteractionService();
        var (services, _, executionFactory) = CreateServicesWithLayout(workspace, interactionService: testInteractionService);
        executionFactory.AttemptCallback = (_, _) => (1, null);
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.DashboardFailure, exitCode);
    }
 
    [Fact]
    public async Task DashboardRunCommand_ProcessFailsToStart_DisplaysErrorAndReturnsFailure()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var testInteractionService = new TestInteractionService();
        var (services, managedPath, executionFactory) = CreateServicesWithLayout(workspace, interactionService: testInteractionService);
 
        // Make CreateExecution return an execution whose StartAsync() returns false,
        // which causes LayoutProcessRunner.StartAsync to throw InvalidOperationException.
        executionFactory.CreateExecutionCallback = (_, _, _, _) =>
            new TestProcessExecution(
                "fake",
                [],
                null,
                new ProcessInvocationOptions(),
                (_, _, _) => Task.FromResult((0, (string?)null)),
                () => 0)
            {
                StartReturnValue = false
            };
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.DashboardFailure, exitCode);
        var errorMessage = Assert.Single(testInteractionService.DisplayedErrors);
        var expectedMessage = string.Format(CultureInfo.CurrentCulture, DashboardCommandStrings.DashboardFailedToStart, $"Failed to start process: {managedPath}");
        Assert.Equal(expectedMessage, errorMessage);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task DashboardRunCommand_WhenCancelled_DisplaysCancellationMessageAndReturnsSuccess(bool slowShutdown)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var testInteractionService = new TestInteractionService();
        var readyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var stoppingMessageDisplayedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var shutdownTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var expectedMessage = DashboardCommandStrings.StoppingDashboard;
        testInteractionService.DisplayCancellationMessageCallback = (message, _) =>
        {
            if (message == expectedMessage)
            {
                stoppingMessageDisplayedTcs.TrySetResult();
            }
        };
        var (services, _, executionFactory) = CreateServicesWithLayout(workspace, interactionService: testInteractionService);
        executionFactory.CreateExecutionCallback = (_, _, _, options) =>
            new TestProcessExecution("fake", [], null, options, (_, _, _) => Task.FromResult((0, (string?)null)), () => 0)
            {
                WaitForExitAsyncCallback = async (processOptions, cancellationToken) =>
                {
                    processOptions.StandardOutputCallback?.Invoke("Now listening on: http://localhost:18888");
                    readyTcs.TrySetResult();
                    try
                    {
                        await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false);
                    }
                    catch (OperationCanceledException)
                    {
                        if (slowShutdown)
                        {
                            await shutdownTcs.Task.ConfigureAwait(false);
                        }
 
                        throw;
                    }
 
                    return 0;
                }
            };
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("dashboard run");
 
        using var cts = new CancellationTokenSource();
        var pendingRun = result.InvokeAsync(cancellationToken: cts.Token);
 
        await readyTcs.Task.DefaultTimeout();
        await cts.CancelAsync();
 
        if (slowShutdown)
        {
            try
            {
                var firstCompletedTask = await Task.WhenAny(stoppingMessageDisplayedTcs.Task, pendingRun).DefaultTimeout();
                Assert.Same(stoppingMessageDisplayedTcs.Task, firstCompletedTask);
            }
            finally
            {
                shutdownTcs.TrySetResult();
            }
        }
 
        var exitCode = await pendingRun.DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
        Assert.Empty(testInteractionService.DisplayedMessages);
        var stoppingMessage = Assert.Single(testInteractionService.DisplayedCancellations);
        Assert.Equal(expectedMessage, stoppingMessage.Message);
        Assert.Null(stoppingMessage.ConsoleOverride);
    }
 
    [Theory]
    [InlineData("", "http://localhost:18888")]
    [InlineData(";;;", "http://localhost:18888")]
    [InlineData("http://first:5000;http://second:5001", "http://first:5000")]
    [InlineData("http://custom:9000", "http://custom:9000")]
    [InlineData("http://trailing:8080/", "http://trailing:8080")]
    public void ResolveDashboardInfo_FrontendUrlVariants_ResolvesExpectedUrl(string urlValue, string expectedDashboardUrl)
    {
        var environment = CreateEnvironment(new Dictionary<string, string?>());
 
        var args = new List<string> { "dashboard", $"--ASPNETCORE_URLS={urlValue}" };
        var unmatchedTokens = Array.Empty<string>();
 
        var info = DashboardRunCommand.ResolveDashboardInfo(args, unmatchedTokens, environment, browserToken: null);
 
        Assert.Equal(expectedDashboardUrl, info.DashboardUrl);
    }
 
    [Fact]
    public void ResolveDashboardInfo_EnvVarFrontendUrl_UsedInSummary()
    {
        var environment = CreateEnvironment(new Dictionary<string, string?>
        {
            [KnownAspNetCoreConfigNames.Urls] = "http://envhost:9999"
        });
 
        // No arg in the list — should fall back to the environment variable.
        var args = new List<string> { "dashboard" };
        var unmatchedTokens = Array.Empty<string>();
 
        var info = DashboardRunCommand.ResolveDashboardInfo(args, unmatchedTokens, environment, browserToken: null);
 
        Assert.Equal("http://envhost:9999", info.DashboardUrl);
    }
 
    [Fact]
    public void ResolveDashboardInfo_SpaceSeparatedUnmatchedToken_UsedInSummary()
    {
        var environment = CreateEnvironment(new Dictionary<string, string?>());
 
        // No --ASPNETCORE_URLS= in the args list; the value comes through space-separated unmatched tokens.
        var args = new List<string> { "dashboard" };
        var unmatchedTokens = new[] { "--ASPNETCORE_URLS", "http://space:7777" };
 
        var info = DashboardRunCommand.ResolveDashboardInfo(args, unmatchedTokens, environment, browserToken: null);
 
        Assert.Equal("http://space:7777", info.DashboardUrl);
    }
 
    [Fact]
    public void ResolveDashboardInfo_EnvVarOtlpUrls_UsedInSummary()
    {
        var environment = CreateEnvironment(new Dictionary<string, string?>
        {
            ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://grpc:1111",
            ["ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"] = "http://http:2222"
        });
 
        var args = new List<string> { "dashboard" };
        var unmatchedTokens = Array.Empty<string>();
 
        var info = DashboardRunCommand.ResolveDashboardInfo(args, unmatchedTokens, environment, browserToken: null);
 
        Assert.Equal("http://grpc:1111", info.OtlpGrpcUrl);
        Assert.Equal("http://http:2222", info.OtlpHttpUrl);
    }
 
    [Fact]
    public void ResolveDashboardInfo_ArgTakesPrecedenceOverEnvVar()
    {
        var environment = CreateEnvironment(new Dictionary<string, string?>
        {
            [KnownAspNetCoreConfigNames.Urls] = "http://envhost:9999"
        });
 
        var args = new List<string> { "dashboard", "--ASPNETCORE_URLS=http://arghost:5555" };
        var unmatchedTokens = Array.Empty<string>();
 
        var info = DashboardRunCommand.ResolveDashboardInfo(args, unmatchedTokens, environment, browserToken: null);
 
        Assert.Equal("http://arghost:5555", info.DashboardUrl);
    }
 
    [Fact]
    public void ResolveDashboardInfo_WithBrowserToken_AppendsLoginPath()
    {
        var environment = CreateEnvironment(new Dictionary<string, string?>());
 
        var args = new List<string> { "dashboard", "--ASPNETCORE_URLS=http://localhost:18888" };
        var unmatchedTokens = Array.Empty<string>();
 
        var info = DashboardRunCommand.ResolveDashboardInfo(args, unmatchedTokens, environment, browserToken: "abc123");
 
        Assert.Equal("http://localhost:18888/login?t=abc123", info.DashboardUrl);
    }
 
    [Fact]
    public void RenderDashboardSummary_RendersLogsPathAsClickableFileLink()
    {
        var output = new StringBuilder();
        var console = AnsiConsole.Create(new AnsiConsoleSettings
        {
            Ansi = AnsiSupport.Yes,
            ColorSystem = ColorSystemSupport.TrueColor,
            Out = new AnsiConsoleOutput(new StringWriter(output)),
            Enrichment = new ProfileEnrichment { UseDefaultEnrichers = false }
        });
        console.Profile.Width = int.MaxValue;
 
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var logFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, "cli [dashboard].log");
        var executionContext = workspace.CreateExecutionContext(logFilePath: logFilePath);
 
        var interactionService = new ConsoleInteractionService(
            new ConsoleEnvironment(console, console),
            executionContext,
            TestHelpers.CreateInteractiveHostEnvironment(),
            new EnvironmentProcessPathProvider(),
            NullLoggerFactory.Instance,
            new ConsoleLogBufferContext());
 
        var dashboardInfo = new DashboardRunCommand.DashboardInfo(
            DashboardUrl: "http://localhost:18888",
            OtlpGrpcUrl: "http://localhost:4317",
            OtlpHttpUrl: "http://localhost:4318");
 
        DashboardRunCommand.RenderDashboardSummary(interactionService, dashboardInfo, logFilePath);
 
        var outputString = output.ToString();
        var fileUri = new Uri(Path.GetFullPath(logFilePath)).AbsoluteUri;
 
        Assert.Contains("Logs", outputString);
        TerminalLinkAssert.ContainsLink(outputString, fileUri, logFilePath);
    }
 
    private (IServiceCollection Services, string ManagedPath, TestProcessExecutionFactory ExecutionFactory) CreateServicesWithLayout(
        TemporaryWorkspace workspace,
        TestInteractionService? interactionService = null,
        TestBundleService? bundleService = null)
    {
        var layoutDir = Path.Combine(workspace.WorkspaceRoot.FullName, "layout");
        var managedDir = Path.Combine(layoutDir, "managed");
        Directory.CreateDirectory(managedDir);
        var managedPath = Path.Combine(managedDir, BundleDiscovery.GetExecutableFileName("aspire-managed"));
        File.WriteAllText(managedPath, "fake");
 
        var layout = new LayoutConfiguration
        {
            LayoutPath = layoutDir,
            Components = new LayoutComponents { Managed = "managed" }
        };
        bundleService ??= new TestBundleService(isBundle: true);
        bundleService.Layout = layout;
 
        var executionFactory = new TestProcessExecutionFactory
        {
            AttemptCallback = (_, _) => (0, "Now listening on: http://localhost:18888")
        };
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.LayoutDiscoveryFactory = _ => new FakeLayoutDiscovery(layout);
            options.BundleServiceFactory = _ => bundleService;
            options.DotNetCliExecutionFactoryFactory = _ => executionFactory;
            if (interactionService is not null)
            {
                options.InteractionServiceFactory = _ => interactionService;
            }
        });
 
        return (services, managedPath, executionFactory);
    }
 
    private static IEnvironment CreateEnvironment(Dictionary<string, string?> envVars)
    {
        return new TestEnvironment(envVars);
    }
 
    private sealed class FakeLayoutDiscovery(LayoutConfiguration layout) : ILayoutDiscovery
    {
        public LayoutConfiguration? DiscoverLayout(string? projectDirectory = null) => layout;
        public string? GetComponentPath(LayoutComponent component, string? projectDirectory = null) => layout.GetComponentPath(component);
        public bool IsBundleModeAvailable(string? projectDirectory = null) => true;
    }
 
}