Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Aspire.Cli/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Aspire.Cli.Telemetry;
using Aspire.Cli.Templating;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Configuration;
using NuGetPackage = Aspire.Shared.NuGetPackageCli;
using Semver;
using Spectre.Console;
Expand Down Expand Up @@ -84,7 +85,8 @@ public InitCommand(
ILanguageDiscovery languageDiscovery,
IScaffoldingService scaffoldingService,
AgentInitCommand agentInitCommand,
ICliHostEnvironment hostEnvironment)
ICliHostEnvironment hostEnvironment,
IConfiguration configuration)
: base("init", InitCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry)
{
_runner = runner;
Expand All @@ -107,7 +109,7 @@ public InitCommand(
Options.Add(s_versionOption);

// Customize description based on whether staging channel is enabled
var isStagingEnabled = features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false);
var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(features, configuration);
_channelOption = new Option<string?>("--channel")
{
Description = isStagingEnabled
Expand Down
6 changes: 4 additions & 2 deletions src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Aspire.Cli.Telemetry;
using Aspire.Cli.Templating;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Configuration;
using Spectre.Console;
using NuGetPackage = Aspire.Shared.NuGetPackageCli;

Expand Down Expand Up @@ -74,7 +75,8 @@ public NewCommand(
IPackagingService packagingService,
IConfigurationService configurationService,
AgentInitCommand agentInitCommand,
ICliHostEnvironment hostEnvironment)
ICliHostEnvironment hostEnvironment,
IConfiguration configuration)
: base("new", NewCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry)
{
_prompter = prompter;
Expand All @@ -91,7 +93,7 @@ public NewCommand(
Options.Add(s_versionOption);

// Customize description based on whether staging channel is enabled
var isStagingEnabled = _features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false);
var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(_features, configuration);
_channelOption = new Option<string?>("--channel")
{
Description = isStagingEnabled
Expand Down
10 changes: 7 additions & 3 deletions src/Aspire.Cli/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Aspire.Cli.Resources;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Spectre.Console;

Expand All @@ -30,6 +31,7 @@ internal sealed class UpdateCommand : BaseCommand
private readonly ICliUpdateNotifier _updateNotifier;
private readonly IFeatures _features;
private readonly IConfigurationService _configurationService;
private readonly IConfiguration _configuration;

private static readonly OptionWithLegacy<FileInfo?> s_appHostOption = new("--apphost", "--project", UpdateCommandStrings.ProjectArgumentDescription);
private static readonly Option<bool> s_selfOption = new("--self")
Expand All @@ -50,7 +52,8 @@ public UpdateCommand(
ICliUpdateNotifier updateNotifier,
CliExecutionContext executionContext,
IConfigurationService configurationService,
AspireCliTelemetry telemetry)
AspireCliTelemetry telemetry,
IConfiguration configuration)
: base("update", UpdateCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry)
{
_projectLocator = projectLocator;
Expand All @@ -61,12 +64,13 @@ public UpdateCommand(
_updateNotifier = updateNotifier;
_features = features;
_configurationService = configurationService;
_configuration = configuration;

Options.Add(s_appHostOption);
Options.Add(s_selfOption);

// Customize description based on whether staging channel is enabled
var isStagingEnabled = _features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false);
var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(_features, _configuration);

_channelOption = new Option<string?>("--channel")
{
Expand Down Expand Up @@ -270,7 +274,7 @@ private async Task<int> ExecuteSelfUpdateAsync(ParseResult parseResult, Cancella
// for future 'aspire new' and 'aspire init' commands.
if (string.IsNullOrEmpty(channel))
{
var isStagingEnabled = _features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false);
var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(_features, _configuration);
var channels = isStagingEnabled
? new[] { PackageChannelNames.Stable, PackageChannelNames.Staging, PackageChannelNames.Daily }
: new[] { PackageChannelNames.Stable, PackageChannelNames.Daily };
Expand Down
19 changes: 19 additions & 0 deletions src/Aspire.Cli/KnownFeatures.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Cli.Configuration;
using Aspire.Cli.Packaging;
using Microsoft.Extensions.Configuration;

namespace Aspire.Cli;

/// <summary>
Expand Down Expand Up @@ -102,4 +106,19 @@ public static IEnumerable<string> GetAllFeatureNames()
{
return s_featureMetadata.Keys.OrderBy(name => name);
}

/// <summary>
/// Determines whether the staging channel is enabled by checking both the feature flag
/// and the configured channel. The staging channel is considered enabled if either the
/// <see cref="StagingChannelEnabled"/> feature flag is <c>true</c>, or the configured
/// channel is set to <c>"staging"</c>.
/// </summary>
/// <param name="features">The feature flags service.</param>
/// <param name="configuration">The configuration to check for the channel setting.</param>
/// <returns><c>true</c> if the staging channel should be available; otherwise, <c>false</c>.</returns>
public static bool IsStagingChannelEnabled(IFeatures features, IConfiguration configuration)
{
return features.IsFeatureEnabled(StagingChannelEnabled, false)
|| string.Equals(configuration["channel"], PackageChannelNames.Staging, StringComparison.OrdinalIgnoreCase);
}
}
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Packaging/PackagingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public Task<IEnumerable<PackageChannel>> GetChannelsAsync(CancellationToken canc
var channels = new List<PackageChannel>([defaultChannel, stableChannel]);

// Add staging channel if feature is enabled (after stable, before daily)
if (features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false))
if (KnownFeatures.IsStagingChannelEnabled(features, configuration))
{
var stagingChannel = CreateStagingChannel();
if (stagingChannel is not null)
Expand Down
11 changes: 3 additions & 8 deletions tests/Aspire.Cli.EndToEnd.Tests/StagingChannelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@ public async Task StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels()
await auto.InstallAspireCliInDockerAsync(installMode, counter);

// Step 1: Configure staging channel settings via aspire config set
// Enable the staging channel feature flag
await auto.TypeAsync("aspire config set features.stagingChannelEnabled true -g");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);
// Note: we do NOT need to enable features.stagingChannelEnabled — setting channel
// to staging is sufficient to enable the staging channel behavior.

// Set quality to Prerelease (triggers shared feed mode)
await auto.TypeAsync("aspire config set overrideStagingQuality Prerelease -g");
Expand All @@ -49,7 +47,7 @@ public async Task StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels()
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);

// Set channel to staging
// Set channel to staging — this alone enables staging channel behavior
await auto.TypeAsync("aspire config set channel staging -g");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);
Comment on lines 36 to 53
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is meant to validate staging works without features.stagingChannelEnabled, but it never explicitly ensures the flag is unset/false. To avoid accidental false positives if the default/fixture environment changes, consider deleting the flag (or setting it to false) at the start of the test before setting channel to staging.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -106,9 +104,6 @@ public async Task StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels()
await auto.WaitForSuccessPromptAsync(counter);

// Clean up: remove staging settings to avoid polluting other tests
await auto.TypeAsync("aspire config delete features.stagingChannelEnabled -g");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);
await auto.TypeAsync("aspire config delete overrideStagingQuality -g");
await auto.EnterAsync();
await auto.WaitForSuccessPromptAsync(counter);
Expand Down
109 changes: 109 additions & 0 deletions tests/Aspire.Cli.Tests/Configuration/KnownFeaturesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Cli.Configuration;
using Aspire.Cli.Packaging;
using Microsoft.Extensions.Configuration;

namespace Aspire.Cli.Tests.Configuration;

public class KnownFeaturesTests
{
[Fact]
public void IsStagingChannelEnabled_ReturnsTrue_WhenChannelIsStaging()
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: PackageChannelNames.Staging);

Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}
Comment on lines +12 to +19
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test IsStagingChannelEnabled_ReturnsTrue_WhenChannelIsStaging duplicates IsStagingChannelEnabled_ReturnsTrue_WhenChannelIsStagingAndFlagExplicitlyFalse (same inputs/expectation). Consider removing one or consolidating into a single theory case to keep the suite focused on distinct behaviors.

Copilot uses AI. Check for mistakes.

[Fact]
public void IsStagingChannelEnabled_ReturnsTrue_WhenFeatureFlagIsTrue()
{
var features = new TestFeatures(stagingChannelEnabled: true);
var configuration = BuildConfiguration(channel: PackageChannelNames.Stable);

Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}

[Fact]
public void IsStagingChannelEnabled_ReturnsTrue_WhenChannelIsStagingAndFlagExplicitlyFalse()
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: PackageChannelNames.Staging);

Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}

[Fact]
Comment on lines +31 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is identical to IsStagingChannelEnabled_ReturnsTrue_WhenChannelIsStaging above — both set stagingChannelEnabled: false and channel: PackageChannelNames.Staging. Consider removing one or making this test meaningfully different (e.g., both flag=true AND channel=staging to test the "both enabled" case).

public void IsStagingChannelEnabled_ReturnsFalse_WhenChannelIsNotStagingAndFlagNotSet()
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: PackageChannelNames.Stable);

Assert.False(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}

[Fact]
public void IsStagingChannelEnabled_ReturnsFalse_WhenChannelIsNullAndFlagNotSet()
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: null);

Assert.False(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}

[Fact]
public void IsStagingChannelEnabled_IsCaseInsensitive_ForChannelValue()
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: "Staging");

Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}

[Fact]
public void IsStagingChannelEnabled_IsCaseInsensitive_ForUppercaseChannelValue()
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: "STAGING");

Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}

[Fact]
Comment on lines +57 to +75
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two case-insensitivity tests ("Staging" and "STAGING") cover the same behavior. Consider using a single [Theory] with multiple InlineData values (or keep one) to reduce duplication while still validating OrdinalIgnoreCase behavior.

Suggested change
[Fact]
public void IsStagingChannelEnabled_IsCaseInsensitive_ForChannelValue()
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: "Staging");
Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}
[Fact]
public void IsStagingChannelEnabled_IsCaseInsensitive_ForUppercaseChannelValue()
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: "STAGING");
Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}
[Fact]
[Theory]
[InlineData("Staging")]
[InlineData("STAGING")]
public void IsStagingChannelEnabled_IsCaseInsensitive_ForChannelValue(string channel)
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: channel);
Assert.True(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}
[Fact]
public void IsStagingChannelEnabled_ReturnsFalse_WhenChannelIsDailyAndFlagNotSet()
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: PackageChannelNames.Daily);
Assert.False(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}

Copilot uses AI. Check for mistakes.
public void IsStagingChannelEnabled_ReturnsFalse_WhenChannelIsDailyAndFlagNotSet()
{
var features = new TestFeatures(stagingChannelEnabled: false);
var configuration = BuildConfiguration(channel: PackageChannelNames.Daily);

Assert.False(KnownFeatures.IsStagingChannelEnabled(features, configuration));
}

private static IConfiguration BuildConfiguration(string? channel)
{
var configData = new Dictionary<string, string?>();

if (channel is not null)
{
configData["channel"] = channel;
}

return new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();
}

private sealed class TestFeatures(bool stagingChannelEnabled) : IFeatures
{
public bool IsFeatureEnabled(string featureFlag, bool defaultValue)
{
return featureFlag switch
{
"stagingChannelEnabled" => stagingChannelEnabled,
_ => defaultValue
Comment on lines +101 to +105
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor fragility: this hardcodes the string "stagingChannelEnabled" instead of using KnownFeatures.StagingChannelEnabled. If the constant ever changes, the switch would silently fall through to _ => defaultValue, and the feature-flag-only test (IsStagingChannelEnabled_ReturnsTrue_WhenFeatureFlagIsTrue) would still pass — for the wrong reason.

Suggested change
{
return featureFlag switch
{
"stagingChannelEnabled" => stagingChannelEnabled,
_ => defaultValue
return featureFlag switch
{
KnownFeatures.StagingChannelEnabled => stagingChannelEnabled,
_ => defaultValue
};

};
}
}
}
Loading