diff --git a/src/Aspire.Cli/Commands/RootCommand.cs b/src/Aspire.Cli/Commands/RootCommand.cs index db956046a9a..6126c8d8761 100644 --- a/src/Aspire.Cli/Commands/RootCommand.cs +++ b/src/Aspire.Cli/Commands/RootCommand.cs @@ -10,6 +10,7 @@ using System.Diagnostics; #endif +using Aspire.Cli.Bundles; using Aspire.Cli.Commands.Sdk; using Aspire.Cli.Configuration; using Aspire.Cli.Interaction; @@ -131,6 +132,7 @@ public RootCommand( SdkCommand sdkCommand, SetupCommand setupCommand, ExtensionInternalCommand extensionInternalCommand, + IBundleService bundleService, IFeatures featureFlags, IInteractionService interactionService) : base(RootCommandStrings.Description) @@ -208,7 +210,11 @@ public RootCommand( Subcommands.Add(agentCommand); Subcommands.Add(telemetryCommand); Subcommands.Add(docsCommand); - Subcommands.Add(setupCommand); + + if (bundleService.IsBundle) + { + Subcommands.Add(setupCommand); + } if (featureFlags.IsFeatureEnabled(KnownFeatures.ExecCommandEnabled, false)) { diff --git a/tests/Aspire.Cli.Tests/Commands/RootCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/RootCommandTests.cs index c58ec07c433..5a11b6da189 100644 --- a/tests/Aspire.Cli.Tests/Commands/RootCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/RootCommandTests.cs @@ -344,4 +344,33 @@ public async Task InformationalFlag_DoesNotCreateSentinel_OnSubsequentFirstRun() Assert.True(sentinel.WasCreated); } + [Fact] + public void SetupCommand_NotAvailable_WhenBundleIsNotAvailable() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var hasSetupCommand = command.Subcommands.Any(cmd => cmd.Name == "setup"); + + Assert.False(hasSetupCommand); + } + + [Fact] + public void SetupCommand_Available_WhenBundleIsAvailable() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.BundleServiceFactory = _ => new TestBundleService(isBundle: true); + }); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var hasSetupCommand = command.Subcommands.Any(cmd => cmd.Name == "setup"); + + Assert.True(hasSetupCommand); + } + } diff --git a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs index 5ef18fe61ad..7b746e42e66 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs @@ -130,7 +130,7 @@ public static IServiceCollection CreateServiceCollection(TemporaryWorkspace work // Bundle layout services - return null/no-op implementations to trigger SDK mode fallback // This ensures backward compatibility: no layout found = use legacy SDK mode services.AddSingleton(options.LayoutDiscoveryFactory); - services.AddSingleton(); + services.AddSingleton(options.BundleServiceFactory); services.AddSingleton(); // AppHost project handlers - must match Program.cs registration pattern @@ -501,6 +501,9 @@ public ISolutionLocator CreateDefaultSolutionLocatorFactory(IServiceProvider ser // Layout discovery - returns null by default (no bundle layout), causing SDK mode fallback public Func LayoutDiscoveryFactory { get; set; } = _ => new NullLayoutDiscovery(); + // Bundle service - returns no-op implementation by default (no embedded bundle) + public Func BundleServiceFactory { get; set; } = _ => new NullBundleService(); + public Func McpServerTransportFactory { get; set; } = (IServiceProvider serviceProvider) => { var loggerFactory = serviceProvider.GetService(); @@ -553,6 +556,22 @@ public Task ExtractAsync(string destinationPath, bool force => Task.FromResult(null); } +/// +/// A configurable bundle service for testing bundle-dependent behavior. +/// +internal sealed class TestBundleService(bool isBundle) : IBundleService +{ + public bool IsBundle => isBundle; + + public Task EnsureExtractedAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; + + public Task ExtractAsync(string destinationPath, bool force = false, CancellationToken cancellationToken = default) + => Task.FromResult(isBundle ? BundleExtractResult.AlreadyUpToDate : BundleExtractResult.NoPayload); + + public Task EnsureExtractedAndGetLayoutAsync(CancellationToken cancellationToken = default) + => Task.FromResult(null); +} + internal sealed class TestOutputTextWriter : TextWriter { private readonly ITestOutputHelper _outputHelper;