Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(cli): add exp task create command#19492

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
DanielleMaywood merged 6 commits intomainfromdanielle/tasks/cli-create
Aug 26, 2025
Merged
Show file tree
Hide file tree
Changes fromall 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
1 change: 1 addition & 0 deletionscli/exp_task.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,7 @@ func (r *RootCmd) tasksCommand() *serpent.Command {
},
Children: []*serpent.Command{
r.taskList(),
r.taskCreate(),
},
}
return cmd
Expand Down
127 changes: 127 additions & 0 deletionscli/exp_taskcreate.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
package cli

import (
"fmt"
"strings"
"time"

"github.com/google/uuid"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)

func (r *RootCmd) taskCreate() *serpent.Command {
var (
orgContext = NewOrganizationContext()
client = new(codersdk.Client)

templateName string
templateVersionName string
presetName string
taskInput string
)

return &serpent.Command{
Use: "create [template]",
Short: "Create an experimental task",
Middleware: serpent.Chain(
serpent.RequireRangeArgs(0, 1),
r.InitClient(client),
),
Options: serpent.OptionSet{
{
Flag: "input",
Env: "CODER_TASK_INPUT",
Value: serpent.StringOf(&taskInput),
Required: true,
},
{
Env: "CODER_TASK_TEMPLATE_NAME",
Value: serpent.StringOf(&templateName),
},
{
Env: "CODER_TASK_TEMPLATE_VERSION",
Value: serpent.StringOf(&templateVersionName),
},
{
Flag: "preset",
Env: "CODER_TASK_PRESET_NAME",
Value: serpent.StringOf(&presetName),
Default: PresetNone,
},
},
Handler: func(inv *serpent.Invocation) error {
var (
ctx = inv.Context()
expClient = codersdk.NewExperimentalClient(client)

templateVersionID uuid.UUID
templateVersionPresetID uuid.UUID
)

organization, err := orgContext.Selected(inv, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
}

if len(inv.Args) > 0 {
templateName, templateVersionName, _ = strings.Cut(inv.Args[0], "@")
}

if templateName == "" {
return xerrors.Errorf("template name not provided")
}

if templateVersionName != "" {
templateVersion, err := client.TemplateVersionByOrganizationAndName(ctx, organization.ID, templateName, templateVersionName)
if err != nil {
return xerrors.Errorf("get template version: %w", err)
}

templateVersionID = templateVersion.ID
} else {
template, err := client.TemplateByName(ctx, organization.ID, templateName)
if err != nil {
return xerrors.Errorf("get template: %w", err)
}

templateVersionID = template.ActiveVersionID
}

if presetName != PresetNone {
templatePresets, err := client.TemplateVersionPresets(ctx, templateVersionID)
if err != nil {
return xerrors.Errorf("get template presets: %w", err)
}

preset, err := resolvePreset(templatePresets, presetName)
if err != nil {
return xerrors.Errorf("resolve preset: %w", err)
}

templateVersionPresetID = preset.ID
}

workspace, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
TemplateVersionID: templateVersionID,
TemplateVersionPresetID: templateVersionPresetID,
Prompt: taskInput,
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: Not sure if we're too far gone to do this, but potential for rename: Prompt -> Input depending on how we want to standardize this.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

As it is all experimental we technically still have time

mafredri reacted with thumbs up emoji
})
if err != nil {
return xerrors.Errorf("create task: %w", err)
}

_, _ = fmt.Fprintf(
inv.Stdout,
"The task %s has been created at %s!\n",
cliui.Keyword(workspace.Name),
cliui.Timestamp(time.Now()),
Copy link
Member

Choose a reason for hiding this comment

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

workspace.CreatedAt?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

The way I've implemented it is how we currently do it for workspace creation but I'm happy to create a quick follow up PR to address this?

Copy link
Member

Choose a reason for hiding this comment

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

Up to you tbh, I'm good with either just thought it made sense so threw it out there. 👍🏻

)

return nil
},
}
}
227 changes: 227 additions & 0 deletionscli/exp_taskcreate_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
package cli_test

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/serpent"
)

func TestTaskCreate(t *testing.T) {
t.Parallel()

var (
organizationID = uuid.New()
templateID = uuid.New()
templateVersionID = uuid.New()
templateVersionPresetID = uuid.New()
)

templateAndVersionFoundHandler := func(t *testing.T, ctx context.Context, templateName, templateVersionName, presetName, prompt string) http.HandlerFunc {
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Decided to create a small helper here instead of writing the same handler over-and-over again. This is following in the footsteps of#19533 by using a http handler to mockcoderd behavior instead of usingcoderdtest.

johnstcn reacted with thumbs up emoji
Copy link
Member

Choose a reason for hiding this comment

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

We might want to add a single "integration"-style test at the end in a separate PR so we know we've all our bases covered.

DanielleMaywood reacted with thumbs up emoji
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Sounds good to me

t.Helper()

return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v2/users/me/organizations":
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
{MinimalOrganization: codersdk.MinimalOrganization{
ID: organizationID,
}},
})
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/my-template-version", organizationID):
httpapi.Write(ctx, w, http.StatusOK, codersdk.TemplateVersion{
ID: templateVersionID,
})
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template", organizationID):
httpapi.Write(ctx, w, http.StatusOK, codersdk.Template{
ID: templateID,
ActiveVersionID: templateVersionID,
})
case fmt.Sprintf("/api/v2/templateversions/%s/presets", templateVersionID):
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Preset{
{
ID: templateVersionPresetID,
Name: presetName,
},
})
case "/api/experimental/tasks/me":
var req codersdk.CreateTaskRequest
if !httpapi.Read(ctx, w, r, &req) {
return
}

assert.Equal(t, prompt, req.Prompt, "prompt mismatch")
assert.Equal(t, templateVersionID, req.TemplateVersionID, "template version mismatch")

if presetName == "" {
assert.Equal(t, uuid.Nil, req.TemplateVersionPresetID, "expected no template preset id")
} else {
assert.Equal(t, templateVersionPresetID, req.TemplateVersionPresetID, "template version preset id mismatch")
}

httpapi.Write(ctx, w, http.StatusCreated, codersdk.Workspace{
Name: "task-wild-goldfish-27",
})
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}
}

tests := []struct {
args []string
env []string
expectError string
expectOutput string
handler func(t *testing.T, ctx context.Context) http.HandlerFunc
}{
{
args: []string{"my-template@my-template-version", "--input", "my custom prompt"},
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
},
},
{
args: []string{"my-template", "--input", "my custom prompt"},
env: []string{"CODER_TASK_TEMPLATE_VERSION=my-template-version"},
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
},
},
{
args: []string{"--input", "my custom prompt"},
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version"},
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
},
},
{
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version", "CODER_TASK_INPUT=my custom prompt"},
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
},
},
{
args: []string{"my-template", "--input", "my custom prompt"},
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "", "my custom prompt")
},
},
{
args: []string{"my-template", "--input", "my custom prompt", "--preset", "my-preset"},
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
},
},
{
args: []string{"my-template", "--input", "my custom prompt"},
env: []string{"CODER_TASK_PRESET_NAME=my-preset"},
expectOutput: fmt.Sprintf("The task %s has been created", cliui.Keyword("task-wild-goldfish-27")),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
},
},
{
args: []string{"my-template", "--input", "my custom prompt", "--preset", "not-real-preset"},
expectError: `preset "not-real-preset" not found`,
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
},
},
{
args: []string{"my-template@not-real-template-version", "--input", "my custom prompt"},
expectError: httpapi.ResourceNotFoundResponse.Message,
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v2/users/me/organizations":
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
{MinimalOrganization: codersdk.MinimalOrganization{
ID: organizationID,
}},
})
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/not-real-template-version", organizationID):
httpapi.ResourceNotFound(w)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}
},
},
{
args: []string{"not-real-template", "--input", "my custom prompt"},
expectError: httpapi.ResourceNotFoundResponse.Message,
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v2/users/me/organizations":
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
{MinimalOrganization: codersdk.MinimalOrganization{
ID: organizationID,
}},
})
case fmt.Sprintf("/api/v2/organizations/%s/templates/not-real-template", organizationID):
httpapi.ResourceNotFound(w)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}
},
},
}

for _, tt := range tests {
t.Run(strings.Join(tt.args, ","), func(t *testing.T) {
t.Parallel()

var (
ctx = testutil.Context(t, testutil.WaitShort)
srv = httptest.NewServer(tt.handler(t, ctx))
client = new(codersdk.Client)
args = []string{"exp", "task", "create"}
sb strings.Builder
err error
)

t.Cleanup(srv.Close)

client.URL, err = url.Parse(srv.URL)
require.NoError(t, err)

inv, root := clitest.New(t, append(args, tt.args...)...)
inv.Environ = serpent.ParseEnviron(tt.env, "")
inv.Stdout = &sb
inv.Stderr = &sb
clitest.SetupConfig(t, client, root)

err = inv.WithContext(ctx).Run()
if tt.expectError == "" {
assert.NoError(t, err)
} else {
assert.ErrorContains(t, err, tt.expectError)
}

assert.Contains(t, sb.String(), tt.expectOutput)
})
}
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp