// 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.Commands;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.InternalTesting;
namespace Aspire.Cli.Tests.Commands;
public class RootCommandTests(ITestOutputHelper outputHelper)
{
[Fact]
public async Task RootCommandVersionOption_PrintsIdentityVersion_WhenIdentityOverridden()
{
// Emulates `ASPIRE_CLI_VERSION=13.4.2` so `--version` must report the identity version,
// not the assembly's build-time stamp.
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.CliExecutionContextFactory = _ => TestExecutionContextHelper.CreateExecutionContext(
workspace.WorkspaceRoot,
identityVersion: "13.4.2",
identityOverridden: true);
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("--version");
var output = new StringWriter();
var exitCode = await result.InvokeAsync(new System.CommandLine.InvocationConfiguration { Output = output }).DefaultTimeout();
Assert.Equal(0, exitCode);
Assert.Equal("13.4.2", output.ToString().Trim());
}
[Fact]
public async Task RootCommandVersionShortAlias_PrintsIdentityVersion_WhenIdentityOverridden()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.CliExecutionContextFactory = _ => TestExecutionContextHelper.CreateExecutionContext(
workspace.WorkspaceRoot,
identityVersion: "13.4.2",
identityOverridden: true);
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("-v");
var output = new StringWriter();
var exitCode = await result.InvokeAsync(new System.CommandLine.InvocationConfiguration { Output = output }).DefaultTimeout();
Assert.Equal(0, exitCode);
Assert.Equal("13.4.2", output.ToString().Trim());
}
[Fact]
public async Task RootCommandVersionOption_IncludesCommitSha_WhenCommitProvided()
{
// When a commit SHA is provided, it should be included in the version output as "+<sha>".
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.CliExecutionContextFactory = _ => TestExecutionContextHelper.CreateExecutionContext(
workspace.WorkspaceRoot,
identityVersion: "13.4.2",
identityCommit: "abcdef01",
identityOverridden: true);
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("--version");
var output = new StringWriter();
var exitCode = await result.InvokeAsync(new System.CommandLine.InvocationConfiguration { Output = output }).DefaultTimeout();
Assert.Equal(0, exitCode);
Assert.Equal("13.4.2+abcdef01", output.ToString().Trim());
}
/// <summary>
/// Theory test validating version output across all build stages/channels:
/// PR builds, daily builds, staging builds, and stable builds.
/// Tests various override combinations (env vars, sidecar) to ensure the output
/// matches the legacy behavior (version + optional +sha).
/// </summary>
[Theory]
[InlineData("pr-18087", "13.5.0-preview.1.26318.5", "abc123def456", "13.5.0-preview.1.26318.5+abc123def456")]
[InlineData("pr-18087", "13.5.0-preview.1.26318.5", null, "13.5.0-preview.1.26318.5")]
[InlineData("daily", "13.5.0-preview.1.26318.1", "95f0d296", "13.5.0-preview.1.26318.1+95f0d296")]
[InlineData("daily", "13.5.0-preview.1.26318.1", null, "13.5.0-preview.1.26318.1")]
[InlineData("staging", "13.4.0-preview.1.26280.6", "abcdef01", "13.4.0-preview.1.26280.6+abcdef01")]
[InlineData("staging", "13.4.0", "abcdef01", "13.4.0+abcdef01")]
[InlineData("stable", "13.4.5", "73114e86c64aeb9f3f3c7da8e37df1ae4281b27e", "13.4.5+73114e86c64aeb9f3f3c7da8e37df1ae4281b27e")]
[InlineData("local", "13.5.0-dev", "localcommit123", "13.5.0-dev+localcommit123")]
[InlineData("local", "13.5.0-dev", null, "13.5.0-dev")]
// IdentityVersion may already carry a "+<sha>" build-metadata suffix (e.g. ASPIRE_CLI_VERSION
// set to a full informational version). The output must keep a single "+" and never double it.
[InlineData("stable", "13.4.5+73114e86", "73114e86", "13.4.5+73114e86")]
[InlineData("stable", "13.4.5+73114e86", null, "13.4.5+73114e86")]
// When the version carries an embedded "+<sha>" AND an explicit commit is provided, the
// explicit commit must win and the embedded sha must be discarded (single "+", explicit value).
[InlineData("stable", "13.4.5+aaaaaaaa", "bbbbbbbb", "13.4.5+bbbbbbbb")]
public async Task RootCommandVersion_ProducesCorrectOutput_AcrossBuildStages(
string channel,
string version,
string? commit,
string expectedOutput)
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.CliExecutionContextFactory = _ => TestExecutionContextHelper.CreateExecutionContext(
workspace.WorkspaceRoot,
identityChannel: channel,
identityVersion: version,
identityCommit: commit,
identityOverridden: true);
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("--version");
var output = new StringWriter();
var exitCode = await result.InvokeAsync(new System.CommandLine.InvocationConfiguration { Output = output }).DefaultTimeout();
Assert.Equal(0, exitCode);
Assert.Equal(expectedOutput, output.ToString().Trim());
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task IdentityOverrideNotice_UsesDedicatedNoticePolicyInsteadOfGenericOverrideState(bool noticeRequired)
{
// Both rows deliberately keep IdentityOverridden=true. Installed sidecar identity needs
// the same non-source-tree behavior as explicit emulation, but only the latter should warn.
// This display-path test catches a tempting regression where startup UI is changed back to
// checking the broader IdentityOverridden property.
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var errorWriter = new StringWriter();
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = true };
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DisableAnsi = true;
options.ErrorTextWriter = errorWriter;
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.CliExecutionContextFactory = _ => workspace.CreateExecutionContext(
identityChannel: "staging",
identityVersion: "13.5.0",
identityOverridden: true,
identityOverrideNoticeRequired: noticeRequired);
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, []);
var errorOutput = errorWriter.ToString();
if (noticeRequired)
{
Assert.Contains(
"Aspire CLI is emulating identity 'staging' version '13.5.0'",
errorOutput,
StringComparison.Ordinal);
}
else
{
Assert.Equal(string.Empty, errorOutput);
}
}
[Fact]
public async Task RootCommandWithHelpArgumentReturnsZero()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("--help");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task RootCommandWithNoLogoArgumentReturnsZero()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("--nologo --help");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task CaptureProfileOptions_AreHiddenFromHelp()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var outputWriter = new TestOutputTextWriter(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.OutputTextWriter = outputWriter;
options.DisableAnsi = true;
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("--help");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
var help = string.Join(Environment.NewLine, outputWriter.Logs);
Assert.DoesNotContain("--capture-profile", help, StringComparison.Ordinal);
Assert.DoesNotContain("--capture-profile-output", help, StringComparison.Ordinal);
Assert.DoesNotContain("--capture-profile-delay", help, StringComparison.Ordinal);
Assert.DoesNotContain("--log-file", help, StringComparison.Ordinal);
}
[Fact]
public void CaptureProfileOptions_ParseOnSubcommands()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("run --capture-profile --capture-profile-output profile.zip --capture-profile-delay 1");
Assert.Empty(result.Errors);
Assert.True(result.GetValue(RootCommand.CaptureProfileOption));
Assert.Equal("profile.zip", result.GetValue(RootCommand.CaptureProfileOutputOption)?.Name);
Assert.Equal(1, result.GetValue(RootCommand.CaptureProfileDelayOption));
Assert.DoesNotContain("--capture-profile", result.UnmatchedTokens);
}
[Fact]
public void InternalLogFileOption_IsConsumedOnSubcommands()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse(
[
"run",
"--log-file", "child.log",
"--launch-profile=E2E",
"--",
"--from-profile", "value"
]);
Assert.Empty(result.Errors);
Assert.Equal("E2E", result.GetValue(AppHostLauncher.s_launchProfileOption));
Assert.Equal(["--from-profile", "value"], result.UnmatchedTokens);
}
[Fact]
public void StartDebugSessionOption_IsOnlyAddedInExtensionContext()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var normalServices = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var normalProvider = normalServices.BuildServiceProvider();
var normalCommand = normalProvider.GetRequiredService<RootCommand>();
Assert.DoesNotContain(normalCommand.Options, option => ReferenceEquals(option, RootCommand.StartDebugSessionOption));
var extensionServices = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ExtensionBackchannelFactory = _ => new TestExtensionBackchannel();
options.InteractionServiceFactory = sp => new TestExtensionInteractionService(sp);
});
using var extensionProvider = extensionServices.BuildServiceProvider();
var extensionCommand = extensionProvider.GetRequiredService<RootCommand>();
Assert.Contains(extensionCommand.Options, option => ReferenceEquals(option, RootCommand.StartDebugSessionOption));
}
[Theory]
[InlineData("1", true)]
[InlineData("true", true)]
[InlineData("TRUE", true)]
[InlineData("True", true)]
[InlineData("0", false)]
[InlineData("false", false)]
[InlineData("", false)]
[InlineData(null, false)]
public async Task NoLogoEnvironmentVariable_ParsedCorrectly(string? value, bool expectedNoLogo)
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ConfigurationCallback = config =>
{
config[CliConfigNames.NoLogo] = value;
};
});
using var provider = services.BuildServiceProvider();
var configuration = provider.GetRequiredService<IConfiguration>();
var isNoLogo = configuration.GetBool(CliConfigNames.NoLogo, defaultValue: false);
Assert.Equal(expectedNoLogo, isNoLogo);
// Also verify command still works with the configuration
var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("--help");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task FirstTimeUseNotice_BannerDisplayedWhenSentinelDoesNotExist()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var errorWriter = new StringWriter();
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = false };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ErrorTextWriter = errorWriter;
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, []);
Assert.True(bannerService.WasBannerDisplayed);
Assert.True(sentinel.WasCreated);
}
[Fact]
public async Task FirstTimeUseNotice_BannerNotDisplayedWhenSentinelExists()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var errorWriter = new StringWriter();
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = true };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ErrorTextWriter = errorWriter;
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, []);
Assert.False(bannerService.WasBannerDisplayed);
Assert.False(sentinel.WasCreated);
}
[Fact]
public async Task FirstTimeUseNotice_BannerNotDisplayedWithNoLogoArgument()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var errorWriter = new StringWriter();
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = false };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ErrorTextWriter = errorWriter;
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, [CommonOptionNames.NoLogo]);
Assert.False(bannerService.WasBannerDisplayed);
Assert.True(sentinel.WasCreated);
}
[Fact]
public async Task FirstTimeUseNotice_BannerNotDisplayedWithNoLogoEnvironmentVariable()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var errorWriter = new StringWriter();
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = false };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ErrorTextWriter = errorWriter;
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.ConfigurationCallback = config =>
{
config[CliConfigNames.NoLogo] = "1";
};
});
using var provider = services.BuildServiceProvider();
var configuration = provider.GetRequiredService<IConfiguration>();
var noLogo = configuration.GetBool(CliConfigNames.NoLogo, defaultValue: false);
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, []);
Assert.False(bannerService.WasBannerDisplayed);
Assert.True(sentinel.WasCreated);
}
[Fact]
public async Task Banner_DisplayedWhenExplicitlyRequested()
{
// When --banner is passed, banner should show even if not first run
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var errorWriter = new StringWriter();
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = true }; // Not first run
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ErrorTextWriter = errorWriter;
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, [CommonOptionNames.Banner]);
Assert.True(bannerService.WasBannerDisplayed);
// Telemetry notice should NOT be shown since it's not first run
var errorOutput = errorWriter.ToString();
Assert.DoesNotContain("Telemetry", errorOutput);
}
[Fact]
public async Task Banner_CanBeInvokedMultipleTimes()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
// Invoke multiple times (simulating multiple --banner calls)
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, [CommonOptionNames.Banner]);
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, [CommonOptionNames.Banner]);
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, [CommonOptionNames.Banner]);
Assert.Equal(3, bannerService.DisplayCount);
}
[Fact]
public void BannerOption_HasCorrectDescription()
{
Assert.Equal("--banner", RootCommand.BannerOption.Name);
Assert.NotNull(RootCommand.BannerOption.Description);
Assert.NotEmpty(RootCommand.BannerOption.Description);
}
[Fact]
public async Task Banner_DisplayedOnFirstRunAndExplicitRequest()
{
// When it's a first run AND user explicitly requests --banner,
// the banner should be shown (only once via the explicit request logic)
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var errorWriter = new StringWriter();
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = false };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ErrorTextWriter = errorWriter;
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, [CommonOptionNames.Banner]);
Assert.True(bannerService.WasBannerDisplayed);
Assert.True(sentinel.WasCreated);
// Telemetry notice should be shown on first run
var errorOutput = errorWriter.ToString();
Assert.Contains("Telemetry", errorOutput);
}
[Fact]
public async Task Banner_TelemetryNoticeShownOnFirstRun()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var errorWriter = new StringWriter();
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = false };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ErrorTextWriter = errorWriter;
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, []);
var errorOutput = errorWriter.ToString();
Assert.Contains("Telemetry", errorOutput);
}
[Fact]
public async Task Banner_TelemetryNoticeNotShownOnSubsequentRuns()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var errorWriter = new StringWriter();
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = true }; // Not first run
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ErrorTextWriter = errorWriter;
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, []);
var errorOutput = errorWriter.ToString();
Assert.DoesNotContain("Telemetry", errorOutput);
}
[Theory]
[InlineData("--version")]
[InlineData("--help")]
[InlineData("-h")]
[InlineData("-?")]
public async Task InformationalFlag_SuppressesBannerAndDoesNotCreateSentinel(string flag)
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = false };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, [flag]);
// Informational flags set noLogo, which suppresses banner and telemetry notice
Assert.False(bannerService.WasBannerDisplayed);
// Sentinel should NOT be created for informational commands
Assert.False(sentinel.WasCreated);
}
[Fact]
public async Task InformationalFlag_DoesNotCreateSentinel_OnSubsequentFirstRun()
{
// Verifies that running --version on first run doesn't mark first-run as complete,
// so a subsequent normal invocation still shows the first-run experience.
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = false };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
// First invocation with --version: should not create sentinel
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, ["--version"]);
Assert.False(sentinel.WasCreated);
// Second invocation without informational flag: should create sentinel and show banner
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, []);
Assert.True(bannerService.WasBannerDisplayed);
Assert.True(sentinel.WasCreated);
}
[Theory]
[InlineData("ps", "--format", "json")]
[InlineData("ps", "--format=json")]
[InlineData("config", "info", "--json")]
[InlineData("resource", "database", "reset-password", "--load-arguments")]
[InlineData("extension", "get-apphosts")]
public async Task MachineReadableCommand_SuppressesBannerAndDoesNotCreateSentinel(params string[] args)
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = false };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, args);
Assert.False(bannerService.WasBannerDisplayed);
Assert.False(sentinel.WasCreated);
}
[Theory]
[InlineData("--json")]
[InlineData("--nologo")]
[InlineData("--help")]
public async Task ArgumentsAfterDelimiter_DoNotSuppressFirstRunNotice(string appArg)
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = false };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, ["run", "--", appArg]);
Assert.True(bannerService.WasBannerDisplayed);
Assert.True(sentinel.WasCreated);
}
[Fact]
public async Task BannerArgumentAfterDelimiter_DoesNotForceBanner()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = true };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, ["run", "--", "--banner"]);
Assert.False(bannerService.WasBannerDisplayed);
Assert.False(sentinel.WasCreated);
}
[Fact]
public async Task FirstTimeUseNotice_BannerNotDisplayedInNonInteractiveEnvironment()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = false };
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateNonInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, []);
Assert.False(bannerService.WasBannerDisplayed);
Assert.True(sentinel.WasCreated);
}
[Fact]
public async Task Banner_NotDisplayedWithExplicitBannerFlag_InNonInteractiveEnvironment()
{
// Even when --banner is explicitly passed, the banner should NOT display in a
// non-interactive environment because Spectre.Console's Live display requires
// valid console handles (e.g., stdout must not be redirected).
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var sentinel = new TestFirstTimeUseNoticeSentinel { SentinelExists = true }; // Not first run
var bannerService = new TestBannerService();
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.FirstTimeUseNoticeSentinelFactory = _ => sentinel;
options.BannerServiceFactory = _ => bannerService;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateNonInteractiveHostEnvironment();
});
using var provider = services.BuildServiceProvider();
await Program.DisplayFirstTimeUseNoticeIfNeededAsync(provider, [CommonOptionNames.Banner]);
Assert.False(bannerService.WasBannerDisplayed);
}
[Fact]
public void SetupCommand_NotAvailable_WhenBundleIsNotAvailable()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var hasSetupCommand = command.Subcommands.Any(cmd => cmd.Name == "setup");
Assert.False(hasSetupCommand);
}
[Fact]
public void SetupCommand_Available_WhenBundleIsAvailable()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.BundleServiceFactory = _ => new TestBundleService(isBundle: true);
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var hasSetupCommand = command.Subcommands.Any(cmd => cmd.Name == "setup");
Assert.True(hasSetupCommand);
}
[Fact]
public void AllVisibleCommands_HaveHelpGroup()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.BundleServiceFactory = _ => new TestBundleService(isBundle: true);
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
// Either the subcommand isn't a BaseCommand, or it hasn't specified a HelpGroup.
var missingGroup = command.Subcommands
.Where(sub => !sub.Hidden)
.Where(cmd => cmd is not BaseCommand baseCmd || baseCmd.HelpGroup is HelpGroup.None)
.Select(cmd => cmd.Name)
.ToList();
Assert.True(missingGroup.Count == 0,
$"The following visible commands are missing a HelpGroup: {string.Join(", ", missingGroup)}. " +
"Add 'internal override HelpGroup HelpGroup => HelpGroup.XXX;' to each command class.");
}
[Fact]
public void GroupedHelp_ContainsAllVisibleCommands()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.BundleServiceFactory = _ => new TestBundleService(isBundle: true);
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
var helpWriter = new StringWriter();
GroupedHelpWriter.WriteHelp(command, helpWriter, 120);
var helpOutput = helpWriter.ToString();
var visibleCommands = command.Subcommands.Where(sub => !sub.Hidden).ToList();
foreach (var sub in visibleCommands)
{
Assert.Contains(sub.Name, helpOutput);
}
}
[Fact]
public void RootCommand_DoesNotExposeRemovedExecSubcommand()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<RootCommand>();
Assert.DoesNotContain(command.Subcommands, subcommand => subcommand.Name == "exec");
}
}