Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion cli/azd/extensions/azure.ai.agents/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.3.0-beta.3
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0
github.com/azure/azure-dev/cli/azd v1.23.14
github.com/azure/azure-dev/cli/azd v1.24.3
github.com/braydonk/yaml v0.9.0
github.com/drone/envsubst v1.0.3
github.com/fatih/color v1.18.0
Expand Down
6 changes: 2 additions & 4 deletions cli/azd/extensions/azure.ai.agents/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthoriza
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2/go.mod h1:jVRrRDLCOuif95HDYC23ADTMlvahB7tMdl519m9Iyjc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2 v2.0.0 h1:pxphC/uRZKNHNPbZ0duDDgKkefju2F03OkG5xF6byHQ=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2 v2.0.0/go.mod h1:twcwRey+l1znKBL5TEzYiZMtiVkWfM7Pq8a9vY04xYc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0 h1:DWlwvVV5r/Wy1561nZ3wrpI1/vDIBRY/Wd1HWaRBZWA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0/go.mod h1:E7ltexgRDmeJ0fJWv0D/HLwY2xbDdN+uv+X2uZtOx3w=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.3.0-beta.3 h1:4qfc7os3wRQcl+ImfeH9z0abWJzuV9IGcN1B9olmPTU=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.3.0-beta.3/go.mod h1:NlNAngH4e++mzPTN0+1EEvyUmwFmR91u/MQUVV230Z4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
Expand Down Expand Up @@ -61,8 +59,8 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/azure/azure-dev/cli/azd v1.23.14 h1:yfmEPu3T+FqFuHhHHX9gEc+yflAu0k1aDOkN00HlFzo=
github.com/azure/azure-dev/cli/azd v1.23.14/go.mod h1:mS/n9XZcwRrTaDcFxWFfgcsmB6AuuTNarB2IDCS5QzI=
github.com/azure/azure-dev/cli/azd v1.24.3 h1:r2kEr2YYLu4ImKo6nR/WjhHg/1SliN1uwmAVqnM8t3o=
github.com/azure/azure-dev/cli/azd v1.24.3/go.mod h1:YANepMw36aWA8/mQyXau6JCAG84oK0ZgfvLF8rN5asU=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import "github.com/azure/azure-dev/cli/azd/pkg/azdext"

// ensureExtensionContext returns a non-nil [azdext.ExtensionContext] so command
// constructors can be safely invoked from tests with a nil receiver. The SDK's
// [azdext.NewExtensionRootCommand] populates the real context (and its env-var
// fallback) before any leaf RunE runs, so tests that don't exercise RunE can
// safely pass nil here.
func ensureExtensionContext(extCtx *azdext.ExtensionContext) *azdext.ExtensionContext {
if extCtx == nil {
return &azdext.ExtensionContext{}
}
return extCtx
}
78 changes: 43 additions & 35 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ type filesFlags struct {
session string // optional: explicit session ID override
}

func newFilesCommand() *cobra.Command {
func newFilesCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
extCtx = ensureExtensionContext(extCtx)

cmd := &cobra.Command{
Use: "files",
Short: "Manage files in a hosted agent session.",
Expand All @@ -37,26 +39,14 @@ Agent details (name, endpoint) are automatically resolved from the
azd environment. Use --agent-name to select a specific agent when the project
has multiple azure.ai.agent services. The session ID is automatically resolved
from the last invoke session, or can be overridden with --session-id.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Chain with root's PersistentPreRunE (root sets NoPrompt).
// Note: cmd.Parent() would return the "files" command itself when
// a subcommand runs, causing infinite recursion.
if root := cmd.Root(); root != nil && root.PersistentPreRunE != nil {
if err := root.PersistentPreRunE(cmd, args); err != nil {
return err
}
}

return nil
},
}

cmd.AddCommand(newFilesUploadCommand())
cmd.AddCommand(newFilesDownloadCommand())
cmd.AddCommand(newFilesListCommand())
cmd.AddCommand(newFilesRemoveCommand())
cmd.AddCommand(newFilesMkdirCommand())
cmd.AddCommand(newFilesStatCommand())
cmd.AddCommand(newFilesUploadCommand(extCtx))
cmd.AddCommand(newFilesDownloadCommand(extCtx))
cmd.AddCommand(newFilesListCommand(extCtx))
cmd.AddCommand(newFilesRemoveCommand(extCtx))
cmd.AddCommand(newFilesMkdirCommand(extCtx))
cmd.AddCommand(newFilesStatCommand(extCtx))

return cmd
}
Expand All @@ -74,14 +64,14 @@ type filesContext struct {
}

// resolveFilesContext resolves agent details and session from the azd environment.
func resolveFilesContext(ctx context.Context, flags *filesFlags) (*filesContext, error) {
func resolveFilesContext(ctx context.Context, flags *filesFlags, noPrompt bool) (*filesContext, error) {
azdClient, err := azdext.NewAzdClient()
if err != nil {
return nil, fmt.Errorf("failed to create azd client: %w", err)
}
defer azdClient.Close()

info, err := resolveAgentServiceFromProject(ctx, azdClient, flags.agentName, rootFlags.NoPrompt)
info, err := resolveAgentServiceFromProject(ctx, azdClient, flags.agentName, noPrompt)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -137,8 +127,9 @@ type FilesUploadAction struct {
sessionID string
}

func newFilesUploadCommand() *cobra.Command {
func newFilesUploadCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
flags := &filesUploadFlags{}
extCtx = ensureExtensionContext(extCtx)

cmd := &cobra.Command{
Use: "upload [file]",
Expand Down Expand Up @@ -174,7 +165,7 @@ Agent details are automatically resolved from the azd environment.`,
)
}

fc, err := resolveFilesContext(ctx, &flags.filesFlags)
fc, err := resolveFilesContext(ctx, &flags.filesFlags, extCtx.NoPrompt)
if err != nil {
return err
}
Expand Down Expand Up @@ -246,8 +237,9 @@ type FilesDownloadAction struct {
sessionID string
}

func newFilesDownloadCommand() *cobra.Command {
func newFilesDownloadCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
flags := &filesDownloadFlags{}
extCtx = ensureExtensionContext(extCtx)

cmd := &cobra.Command{
Use: "download [file]",
Expand Down Expand Up @@ -283,7 +275,7 @@ Agent details are automatically resolved from the azd environment.`,
)
}

fc, err := resolveFilesContext(ctx, &flags.filesFlags)
fc, err := resolveFilesContext(ctx, &flags.filesFlags, extCtx.NoPrompt)
if err != nil {
return err
}
Expand Down Expand Up @@ -359,8 +351,9 @@ type FilesListAction struct {
remotePath string
}

func newFilesListCommand() *cobra.Command {
func newFilesListCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
flags := &filesListFlags{}
extCtx = ensureExtensionContext(extCtx)

cmd := &cobra.Command{
Use: "list [remote-path]",
Expand All @@ -385,11 +378,13 @@ Agent details are automatically resolved from the azd environment.`,
azd ai agent files list --session-id <session-id>`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
flags.output = extCtx.OutputFormat

ctx := azdext.WithAccessToken(cmd.Context())
logCleanup := setupDebugLogging(cmd.Flags())
defer logCleanup()

fc, err := resolveFilesContext(ctx, &flags.filesFlags)
fc, err := resolveFilesContext(ctx, &flags.filesFlags, extCtx.NoPrompt)
if err != nil {
return err
}
Expand All @@ -411,7 +406,11 @@ Agent details are automatically resolved from the azd environment.`,
}

addFilesFlags(cmd, &flags.filesFlags)
cmd.Flags().StringVar(&flags.output, "output", "json", "Output format (json or table)")
azdext.RegisterFlagOptions(cmd, azdext.FlagOptions{
Name: "output",
AllowedValues: []string{"json", "table"},
Default: "json",
})

return cmd
}
Expand Down Expand Up @@ -486,9 +485,10 @@ type FilesRemoveAction struct {
remotePath string
}

func newFilesRemoveCommand() *cobra.Command {
func newFilesRemoveCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
flags := &filesRemoveFlags{}
var filePath string
extCtx = ensureExtensionContext(extCtx)

cmd := &cobra.Command{
Use: "delete [file]",
Expand Down Expand Up @@ -524,7 +524,7 @@ Agent details are automatically resolved from the azd environment.`,
)
}

fc, err := resolveFilesContext(ctx, &flags.filesFlags)
fc, err := resolveFilesContext(ctx, &flags.filesFlags, extCtx.NoPrompt)
if err != nil {
return err
}
Expand Down Expand Up @@ -579,9 +579,10 @@ type FilesMkdirAction struct {
remotePath string
}

func newFilesMkdirCommand() *cobra.Command {
func newFilesMkdirCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
flags := &filesFlags{}
var dirPath string
extCtx = ensureExtensionContext(extCtx)

cmd := &cobra.Command{
Use: "mkdir [dir]",
Expand Down Expand Up @@ -613,7 +614,7 @@ Agent details are automatically resolved from the azd environment.`,
)
}

fc, err := resolveFilesContext(ctx, flags)
fc, err := resolveFilesContext(ctx, flags, extCtx.NoPrompt)
if err != nil {
return err
}
Expand Down Expand Up @@ -671,8 +672,9 @@ type FilesStatAction struct {
remotePath string
}

func newFilesStatCommand() *cobra.Command {
func newFilesStatCommand(extCtx *azdext.ExtensionContext) *cobra.Command {
flags := &filesStatFlags{}
extCtx = ensureExtensionContext(extCtx)

cmd := &cobra.Command{
Use: "stat <remote-path>",
Expand All @@ -692,11 +694,13 @@ Agent details are automatically resolved from the azd environment.`,
azd ai agent files stat /data/output.csv --session-id <session-id>`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
flags.output = extCtx.OutputFormat

ctx := azdext.WithAccessToken(cmd.Context())
logCleanup := setupDebugLogging(cmd.Flags())
defer logCleanup()

fc, err := resolveFilesContext(ctx, &flags.filesFlags)
fc, err := resolveFilesContext(ctx, &flags.filesFlags, extCtx.NoPrompt)
if err != nil {
return err
}
Expand All @@ -713,7 +717,11 @@ Agent details are automatically resolved from the azd environment.`,
}

addFilesFlags(cmd, &flags.filesFlags)
cmd.Flags().StringVarP(&flags.output, "output", "o", "json", "Output format (json or table)")
azdext.RegisterFlagOptions(cmd, azdext.FlagOptions{
Name: "output",
AllowedValues: []string{"json", "table"},
Default: "json",
})

return cmd
}
Expand Down
45 changes: 12 additions & 33 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,12 @@ import (

"azureaiagent/internal/pkg/agents/agent_api"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFilesCommand_PersistentPreRunE_NoRecursion(t *testing.T) {
// Regression: PersistentPreRunE previously used cmd.Parent() which returned
// the "files" command itself when a subcommand ran, causing infinite recursion
// (stack overflow). The fix uses cmd.Root() to chain to the root command.
root := NewRootCommand()

// Override the list subcommand's RunE so it doesn't need a real environment.
filesCmd, _, _ := root.Find([]string{"files"})
require.NotNil(t, filesCmd)
for _, sub := range filesCmd.Commands() {
sub.RunE = func(cmd *cobra.Command, args []string) error { return nil }
}

root.SetArgs([]string{"files", "list"})
// If the bug is present this will stack-overflow instead of returning.
_ = root.Execute()
}

func TestFilesCommand_HasSubcommands(t *testing.T) {
cmd := newFilesCommand()
cmd := newFilesCommand(nil)

subcommands := cmd.Commands()
names := make([]string, len(subcommands))
Expand All @@ -47,7 +28,7 @@ func TestFilesCommand_HasSubcommands(t *testing.T) {
}

func TestFilesUploadCommand_MissingFile(t *testing.T) {
cmd := newFilesUploadCommand()
cmd := newFilesUploadCommand(nil)

// Missing required --file flag
cmd.SetArgs([]string{})
Expand All @@ -57,7 +38,7 @@ func TestFilesUploadCommand_MissingFile(t *testing.T) {
}

func TestFilesUploadCommand_HasFlags(t *testing.T) {
cmd := newFilesUploadCommand()
cmd := newFilesUploadCommand(nil)

for _, name := range []string{"file", "target-path", "agent-name", "session-id"} {
f := cmd.Flags().Lookup(name)
Expand All @@ -67,7 +48,7 @@ func TestFilesUploadCommand_HasFlags(t *testing.T) {
}

func TestFilesDownloadCommand_MissingFile(t *testing.T) {
cmd := newFilesDownloadCommand()
cmd := newFilesDownloadCommand(nil)

// Missing required --file flag
cmd.SetArgs([]string{})
Expand All @@ -77,7 +58,7 @@ func TestFilesDownloadCommand_MissingFile(t *testing.T) {
}

func TestFilesDownloadCommand_HasFlags(t *testing.T) {
cmd := newFilesDownloadCommand()
cmd := newFilesDownloadCommand(nil)

for _, name := range []string{"file", "target-path", "agent-name", "session-id"} {
f := cmd.Flags().Lookup(name)
Expand All @@ -87,21 +68,19 @@ func TestFilesDownloadCommand_HasFlags(t *testing.T) {
}

func TestFilesListCommand_DefaultOutputFormat(t *testing.T) {
cmd := newFilesListCommand()

output, _ := cmd.Flags().GetString("output")
assert.Equal(t, "json", output)
cmd := newFilesListCommand(nil)
assertOutputFlagOptions(t, cmd, "json", []string{"json", "table"})
}

func TestFilesListCommand_OptionalRemotePath(t *testing.T) {
cmd := newFilesListCommand()
cmd := newFilesListCommand(nil)

// Verify the command accepts 0 or 1 args
assert.NotNil(t, cmd.Args)
}

func TestFilesDeleteCommand_MissingFile(t *testing.T) {
cmd := newFilesRemoveCommand()
cmd := newFilesRemoveCommand(nil)

// Missing required --file flag
cmd.SetArgs([]string{})
Expand All @@ -111,7 +90,7 @@ func TestFilesDeleteCommand_MissingFile(t *testing.T) {
}

func TestFilesDeleteCommand_HasFlags(t *testing.T) {
cmd := newFilesRemoveCommand()
cmd := newFilesRemoveCommand(nil)

for _, name := range []string{"file", "recursive", "agent-name", "session-id"} {
f := cmd.Flags().Lookup(name)
Expand All @@ -123,7 +102,7 @@ func TestFilesDeleteCommand_HasFlags(t *testing.T) {
}

func TestFilesMkdirCommand_MissingDir(t *testing.T) {
cmd := newFilesMkdirCommand()
cmd := newFilesMkdirCommand(nil)

// Missing required --dir flag
cmd.SetArgs([]string{})
Expand All @@ -133,7 +112,7 @@ func TestFilesMkdirCommand_MissingDir(t *testing.T) {
}

func TestFilesMkdirCommand_HasFlags(t *testing.T) {
cmd := newFilesMkdirCommand()
cmd := newFilesMkdirCommand(nil)

for _, name := range []string{"dir", "agent-name", "session-id"} {
f := cmd.Flags().Lookup(name)
Expand Down
Loading
Loading