diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..aa185703373 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +# Ignore build artifacts (will be rebuilt inside Docker) +artifacts/ +.dotnet/ + +# IDE and editor files +.vs/ +.vscode/ +*.user +*.suo + +# Git data (not needed for build) +.git/ + +# Node modules +**/node_modules/ + +# OS files +.DS_Store +Thumbs.db diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 59a47c2cc4b..3dc2247441e 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -40,7 +40,7 @@ "type": "stdio", "command": "dnx", "args": [ - "Hex1b.McpServer@0.47.0", + "Hex1b.McpServer@0.105.0", "--yes" ] }, diff --git a/Directory.Packages.props b/Directory.Packages.props index 8c2ea50095c..88c4bf23654 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -96,9 +96,9 @@ - - - + + + diff --git a/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs index a30b83efd18..3b657da9596 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/AgentCommandTests.cs @@ -25,12 +25,11 @@ public sealed class AgentCommandTests(ITestOutputHelper output) [Fact] public async Task AgentCommands_AllHelpOutputs_AreCorrect() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -41,14 +40,9 @@ public async Task AgentCommands_AllHelpOutputs_AreCorrect() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Test 1: aspire agent --help sequenceBuilder @@ -119,18 +113,18 @@ public async Task AgentCommands_AllHelpOutputs_AreCorrect() [Fact] public async Task AgentInitCommand_MigratesDeprecatedConfig() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); // Use .mcp.json (Claude Code format) for simpler testing // This is the same format used by the doctor test that passes var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".mcp.json"); + var containerConfigPath = CliE2ETestHelpers.ToContainerPath(configPath, workspace); // Patterns for agent init prompts - look for the colon at the end which indicates // the prompt is ready for input @@ -142,14 +136,9 @@ public async Task AgentInitCommand_MigratesDeprecatedConfig() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Step 1: Create deprecated config file using Claude Code format (.mcp.json) // This simulates a config that was created by an older version of the CLI @@ -165,7 +154,7 @@ public async Task AgentInitCommand_MigratesDeprecatedConfig() // Debug: Show that the file exists and where we are var fileExistsPattern = new CellPatternSearcher().Find(".mcp.json"); sequenceBuilder - .Type($"ls -la {configPath} && pwd") + .Type($"ls -la {containerConfigPath} && pwd") .Enter() .WaitUntil(s => fileExistsPattern.Search(s).Count > 0, TimeSpan.FromSeconds(10)) .WaitForSuccessPrompt(counter); @@ -200,10 +189,9 @@ public async Task AgentInitCommand_MigratesDeprecatedConfig() .WaitForSuccessPrompt(counter); // Debug: Show the scanner log file to diagnose what the scanner found - var debugLogPath = Path.Combine(Path.GetTempPath(), "aspire-deprecated-scan.log"); var debugLogPattern = new CellPatternSearcher().Find("Scanning context"); sequenceBuilder - .Type($"cat {debugLogPath} 2>/dev/null || echo 'No debug log found'") + .Type("cat /tmp/aspire-deprecated-scan.log 2>/dev/null || echo 'No debug log found'") .Enter() .WaitUntil(s => debugLogPattern.Search(s).Count > 0, TimeSpan.FromSeconds(10)) .WaitForSuccessPrompt(counter); @@ -232,12 +220,11 @@ public async Task AgentInitCommand_MigratesDeprecatedConfig() [Fact] public async Task DoctorCommand_DetectsDeprecatedAgentConfig() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -255,14 +242,9 @@ public async Task DoctorCommand_DetectsDeprecatedAgentConfig() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create deprecated config file sequenceBuilder @@ -297,13 +279,11 @@ public async Task DoctorCommand_DetectsDeprecatedAgentConfig() [Fact] public async Task AgentInitCommand_DefaultSelection_InstallsSkillOnly() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -319,14 +299,9 @@ public async Task AgentInitCommand_DefaultSelection_InstallsSkillOnly() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create .vscode folder so the scanner detects VS Code environment sequenceBuilder diff --git a/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs index 5bbd4a2048e..b94e710f5b2 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/BannerTests.cs @@ -17,13 +17,11 @@ public sealed class BannerTests(ITestOutputHelper output) [Fact] public async Task Banner_DisplayedOnFirstRun() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -39,14 +37,9 @@ public async Task Banner_DisplayedOnFirstRun() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Delete the first-time use sentinel file to simulate first run // The sentinel is stored at ~/.aspire/cli/cli.firstUseSentinel @@ -81,13 +74,11 @@ public async Task Banner_DisplayedOnFirstRun() [Fact] public async Task Banner_DisplayedWithExplicitFlag() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -104,14 +95,9 @@ public async Task Banner_DisplayedWithExplicitFlag() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Clear screen to have a clean slate for pattern matching sequenceBuilder @@ -141,13 +127,11 @@ public async Task Banner_DisplayedWithExplicitFlag() [ActiveIssue("https://github.com/dotnet/aspire/issues/14307")] public async Task Banner_NotDisplayedWithNoLogoFlag() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -163,14 +147,9 @@ public async Task Banner_NotDisplayedWithNoLogoFlag() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Delete the first-time use sentinel file to simulate first run, // but use --nologo to suppress the banner diff --git a/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs index 9b75846093d..39b09a14146 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs @@ -18,12 +18,12 @@ public sealed class BundleSmokeTests(ITestOutputHelper output) [Fact] public async Task CreateAndRunAspireStarterProjectWithBundle() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -36,17 +36,9 @@ public async Task CreateAndRunAspireStarterProjectWithBundle() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - // Install the full bundle (not just CLI) so that ASPIRE_LAYOUT_PATH is set. - // For .NET csproj app hosts, the hosting infrastructure resolves DCP and Dashboard - // paths through NuGet assembly metadata, NOT through bundle env vars. - sequenceBuilder.InstallAspireBundleFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireBundleEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); sequenceBuilder.AspireNew("BundleStarterApp", counter) // Start AppHost in detached mode and capture JSON output diff --git a/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs index 0bc20f3196b..08153aab4d3 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/CentralPackageManagementTests.cs @@ -18,12 +18,11 @@ public sealed class CentralPackageManagementTests(ITestOutputHelper output) [Fact] public async Task AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -43,14 +42,9 @@ public async Task AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesP var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Disable update notifications to prevent the CLI self-update prompt // from appearing after "Update successful!" and blocking the test. @@ -66,6 +60,7 @@ public async Task AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesP var appHostDir = Path.Combine(projectDir, "CpmTest.AppHost"); var appHostCsprojPath = Path.Combine(appHostDir, "CpmTest.AppHost.csproj"); var directoryPackagesPropsPath = Path.Combine(projectDir, "Directory.Packages.props"); + var containerAppHostCsprojPath = CliE2ETestHelpers.ToContainerPath(appHostCsprojPath, workspace); sequenceBuilder .ExecuteCallback(() => @@ -104,7 +99,7 @@ public async Task AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesP }) // Use --channel stable to skip the channel selection prompt that appears // in CI when PR hive directories are present. - .Type($"aspire update --project \"{appHostCsprojPath}\" --channel stable") + .Type($"aspire update --project \"{containerAppHostCsprojPath}\" --channel stable") .Enter() .WaitUntil(s => waitingForPerformUpdates.Search(s).Count > 0, TimeSpan.FromSeconds(60)) .Enter() // confirm "Perform updates?" (default: Yes) @@ -119,7 +114,7 @@ public async Task AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesP // Verify the PackageVersion for Aspire.Hosting.AppHost was removed .VerifyFileDoesNotContain(directoryPackagesPropsPath, "Aspire.Hosting.AppHost") // Verify dotnet restore succeeds (would fail with NU1009 without the fix) - .Type($"dotnet restore \"{appHostCsprojPath}\"") + .Type($"dotnet restore \"{containerAppHostCsprojPath}\"") .Enter() .WaitForSuccessPrompt(counter, TimeSpan.FromSeconds(120)) // Clean up: re-enable update notifications @@ -139,32 +134,27 @@ public async Task AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesP [Fact] public async Task AspireAddPackageVersionToDirectoryPackagesProps() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Set up an AppHost project with CPM, but no installed packages var projectDir = Path.Combine(workspace.WorkspaceRoot.FullName, "CpmTest"); var appHostDir = Path.Combine(projectDir, "CpmTest.AppHost"); var appHostCsprojPath = Path.Combine(appHostDir, "CpmTest.AppHost.csproj"); var directoryPackagesPropsPath = Path.Combine(projectDir, "Directory.Packages.props"); + var containerAppHostCsprojPath = CliE2ETestHelpers.ToContainerPath(appHostCsprojPath, workspace); sequenceBuilder .ExecuteCallback(() => @@ -200,7 +190,7 @@ public async Task AspireAddPackageVersionToDirectoryPackagesProps() // Verify the PackageVersion for Aspire.Hosting.AppHost was removed .VerifyFileDoesNotContain(appHostCsprojPath, "Version=\"13.1.2\"") // Verify dotnet restore succeeds (would fail with NU1009 if AppHost.csproj contained a version) - .Type($"dotnet restore \"{appHostCsprojPath}\"") + .Type($"dotnet restore \"{containerAppHostCsprojPath}\"") .Enter() .WaitForSuccessPrompt(counter, TimeSpan.FromSeconds(120)) .Type("exit") diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs index d28ea15d4d8..cc41b41cea1 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs @@ -17,12 +17,12 @@ 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); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -51,14 +51,9 @@ public async Task DescribeCommandShowsRunningResources() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create a new project using aspire new sequenceBuilder.AspireNew("AspireResourcesTestApp", counter); @@ -118,12 +113,12 @@ public async Task DescribeCommandShowsRunningResources() [Fact] public async Task DescribeCommandResolvesReplicaNames() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -145,14 +140,9 @@ public async Task DescribeCommandResolvesReplicaNames() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create a new project using aspire new sequenceBuilder.AspireNew("AspireReplicaTestApp", counter); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs index 98bfa6bd535..0ef1f67a54a 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/DoctorCommandTests.cs @@ -17,12 +17,11 @@ public sealed class DoctorCommandTests(ITestOutputHelper output) [Fact] public async Task DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -37,14 +36,15 @@ public async Task DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); + + // Generate and trust dev certs inside the container (Docker images don't have them by default) + sequenceBuilder + .Type("dotnet dev-certs https --trust 2>/dev/null || dotnet dev-certs https") + .Enter() + .WaitForSuccessPrompt(counter); // Unset SSL_CERT_DIR to trigger partial trust detection on Linux sequenceBuilder @@ -72,12 +72,11 @@ public async Task DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted() [Fact] public async Task DoctorCommand_WithSslCertDir_ShowsTrusted() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -96,14 +95,15 @@ public async Task DoctorCommand_WithSslCertDir_ShowsTrusted() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); + + // Generate and trust dev certs inside the container (Docker images don't have them by default) + sequenceBuilder + .Type("dotnet dev-certs https --trust 2>/dev/null || dotnet dev-certs https") + .Enter() + .WaitForSuccessPrompt(counter); // Set SSL_CERT_DIR to include dev-certs trust path for full trust sequenceBuilder diff --git a/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs index f526dae6011..37bb020a986 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/EmptyAppHostTemplateTests.cs @@ -17,12 +17,11 @@ public sealed class EmptyAppHostTemplateTests(ITestOutputHelper output) [Fact] public async Task CreateEmptyAppHostProject() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -34,14 +33,9 @@ public async Task CreateEmptyAppHostProject() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); sequenceBuilder.AspireNew("AspireEmptyApp", counter, template: AspireTemplate.EmptyAppHost); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs index 1ba32ab0d9c..a1a0b92f1af 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs @@ -422,4 +422,326 @@ internal static Hex1bTerminalInputSequenceBuilder VerifyFileDoesNotContain( } }); } + + /// + /// Specifies how the Aspire CLI should be installed inside a Docker container. + /// + internal enum DockerInstallMode + { + /// + /// The CLI was built from source by the Dockerfile and is already on PATH. + /// + SourceBuild, + + /// + /// Install the latest GA release from aspire.dev. + /// + GaRelease, + + /// + /// Install from PR artifacts using the get-aspire-cli-pr.sh script. + /// + PullRequest, + } + + /// + /// Specifies which Dockerfile variant to use for the test container. + /// + internal enum DockerfileVariant + { + /// + /// .NET SDK + Docker + Python + Node.js. For tests that create/run .NET AppHosts. + /// + DotNet, + + /// + /// Docker + Python + Node.js (no .NET SDK). For TypeScript-only AppHost tests. + /// + Polyglot, + } + + /// + /// Detects the install mode for Docker-based tests based on the current environment. + /// + /// The repo root directory on the host. + /// The detected . + internal static DockerInstallMode DetectDockerInstallMode(string repoRoot) + { + if (IsRunningInCI) + { + return DockerInstallMode.PullRequest; + } + + // Check if a locally-built native AOT CLI binary exists (developer has run ./build.sh --bundle). + var cliPublishDir = FindLocalCliBinary(repoRoot); + if (cliPublishDir is not null) + { + return DockerInstallMode.SourceBuild; + } + + return DockerInstallMode.GaRelease; + } + + /// + /// Finds the locally-built native AOT CLI publish directory. + /// Searches for the aspire binary under artifacts/bin/Aspire.Cli/*/net*/linux-x64/publish/. + /// + /// The publish directory path, or null if not found. + internal static string? FindLocalCliBinary(string repoRoot) + { + var cliBaseDir = Path.Combine(repoRoot, "artifacts", "bin", "Aspire.Cli"); + if (!Directory.Exists(cliBaseDir)) + { + return null; + } + + // Search for the native AOT binary under any config/TFM combination. + var matches = Directory.GetFiles(cliBaseDir, "aspire", SearchOption.AllDirectories) + .Where(f => f.Contains("linux-x64") && f.Contains("publish")) + .ToArray(); + + return matches.Length > 0 ? Path.GetDirectoryName(matches[0]) : null; + } + + /// + /// Creates a Hex1b terminal that runs inside a Docker container built from the shared E2E Dockerfile. + /// The Dockerfile builds the CLI from source (local dev) or accepts pre-built artifacts (CI). + /// + /// The repo root directory, used as the Docker build context. + /// The detected install mode, controlling Docker build args and volumes. + /// Test output helper for logging configuration details. + /// Which Dockerfile variant to use (DotNet or Polyglot). + /// Whether to mount the Docker socket for DCP/container access. + /// Optional workspace to mount into the container at /workspace. + /// Terminal width in columns. + /// Terminal height in rows. + /// The test name for the recording file path. + /// A configured . Caller is responsible for disposal. + internal static Hex1bTerminal CreateDockerTestTerminal( + string repoRoot, + DockerInstallMode installMode, + ITestOutputHelper output, + DockerfileVariant variant = DockerfileVariant.DotNet, + bool mountDockerSocket = false, + TemporaryWorkspace? workspace = null, + int width = 160, + int height = 48, + [CallerMemberName] string testName = "") + { + var recordingPath = GetTestResultsRecordingPath(testName); + var dockerfileName = variant switch + { + DockerfileVariant.DotNet => "Dockerfile.e2e", + DockerfileVariant.Polyglot => "Dockerfile.e2e-polyglot", + _ => throw new ArgumentOutOfRangeException(nameof(variant)), + }; + var dockerfilePath = Path.Combine(repoRoot, "tests", "Shared", "Docker", dockerfileName); + + output.WriteLine($"Creating Docker test terminal:"); + output.WriteLine($" Test name: {testName}"); + output.WriteLine($" Install mode: {installMode}"); + output.WriteLine($" Variant: {variant}"); + output.WriteLine($" Dockerfile: {dockerfilePath}"); + output.WriteLine($" Workspace: {workspace?.WorkspaceRoot.FullName ?? "(none)"}"); + output.WriteLine($" Docker socket: {mountDockerSocket}"); + output.WriteLine($" Dimensions: {width}x{height}"); + output.WriteLine($" Recording: {recordingPath}"); + + var builder = Hex1bTerminal.CreateBuilder() + .WithHeadless() + .WithDimensions(width, height) + .WithAsciinemaRecording(recordingPath) + .WithDockerContainer(c => + { + c.DockerfilePath = dockerfilePath; + c.BuildContext = repoRoot; + + if (mountDockerSocket) + { + c.MountDockerSocket = true; + } + + if (workspace is not null) + { + // Mount using the same directory name so that + // workspace.WorkspaceRoot.Name matches inside the container + // (e.g., aspire CLI uses the dir name as the default project name). + c.Volumes.Add($"{workspace.WorkspaceRoot.FullName}:/workspace/{workspace.WorkspaceRoot.Name}"); + } + + // Always skip the expensive source build inside Docker. + // For SourceBuild mode, the CLI is installed from a mounted local bundle. + // For PullRequest/GaRelease, it's installed via scripts after container start. + c.BuildArgs["SKIP_SOURCE_BUILD"] = "true"; + + if (installMode == DockerInstallMode.SourceBuild) + { + // Mount the locally-built native AOT CLI binary into the container. + var cliPublishDir = FindLocalCliBinary(repoRoot) + ?? throw new InvalidOperationException("SourceBuild mode detected but CLI binary not found"); + c.Volumes.Add($"{cliPublishDir}:/opt/aspire-cli:ro"); + output.WriteLine($" CLI binary: {cliPublishDir}"); + } + + if (installMode == DockerInstallMode.PullRequest) + { + var ghToken = Environment.GetEnvironmentVariable("GH_TOKEN"); + if (!string.IsNullOrEmpty(ghToken)) + { + c.Environment["GH_TOKEN"] = ghToken; + } + + var prNumber = Environment.GetEnvironmentVariable("GITHUB_PR_NUMBER") ?? ""; + var prSha = Environment.GetEnvironmentVariable("GITHUB_PR_HEAD_SHA") ?? ""; + c.Environment["GITHUB_PR_NUMBER"] = prNumber; + c.Environment["GITHUB_PR_HEAD_SHA"] = prSha; + output.WriteLine($" PR number: {prNumber}"); + output.WriteLine($" PR head SHA: {prSha}"); + } + }); + + return builder.Build(); + } + + /// + /// Sets up the bash prompt tracking inside a Docker container. + /// Docker containers run as root, so the default prompt uses '#' instead of '$'. + /// Optionally changes to the /workspace directory if a workspace is mounted. + /// + /// The sequence builder. + /// The sequence counter. + /// Optional workspace — when provided, cd into /workspace. + internal static Hex1bTerminalInputSequenceBuilder PrepareDockerEnvironment( + this Hex1bTerminalInputSequenceBuilder builder, + SequenceCounter counter, + TemporaryWorkspace? workspace = null) + { + // Docker containers run as root, so bash shows '# ' (not '$ '). + var waitingForContainerReady = new CellPatternSearcher() + .Find("# "); + + builder + .WaitUntil(s => waitingForContainerReady.Search(s).Count > 0, TimeSpan.FromSeconds(60)) + .Wait(500); + + // Set up the same prompt counting mechanism used by all E2E tests. + const string promptSetup = "CMDCOUNT=0; PROMPT_COMMAND='s=$?;((CMDCOUNT++));PS1=\"[$CMDCOUNT $([ $s -eq 0 ] && echo OK || echo ERR:$s)] \\$ \"'"; + + builder + .Type(promptSetup) + .Enter() + .WaitForSuccessPrompt(counter); + + // Set permissive umask so files created by the container (as root) are + // writable by the host-side test process via the volume mount. + builder + .Type("umask 000") + .Enter() + .WaitForSuccessPrompt(counter); + + // Set TERM and other environment variables needed by all Docker tests. + // TERM=xterm is also in the Dockerfile but re-exporting ensures it + // survives any login-shell profile resets. + builder + .Type("export ASPIRE_PLAYGROUND=true TERM=xterm DOTNET_CLI_TELEMETRY_OPTOUT=true DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true DOTNET_GENERATE_ASPNET_CERTIFICATE=false") + .Enter() + .WaitForSuccessPrompt(counter); + + if (workspace is not null) + { + builder + .Type($"cd /workspace/{workspace.WorkspaceRoot.Name}") + .Enter() + .WaitForSuccessPrompt(counter); + } + + return builder; + } + + /// + /// Installs the Aspire CLI inside a Docker container based on the detected install mode. + /// For , the CLI is already installed by the Dockerfile. + /// For , downloads and installs from aspire.dev. + /// For , uses the PR install script. + /// + internal static Hex1bTerminalInputSequenceBuilder InstallAspireCliInDocker( + this Hex1bTerminalInputSequenceBuilder builder, + DockerInstallMode installMode, + SequenceCounter counter) + { + switch (installMode) + { + case DockerInstallMode.SourceBuild: + // Copy the mounted native AOT CLI binary to ~/.aspire/bin and add to PATH. + return builder + .Type("mkdir -p ~/.aspire/bin && cp /opt/aspire-cli/aspire ~/.aspire/bin/aspire && chmod +x ~/.aspire/bin/aspire") + .Enter() + .WaitForSuccessPrompt(counter, TimeSpan.FromSeconds(30)) + .Type("export PATH=~/.aspire/bin:$PATH") + .Enter() + .WaitForSuccessPrompt(counter); + + case DockerInstallMode.GaRelease: + // Install the latest GA release using the script baked into the container image. + return builder + .Type("/opt/aspire-scripts/get-aspire-cli.sh") + .Enter() + .WaitForSuccessPrompt(counter, TimeSpan.FromSeconds(120)) + .Type("export PATH=~/.aspire/bin:$PATH") + .Enter() + .WaitForSuccessPrompt(counter); + + case DockerInstallMode.PullRequest: + var prNumber = GetRequiredPrNumber(); + // Use the local script instead of downloading from raw.githubusercontent.com. + // The PR bundle installs binaries to both ~/.aspire/bin and ~/.aspire. + return builder + .Type($"/opt/aspire-scripts/get-aspire-cli-pr.sh {prNumber}") + .Enter() + .WaitForSuccessPrompt(counter, TimeSpan.FromSeconds(300)) + .Type("export PATH=~/.aspire/bin:~/.aspire:$PATH") + .Enter() + .WaitForSuccessPrompt(counter); + + default: + throw new ArgumentOutOfRangeException(nameof(installMode)); + } + } + + /// + /// Walks up from the test assembly directory to find the repo root (contains Aspire.slnx). + /// + internal static string GetRepoRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + + while (dir is not null) + { + if (File.Exists(Path.Combine(dir.FullName, "Aspire.slnx"))) + { + return dir.FullName; + } + + dir = dir.Parent; + } + + throw new InvalidOperationException( + "Could not find repo root (directory containing Aspire.slnx) " + + $"by walking up from {AppContext.BaseDirectory}"); + } + + /// + /// Converts a host-side path (under the workspace root) to the corresponding + /// container-side path (under /workspace/{workspaceName}). Use this when a path + /// constructed from needs to be + /// used in a command typed into the Docker container terminal. + /// + /// The full host-side path. + /// The workspace whose root is mounted at /workspace/{name}. + /// The equivalent path inside the container. + internal static string ToContainerPath(string hostPath, TemporaryWorkspace workspace) + { + var relativePath = Path.GetRelativePath(workspace.WorkspaceRoot.FullName, hostPath); + return $"/workspace/{workspace.WorkspaceRoot.Name}/" + relativePath.Replace('\\', '/'); + } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs index 61ca6b035b8..5324ebcceda 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/JsReactTemplateTests.cs @@ -17,12 +17,11 @@ public sealed class JsReactTemplateTests(ITestOutputHelper output) [Fact] public async Task CreateAndRunJsReactProject() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -43,14 +42,9 @@ public async Task CreateAndRunJsReactProject() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); sequenceBuilder.AspireNew("AspireJsReactApp", counter, template: AspireTemplate.JsReact, useRedisCache: false); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/LogsCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/LogsCommandTests.cs index 89de66ecb67..c63cdda97c7 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/LogsCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/LogsCommandTests.cs @@ -17,12 +17,12 @@ public sealed class LogsCommandTests(ITestOutputHelper output) [Fact] public async Task LogsCommandShowsResourceLogs() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -48,14 +48,9 @@ public async Task LogsCommandShowsResourceLogs() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create a new project using aspire new sequenceBuilder.AspireNew("AspireLogsTestApp", counter); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/McpDocsE2ETests.cs b/tests/Aspire.Cli.EndToEnd.Tests/McpDocsE2ETests.cs index d08b4c1dca0..9b04b2a2754 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/McpDocsE2ETests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/McpDocsE2ETests.cs @@ -3,7 +3,7 @@ using System.Text.Json; using System.Text.RegularExpressions; -using Aspire.Hosting.Tests; +using Aspire.Cli.EndToEnd.Tests.Helpers; using Aspire.TestUtilities; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; @@ -26,7 +26,7 @@ public partial class McpDocsE2ETests : IAsyncLifetime public async ValueTask InitializeAsync() { - var repoRoot = MSBuildUtils.GetRepoRoot(); + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var cliProjectPath = Path.Combine(repoRoot, "src", "Aspire.Cli", "Aspire.Cli.csproj"); if (!File.Exists(cliProjectPath)) @@ -34,13 +34,7 @@ public async ValueTask InitializeAsync() throw new InvalidOperationException($"Could not find CLI project at: {cliProjectPath}"); } - // Use --no-build when running locally (not in CI) to speed up iteration - var isCi = Environment.GetEnvironmentVariable("CI") == "true" || - Environment.GetEnvironmentVariable("TF_BUILD") == "True"; - - string[] arguments = isCi - ? ["run", "--project", cliProjectPath, "--", "agent", "mcp"] - : ["run", "--project", cliProjectPath, "--no-build", "--", "agent", "mcp"]; + string[] arguments = ["run", "--project", cliProjectPath, "--", "agent", "mcp"]; var options = new StdioClientTransportOptions { diff --git a/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs index 4acdefe7856..a6b921b0ba2 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/MultipleAppHostTests.cs @@ -17,26 +17,21 @@ public sealed class MultipleAppHostTests(ITestOutputHelper output) [Fact] public async Task DetachFormatJsonProducesValidJson() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create a single project using aspire new sequenceBuilder.AspireNew("TestApp", counter); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PlaywrightCliInstallTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PlaywrightCliInstallTests.cs index e09efd911a6..d8a3664deee 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/PlaywrightCliInstallTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/PlaywrightCliInstallTests.cs @@ -3,7 +3,6 @@ using Aspire.Cli.EndToEnd.Tests.Helpers; using Aspire.Cli.Tests.Utils; -using Hex1b; using Hex1b.Automation; using Aspire.TestUtilities; using Xunit; @@ -28,21 +27,11 @@ public sealed class PlaywrightCliInstallTests(ITestOutputHelper output) [Fact] public async Task AgentInit_InstallsPlaywrightCli_AndGeneratesSkillFiles() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - var recordingPath = CliE2ETestHelpers.GetTestResultsRecordingPath( - nameof(AgentInit_InstallsPlaywrightCli_AndGeneratesSkillFiles)); - - var builder = Hex1bTerminal.CreateBuilder() - .WithHeadless() - .WithDimensions(160, 48) - .WithAsciinemaRecording(recordingPath) - .WithPtyProcess("/bin/bash", ["--norc"]); - - using var terminal = builder.Build(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -57,14 +46,9 @@ public async Task AgentInit_InstallsPlaywrightCli_AndGeneratesSkillFiles() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Step 1: Verify playwright-cli is not installed. sequenceBuilder diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ProjectReferenceTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ProjectReferenceTests.cs index 169a918f438..45e89f9ae54 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/ProjectReferenceTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/ProjectReferenceTests.cs @@ -19,12 +19,11 @@ public sealed class ProjectReferenceTests(ITestOutputHelper output) [Fact] public async Task TypeScriptAppHostWithProjectReferenceIntegration() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -45,14 +44,9 @@ public async Task TypeScriptAppHostWithProjectReferenceIntegration() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireBundleFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireBundleEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Step 1: Create a TypeScript AppHost (so we get the sdkVersion in settings.json) sequenceBuilder diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PsCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PsCommandTests.cs index 310d0696263..e073cf8c7c5 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/PsCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/PsCommandTests.cs @@ -17,12 +17,12 @@ public sealed class PsCommandTests(ITestOutputHelper output) [Fact] public async Task PsCommandListsRunningAppHost() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -48,14 +48,9 @@ public async Task PsCommandListsRunningAppHost() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create a new project using aspire new sequenceBuilder.AspireNew("AspirePsTestApp", counter); @@ -115,35 +110,31 @@ public async Task PsCommandListsRunningAppHost() [Fact] public async Task PsFormatJsonOutputsOnlyJsonToStdout() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); var outputFilePath = Path.Combine(workspace.WorkspaceRoot.FullName, "ps-output.json"); + var containerOutputFilePath = CliE2ETestHelpers.ToContainerPath(outputFilePath, workspace); // Run aspire ps --format json with stdout redirected to a file. // Status messages go to stderr (Spectre.Console spinner, cleared on completion), // JSON output goes to stdout (redirected to the file). // We only wait for the success prompt since the Spectre status spinner is // transient and erased before WaitUntil polling can observe it. - sequenceBuilder.Type($"aspire ps --format json > {outputFilePath}") + sequenceBuilder.Type($"aspire ps --format json > {containerOutputFilePath}") .Enter() .WaitForSuccessPrompt(counter); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs index d0e18bd9c7b..ba1dd10b71d 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/PythonReactTemplateTests.cs @@ -17,12 +17,11 @@ public sealed class PythonReactTemplateTests(ITestOutputHelper output) [Fact] public async Task CreateAndRunPythonReactProject() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -37,14 +36,9 @@ public async Task CreateAndRunPythonReactProject() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); sequenceBuilder.AspireNew("AspirePyReactApp", counter, template: AspireTemplate.PythonReact, useRedisCache: false); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/SecretDotNetAppHostTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/SecretDotNetAppHostTests.cs index 058fcac898d..8ca27d32252 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/SecretDotNetAppHostTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/SecretDotNetAppHostTests.cs @@ -16,25 +16,20 @@ public sealed class SecretDotNetAppHostTests(ITestOutputHelper output) [Fact] public async Task SecretCrudOnDotNetAppHost() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create an Empty AppHost project interactively sequenceBuilder.AspireNew("TestSecrets", counter, template: AspireTemplate.EmptyAppHost); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/SecretTypeScriptAppHostTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/SecretTypeScriptAppHostTests.cs index 7172123b457..52d7cc46810 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/SecretTypeScriptAppHostTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/SecretTypeScriptAppHostTests.cs @@ -16,12 +16,11 @@ public sealed class SecretTypeScriptAppHostTests(ITestOutputHelper output) [Fact] public async Task SecretCrudOnTypeScriptAppHost() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -38,13 +37,9 @@ public async Task SecretCrudOnTypeScriptAppHost() var waitingForAppHostCreated = new CellPatternSearcher() .Find("Created apphost.ts"); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Enable polyglot support sequenceBuilder.EnablePolyglotSupport(counter); @@ -58,8 +53,7 @@ public async Task SecretCrudOnTypeScriptAppHost() .WaitUntil(s => waitingForTypeScriptSelected.Search(s).Count > 0, TimeSpan.FromSeconds(5)) .Enter() .WaitUntil(s => waitingForAppHostCreated.Search(s).Count > 0, TimeSpan.FromMinutes(2)) - .DeclineAgentInitPrompt() - .WaitForSuccessPrompt(counter); + .DeclineAgentInitPrompt(counter); // Set secrets using --apphost var waitingForSetSuccess = new CellPatternSearcher() diff --git a/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs index 8858c910081..2635cade807 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/SmokeTests.cs @@ -17,12 +17,12 @@ public sealed class SmokeTests(ITestOutputHelper output) [Fact] public async Task CreateAndRunAspireStarterProject() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -49,14 +49,9 @@ public async Task CreateAndRunAspireStarterProject() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); sequenceBuilder.AspireNew("AspireStarterApp", counter) .Type("aspire run") diff --git a/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs index eb5a9b163bc..fe8d4217ad3 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs @@ -18,26 +18,20 @@ public sealed class StagingChannelTests(ITestOutputHelper output) [Fact] public async Task StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Step 1: Configure staging channel settings via aspire config set // Enable the staging channel feature flag diff --git a/tests/Aspire.Cli.EndToEnd.Tests/StartStopTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/StartStopTests.cs index 0a55d0edaee..7db9cbcdd08 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/StartStopTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/StartStopTests.cs @@ -17,12 +17,12 @@ public sealed class StartStopTests(ITestOutputHelper output) [Fact] public async Task CreateStartAndStopAspireProject() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -36,14 +36,9 @@ public async Task CreateStartAndStopAspireProject() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create a new project using aspire new sequenceBuilder.AspireNew("AspireStarterApp", counter); @@ -79,12 +74,12 @@ public async Task CreateStartAndStopAspireProject() [Fact] public async Task StopWithNoRunningAppHostExitsSuccessfully() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -95,14 +90,9 @@ public async Task StopWithNoRunningAppHostExitsSuccessfully() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Run aspire stop with no running AppHost - should exit with code 0 sequenceBuilder.Type("aspire stop") @@ -124,12 +114,12 @@ public async Task StopWithNoRunningAppHostExitsSuccessfully() [Fact] public async Task AddPackageWhileAppHostRunningDetached() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -143,14 +133,9 @@ public async Task AddPackageWhileAppHostRunningDetached() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create a new project using aspire new sequenceBuilder.AspireNew("AspireAddTestApp", counter); @@ -201,12 +186,12 @@ public async Task AddPackageWhileAppHostRunningDetached() [Fact] public async Task AddPackageInteractiveWhileAppHostRunningDetached() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -226,14 +211,9 @@ public async Task AddPackageInteractiveWhileAppHostRunningDetached() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create a new project using aspire new sequenceBuilder.AspireNew("AspireAddInteractiveApp", counter); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/StopNonInteractiveTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/StopNonInteractiveTests.cs index a07b22e68e9..b282ed04333 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/StopNonInteractiveTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/StopNonInteractiveTests.cs @@ -17,12 +17,12 @@ public sealed class StopNonInteractiveTests(ITestOutputHelper output) [Fact] public async Task StopNonInteractiveSingleAppHost() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -39,14 +39,9 @@ public async Task StopNonInteractiveSingleAppHost() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create a new project using aspire new sequenceBuilder.AspireNew("TestStopApp", counter); @@ -94,12 +89,12 @@ public async Task StopNonInteractiveSingleAppHost() [Fact] public async Task StopAllAppHostsFromAppHostDirectory() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -116,14 +111,9 @@ public async Task StopAllAppHostsFromAppHostDirectory() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create first project sequenceBuilder.AspireNew("App1", counter); @@ -181,12 +171,12 @@ public async Task StopAllAppHostsFromAppHostDirectory() [Fact] public async Task StopAllAppHostsFromUnrelatedDirectory() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -203,14 +193,9 @@ public async Task StopAllAppHostsFromUnrelatedDirectory() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create first project sequenceBuilder.AspireNew("App1", counter); @@ -237,7 +222,7 @@ public async Task StopAllAppHostsFromUnrelatedDirectory() .WaitForSuccessPrompt(counter); // Navigate to workspace root (unrelated to any AppHost directory) - sequenceBuilder.Type($"cd {workspace.WorkspaceRoot.FullName}") + sequenceBuilder.Type($"cd {CliE2ETestHelpers.ToContainerPath(workspace.WorkspaceRoot.FullName, workspace)}") .Enter() .WaitForSuccessPrompt(counter); @@ -273,12 +258,12 @@ public async Task StopAllAppHostsFromUnrelatedDirectory() [Fact] public async Task StopNonInteractiveMultipleAppHostsShowsError() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -295,14 +280,9 @@ public async Task StopNonInteractiveMultipleAppHostsShowsError() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create first project sequenceBuilder.AspireNew("App1", counter); @@ -329,7 +309,7 @@ public async Task StopNonInteractiveMultipleAppHostsShowsError() .WaitForSuccessPrompt(counter); // Navigate to workspace root - sequenceBuilder.Type($"cd {workspace.WorkspaceRoot.FullName}") + sequenceBuilder.Type($"cd {CliE2ETestHelpers.ToContainerPath(workspace.WorkspaceRoot.FullName, workspace)}") .Enter() .WaitForSuccessPrompt(counter); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptCodegenValidationTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptCodegenValidationTests.cs index fd936e69a6b..5e9d4c967c1 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptCodegenValidationTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptCodegenValidationTests.cs @@ -18,13 +18,12 @@ public sealed class TypeScriptCodegenValidationTests(ITestOutputHelper output) [Fact] public async Task RestoreGeneratesSdkFiles() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, workspace: workspace); - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); @@ -39,15 +38,9 @@ public async Task RestoreGeneratesSdkFiles() var waitingForRestoreSuccess = new CellPatternSearcher() .Find("SDK code restored successfully"); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - // Polyglot tests require the bundle because the AppHost server is bundled - sequenceBuilder.InstallAspireBundleFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireBundleEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Enable polyglot support sequenceBuilder.EnablePolyglotSupport(counter); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotTests.cs index 24e534b004d..45b0a49f02c 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptPolyglotTests.cs @@ -17,12 +17,11 @@ public sealed class TypeScriptPolyglotTests(ITestOutputHelper output) [Fact] public async Task CreateTypeScriptAppHostWithViteApp() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -49,16 +48,9 @@ public async Task CreateTypeScriptAppHostWithViteApp() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - // Polyglot tests require the bundle (not just CLI) because the AppHost server - // is bundled and cannot be obtained via NuGet packages in SDK-based fallback mode - sequenceBuilder.InstallAspireBundleFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireBundleEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Enable polyglot support feature flag sequenceBuilder.EnablePolyglotSupport(counter); @@ -73,8 +65,7 @@ public async Task CreateTypeScriptAppHostWithViteApp() .WaitUntil(s => waitingForTypeScriptSelected.Search(s).Count > 0, TimeSpan.FromSeconds(5)) .Enter() // select TypeScript .WaitUntil(s => waitingForAppHostCreated.Search(s).Count > 0, TimeSpan.FromMinutes(2)) - .DeclineAgentInitPrompt() - .WaitForSuccessPrompt(counter); + .DeclineAgentInitPrompt(counter); // Step 2: Create a Vite app using npm create vite // Using --template vanilla-ts for a minimal TypeScript Vite app diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs index d5d2d49ab00..38fde104a2e 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptStarterTemplateTests.cs @@ -18,12 +18,11 @@ public sealed class TypeScriptStarterTemplateTests(ITestOutputHelper output) [Fact] public async Task CreateAndRunTypeScriptStarterProject() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -33,16 +32,9 @@ public async Task CreateAndRunTypeScriptStarterProject() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - // TypeScript starter requires the bundle (not just CLI) because the AppHost server - // is bundled and cannot be obtained via NuGet packages in SDK-based fallback mode - sequenceBuilder.InstallAspireBundleFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireBundleEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Step 1: Create project using aspire new, selecting the Express/React template sequenceBuilder.AspireNew("TsStarterApp", counter, template: AspireTemplate.ExpressReact); diff --git a/tests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs index 586c90e6e6a..7a4d66d1248 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/WaitCommandTests.cs @@ -3,6 +3,7 @@ using Aspire.Cli.EndToEnd.Tests.Helpers; using Aspire.Cli.Tests.Utils; +using Aspire.TestUtilities; using Hex1b.Automation; using Xunit; @@ -15,14 +16,15 @@ namespace Aspire.Cli.EndToEnd.Tests; public sealed class WaitCommandTests(ITestOutputHelper output) { [Fact] + [QuarantinedTest("https://github.com/dotnet/aspire/issues/14993")] public async Task CreateStartWaitAndStopAspireProject() { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); - var prNumber = CliE2ETestHelpers.GetRequiredPrNumber(); - var commitSha = CliE2ETestHelpers.GetRequiredCommitSha(); - var isCI = CliE2ETestHelpers.IsRunningInCI; - using var terminal = CliE2ETestHelpers.CreateTestTerminal(); + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); @@ -39,14 +41,9 @@ public async Task CreateStartWaitAndStopAspireProject() var counter = new SequenceCounter(); var sequenceBuilder = new Hex1bTerminalInputSequenceBuilder(); - sequenceBuilder.PrepareEnvironment(workspace, counter); + sequenceBuilder.PrepareDockerEnvironment(counter, workspace); - if (isCI) - { - sequenceBuilder.InstallAspireCliFromPullRequest(prNumber, counter); - sequenceBuilder.SourceAspireCliEnvironment(counter); - sequenceBuilder.VerifyAspireCliVersion(commitSha, counter); - } + sequenceBuilder.InstallAspireCliInDocker(installMode, counter); // Create a new project using aspire new sequenceBuilder.AspireNew("AspireWaitApp", counter); @@ -62,10 +59,11 @@ public async Task CreateStartWaitAndStopAspireProject() .WaitUntil(s => waitForAppHostStartedSuccessfully.Search(s).Count > 0, TimeSpan.FromMinutes(3)) .WaitForSuccessPrompt(counter); - // Wait for the webfrontend resource to be up (running) - sequenceBuilder.Type("aspire wait webfrontend --status up --timeout 120") + // Wait for the webfrontend resource to be up (running). + // Use a longer timeout in Docker-in-Docker where container startup is slower. + sequenceBuilder.Type("aspire wait webfrontend --status up --timeout 300") .Enter() - .WaitUntil(s => waitForResourceUp.Search(s).Count > 0, TimeSpan.FromMinutes(3)) + .WaitUntil(s => waitForResourceUp.Search(s).Count > 0, TimeSpan.FromMinutes(6)) .WaitForSuccessPrompt(counter); // Stop the AppHost using aspire stop diff --git a/tests/Shared/Docker/Dockerfile.e2e b/tests/Shared/Docker/Dockerfile.e2e new file mode 100644 index 00000000000..883ca5c8d68 --- /dev/null +++ b/tests/Shared/Docker/Dockerfile.e2e @@ -0,0 +1,101 @@ +# Multi-stage Dockerfile for Aspire E2E testing (.NET variant). +# +# Includes: .NET SDK 10.0, Docker CLI (via host socket mount), gh CLI, +# Node.js, Python, Aspire install scripts. +# +# Usage: +# Local dev (build from source): +# BuildContext = repo root, no special build args needed. +# Docker builds all packages and the native AOT CLI from source. +# +# CI (pre-built artifacts): +# Pass SKIP_SOURCE_BUILD=true as a build arg. +# The test code installs from CI artifacts or aspire.dev instead. + +# ============================================================ +# Stage 1: Build Aspire from source +# ============================================================ +# Produces NuGet packages and the native AOT CLI binary. +# Skipped when SKIP_SOURCE_BUILD=true (CI mode). +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build + +ARG SKIP_SOURCE_BUILD=false + +# Install native AOT build toolchain. +RUN if [ "$SKIP_SOURCE_BUILD" != "true" ]; then \ + apt-get update -qq && \ + apt-get install -y --no-install-recommends clang zlib1g-dev && \ + rm -rf /var/lib/apt/lists/*; \ + fi + +WORKDIR /repo + +# Copy everything (filtered by .dockerignore at repo root). +COPY . . + +# Full build: restore, build, pack, then bundle (native AOT CLI). +RUN if [ "$SKIP_SOURCE_BUILD" != "true" ]; then \ + ./restore.sh && \ + ./build.sh --pack -c Release && \ + ./build.sh --bundle -c Release; \ + else \ + mkdir -p /repo/artifacts/bundle; \ + fi + +# ============================================================ +# Stage 2: .NET test runtime environment +# ============================================================ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime + +# --- Common tooling (shared between dotnet and polyglot variants) --- + +# Install gh CLI (needed by get-aspire-cli-pr.sh to download PR artifacts). +RUN apt-get update -qq && \ + apt-get install -y --no-install-recommends gpg docker.io && \ + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + | gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list && \ + apt-get update -qq && \ + apt-get install -y --no-install-recommends gh && \ + rm -rf /var/lib/apt/lists/* + +# Install Node.js (needed for TypeScript/JS templates and polyglot AppHosts). +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y --no-install-recommends nodejs && \ + rm -rf /var/lib/apt/lists/* + +# Install Python (needed for Python templates). +RUN apt-get update -qq && \ + apt-get install -y --no-install-recommends python3 python3-pip python3-venv && \ + rm -rf /var/lib/apt/lists/* + +# --- Aspire CLI setup --- + +# Copy the install scripts. +COPY eng/scripts/get-aspire-cli.sh /opt/aspire-scripts/ +COPY eng/scripts/get-aspire-cli-pr.sh /opt/aspire-scripts/ +RUN chmod +x /opt/aspire-scripts/*.sh + +# Copy the bundle directory from the build stage. +# When SKIP_SOURCE_BUILD=true the directory is empty; the conditional RUN below handles both cases. +COPY --from=build /repo/artifacts/bundle /tmp/bundle + +# Install the CLI from the bundle archive (no-op when the bundle directory is empty). +RUN if ls /tmp/bundle/aspire-*-linux-x64.tar.gz 1>/dev/null 2>&1; then \ + mkdir -p /root/.aspire && \ + tar -xzf /tmp/bundle/aspire-*-linux-x64.tar.gz -C /root/.aspire && \ + rm -f /tmp/bundle/aspire-*-linux-x64.tar.gz; \ + fi + +# Create the workspace mount point. +RUN mkdir -p /workspace +WORKDIR /workspace + +ENV PATH="/root/.aspire/bin:/root/.aspire:${PATH}" +ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 +ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +ENV DOTNET_GENERATE_ASPNET_CERTIFICATE=false +ENV ASPIRE_PLAYGROUND=true +ENV TERM=xterm + diff --git a/tests/Shared/Docker/Dockerfile.e2e-polyglot b/tests/Shared/Docker/Dockerfile.e2e-polyglot new file mode 100644 index 00000000000..e56a6c11a0c --- /dev/null +++ b/tests/Shared/Docker/Dockerfile.e2e-polyglot @@ -0,0 +1,99 @@ +# Multi-stage Dockerfile for Aspire E2E testing (polyglot variant). +# +# Includes: Docker CLI (via host socket mount), gh CLI, +# Node.js, Python, Aspire install scripts. +# Does NOT include .NET SDK — for TypeScript-only AppHost tests. +# +# Usage: +# Local dev (build from source): +# BuildContext = repo root, no special build args needed. +# Docker builds all packages and the native AOT CLI from source. +# +# CI (pre-built artifacts): +# Pass SKIP_SOURCE_BUILD=true as a build arg. +# The test code installs from CI artifacts or aspire.dev instead. + +# ============================================================ +# Stage 1: Build Aspire from source +# ============================================================ +# Produces NuGet packages and the native AOT CLI binary. +# Skipped when SKIP_SOURCE_BUILD=true (CI mode). +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build + +ARG SKIP_SOURCE_BUILD=false + +# Install native AOT build toolchain. +RUN if [ "$SKIP_SOURCE_BUILD" != "true" ]; then \ + apt-get update -qq && \ + apt-get install -y --no-install-recommends clang zlib1g-dev && \ + rm -rf /var/lib/apt/lists/*; \ + fi + +WORKDIR /repo + +# Copy everything (filtered by .dockerignore at repo root). +COPY . . + +# Full build: restore, build, pack, then bundle (native AOT CLI). +RUN if [ "$SKIP_SOURCE_BUILD" != "true" ]; then \ + ./restore.sh && \ + ./build.sh --pack -c Release && \ + ./build.sh --bundle -c Release; \ + else \ + mkdir -p /repo/artifacts/bundle; \ + fi + +# ============================================================ +# Stage 2: Polyglot test runtime environment (no .NET SDK) +# ============================================================ +FROM ubuntu:24.04 AS runtime + +# Install base tools and Docker CLI. +RUN apt-get update -qq && \ + apt-get install -y --no-install-recommends \ + ca-certificates curl gpg docker.io git libicu74 && \ + rm -rf /var/lib/apt/lists/* + +# Install gh CLI (needed by get-aspire-cli-pr.sh to download PR artifacts). +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + | gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list && \ + apt-get update -qq && \ + apt-get install -y --no-install-recommends gh && \ + rm -rf /var/lib/apt/lists/* + +# Install Node.js (needed for TypeScript AppHosts and JS templates). +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y --no-install-recommends nodejs && \ + rm -rf /var/lib/apt/lists/* + +# Install Python (needed for Python templates). +RUN apt-get update -qq && \ + apt-get install -y --no-install-recommends python3 python3-pip python3-venv && \ + rm -rf /var/lib/apt/lists/* + +# --- Aspire CLI setup --- + +# Copy the install scripts. +COPY eng/scripts/get-aspire-cli.sh /opt/aspire-scripts/ +COPY eng/scripts/get-aspire-cli-pr.sh /opt/aspire-scripts/ +RUN chmod +x /opt/aspire-scripts/*.sh + +# Copy the bundle archive from the build stage. +COPY --from=build /repo/artifacts/bundle/aspire-*-linux-x64.tar.gz /tmp/ + +# Install the CLI from the bundle archive (no-op when SKIP_SOURCE_BUILD=true). +RUN if ls /tmp/aspire-*-linux-x64.tar.gz 1>/dev/null 2>&1; then \ + mkdir -p /root/.aspire && \ + tar -xzf /tmp/aspire-*-linux-x64.tar.gz -C /root/.aspire && \ + rm -f /tmp/aspire-*-linux-x64.tar.gz; \ + fi + +# Create the workspace mount point. +RUN mkdir -p /workspace +WORKDIR /workspace + +ENV PATH="/root/.aspire/bin:/root/.aspire:${PATH}" +ENV ASPIRE_PLAYGROUND=true +ENV TERM=xterm diff --git a/tests/Shared/Hex1bTestHelpers.cs b/tests/Shared/Hex1bTestHelpers.cs index 13eac06dca7..a468815838b 100644 --- a/tests/Shared/Hex1bTestHelpers.cs +++ b/tests/Shared/Hex1bTestHelpers.cs @@ -276,8 +276,10 @@ internal static Hex1bTerminalInputSequenceBuilder ExecuteCallback( } /// - /// Handles the agent init confirmation prompt that appears after aspire init or aspire new. - /// Declines the prompt so the command exits cleanly. + /// Declines the agent init confirmation prompt that appears after aspire init or aspire new. + /// Does NOT wait for the shell success prompt — callers must chain their own + /// when using this overload. + /// Used by deployment tests that need custom timeouts for WaitForSuccessPrompt. /// /// The sequence builder. /// @@ -299,6 +301,72 @@ internal static Hex1bTerminalInputSequenceBuilder DeclineAgentInitPrompt( .Enter(); } + /// + /// Handles the agent init confirmation prompt that appears after aspire init or aspire new, + /// then waits for the shell success prompt. Supports CLI versions both with and without agent init chaining. + /// Replaces the separate .DeclineAgentInitPrompt().WaitForSuccessPrompt(counter) pattern. + /// + /// The sequence builder. + /// The sequence counter for prompt detection. + /// + /// Maximum time to wait for the command to complete. Defaults to 500 seconds to match + /// . Must be long enough to cover project creation time. + /// + internal static Hex1bTerminalInputSequenceBuilder DeclineAgentInitPrompt( + this Hex1bTerminalInputSequenceBuilder builder, + SequenceCounter counter, + TimeSpan? timeout = null) + { + var effectiveTimeout = timeout ?? TimeSpan.FromSeconds(500); + + var agentInitPrompt = new CellPatternSearcher() + .Find("configure AI agent environments"); + + var agentInitFound = false; + + return builder + // Wait for either the agent init prompt (new CLI) or the success prompt (old CLI). + // Uses the full timeout since aspire new project creation can take minutes. + .WaitUntil(s => + { + if (agentInitPrompt.Search(s).Count > 0) + { + agentInitFound = true; + return true; + } + var successSearcher = new CellPatternSearcher() + .FindPattern(counter.Value.ToString()) + .RightText(" OK] $ "); + return successSearcher.Search(s).Count > 0; + }, effectiveTimeout) + .Wait(500) + // Type 'n' + Enter unconditionally: + // - Agent init: declines the prompt, CLI exits, success prompt appears + // - No agent init: 'n' runs at bash (command not found), produces error prompt + .Type("n") + .Enter() + // Wait for the aspire command's success prompt (already on screen or appears after decline) + .WaitUntil(s => + { + var successSearcher = new CellPatternSearcher() + .FindPattern(counter.Value.ToString()) + .RightText(" OK] $ "); + return successSearcher.Search(s).Count > 0; + }, effectiveTimeout) + // Increment counter correctly for both cases: + // - Agent init: one increment for the aspire command + // - No agent init: two increments (aspire command + the 'n' error command) + .WaitUntil(_ => + { + if (!agentInitFound) + { + counter.Increment(); + } + counter.Increment(); + return true; + }, TimeSpan.FromSeconds(1)); + } + /// /// Runs aspire new interactively, selecting the specified template and responding to all prompts. /// This centralizes the multi-step interactive flow so that changes to aspire new prompts @@ -426,11 +494,8 @@ internal static Hex1bTerminalInputSequenceBuilder AspireNew( .Enter(); // Accept default "No" } - // Step 8: Decline the agent init prompt - builder.DeclineAgentInitPrompt(); - - // Wait for project creation to complete - return builder.WaitForSuccessPrompt(counter); + // Step 8: Decline the agent init prompt and wait for success + return builder.DeclineAgentInitPrompt(counter); } ///