Skip to content
Merged
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
1 change: 1 addition & 0 deletions cli/exp_task.go
Original file line number Diff line number Diff 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 deletions cli/exp_taskcreate.go
Original file line number Diff line number Diff 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,
})
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()),
)

return nil
},
}
}
227 changes: 227 additions & 0 deletions cli/exp_taskcreate_test.go
Original file line number Diff line number Diff 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 {
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
Loading