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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices"
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/braydonk/yaml"
"github.com/drone/envsubst"
"github.com/fatih/color"
Expand Down Expand Up @@ -632,6 +633,8 @@ func (p *AgentServiceTargetProvider) deployArtifacts(
"agentName": agentName,
"agentVersion": agentVersion,
"label": "Agent endpoint",
"clickable": "false",
"note": "For information on invoking the agent, see " + output.WithLinkFormat("https://aka.ms/azd-agents-invoke"),
},
})
}
Expand Down
32 changes: 30 additions & 2 deletions cli/azd/pkg/project/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/output"
)

// Metadata key constants for artifact configuration
const (
// MetadataKeyClickable controls whether an endpoint should be rendered as a clickable hyperlink.
// Set to "false" to display the URL as plain text (users can still Ctrl+Click in supported terminals).
MetadataKeyClickable = "clickable"

// MetadataKeyNote adds a note line below the artifact output.
MetadataKeyNote = "note"
)

// ArtifactKind represents well-known artifact types in the Azure Developer CLI
type ArtifactKind string

Expand Down Expand Up @@ -95,14 +105,32 @@ func (a *Artifact) ToString(currentIndentation string) string {
discriminator = customDiscriminator
}

return fmt.Sprintf(
// Endpoints are clickable (hyperlinked) by default, but can be disabled via metadata
clickable := true
if val, has := a.Metadata[MetadataKeyClickable]; has && strings.EqualFold(val, "false") {
clickable = false
}

displayLocation := location
if clickable {
displayLocation = output.WithHyperlink(location, location)
}

result := fmt.Sprintf(
"%s- %s: %s %s",
currentIndentation,
label,
output.WithHyperlink(location, location),
displayLocation,
discriminator,
)

// Append note if present
if note, has := a.Metadata[MetadataKeyNote]; has && note != "" {
result += fmt.Sprintf("\n%s %s", currentIndentation, note)
}

return result

case ArtifactKindContainer:
if a.LocationKind == LocationKindRemote {
return fmt.Sprintf("%s- Remote Image: %s", currentIndentation, output.WithLinkFormat(location))
Expand Down
206 changes: 206 additions & 0 deletions cli/azd/pkg/project/artifact_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package project

import (
"strings"
"testing"

"github.com/stretchr/testify/require"
)

// hyperlinkPrefix is the OSC 8 escape sequence prefix used for terminal hyperlinks.
// The actual WithHyperlink function may not emit this in non-terminal environments,
// so we check for its absence to verify non-clickable behavior.
const hyperlinkPrefix = "\x1b]8;;"

func TestArtifactToString_Endpoint(t *testing.T) {
tests := []struct {
name string
artifact *Artifact
contains []string
shouldBeClickable bool
}{
{
name: "remote endpoint is clickable by default",
artifact: &Artifact{
Kind: ArtifactKindEndpoint,
Location: "https://example.com/api",
LocationKind: LocationKindRemote,
},
contains: []string{
"- Endpoint:",
"https://example.com/api",
},
shouldBeClickable: true,
},
{
name: "remote endpoint with clickable=false is not hyperlinked",
artifact: &Artifact{
Kind: ArtifactKindEndpoint,
Location: "https://example.com/agents/myagent",
LocationKind: LocationKindRemote,
Metadata: map[string]string{
MetadataKeyClickable: "false",
},
},
contains: []string{
"- Endpoint:",
"https://example.com/agents/myagent",
},
shouldBeClickable: false,
},
{
name: "agent endpoint with custom label and note",
artifact: &Artifact{
Kind: ArtifactKindEndpoint,
Location: "https://example.com/agents/myagent/versions/1",
LocationKind: LocationKindRemote,
Metadata: map[string]string{
"label": "Agent endpoint",
MetadataKeyClickable: "false",
MetadataKeyNote: "For information on invoking the agent, see https://aka.ms/azd-agents-invoke",
},
},
contains: []string{
"- Agent endpoint:",
"https://example.com/agents/myagent/versions/1",
"For information on invoking the agent, see https://aka.ms/azd-agents-invoke",
},
shouldBeClickable: false,
},
{
name: "local endpoint is clickable by default",
artifact: &Artifact{
Kind: ArtifactKindEndpoint,
Location: "http://localhost:8080",
LocationKind: LocationKindLocal,
},
contains: []string{
"- Endpoint:",
"http://localhost:8080",
},
shouldBeClickable: true,
},
{
name: "endpoint with discriminator",
artifact: &Artifact{
Kind: ArtifactKindEndpoint,
Location: "https://example.com/api",
LocationKind: LocationKindRemote,
Metadata: map[string]string{
"discriminator": "(primary)",
},
},
contains: []string{
"- Endpoint:",
"https://example.com/api",
"(primary)",
},
shouldBeClickable: true,
},
{
name: "clickable=FALSE is case insensitive",
artifact: &Artifact{
Kind: ArtifactKindEndpoint,
Location: "https://example.com/api",
LocationKind: LocationKindRemote,
Metadata: map[string]string{
MetadataKeyClickable: "FALSE",
},
},
contains: []string{
"- Endpoint:",
"https://example.com/api",
},
shouldBeClickable: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.artifact.ToString("")

for _, expected := range tt.contains {
require.True(t, strings.Contains(result, expected),
"Expected output to contain %q, got: %s", expected, result)
}

// Check clickability by looking for hyperlink escape sequence
hasHyperlink := strings.Contains(result, hyperlinkPrefix)
if tt.shouldBeClickable {
// In terminal environments, should have hyperlink; in non-terminal, won't have it
// We can't directly test this without mocking terminal, so we just verify the URL is present
require.Contains(t, result, tt.artifact.Location)
} else {
// Should NOT have hyperlink escape sequence
require.False(t, hasHyperlink,
"Expected output NOT to contain hyperlink escape sequence for non-clickable endpoint, got: %q", result)
}
})
}
}

func TestArtifactToString_OtherKinds(t *testing.T) {
tests := []struct {
name string
artifact *Artifact
contains string
}{
{
name: "container remote",
artifact: &Artifact{
Kind: ArtifactKindContainer,
Location: "myregistry.azurecr.io/myimage:latest",
LocationKind: LocationKindRemote,
},
contains: "- Remote Image:",
},
{
name: "container local",
artifact: &Artifact{
Kind: ArtifactKindContainer,
Location: "myimage:latest",
LocationKind: LocationKindLocal,
},
contains: "- Container:",
},
{
name: "archive",
artifact: &Artifact{
Kind: ArtifactKindArchive,
Location: "/path/to/output.zip",
LocationKind: LocationKindLocal,
},
contains: "- Package Output:",
},
{
name: "directory",
artifact: &Artifact{
Kind: ArtifactKindDirectory,
Location: "/path/to/build",
LocationKind: LocationKindLocal,
},
contains: "- Build Output:",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.artifact.ToString("")
require.Contains(t, result, tt.contains)
})
}
}

func TestArtifactToString_EmptyLocation(t *testing.T) {
artifact := &Artifact{
Kind: ArtifactKindEndpoint,
Location: "",
LocationKind: LocationKindRemote,
}

result := artifact.ToString("")
require.Empty(t, result)
}
Loading