File: DescribeCommandTests.cs
Web Access
Project: src\tests\Aspire.Cli.EndToEnd.Tests\Aspire.Cli.EndToEnd.Tests.csproj (Aspire.Cli.EndToEnd.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.EndToEnd.Tests.Helpers;
using Aspire.Cli.Resources;
using Aspire.Cli.Tests.Utils;
using Hex1b.Automation;
using Xunit;
 
namespace Aspire.Cli.EndToEnd.Tests;
 
/// <summary>
/// End-to-end tests for the aspire describe command.
/// Each test class runs as a separate CI job for parallelization.
/// </summary>
public sealed class DescribeCommandTests(ITestOutputHelper output)
{
    [Fact]
    public async Task DescribeCommandShowsRunningResources()
    {
        var repoRoot = CliE2ETestHelpers.GetRepoRoot();
        var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot);
 
        var workspace = TemporaryWorkspace.Create(output);
 
        using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace);
 
        var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
 
        var counter = new SequenceCounter();
        var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
 
        await auto.PrepareDockerEnvironmentAsync(counter, workspace);
 
        await auto.InstallAspireCliInDockerAsync(installMode, counter);
 
        // Create a new project using aspire new
        await auto.AspireNewAsync("AspireResourcesTestApp", counter);
 
        // Navigate to the AppHost directory
        await auto.TypeAsync("cd AspireResourcesTestApp/AspireResourcesTestApp.AppHost");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Start the AppHost in the background using aspire start
        await auto.TypeAsync("aspire start");
        await auto.EnterAsync();
        await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3));
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Wait a bit for resources to stabilize
        await auto.TypeAsync("sleep 5");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Now verify aspire describe shows the running resources (human-readable table)
        await auto.TypeAsync("aspire describe");
        await auto.EnterAsync();
        await auto.WaitUntilTextAsync("Name", timeout: TimeSpan.FromSeconds(30));
        await auto.WaitUntilTextAsync("webfrontend", timeout: TimeSpan.FromSeconds(5));
        await auto.WaitUntilTextAsync("apiservice", timeout: TimeSpan.FromSeconds(5));
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Test aspire describe --format json output - pipe to file to avoid terminal buffer issues
        await auto.TypeAsync("aspire describe --format json > resources.json");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Verify the JSON file contains expected resources
        await auto.TypeAsync("cat resources.json | grep webfrontend");
        await auto.EnterAsync();
        await auto.WaitUntilTextAsync("webfrontend", timeout: TimeSpan.FromSeconds(10));
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Stop the AppHost using aspire stop
        await auto.TypeAsync("aspire stop");
        await auto.EnterAsync();
        await auto.WaitUntilTextAsync(StopCommandStrings.AppHostStoppedSuccessfully, timeout: TimeSpan.FromMinutes(1));
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Exit the shell
        await auto.TypeAsync("exit");
        await auto.EnterAsync();
 
        await pendingRun;
    }
 
    [Fact]
    public async Task DescribeCommandResolvesReplicaNames()
    {
        var repoRoot = CliE2ETestHelpers.GetRepoRoot();
        var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot);
 
        var workspace = TemporaryWorkspace.Create(output);
 
        using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace);
 
        var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken);
 
        // Pattern for describe output showing a specific replica
        var waitForApiserviceReplicaName = new CellPatternSearcher()
            .FindPattern("apiservice-[a-z0-9]+");
 
        var counter = new SequenceCounter();
        var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500));
 
        await auto.PrepareDockerEnvironmentAsync(counter, workspace);
 
        await auto.InstallAspireCliInDockerAsync(installMode, counter);
 
        // Create a new project using aspire new
        await auto.AspireNewAsync("AspireReplicaTestApp", counter);
 
        // Navigate to the AppHost directory
        await auto.TypeAsync("cd AspireReplicaTestApp/AspireReplicaTestApp.AppHost");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Add .WithReplicas(2) to the apiservice resource in the AppHost
        var projectDir = Path.Combine(workspace.WorkspaceRoot.FullName, "AspireReplicaTestApp");
        var appHostDir = Path.Combine(projectDir, "AspireReplicaTestApp.AppHost");
        var appHostFilePath = Path.Combine(appHostDir, "AppHost.cs");
 
        output.WriteLine($"Looking for AppHost.cs at: {appHostFilePath}");
 
        var content = File.ReadAllText(appHostFilePath);
 
        // Add .WithReplicas(2) to the first .WithHttpHealthCheck("/health"); occurrence (apiservice)
        var originalPattern = ".WithHttpHealthCheck(\"/health\");";
        var replacement = ".WithHttpHealthCheck(\"/health\").WithReplicas(2);";
 
        // Only replace the first occurrence (apiservice), not the second (webfrontend)
        var index = content.IndexOf(originalPattern);
        if (index >= 0)
        {
            content = content[..index] + replacement + content[(index + originalPattern.Length)..];
        }
 
        File.WriteAllText(appHostFilePath, content);
 
        output.WriteLine($"Modified AppHost.cs to add .WithReplicas(2) to apiservice");
 
        // Start the AppHost in the background using aspire start
        await auto.TypeAsync("aspire start");
        await auto.EnterAsync();
        await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3));
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Wait for resources to stabilize
        await auto.TypeAsync("sleep 10");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Test 1: aspire describe with friendly name for a non-replicated resource (cache)
        // This should resolve via DisplayName since cache has only one instance
        await auto.TypeAsync("aspire describe cache --format json > cache-describe.json");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Verify cache resource was found in the output
        await auto.TypeAsync("cat cache-describe.json | grep cache");
        await auto.EnterAsync();
        await auto.WaitUntilTextAsync("cache", timeout: TimeSpan.FromSeconds(10));
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Test 2: Get all resources to find an apiservice replica name
        await auto.TypeAsync("aspire describe --format json > all-resources.json");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Extract a replica name from the JSON - apiservice replicas have names like apiservice-<suffix>
        await auto.TypeAsync("REPLICA_NAME=$(cat all-resources.json | grep -o '\"name\": *\"apiservice-[a-z0-9]*\"' | head -1 | sed 's/.*\"\\(apiservice-[a-z0-9]*\\)\"/\\1/')");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Verify we captured a replica name
        await auto.TypeAsync("echo \"Found replica: $REPLICA_NAME\"");
        await auto.EnterAsync();
        await auto.WaitUntilAsync(s => waitForApiserviceReplicaName.Search(s).Count > 0, timeout: TimeSpan.FromSeconds(10), description: "waiting for apiservice replica name");
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Test 3: aspire describe with the replica name
        // This should resolve via exact Name match
        await auto.TypeAsync("aspire describe $REPLICA_NAME --format json > replica-describe.json");
        await auto.EnterAsync();
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Verify the replica was found and output contains the replica name
        await auto.TypeAsync("cat replica-describe.json | grep apiservice");
        await auto.EnterAsync();
        await auto.WaitUntilAsync(s => waitForApiserviceReplicaName.Search(s).Count > 0, timeout: TimeSpan.FromSeconds(10), description: "waiting for apiservice replica in describe output");
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Stop the AppHost using aspire stop
        await auto.TypeAsync("aspire stop");
        await auto.EnterAsync();
        await auto.WaitUntilTextAsync(StopCommandStrings.AppHostStoppedSuccessfully, timeout: TimeSpan.FromMinutes(1));
        await auto.WaitForSuccessPromptAsync(counter);
 
        // Exit the shell
        await auto.TypeAsync("exit");
        await auto.EnterAsync();
 
        await pendingRun;
    }
}