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);
}
///