diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index 0276ca8369c..8803768083c 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -138,13 +138,11 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell await _certificateService.EnsureCertificatesTrustedAsync(_runner, cancellationToken); - var shouldBuildAppHostInExtension = await ShouldBuildAppHostInExtensionAsync(InteractionService, isSingleFileAppHost, cancellationToken); - var watch = !isSingleFileAppHost && (_features.IsFeatureEnabled(KnownFeatures.DefaultWatchEnabled, defaultValue: false) || (isExtensionHost && !startDebugSession)); if (!watch) { - if (!isSingleFileAppHost || isExtensionHost) + if (!isSingleFileAppHost && !isExtensionHost) { var buildOptions = new DotNetCliRunnerInvocationOptions { @@ -152,17 +150,13 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell StandardErrorCallback = buildOutputCollector.AppendError, }; - // The extension host will build the app host project itself, so we don't need to do it here if host exists. - if (!shouldBuildAppHostInExtension) - { - var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, InteractionService, effectiveAppHostFile, buildOptions, ExecutionContext.WorkingDirectory, cancellationToken); + var buildExitCode = await AppHostHelper.BuildAppHostAsync(_runner, InteractionService, effectiveAppHostFile, buildOptions, ExecutionContext.WorkingDirectory, cancellationToken); - if (buildExitCode != 0) - { - InteractionService.DisplayLines(buildOutputCollector.GetLines()); - InteractionService.DisplayError(InteractionServiceStrings.ProjectCouldNotBeBuilt); - return ExitCodeConstants.FailedToBuildArtifacts; - } + if (buildExitCode != 0) + { + InteractionService.DisplayLines(buildOutputCollector.GetLines()); + InteractionService.DisplayError(InteractionServiceStrings.ProjectCouldNotBeBuilt); + return ExitCodeConstants.FailedToBuildArtifacts; } } } @@ -224,7 +218,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell cancellationToken); // Wait for the backchannel to be established. - var backchannel = await InteractionService.ShowStatusAsync(shouldBuildAppHostInExtension ? InteractionServiceStrings.BuildingAppHost : RunCommandStrings.ConnectingToAppHost, async () => { return await backchannelCompletitionSource.Task.WaitAsync(cancellationToken); }); + var backchannel = await InteractionService.ShowStatusAsync(InteractionServiceStrings.BuildingAppHost, async () => { return await backchannelCompletitionSource.Task.WaitAsync(cancellationToken); }); var logFile = GetAppHostLogFile(); @@ -491,11 +485,4 @@ public void ProcessResourceState(RpcResourceState resourceState, Action ShouldBuildAppHostInExtensionAsync(IInteractionService interactionService, bool isSingleFileAppHost, CancellationToken cancellationToken) - { - return ExtensionHelper.IsExtensionHost(interactionService, out _, out var extensionBackchannel) - && await extensionBackchannel.HasCapabilityAsync(KnownCapabilities.DevKit, cancellationToken) - && !isSingleFileAppHost; - } } diff --git a/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs index 797e14156e3..dec456abf28 100644 --- a/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs @@ -491,6 +491,69 @@ public async Task RunCommand_SkipsBuild_WhenExtensionDevKitCapabilityIsAvailable Assert.False(buildCalled, "Build should be skipped when extension DevKit capability is available."); } + [Fact] + public async Task RunCommand_SkipsBuild_WhenRunningInExtension() + { + var buildCalled = false; + + var extensionBackchannel = new TestExtensionBackchannel(); + extensionBackchannel.GetCapabilitiesAsyncCallback = ct => Task.FromResult(Array.Empty()); + + var appHostBackchannel = new TestAppHostBackchannel(); + appHostBackchannel.GetDashboardUrlsAsyncCallback = (ct) => Task.FromResult(new DashboardUrlsState + { + DashboardHealthy = true, + BaseUrlWithLoginToken = "http://localhost/dashboard", + CodespacesUrlWithLoginToken = null + }); + appHostBackchannel.GetAppHostLogEntriesAsyncCallback = ReturnLogEntriesUntilCancelledAsync; + + var backchannelFactory = (IServiceProvider sp) => appHostBackchannel; + + var extensionInteractionServiceFactory = (IServiceProvider sp) => new TestExtensionInteractionService(sp); + + var runnerFactory = (IServiceProvider sp) => { + var runner = new TestDotNetCliRunner(); + runner.CheckHttpCertificateAsyncCallback = (options, ct) => 0; + runner.BuildAsyncCallback = (projectFile, options, ct) => { + buildCalled = true; + return 0; + }; + runner.GetAppHostInformationAsyncCallback = (projectFile, options, ct) => (0, true, VersionHelper.GetDefaultTemplateVersion()); + runner.RunAsyncCallback = async (projectFile, watch, noBuild, args, env, backchannelCompletionSource, options, ct) => { + var backchannel = sp.GetRequiredService(); + backchannelCompletionSource!.SetResult(backchannel); + await Task.Delay(Timeout.InfiniteTimeSpan, ct); + return 0; + }; + return runner; + }; + + var projectLocatorFactory = (IServiceProvider sp) => new TestProjectLocator(); + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.ProjectLocatorFactory = projectLocatorFactory; + options.AppHostBackchannelFactory = backchannelFactory; + options.DotNetCliRunnerFactory = runnerFactory; + options.ExtensionBackchannelFactory = _ => extensionBackchannel; + options.InteractionServiceFactory = extensionInteractionServiceFactory; + }); + + var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("run"); + + using var cts = new CancellationTokenSource(); + var pendingRun = result.InvokeAsync(cancellationToken: cts.Token); + cts.Cancel(); + var exitCode = await pendingRun.WaitAsync(CliTestConstants.DefaultTimeout); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + Assert.False(buildCalled, "Build should be skipped when running in extension."); + } + [Fact] public async Task RunCommand_WhenSingleFileAppHostAndDefaultWatchEnabled_DoesNotUseWatchMode() {