From b94ffe90c4bac3389659c8b663d831871cc00d0d Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:24:01 -0600 Subject: [PATCH] Add file input support to agent-task create command The agent-task create command now accepts a task description from a file using the -F/--from-file flag, with mutual exclusivity enforced between inline and file input. Tests were updated to cover new input scenarios and error cases, and usage examples were added to the command help. --- pkg/cmd/agent-task/create/create.go | 41 +++++++++-- pkg/cmd/agent-task/create/create_test.go | 94 ++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 13 deletions(-) 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) {