From c67113e6551a2fa69ea2fc74679af42263125032 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 16 Mar 2026 16:57:44 -0700 Subject: [PATCH] Address apphost stdout review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Interaction/ConsoleInteractionService.cs | 12 ++++----- src/Aspire.Cli/Interaction/ConsoleLogTypes.cs | 15 +++++++++++ .../ExtensionInteractionService.cs | 6 +++-- .../Interaction/IInteractionService.cs | 5 +++- .../Projects/DotNetAppHostProject.cs | 4 +-- .../Projects/GuestAppHostProject.cs | 6 ++--- src/Aspire.Cli/Projects/GuestRuntime.cs | 4 ++- .../Projects/ProcessGuestLauncher.cs | 7 +++-- src/Aspire.Cli/Utils/OutputCollector.cs | 27 ++++++++++++------- .../Commands/NewCommandTests.cs | 2 +- ...PublishCommandPromptingIntegrationTests.cs | 2 +- .../Commands/UpdateCommandTests.cs | 3 ++- .../ConsoleInteractionServiceTests.cs | 4 +-- .../Projects/ExtensionGuestLauncherTests.cs | 3 ++- .../Projects/GuestRuntimeTests.cs | 4 +-- .../Projects/ProcessGuestLauncherTests.cs | 5 ++-- .../Templating/DotNetTemplateFactoryTests.cs | 2 +- .../TestExtensionInteractionService.cs | 3 ++- .../TestServices/TestInteractionService.cs | 3 ++- .../Utils/OutputCollectorTests.cs | 10 +++---- 20 files changed, 83 insertions(+), 44 deletions(-) create mode 100644 src/Aspire.Cli/Interaction/ConsoleLogTypes.cs diff --git a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs index 6717f757c9a..d93eb6448f6 100644 --- a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs @@ -286,10 +286,10 @@ public void WriteConsoleLog(string message, int? lineNumber = null, string? type var style = isErrorMessage ? s_errorMessageStyle : type switch { - "waiting" => s_waitingMessageStyle, - "running" => s_infoMessageStyle, - "exitCode" => s_exitCodeMessageStyle, - "failedToStart" => s_errorMessageStyle, + ConsoleLogTypes.Waiting => s_waitingMessageStyle, + ConsoleLogTypes.Running => s_infoMessageStyle, + ConsoleLogTypes.ExitCode => s_exitCodeMessageStyle, + ConsoleLogTypes.FailedToStart => s_errorMessageStyle, _ => s_infoMessageStyle }; @@ -302,11 +302,11 @@ public void DisplaySuccess(string message, bool allowMarkup = false) DisplayMessage(KnownEmojis.CheckMark, message, allowMarkup); } - public void DisplayLines(IEnumerable<(string Stream, string Line)> lines) + public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) { foreach (var (stream, line) in lines) { - if (stream == "stdout") + if (stream == OutputLineStream.StdOut) { MessageConsole.MarkupLineInterpolated($"{line.EscapeMarkup()}"); } diff --git a/src/Aspire.Cli/Interaction/ConsoleLogTypes.cs b/src/Aspire.Cli/Interaction/ConsoleLogTypes.cs new file mode 100644 index 00000000000..d89093eedb6 --- /dev/null +++ b/src/Aspire.Cli/Interaction/ConsoleLogTypes.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Cli.Interaction; + +/// +/// Known semantic types for console log messages emitted directly by the CLI. +/// +internal static class ConsoleLogTypes +{ + public const string Waiting = "waiting"; + public const string Running = "running"; + public const string ExitCode = "exitCode"; + public const string FailedToStart = "failedToStart"; +} diff --git a/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs b/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs index e26db83e942..c6060ec1496 100644 --- a/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs @@ -342,9 +342,11 @@ public void DisplayDashboardUrls(DashboardUrlsState dashboardUrls) Debug.Assert(result); } - public void DisplayLines(IEnumerable<(string Stream, string Line)> lines) + public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) { - var result = _extensionTaskChannel.Writer.TryWrite(() => Backchannel.DisplayLinesAsync(lines.Select(line => new DisplayLineState(line.Stream.RemoveSpectreFormatting(), line.Line.RemoveSpectreFormatting())), _cancellationToken)); + var result = _extensionTaskChannel.Writer.TryWrite(() => Backchannel.DisplayLinesAsync(lines.Select(line => new DisplayLineState( + line.Stream == OutputLineStream.StdOut ? "stdout" : "stderr", + line.Line.RemoveSpectreFormatting())), _cancellationToken)); Debug.Assert(result); _consoleInteractionService.DisplayLines(lines); } diff --git a/src/Aspire.Cli/Interaction/IInteractionService.cs b/src/Aspire.Cli/Interaction/IInteractionService.cs index b3765c4bc59..0b2711e4adb 100644 --- a/src/Aspire.Cli/Interaction/IInteractionService.cs +++ b/src/Aspire.Cli/Interaction/IInteractionService.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Cli.Backchannel; +using Aspire.Cli.Utils; using Spectre.Console; using Spectre.Console.Rendering; @@ -25,7 +26,7 @@ internal interface IInteractionService void DisplayMarkupLine(string markup); void DisplaySuccess(string message, bool allowMarkup = false); void DisplaySubtleMessage(string message, bool allowMarkup = false); - void DisplayLines(IEnumerable<(string Stream, string Line)> lines); + void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines); void DisplayRenderable(IRenderable renderable); Task DisplayLiveAsync(IRenderable initialRenderable, Func, Task> callback); void DisplayCancellationMessage(); @@ -39,5 +40,7 @@ internal interface IInteractionService ConsoleOutput Console { get; set; } void DisplayVersionUpdateNotification(string newerVersion, string? updateCommand = null); + // The semantic type is stringly-typed because some values originate from backchannel payloads. + // Use ConsoleLogTypes for CLI-defined values. void WriteConsoleLog(string message, int? lineNumber = null, string? type = null, bool isErrorMessage = false); } diff --git a/src/Aspire.Cli/Projects/DotNetAppHostProject.cs b/src/Aspire.Cli/Projects/DotNetAppHostProject.cs index b500532975b..068be414192 100644 --- a/src/Aspire.Cli/Projects/DotNetAppHostProject.cs +++ b/src/Aspire.Cli/Projects/DotNetAppHostProject.cs @@ -308,8 +308,8 @@ public async Task RunAsync(AppHostProjectContext context, CancellationToken "AppHost", (stream, line) => _interactionService.WriteConsoleLog( line, - type: "running", - isErrorMessage: stream == "stderr")); + type: ConsoleLogTypes.Running, + isErrorMessage: stream == OutputLineStream.StdErr)); context.OutputCollector = runOutputCollector; // Signal that build/preparation is complete diff --git a/src/Aspire.Cli/Projects/GuestAppHostProject.cs b/src/Aspire.Cli/Projects/GuestAppHostProject.cs index 7597f93d2fa..b9c9e49bc69 100644 --- a/src/Aspire.Cli/Projects/GuestAppHostProject.cs +++ b/src/Aspire.Cli/Projects/GuestAppHostProject.cs @@ -506,8 +506,8 @@ await GenerateCodeViaRpcAsync( { launcher = _guestRuntime.CreateDefaultLauncher((stream, line) => _interactionService.WriteConsoleLog( line, - type: "running", - isErrorMessage: stream == "stderr")); + type: ConsoleLogTypes.Running, + isErrorMessage: stream == OutputLineStream.StdErr)); } // Start guest apphost - it will connect to AppHost server, define resources. @@ -527,7 +527,7 @@ await GenerateCodeViaRpcAsync( { _logger.LogError("{Language} apphost exited with code {ExitCode}", DisplayName, guestExitCode); - // Display the output (same pattern as DotNetCliRunner) + // Replay buffered output only when the process failed before live streaming began. if (guestOutput is { HasLiveOutputCallback: false }) { _interactionService.DisplayLines(guestOutput.GetLines()); diff --git a/src/Aspire.Cli/Projects/GuestRuntime.cs b/src/Aspire.Cli/Projects/GuestRuntime.cs index 71c09f7886e..7337ee3b70e 100644 --- a/src/Aspire.Cli/Projects/GuestRuntime.cs +++ b/src/Aspire.Cli/Projects/GuestRuntime.cs @@ -150,8 +150,10 @@ public GuestRuntime(RuntimeSpec spec, ILogger logger, Func? com /// /// Creates the default process-based launcher for this runtime. + /// Pass a callback only for interactive run flows that should stream output live. + /// Install and publish flows leave this and replay buffered output later if needed. /// - public ProcessGuestLauncher CreateDefaultLauncher(Action? liveOutputCallback = null) => new(_spec.Language, _logger, _commandResolver, liveOutputCallback); + public ProcessGuestLauncher CreateDefaultLauncher(Action? liveOutputCallback = null) => new(_spec.Language, _logger, _commandResolver, liveOutputCallback); /// /// Replaces placeholders in command arguments with actual values. diff --git a/src/Aspire.Cli/Projects/ProcessGuestLauncher.cs b/src/Aspire.Cli/Projects/ProcessGuestLauncher.cs index 9d6de7bc190..df3eae2bacd 100644 --- a/src/Aspire.Cli/Projects/ProcessGuestLauncher.cs +++ b/src/Aspire.Cli/Projects/ProcessGuestLauncher.cs @@ -15,9 +15,9 @@ internal sealed class ProcessGuestLauncher : IGuestProcessLauncher private readonly string _language; private readonly ILogger _logger; private readonly Func _commandResolver; - private readonly Action? _liveOutputCallback; + private readonly Action? _liveOutputCallback; - public ProcessGuestLauncher(string language, ILogger logger, Func? commandResolver = null, Action? liveOutputCallback = null) + public ProcessGuestLauncher(string language, ILogger logger, Func? commandResolver = null, Action? liveOutputCallback = null) { _language = language; _logger = logger; @@ -72,6 +72,7 @@ public ProcessGuestLauncher(string language, ILogger logger, Func _lines = new(10000); // 10k lines. + private readonly CircularBuffer<(OutputLineStream Stream, string Line)> _lines = new(10000); // 10k lines. private readonly object _lock = new object(); private readonly FileLoggerProvider? _fileLogger; private readonly string _category; - private readonly Action? _liveOutputCallback; + private readonly Action? _liveOutputCallback; /// /// Creates an OutputCollector that only buffers output in memory. @@ -26,8 +32,11 @@ public OutputCollector() : this(null, "AppHost") /// /// Optional file logger for writing output to disk. /// Category for log entries (e.g., "Build", "AppHost"). - /// Optional callback invoked immediately when a line is appended. - public OutputCollector(FileLoggerProvider? fileLogger, string category = "AppHost", Action? liveOutputCallback = null) + /// + /// Optional callback used by interactive run flows that need lines as they arrive. + /// Leave this for buffer-only flows that replay output later on failure. + /// + public OutputCollector(FileLoggerProvider? fileLogger, string category = "AppHost", Action? liveOutputCallback = null) { _fileLogger = fileLogger; _category = category; @@ -38,15 +47,15 @@ public OutputCollector(FileLoggerProvider? fileLogger, string category = "AppHos public void AppendOutput(string line) { - AppendLine("stdout", line); + AppendLine(OutputLineStream.StdOut, line); } public void AppendError(string line) { - AppendLine("stderr", line); + AppendLine(OutputLineStream.StdErr, line); } - public IEnumerable<(string Stream, string Line)> GetLines() + public IEnumerable<(OutputLineStream Stream, string Line)> GetLines() { lock (_lock) { @@ -54,12 +63,12 @@ public void AppendError(string line) } } - private void AppendLine(string stream, string line) + private void AppendLine(OutputLineStream stream, string line) { lock (_lock) { _lines.Add((stream, line)); - _fileLogger?.WriteLog(DateTimeOffset.UtcNow, stream == "stderr" ? LogLevel.Error : LogLevel.Information, _category, line); + _fileLogger?.WriteLog(DateTimeOffset.UtcNow, stream == OutputLineStream.StdErr ? LogLevel.Error : LogLevel.Information, _category, line); } _liveOutputCallback?.Invoke(stream, line); diff --git a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs index ca386eee71e..d671a48055d 100644 --- a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs @@ -1574,7 +1574,7 @@ public Task> PromptForSelectionsAsync(string promptText, IEn public void DisplayError(string errorMessage) { } public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) { } public void DisplaySuccess(string message, bool allowMarkup = false) { } - public void DisplayLines(IEnumerable<(string Stream, string Line)> lines) { } + public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) { } public void DisplayCancellationMessage() { } public Task ConfirmAsync(string promptText, bool defaultValue = true, CancellationToken cancellationToken = default) => Task.FromResult(true); public Task PromptForFilePathAsync(string promptText, string? defaultValue = null, Func? validator = null, bool directory = false, bool required = false, CancellationToken cancellationToken = default) diff --git a/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs b/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs index 4e10044b760..a3e046d8e77 100644 --- a/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs @@ -953,7 +953,7 @@ public Task ConfirmAsync(string promptText, bool defaultValue = true, Canc public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) { } public void DisplaySuccess(string message, bool allowMarkup = false) { } public void DisplaySubtleMessage(string message, bool allowMarkup = false) { } - public void DisplayLines(IEnumerable<(string Stream, string Line)> lines) { } + public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) { } public void DisplayCancellationMessage() { } public void DisplayEmptyLine() { } public void DisplayPlainText(string text) { } diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs index 3b8e52d32f4..6c8d9d1acc5 100644 --- a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs @@ -11,6 +11,7 @@ using Aspire.Cli.Resources; using Aspire.Cli.Tests.TestServices; using Aspire.Cli.Tests.Utils; +using Aspire.Cli.Utils; using Microsoft.Extensions.DependencyInjection; using Spectre.Console; using Spectre.Console.Rendering; @@ -1062,7 +1063,7 @@ public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, stri public void DisplayMarkupLine(string markup) => _innerService.DisplayMarkupLine(markup); public void DisplaySuccess(string message, bool allowMarkup = false) => _innerService.DisplaySuccess(message, allowMarkup); public void DisplaySubtleMessage(string message, bool allowMarkup = false) => _innerService.DisplaySubtleMessage(message, allowMarkup); - public void DisplayLines(IEnumerable<(string Stream, string Line)> lines) => _innerService.DisplayLines(lines); + public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) => _innerService.DisplayLines(lines); public void DisplayCancellationMessage() { OnCancellationMessageDisplayed?.Invoke(); diff --git a/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs b/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs index 72055c4232f..50d85907190 100644 --- a/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs @@ -112,8 +112,8 @@ public void DisplayLines_WithMarkupCharacters_DoesNotCauseMarkupParsingError() var interactionService = CreateInteractionService(console, executionContext); var lines = new[] { - ("stdout", "Command output with brackets"), - ("stderr", "Error output with [square] brackets") + (OutputLineStream.StdOut, "Command output with brackets"), + (OutputLineStream.StdErr, "Error output with [square] brackets") }; // Act - this should not throw an exception due to markup parsing diff --git a/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs b/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs index dace11a8043..52670fb8571 100644 --- a/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs @@ -4,6 +4,7 @@ using Aspire.Cli.Backchannel; using Aspire.Cli.Interaction; using Aspire.Cli.Projects; +using Aspire.Cli.Utils; namespace Aspire.Cli.Tests.Projects; @@ -165,7 +166,7 @@ public Task LaunchAppHostAsync(string projectFile, List arguments, List< public void DisplayError(string errorMessage) => throw new NotImplementedException(); public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) => throw new NotImplementedException(); public void DisplaySuccess(string message, bool allowMarkup = false) => throw new NotImplementedException(); - public void DisplayLines(IEnumerable<(string Stream, string Line)> lines) => throw new NotImplementedException(); + public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) => throw new NotImplementedException(); public void DisplayCancellationMessage() => throw new NotImplementedException(); public Task ConfirmAsync(string promptText, bool defaultValue = true, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public void DisplaySubtleMessage(string message, bool allowMarkup = false) => throw new NotImplementedException(); diff --git a/tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs b/tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs index 973377b17d0..9a25ea74d78 100644 --- a/tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/GuestRuntimeTests.cs @@ -319,7 +319,7 @@ public async Task InstallDependenciesAsync_WhenNpmIsMissing_ReturnsNodeInstallMe output.GetLines(), line => { - Assert.Equal("stderr", line.Stream); + Assert.Equal(OutputLineStream.StdErr, line.Stream); Assert.Equal("npm is not installed or not found in PATH. Please install Node.js and try again.", line.Line); }); } @@ -354,7 +354,7 @@ public async Task RunAsync_WhenNpxIsMissing_ReturnsNodeInstallMessage() resolvedOutput.GetLines(), line => { - Assert.Equal("stderr", line.Stream); + Assert.Equal(OutputLineStream.StdErr, line.Stream); Assert.Equal("npx is not installed or not found in PATH. Please install Node.js and try again.", line.Line); }); } diff --git a/tests/Aspire.Cli.Tests/Projects/ProcessGuestLauncherTests.cs b/tests/Aspire.Cli.Tests/Projects/ProcessGuestLauncherTests.cs index 6a177d21e9e..a01bbc50c66 100644 --- a/tests/Aspire.Cli.Tests/Projects/ProcessGuestLauncherTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ProcessGuestLauncherTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Cli.Projects; +using Aspire.Cli.Utils; using Microsoft.Extensions.Logging.Abstractions; namespace Aspire.Cli.Tests.Projects; @@ -12,7 +13,7 @@ public class ProcessGuestLauncherTests public async Task LaunchAsync_ForwardsStdoutToLiveCallback() { // Arrange - var forwardedLines = new List<(string Stream, string Line)>(); + var forwardedLines = new List<(OutputLineStream Stream, string Line)>(); var launcher = new ProcessGuestLauncher( "typescript", NullLogger.Instance, @@ -29,6 +30,6 @@ public async Task LaunchAsync_ForwardsStdoutToLiveCallback() // Assert Assert.Equal(0, exitCode); Assert.NotNull(output); - Assert.Contains(forwardedLines, line => line.Stream == "stdout" && !string.IsNullOrWhiteSpace(line.Line)); + Assert.Contains(forwardedLines, line => line.Stream == OutputLineStream.StdOut && !string.IsNullOrWhiteSpace(line.Line)); } } diff --git a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs index 7433f9d9510..3bf653368cc 100644 --- a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs +++ b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs @@ -481,7 +481,7 @@ public void ShowStatus(string message, Action work, KnownEmoji? emoji = null, bo public void DisplaySuccess(string message, bool allowMarkup = false) { } public void DisplayError(string message) { } public void DisplayMessage(KnownEmoji emoji, string message, bool allowMarkup = false) { } - public void DisplayLines(IEnumerable<(string Stream, string Line)> lines) { } + public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) { } public void DisplayCancellationMessage() { } public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingVersion) => 0; public void DisplayPlainText(string text) { } diff --git a/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs b/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs index aa3bc81bc9b..a27e9ae7c6c 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs @@ -3,6 +3,7 @@ using Aspire.Cli.Backchannel; using Aspire.Cli.Interaction; +using Aspire.Cli.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Spectre.Console; @@ -112,7 +113,7 @@ public void WriteDebugSessionMessage(string message, bool stdout, string? textSt { } - public void DisplayLines(IEnumerable<(string Stream, string Line)> lines) + public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) { } diff --git a/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs b/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs index 7bf0f3c190e..befce116ddf 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs @@ -4,6 +4,7 @@ using System.Collections; using Aspire.Cli.Backchannel; using Aspire.Cli.Interaction; +using Aspire.Cli.Utils; using Spectre.Console; using Spectre.Console.Rendering; @@ -153,7 +154,7 @@ public void DisplaySuccess(string message, bool allowMarkup = false) { } - public void DisplayLines(IEnumerable<(string Stream, string Line)> lines) + public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) { } diff --git a/tests/Aspire.Cli.Tests/Utils/OutputCollectorTests.cs b/tests/Aspire.Cli.Tests/Utils/OutputCollectorTests.cs index 7a1d23a49b8..d16efe2de02 100644 --- a/tests/Aspire.Cli.Tests/Utils/OutputCollectorTests.cs +++ b/tests/Aspire.Cli.Tests/Utils/OutputCollectorTests.cs @@ -44,8 +44,8 @@ public async Task OutputCollector_ThreadSafety_MultipleThreadsAddingLines() Assert.Equal(threadCount * linesPerThread, lines.Count); // Check that we have both stdout and stderr entries - var stdoutLines = lines.Where(l => l.Stream == "stdout").ToList(); - var stderrLines = lines.Where(l => l.Stream == "stderr").ToList(); + var stdoutLines = lines.Where(l => l.Stream == OutputLineStream.StdOut).ToList(); + var stderrLines = lines.Where(l => l.Stream == OutputLineStream.StdErr).ToList(); Assert.Equal(threadCount * linesPerThread / 2, stdoutLines.Count); Assert.Equal(threadCount * linesPerThread / 2, stderrLines.Count); @@ -65,7 +65,7 @@ public void OutputCollector_GetLines_ReturnsSnapshotNotLiveReference() // Assert - Snapshot should not be affected by subsequent additions Assert.Single(snapshot); Assert.Equal("initial line", snapshot[0].Line); - Assert.Equal("stdout", snapshot[0].Stream); + Assert.Equal(OutputLineStream.StdOut, snapshot[0].Stream); // New call should include the additional line var newSnapshot = collector.GetLines().ToList(); @@ -107,7 +107,7 @@ public async Task OutputCollector_ConcurrentReadWrite_ShouldNotCrash() public void OutputCollector_LiveCallback_ReceivesStdoutAndStderr() { // Arrange - var forwardedLines = new List<(string Stream, string Line)>(); + var forwardedLines = new List<(OutputLineStream Stream, string Line)>(); var collector = new OutputCollector(fileLogger: null, liveOutputCallback: (stream, line) => forwardedLines.Add((stream, line))); // Act @@ -117,7 +117,7 @@ public void OutputCollector_LiveCallback_ReceivesStdoutAndStderr() // Assert Assert.True(collector.HasLiveOutputCallback); Assert.Equal( - [("stdout", "hello"), ("stderr", "oops")], + [(OutputLineStream.StdOut, "hello"), (OutputLineStream.StdErr, "oops")], forwardedLines); } }