diff --git a/pkg/cmd/agent-task/create/create.go b/pkg/cmd/agent-task/create/create.go index 41f615c3f..efc930621 100644 --- a/pkg/cmd/agent-task/create/create.go +++ b/pkg/cmd/agent-task/create/create.go @@ -5,10 +5,13 @@ import ( "errors" "fmt" "net/url" + "os" + "strings" "time" "github.com/cenkalti/backoff/v4" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/agent-task/capi" @@ -31,18 +34,35 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co opts := &CreateOptions{ IO: f.IOStreams, } + + var fromFileName string + cmd := &cobra.Command{ - Use: "create \"\"", + Use: "create [] [flags]", Short: "Create an agent task (preview)", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - // TODO: We'll support prompting for the problem statement if not provided - // and from file flags, later. - if len(args) == 0 { - return cmdutil.FlagErrorf("a task description is required") + if err := cmdutil.MutuallyExclusive("only one of -F or arg can be provided", len(args) > 0, fromFileName != ""); err != nil { + return err } - opts.ProblemStatement = args[0] + // Populate ProblemStatement from either arg or file + if len(args) > 0 { + opts.ProblemStatement = args[0] + } else if fromFileName != "" { + fileContent, err := os.ReadFile(fromFileName) + if err != nil { + return cmdutil.FlagErrorf("could not read task description file: %v", err) + } + trimmed := strings.TrimSpace(string(fileContent)) + if trimmed == "" { + return cmdutil.FlagErrorf("task description file is empty") + } + opts.ProblemStatement = trimmed + } + if opts.ProblemStatement == "" { + return cmdutil.FlagErrorf("a task description is required") + } // Support -R/--repo override if f != nil { opts.BaseRepo = f.BaseRepo @@ -52,11 +72,20 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co } return createRun(opts) }, + Example: heredoc.Doc(` + # Create a task from an inline description + $ gh agent-task create "build me a new app" + + # Create a task from a file + $ gh agent-task create -F task-desc.md + `), } if f != nil { cmdutil.EnableRepoOverride(cmd, f) } + cmd.Flags().StringVarP(&fromFileName, "from-file", "F", "", "Read task description from file") + opts.CapiClient = func() (capi.CapiClient, error) { cfg, err := f.Config() if err != nil { diff --git a/pkg/cmd/agent-task/create/create_test.go b/pkg/cmd/agent-task/create/create_test.go index 977d32dfb..e1aebab1e 100644 --- a/pkg/cmd/agent-task/create/create_test.go +++ b/pkg/cmd/agent-task/create/create_test.go @@ -2,6 +2,7 @@ package create import ( "net/http" + "os" "testing" "github.com/MakeNowJust/heredoc" @@ -17,13 +18,92 @@ import ( // Test basic option parsing & repository requirement func TestNewCmdCreate_Args(t *testing.T) { - f := &cmdutil.Factory{} - cmd := NewCmdCreate(f, func(o *CreateOptions) error { return nil }) - // no args should error via cobra MinimumNArgs before our runF - // TODO once we support more sources of problem statement input, - // this will change. - _, err := cmd.ExecuteC() - require.Error(t, err) + type tc struct { + name string + args []string + fileContent string // if non-empty, create temp file and substitute {{FILE}} token in args + wantOpts *CreateOptions // nil when expecting error + expectedErr string + } + + tests := []tc{ + { + name: "no args nor file", + args: []string{}, + expectedErr: "a task description is required", + }, + { + name: "arg only success", + args: []string{"task description from args"}, + wantOpts: &CreateOptions{ + ProblemStatement: "task description from args", + }, + }, + { + name: "from-file success", + args: []string{"-F", "{{FILE}}"}, + fileContent: "task description from file", + wantOpts: &CreateOptions{ + ProblemStatement: "task description from file", + }, + }, + { + name: "mutually exclusive arg and file", + args: []string{"Some task inline", "-F", "{{FILE}}"}, + fileContent: "Some task", + expectedErr: "only one of -F or arg can be provided", + }, + { + name: "missing file path", + args: []string{"-F", "does-not-exist.md"}, + expectedErr: "could not read task description file: open does-not-exist.md: no such file or directory", + }, + { + name: "empty file", + args: []string{"-F", "{{FILE}}"}, + fileContent: " \n\n", + expectedErr: "task description file is empty", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test file creation + var filePath string + if tt.fileContent != "" { + dir := t.TempDir() + filePath = dir + "/task.md" + if err := os.WriteFile(filePath, []byte(tt.fileContent), 0o600); err != nil { + t.Fatalf("failed to write temp file: %v", err) + } + // substitute placeholder + for i, a := range tt.args { + if a == "{{FILE}}" { + tt.args[i] = filePath + } + } + } + + f := &cmdutil.Factory{} + var gotOpts *CreateOptions + cmd := NewCmdCreate(f, func(o *CreateOptions) error { + gotOpts = o + return nil + }) + cmd.SetArgs(tt.args) + _, err := cmd.ExecuteC() + + if tt.expectedErr != "" { + require.Error(t, err) + require.Equal(t, tt.expectedErr, err.Error()) + return + } + require.NoError(t, err) + if tt.wantOpts != nil { + require.Equal(t, tt.wantOpts.ProblemStatement, gotOpts.ProblemStatement) + } + }) + } } func Test_createRun(t *testing.T) {