| File: DashboardRunTests.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 Hex1b.Automation; using Xunit; namespace Aspire.Cli.EndToEnd.Tests; /// <summary> /// End-to-end tests for the aspire dashboard run command combined with aspire otel and aspire agent mcp. /// Each test class runs as a separate CI job for parallelization. /// </summary> public sealed class DashboardRunTests(ITestOutputHelper output) { [Fact] [CaptureWorkspaceOnFailure] public async Task DashboardRunWithOtelTracesReturnsNoTraces() { await DashboardRunWithOtelTracesReturnsNoTracesCore("http://localhost:18888", "http://localhost:18888"); } [Fact] [CaptureWorkspaceOnFailure] public async Task DashboardRunWithOtelTracesReturnsNoTraces_DevLocalhost() { await DashboardRunWithOtelTracesReturnsNoTracesCore("http://dashboard.dev.localhost:18888", "http://localhost:18888"); } private async Task DashboardRunWithOtelTracesReturnsNoTracesCore(string frontendUrl, string localhostUrl) { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var strategy = CliInstallStrategy.Detect(output.WriteLine); var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); // Use a known browser token so we can construct the dashboard URL directly, // avoiding the need to parse it from logs (which contain Spectre Console OSC 8 // escape sequences that break grep-based URL extraction). var browserToken = "testtoken1234567890abcdef12345678"; // Set the browser token env var before starting the dashboard await auto.TypeAsync($"export DASHBOARD__FRONTEND__BROWSERTOKEN={browserToken}"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Store the dashboard log path inside the workspace so it gets captured on failure var dashboardLogPath = $"/workspace/{workspace.WorkspaceRoot.Name}/dashboard.log"; // Start the dashboard in the background with the specified frontend URL await auto.TypeAsync($"aspire dashboard run --frontend-url {frontendUrl} > {dashboardLogPath} 2>&1 &"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Store the dashboard PID for cleanup await auto.TypeAsync("DASHBOARD_PID=$!"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Wait for the dashboard to become ready by polling the localhost URL await auto.TypeAsync($"for i in $(seq 1 30); do curl -ksSL -o /dev/null -w '%{{http_code}}' {localhostUrl} 2>/dev/null | grep -q 200 && break; sleep 1; done"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); // Dump dashboard log for debugging visibility in the recording await auto.TypeAsync($"echo '=== DASHBOARD LOG ==='; cat {dashboardLogPath}; echo '=== END DASHBOARD LOG ==='"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Dump CLI logs for debugging await auto.TypeAsync("echo '=== CLI LOGS ==='; ls -lt ~/.aspire/logs/ 2>/dev/null; CLI_LOG=$(ls -t ~/.aspire/logs/cli_*.log 2>/dev/null | head -1); [ -n \"$CLI_LOG\" ] && tail -50 \"$CLI_LOG\"; echo '=== END CLI LOGS ==='"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Construct the dashboard URL using the known token and the frontend URL. // In the dev.localhost variant this exercises the CLI's NormalizeDashboardUrl path end-to-end. var dashboardUrl = $"{frontendUrl}/login?t={browserToken}"; await auto.TypeAsync($"aspire otel traces --dashboard-url \"{dashboardUrl}\""); await auto.EnterAsync(); await auto.WaitUntilTextAsync("No traces found", timeout: TimeSpan.FromSeconds(30)); await auto.WaitForSuccessPromptAsync(counter); // Clean up: kill the background dashboard process await auto.TypeAsync("kill -9 $DASHBOARD_PID 2>/dev/null; wait $DASHBOARD_PID 2>/dev/null; true"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); } [Fact] [CaptureWorkspaceOnFailure] public Task DashboardRunWithAgentMcpListTracesReturnsNoTraces() => DashboardRunWithAgentMcpCore("http://localhost:18888", "http://localhost:18888", "list_traces", "TRACES DATA"); [Fact] [CaptureWorkspaceOnFailure] public Task DashboardRunWithAgentMcpListTracesReturnsNoTraces_DevLocalhost() => DashboardRunWithAgentMcpCore("http://dashboard.dev.localhost:18888", "http://localhost:18888", "list_traces", "TRACES DATA"); private async Task DashboardRunWithAgentMcpCore(string frontendUrl, string localhostUrl, string toolName, string expectedMarker) { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var strategy = CliInstallStrategy.Detect(output.WriteLine); var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, strategy, output, mountDockerSocket: false, workspace: workspace); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); await using var terminalRun = CliE2ETestHelpers.StartRun(terminal, workspace, auto, counter, output, TestContext.Current.CancellationToken); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliAsync(strategy, counter); // Use a known browser token so we can construct the dashboard URL directly var browserToken = "testtoken1234567890abcdef12345678"; // Set the browser token env var before starting the dashboard await auto.TypeAsync($"export DASHBOARD__FRONTEND__BROWSERTOKEN={browserToken}"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Store the dashboard log path inside the workspace so it gets captured on failure var dashboardLogPath = $"/workspace/{workspace.WorkspaceRoot.Name}/dashboard.log"; // Start the dashboard in the background with the specified frontend URL await auto.TypeAsync($"aspire dashboard run --frontend-url {frontendUrl} > {dashboardLogPath} 2>&1 &"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Store the dashboard PID for cleanup await auto.TypeAsync("DASHBOARD_PID=$!"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Wait for the dashboard to become ready by polling the localhost URL await auto.TypeAsync($"for i in $(seq 1 30); do curl -ksSL -o /dev/null -w '%{{http_code}}' {localhostUrl} 2>/dev/null | grep -q 200 && break; sleep 1; done"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter, TimeSpan.FromSeconds(60)); // Construct the dashboard URL using the known token and the frontend URL var dashboardUrl = $"{frontendUrl}/login?t={browserToken}"; // Call the MCP tool against the standalone dashboard await auto.CallAgentMcpToolAsync(counter, toolName, expectedMarker, $"--dashboard-url \"{dashboardUrl}\""); // Clean up: kill the background dashboard process await auto.TypeAsync("kill -9 $DASHBOARD_PID 2>/dev/null; wait $DASHBOARD_PID 2>/dev/null; true"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); } }