Skip to content
Merged
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
9 changes: 6 additions & 3 deletions src/Aspire.Cli/Utils/ConsoleActivityLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,18 +304,21 @@ public void WriteSummary()

/// <summary>
/// Formats a single key-value pair for the pipeline summary display.
/// Values may contain markdown links which are converted to clickable links when supported.
/// </summary>
private string FormatPipelineSummaryKvp(string key, string value)
{
if (_enableColor)
{
var escapedKey = key.EscapeMarkup();
var escapedValue = value.EscapeMarkup();
return $" [blue]{escapedKey}[/]: {escapedValue}";
var convertedValue = MarkdownToSpectreConverter.ConvertToSpectre(value);
convertedValue = HighlightMessage(convertedValue);
return $" [blue]{escapedKey}[/]: {convertedValue}";
}
else
{
return $" {key}: {value}";
var plainValue = MarkdownToSpectreConverter.ConvertLinksToPlainText(value);
return $" {key}: {plainValue}";
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/Aspire.Cli/Utils/MarkdownToSpectreConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ public static string ConvertToSpectre(string markdown)
return result;
}

/// <summary>
/// Converts markdown links to plain text.
/// </summary>
/// <param name="markdown">The markdown text to convert.</param>
/// <returns>The text with markdown links converted to the plain text format <c>text (url)</c>.</returns>
public static string ConvertLinksToPlainText(string markdown)
{
return LinkRegex().Replace(markdown, "$1 ($2)");
}

private static string ConvertHeaders(string text)
{
// Convert ###### Header 6 (most specific first)
Expand Down
15 changes: 8 additions & 7 deletions src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,19 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet
/// <param name="provisioningContext">The Azure provisioning context.</param>
private static void AddToPipelineSummary(PipelineStepContext ctx, ProvisioningContext provisioningContext)
{
// Safely access the nested properties with null checks for reference types
// AzureLocation is a struct so it cannot be null
var resourceGroupName = provisioningContext.ResourceGroup?.Name ?? "unknown";
var subscriptionId = provisioningContext.Subscription?.Id.Name ?? "unknown";
var resourceGroupName = provisioningContext.ResourceGroup.Name;
var subscriptionId = provisioningContext.Subscription.Id.Name;
var location = provisioningContext.Location.Name;

#pragma warning disable ASPIREPIPELINES001 // PipelineSummary is experimental
var tenantId = provisioningContext.Tenant.TenantId;
var tenantSegment = tenantId.HasValue ? $"#@{tenantId.Value}" : "#";
var portalUrl = $"https://portal.azure.com/{tenantSegment}/resource/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/overview";
var resourceGroupValue = $"[{resourceGroupName}]({portalUrl})";

ctx.Summary.Add("☁️ Target", "Azure");
ctx.Summary.Add("📦 Resource Group", resourceGroupName);
ctx.Summary.Add("📦 Resource Group", resourceGroupValue);
ctx.Summary.Add("🔑 Subscription", subscriptionId);
ctx.Summary.Add("🌐 Location", location);
#pragma warning restore ASPIREPIPELINES001
}

private Task PublishAsync(PipelineStepContext context)
Expand Down
141 changes: 141 additions & 0 deletions tests/Aspire.Cli.Tests/Utils/ConsoleActivityLoggerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using Aspire.Cli.Utils;
using Spectre.Console;

namespace Aspire.Cli.Tests.Utils;

public class ConsoleActivityLoggerTests
{
[Fact]
public void WriteSummary_WithMarkdownLinkInPipelineSummary_RendersClickableLink()
{
var output = new StringBuilder();
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = AnsiSupport.Yes,
ColorSystem = ColorSystemSupport.TrueColor,
Out = new AnsiConsoleOutput(new StringWriter(output))
});

var hostEnvironment = TestHelpers.CreateInteractiveHostEnvironment();
var logger = new ConsoleActivityLogger(console, hostEnvironment, forceColor: true);

var summary = new List<KeyValuePair<string, string>>
{
new("☁️ Target", "Azure"),
new("📦 Resource Group", "VNetTest5 [link](https://portal.azure.com/#/resource/subscriptions/sub-id/resourceGroups/VNetTest5/overview)"),
new("🔑 Subscription", "sub-id"),
new("🌐 Location", "eastus"),
};

logger.SetFinalResult(true, summary);
logger.WriteSummary();

var result = output.ToString();

// Verify the markdown link was converted to a Spectre link
Assert.Contains("VNetTest5", result);

const string expectedUrl =
@"https://portal\.azure\.com/#/resource/subscriptions/sub-id/resourceGroups/VNetTest5/overview";
string hyperlinkPattern =
$@"\u001b\]8;[^;]*;{expectedUrl}\u001b\\.*link.*\u001b\]8;;\u001b\\";
Assert.Matches(hyperlinkPattern, result);
}

[Fact]
public void WriteSummary_WithMarkdownLinkInPipelineSummary_NoColor_RendersPlainTextWithUrl()
{
var output = new StringBuilder();
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = AnsiSupport.No,
ColorSystem = ColorSystemSupport.NoColors,
Out = new AnsiConsoleOutput(new StringWriter(output))
});

var hostEnvironment = TestHelpers.CreateNonInteractiveHostEnvironment();
var logger = new ConsoleActivityLogger(console, hostEnvironment, forceColor: false);

var portalUrl = "https://portal.azure.com/";
var summary = new List<KeyValuePair<string, string>>
{
new("📦 Resource Group", $"VNetTest5 [link]({portalUrl})"),
};

logger.SetFinalResult(true, summary);
logger.WriteSummary();

var result = output.ToString();

// When color is disabled, markdown links should be converted to plain text: text (url)
Assert.Contains($"VNetTest5 link ({portalUrl})", result);
}

[Fact]
public void WriteSummary_WithMarkdownLinkInPipelineSummary_ColorWithoutInteractive_RendersPlainUrl()
{
var output = new StringBuilder();
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = AnsiSupport.Yes,
ColorSystem = ColorSystemSupport.TrueColor,
Out = new AnsiConsoleOutput(new StringWriter(output))
});

// Non-interactive host but color enabled (e.g., CI environments with ANSI support)
var hostEnvironment = TestHelpers.CreateNonInteractiveHostEnvironment();
var logger = new ConsoleActivityLogger(console, hostEnvironment, forceColor: true);

var portalUrl = "https://portal.azure.com/";
var summary = new List<KeyValuePair<string, string>>
{
new("📦 Resource Group", $"VNetTest5 [link]({portalUrl})"),
};

logger.SetFinalResult(true, summary);
logger.WriteSummary();

var result = output.ToString();

// When color is enabled but interactive output is not supported,
// HighlightMessage converts Spectre link markup to plain URLs
Assert.Contains("VNetTest5", result);
Assert.Contains(portalUrl, result);

// Should NOT contain the OSC 8 hyperlink escape sequence since we're non-interactive
Assert.DoesNotContain("\u001b]8;", result);
}

[Fact]
public void WriteSummary_WithPlainTextPipelineSummary_RendersCorrectly()
{
var output = new StringBuilder();
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = AnsiSupport.Yes,
ColorSystem = ColorSystemSupport.TrueColor,
Out = new AnsiConsoleOutput(new StringWriter(output))
});

var hostEnvironment = TestHelpers.CreateInteractiveHostEnvironment();
var logger = new ConsoleActivityLogger(console, hostEnvironment, forceColor: true);

var summary = new List<KeyValuePair<string, string>>
{
new("☁️ Target", "Azure"),
new("🌐 Location", "eastus"),
};

logger.SetFinalResult(true, summary);
logger.WriteSummary();

var result = output.ToString();

Assert.Contains("Azure", result);
Assert.Contains("eastus", result);
}
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The PR description mentions "tests for all three rendering paths" but there's no test for the case where color is enabled but interactive output is not supported.

This scenario occurs when ANSI color is supported (e.g., in CI environments) but interactive features like clickable links are not. In this case, the HighlightMessage method at ConsoleActivityLogger.cs:447-460 converts Spectre link markup to plain URLs.

Consider adding a test case like:

[Fact]
public void WriteSummary_WithMarkdownLinkInPipelineSummary_ColorWithoutInteractive_RendersPlainUrl()
{
    // Test with color enabled but non-interactive environment
    // Should use HighlightMessage to convert links to plain URLs
}

This would test the code path at FormatPipelineSummaryKvp lines 311-316 combined with HighlightMessage's non-interactive behavior.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

@copilot - add the suggested test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the suggested test in 78e16b9. The new WriteSummary_WithMarkdownLinkInPipelineSummary_ColorWithoutInteractive_RendersPlainUrl test uses forceColor: true with CreateNonInteractiveHostEnvironment() to exercise the HighlightMessage path that converts Spectre link markup back to plain URLs. It also asserts that OSC 8 hyperlink escape sequences are NOT present in the output.

Loading