Skip to content
Draft
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
39 changes: 31 additions & 8 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"os"
"strings"
"text/tabwriter"
"time"

Expand Down Expand Up @@ -108,31 +109,40 @@ func (a *ShowAction) Run(ctx context.Context) error {
return err
}

version, err := agentClient.GetAgentVersion(
ctx, a.Name, a.Version, DefaultAgentAPIVersion,
)
version, err := agentClient.GetAgentVersion(ctx, a.Name, a.Version, DefaultAgentAPIVersion)
if err != nil {
return fmt.Errorf("failed to get agent version: %w", err)
}

var endpoint *agent_api.AgentEndpointInfo
if agent, err := agentClient.GetAgent(ctx, a.Name, DefaultAgentAPIVersion); err == nil {
endpoint = agent.AgentEndpoint
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider logging the error in debug mode when GetAgent fails. Without it, users can't distinguish 'the agent has no endpoint' from 'the fetch failed silently'. Something like:

Suggested change
}
var endpoint *agent_api.AgentEndpointInfo
if agent, err := agentClient.GetAgent(ctx, a.Name, DefaultAgentAPIVersion); err == nil {
endpoint = agent.AgentEndpoint
} else {
log.Printf("show: GetAgent for %q: %v", a.Name, err)
}

This aligns with the log.Printf pattern used in helpers.go for non-blocking errors.


switch a.flags.output {
case "table":
return printAgentVersionTable(version)
return printAgentVersionTable(version, endpoint)
default:
return printAgentVersionJSON(version)
return printAgentVersionJSON(version, endpoint)
}
}

func printAgentVersionJSON(version *agent_api.AgentVersionObject) error {
jsonBytes, err := json.MarshalIndent(version, "", " ")
type agentShowOutput struct {
*agent_api.AgentVersionObject
AgentEndpoint *agent_api.AgentEndpointInfo `json:"agent_endpoint,omitempty"`
}

func printAgentVersionJSON(version *agent_api.AgentVersionObject, endpoint *agent_api.AgentEndpointInfo) error {
out := agentShowOutput{AgentVersionObject: version, AgentEndpoint: endpoint}
jsonBytes, err := json.MarshalIndent(out, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal agent version to JSON: %w", err)
}
fmt.Println(string(jsonBytes))
return nil
}

func printAgentVersionTable(version *agent_api.AgentVersionObject) error {
func printAgentVersionTable(version *agent_api.AgentVersionObject, endpoint *agent_api.AgentEndpointInfo) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "FIELD\tVALUE")
fmt.Fprintln(w, "-----\t-----")
Expand Down Expand Up @@ -168,6 +178,19 @@ func printAgentVersionTable(version *agent_api.AgentVersionObject) error {
for k, v := range version.Metadata {
fmt.Fprintf(w, "Metadata[%s]\t%s\n", k, v)
}
if endpoint != nil {
if len(endpoint.Protocols) > 0 {
fmt.Fprintf(w, "Endpoint Protocols\t%s\n", strings.Join(endpoint.Protocols, ", "))
}
for i, scheme := range endpoint.AuthorizationSchemes {
label := fmt.Sprintf("Endpoint Auth[%d]", i)
if scheme.IsolationKeySource != nil {
fmt.Fprintf(w, "%s\t%s (isolation: %s)\n", label, scheme.Type, scheme.IsolationKeySource.Kind)
} else {
fmt.Fprintf(w, "%s\t%s\n", label, scheme.Type)
}
}
}

return w.Flush()
}
103 changes: 100 additions & 3 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/show_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
package cmd

import (
"bytes"
"encoding/json"
"io"
"os"
"testing"

"azureaiagent/internal/pkg/agents/agent_api"
Expand All @@ -13,6 +16,25 @@
"github.com/stretchr/testify/require"
)

func captureStdout(t *testing.T, fn func() error) string {
t.Helper()
orig := os.Stdout
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stdout = w

runErr := fn()

require.NoError(t, w.Close())
os.Stdout = orig

var buf bytes.Buffer
_, err = io.Copy(&buf, r)
require.NoError(t, err)
require.NoError(t, runErr)
return buf.String()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: consider using .Cleanup to ensure stdout is restored even if the callback panics or a require trips early.

}

func TestShowCommand_AcceptsPositionalArg(t *testing.T) {
cmd := newShowCommand()
err := cmd.Args(cmd, []string{"my-agent"})
Expand Down Expand Up @@ -83,7 +105,7 @@
CreatedAt: 1735689600, // 2025-01-01T00:00:00Z
}

err := printAgentVersionJSON(version)
err := printAgentVersionJSON(version, nil)
require.NoError(t, err)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The rendering tests are thorough. One gap: the key contract that show still succeeds when GetAgent fails (but fails when GetAgentVersion fails) doesn't have a direct test. If Run() is later refactored, this fallback behavior could regress without failing any test.

Worth adding a test that exercises Run() with a mocked/fake client where GetAgent returns an error and verifying the command still produces valid output.


Expand Down Expand Up @@ -165,10 +187,85 @@
},
}

err := printAgentVersionTable(version)
err := printAgentVersionTable(version, nil)
require.NoError(t, err)
}
Comment on lines 188 to 192
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

printAgentVersionTable gained endpoint rows, but the table tests only assert the function returns no error and don’t verify the new rows render. Consider adding a test that passes a non-nil endpoint and captures stdout to assert the protocols/auth scheme lines are emitted (and remain stable).

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added TestPrintAgentVersionTable_WithEndpoint in cec7ef1 — captures stdout and asserts the Endpoint Protocols, Endpoint Auth[i], and isolation: rows render as expected.


func TestPrintAgentVersionJSON_WithEndpoint(t *testing.T) {
version := &agent_api.AgentVersionObject{
Object: "agent.version",
ID: "ver-ep",
Name: "ep-agent",
Version: "1",
}
endpoint := &agent_api.AgentEndpointInfo{
Protocols: []string{"responses", "a2a"},
AuthorizationSchemes: []agent_api.AuthorizationScheme{
{
Type: "EntraIDAuth",

Check failure on line 205 in cli/azd/extensions/azure.ai.agents/internal/cmd/show_test.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (ubuntu-latest)

File is not properly formatted (gofmt)
IsolationKeySource: &agent_api.IsolationKeySource{Kind: "ProjectScopedManagedIdentity"},
},
},
}

out := captureStdout(t, func() error { return printAgentVersionJSON(version, endpoint) })

var result map[string]any
require.NoError(t, json.Unmarshal([]byte(out), &result))

ep, ok := result["agent_endpoint"].(map[string]any)
require.True(t, ok, "agent_endpoint missing in JSON output")
protocols := ep["protocols"].([]any)
assert.ElementsMatch(t, []any{"responses", "a2a"}, protocols)
schemes := ep["authorization_schemes"].([]any)
require.Len(t, schemes, 1)
scheme := schemes[0].(map[string]any)
assert.Equal(t, "EntraIDAuth", scheme["type"])
assert.Equal(t, "ProjectScopedManagedIdentity", scheme["isolation_key_source"].(map[string]any)["kind"])
}

func TestPrintAgentVersionJSON_NilEndpointOmitsField(t *testing.T) {
version := &agent_api.AgentVersionObject{
Object: "agent.version",
ID: "ver-no-ep",
Name: "no-ep-agent",
Version: "1",
}

out := captureStdout(t, func() error { return printAgentVersionJSON(version, nil) })

var result map[string]any
require.NoError(t, json.Unmarshal([]byte(out), &result))
_, hasEndpoint := result["agent_endpoint"]
assert.False(t, hasEndpoint, "agent_endpoint should be omitted when nil")
}

func TestPrintAgentVersionTable_WithEndpoint(t *testing.T) {
version := &agent_api.AgentVersionObject{
Object: "agent.version",
ID: "ver-ep",
Name: "ep-agent",
Version: "1",
}
endpoint := &agent_api.AgentEndpointInfo{
Protocols: []string{"responses", "a2a"},
AuthorizationSchemes: []agent_api.AuthorizationScheme{
{Type: "EntraIDAuth", IsolationKeySource: &agent_api.IsolationKeySource{Kind: "ProjectScopedManagedIdentity"}},
{Type: "ApiKeyAuth"},
},
}

out := captureStdout(t, func() error { return printAgentVersionTable(version, endpoint) })

assert.Contains(t, out, "Endpoint Protocols")
assert.Contains(t, out, "responses, a2a")
assert.Contains(t, out, "Endpoint Auth[0]")
assert.Contains(t, out, "EntraIDAuth")
assert.Contains(t, out, "isolation: ProjectScopedManagedIdentity")
assert.Contains(t, out, "Endpoint Auth[1]")
assert.Contains(t, out, "ApiKeyAuth")
}

func TestPrintAgentVersionTable_MinimalFields(t *testing.T) {
version := &agent_api.AgentVersionObject{
Object: "agent.version",
Expand All @@ -177,6 +274,6 @@
Version: "1",
}

err := printAgentVersionTable(version)
err := printAgentVersionTable(version, nil)
require.NoError(t, err)
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,24 @@ type AgentObject struct {
Versions struct {
Latest AgentVersionObject `json:"latest"`
} `json:"versions"`
AgentEndpoint *AgentEndpointInfo `json:"agent_endpoint,omitempty"`
}

// AgentEndpointInfo describes the public endpoint of an agent.
type AgentEndpointInfo struct {
Protocols []string `json:"protocols,omitempty"`
AuthorizationSchemes []AuthorizationScheme `json:"authorization_schemes,omitempty"`
}

// AuthorizationScheme describes one authentication mechanism accepted by an agent endpoint.
type AuthorizationScheme struct {
Type string `json:"type"`
IsolationKeySource *IsolationKeySource `json:"isolation_key_source,omitempty"`
}

Comment thread
antriksh30 marked this conversation as resolved.
// IsolationKeySource indicates how per-caller isolation keys are derived.
type IsolationKeySource struct {
Kind string `json:"kind"`
}

// CommonListObjectProperties represents common properties for list responses
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package agent_api

import (
"encoding/json"
"slices"
"strings"
"testing"
)
Expand Down Expand Up @@ -79,6 +80,12 @@ func TestAgentObject_RoundTrip(t *testing.T) {
CreatedAt: 1700000000,
},
},
AgentEndpoint: &AgentEndpointInfo{
Protocols: []string{"responses", "a2a"},
AuthorizationSchemes: []AuthorizationScheme{
{Type: "Entra", IsolationKeySource: &IsolationKeySource{Kind: "Entra"}},
},
},
}

data, err := json.Marshal(original)
Expand All @@ -87,7 +94,10 @@ func TestAgentObject_RoundTrip(t *testing.T) {
}

s := string(data)
for _, field := range []string{`"object"`, `"id"`, `"name"`, `"versions"`, `"latest"`} {
for _, field := range []string{
`"object"`, `"id"`, `"name"`, `"versions"`, `"latest"`,
`"agent_endpoint"`, `"protocols"`, `"authorization_schemes"`,
} {
if !strings.Contains(s, field) {
t.Errorf("expected JSON to contain %s", field)
}
Expand All @@ -107,6 +117,17 @@ func TestAgentObject_RoundTrip(t *testing.T) {
if got.Versions.Latest.CreatedAt != 1700000000 {
t.Errorf("Latest.CreatedAt = %d, want %d", got.Versions.Latest.CreatedAt, int64(1700000000))
}
if got.AgentEndpoint == nil {
t.Fatalf("AgentEndpoint = nil, want non-nil")
}
if !slices.Contains(got.AgentEndpoint.Protocols, "responses") {
t.Errorf("AgentEndpoint.Protocols = %v, want to contain %q", got.AgentEndpoint.Protocols, "responses")
}
if len(got.AgentEndpoint.AuthorizationSchemes) != 1 ||
got.AgentEndpoint.AuthorizationSchemes[0].Type != "Entra" {
t.Errorf("AgentEndpoint.AuthorizationSchemes = %+v, want one entry of type Entra",
got.AgentEndpoint.AuthorizationSchemes)
}
}

func TestPromptAgentDefinition_RoundTrip(t *testing.T) {
Expand Down
Loading