Skip to content

Commit 91ee0cf

Browse files
authored
Update CLI confirmation prompts to use [Y/n] convention (#15663)
1 parent c0c721a commit 91ee0cf

File tree

2 files changed

+124
-1
lines changed

2 files changed

+124
-1
lines changed

src/Aspire.Cli/Interaction/ConsoleInteractionService.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,21 @@ public async Task<bool> ConfirmAsync(string promptText, bool defaultValue = true
439439
}
440440

441441
MessageLogger.LogInformation("Confirm: {PromptText} (default: {DefaultValue})", promptText, defaultValue);
442-
var result = await MessageConsole.ConfirmAsync(promptText, defaultValue, cancellationToken);
442+
443+
// Use [Y/n] or [y/N] convention where the capitalized letter indicates the default value.
444+
// Double brackets [[ ]] are used to escape literal brackets in Spectre.Console markup.
445+
var yesChoice = defaultValue ? "Y" : "y";
446+
var noChoice = defaultValue ? "n" : "N";
447+
var fullPromptText = $"{promptText} [[{yesChoice}/{noChoice}]]";
448+
449+
var prompt = new ConfirmationPrompt(fullPromptText)
450+
{
451+
ShowChoices = false,
452+
ShowDefaultValue = false,
453+
DefaultValue = defaultValue,
454+
};
455+
456+
var result = await MessageConsole.PromptAsync(prompt, cancellationToken);
443457
MessageLogger.LogInformation("Confirm result: {Result}", result);
444458
return result;
445459
}

tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Aspire.Cli.Utils;
99
using Microsoft.Extensions.Logging.Abstractions;
1010
using Spectre.Console;
11+
using Spectre.Console.Rendering;
1112

1213
using System.Text;
1314

@@ -1038,4 +1039,112 @@ public void MakeSafeFormatter_WithEscapedMarkupAndBracketsInData_HandlesCorrectl
10381039
// Assert - markup is stripped, brackets in data are replaced
10391040
Assert.Equal("Service (Prod)", result);
10401041
}
1042+
1043+
[Theory]
1044+
[InlineData(true, "[Y/n]")]
1045+
[InlineData(false, "[y/N]")]
1046+
public async Task ConfirmAsync_DisplaysCapitalizedDefaultChoice(bool defaultValue, string expectedChoiceSuffix)
1047+
{
1048+
// Arrange - simulate pressing Enter (accepts default)
1049+
var output = new StringBuilder();
1050+
var console = CreateInteractiveConsoleWithInput(output, "\n");
1051+
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log");
1052+
var interactionService = CreateInteractionService(console, executionContext);
1053+
1054+
// Act
1055+
await interactionService.ConfirmAsync("Proceed?", defaultValue, CancellationToken.None);
1056+
1057+
// Assert - the output should contain the [Y/n] or [y/N] suffix
1058+
var outputString = output.ToString();
1059+
Assert.Contains(expectedChoiceSuffix, outputString);
1060+
}
1061+
1062+
[Theory]
1063+
[InlineData(true)]
1064+
[InlineData(false)]
1065+
public async Task ConfirmAsync_WhenUserPressesEnter_ReturnsDefaultValue(bool defaultValue)
1066+
{
1067+
// Arrange - simulate pressing Enter (empty input selects default)
1068+
var output = new StringBuilder();
1069+
var console = CreateInteractiveConsoleWithInput(output, "\n");
1070+
var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log");
1071+
var interactionService = CreateInteractionService(console, executionContext);
1072+
1073+
// Act
1074+
var result = await interactionService.ConfirmAsync("Proceed?", defaultValue, CancellationToken.None);
1075+
1076+
// Assert - pressing Enter should accept the default value
1077+
Assert.Equal(defaultValue, result);
1078+
}
1079+
1080+
private static IAnsiConsole CreateInteractiveConsoleWithInput(StringBuilder output, string input)
1081+
{
1082+
var settings = new AnsiConsoleSettings
1083+
{
1084+
Ansi = AnsiSupport.No,
1085+
ColorSystem = ColorSystemSupport.NoColors,
1086+
Interactive = InteractionSupport.Yes,
1087+
Out = new AnsiConsoleOutput(new StringWriter(output)),
1088+
};
1089+
var console = AnsiConsole.Create(settings);
1090+
console.Profile.Width = int.MaxValue;
1091+
return new TestAnsiConsoleWithInput(console, new StringReader(input));
1092+
}
1093+
}
1094+
1095+
/// <summary>
1096+
/// A test <see cref="IAnsiConsole"/> wrapper that redirects input reads to a <see cref="TextReader"/>,
1097+
/// allowing prompts to be answered in unit tests without blocking on real console input.
1098+
/// </summary>
1099+
file sealed class TestAnsiConsoleWithInput : IAnsiConsole
1100+
{
1101+
private readonly IAnsiConsole _inner;
1102+
private readonly IAnsiConsoleInput _testInput;
1103+
1104+
public TestAnsiConsoleWithInput(IAnsiConsole inner, TextReader inputReader)
1105+
{
1106+
_inner = inner;
1107+
_testInput = new TextReaderInput(inputReader);
1108+
}
1109+
1110+
public Profile Profile => _inner.Profile;
1111+
public IAnsiConsoleCursor Cursor => _inner.Cursor;
1112+
public IAnsiConsoleInput Input => _testInput;
1113+
public IExclusivityMode ExclusivityMode => _inner.ExclusivityMode;
1114+
public RenderPipeline Pipeline => _inner.Pipeline;
1115+
1116+
public void Clear(bool home) => _inner.Clear(home);
1117+
public void Write(IRenderable renderable) => _inner.Write(renderable);
1118+
1119+
private sealed class TextReaderInput : IAnsiConsoleInput
1120+
{
1121+
private readonly TextReader _reader;
1122+
1123+
public TextReaderInput(TextReader reader) => _reader = reader;
1124+
1125+
public bool IsKeyAvailable() => true;
1126+
1127+
public ConsoleKeyInfo? ReadKey(bool intercept)
1128+
{
1129+
var read = _reader.Read();
1130+
if (read == -1)
1131+
{
1132+
// End of stream - return Enter as a safe fallback
1133+
return new ConsoleKeyInfo('\r', ConsoleKey.Enter, shift: false, alt: false, control: false);
1134+
}
1135+
1136+
var ch = (char)read;
1137+
var key = ch switch
1138+
{
1139+
'\n' or '\r' => ConsoleKey.Enter,
1140+
'y' or 'Y' => ConsoleKey.Y,
1141+
'n' or 'N' => ConsoleKey.N,
1142+
_ => ConsoleKey.Enter,
1143+
};
1144+
return new ConsoleKeyInfo(ch, key, shift: char.IsUpper(ch), alt: false, control: false);
1145+
}
1146+
1147+
public Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
1148+
=> Task.FromResult(ReadKey(intercept));
1149+
}
10411150
}

0 commit comments

Comments
 (0)