mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
bfa6ce32a6
Part of [coder/internal#1400](https://github.com/coder/internal/issues/1400) Batch of refactored CLI tests to avoid creating PTYs.
280 lines
9.5 KiB
Go
280 lines
9.5 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"cdr.dev/slog/v3/sloggers/sloghuman"
|
|
"github.com/coder/coder/v2/cli/clitest"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbfake"
|
|
"github.com/coder/coder/v2/coderd/util/slice"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
"github.com/coder/coder/v2/testutil/expecter"
|
|
)
|
|
|
|
// makeAITask creates an AI-task workspace.
|
|
func makeAITask(t *testing.T, db database.Store, orgID, adminID, ownerID uuid.UUID, transition database.WorkspaceTransition, prompt string) database.Task {
|
|
t.Helper()
|
|
|
|
tv := dbfake.TemplateVersion(t, db).
|
|
Seed(database.TemplateVersion{
|
|
OrganizationID: orgID,
|
|
CreatedBy: adminID,
|
|
HasAITask: sql.NullBool{
|
|
Bool: true,
|
|
Valid: true,
|
|
},
|
|
}).Do()
|
|
|
|
build := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OrganizationID: orgID,
|
|
OwnerID: ownerID,
|
|
TemplateID: tv.Template.ID,
|
|
}).
|
|
Seed(database.WorkspaceBuild{
|
|
TemplateVersionID: tv.TemplateVersion.ID,
|
|
Transition: transition,
|
|
}).
|
|
WithAgent().
|
|
WithTask(database.TaskTable{
|
|
Prompt: prompt,
|
|
}, nil).
|
|
Do()
|
|
|
|
return build.Task
|
|
}
|
|
|
|
func TestExpTaskList(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("NoTasks_Table", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Quiet logger to reduce noise.
|
|
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
|
client, _ := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
inv, root := clitest.New(t, "task", "list")
|
|
clitest.SetupConfig(t, memberClient, root)
|
|
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
stdout.ExpectMatchContext(ctx, "No tasks found.")
|
|
})
|
|
|
|
t.Run("Single_Table", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Quiet logger to reduce noise.
|
|
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
|
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
wantPrompt := "build me a web app"
|
|
task := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, wantPrompt)
|
|
|
|
inv, root := clitest.New(t, "task", "list", "--column", "id,name,status,initial prompt")
|
|
clitest.SetupConfig(t, memberClient, root)
|
|
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
// Validate the table includes the task and status.
|
|
stdout.ExpectMatchContext(ctx, task.Name)
|
|
stdout.ExpectMatchContext(ctx, "initializing")
|
|
stdout.ExpectMatchContext(ctx, wantPrompt)
|
|
})
|
|
|
|
t.Run("StatusFilter_JSON", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Quiet logger to reduce noise.
|
|
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
|
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
// Create two AI tasks: one initializing, one paused.
|
|
initializingTask := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, "keep me initializing")
|
|
pausedTask := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStop, "stop me please")
|
|
|
|
// Use JSON output to reliably validate filtering.
|
|
inv, root := clitest.New(t, "task", "list", "--status=paused", "--output=json")
|
|
clitest.SetupConfig(t, memberClient, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
var stdout bytes.Buffer
|
|
inv.Stdout = &stdout
|
|
inv.Stderr = &stdout
|
|
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
var tasks []codersdk.Task
|
|
require.NoError(t, json.Unmarshal(stdout.Bytes(), &tasks))
|
|
|
|
// Only the paused task is returned.
|
|
require.Len(t, tasks, 1, "expected one task after filtering")
|
|
require.Equal(t, pausedTask.ID, tasks[0].ID)
|
|
require.NotEqual(t, initializingTask.ID, tasks[0].ID)
|
|
})
|
|
|
|
t.Run("UserFlag_Me_Table", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
|
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
_, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
_ = makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, "other-task")
|
|
task := makeAITask(t, db, owner.OrganizationID, owner.UserID, owner.UserID, database.WorkspaceTransitionStart, "me-task")
|
|
|
|
inv, root := clitest.New(t, "task", "list", "--user", "me")
|
|
//nolint:gocritic // Owner client is intended here smoke test the member task not showing up.
|
|
clitest.SetupConfig(t, client, root)
|
|
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
stdout.ExpectMatchContext(ctx, task.Name)
|
|
})
|
|
|
|
t.Run("Quiet", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Quiet logger to reduce noise.
|
|
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
|
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
// Given: We have two tasks
|
|
task1 := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStart, "keep me active")
|
|
task2 := makeAITask(t, db, owner.OrganizationID, owner.UserID, memberUser.ID, database.WorkspaceTransitionStop, "stop me please")
|
|
|
|
// Given: We add the `--quiet` flag
|
|
inv, root := clitest.New(t, "task", "list", "--quiet")
|
|
clitest.SetupConfig(t, memberClient, root)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
var stdout bytes.Buffer
|
|
inv.Stdout = &stdout
|
|
inv.Stderr = &stdout
|
|
|
|
// When: We run the command
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
want := []string{task1.ID.String(), task2.ID.String()}
|
|
got := slice.Filter(strings.Split(stdout.String(), "\n"), func(s string) bool {
|
|
return len(s) != 0
|
|
})
|
|
|
|
slices.Sort(want)
|
|
slices.Sort(got)
|
|
|
|
require.Equal(t, want, got)
|
|
})
|
|
}
|
|
|
|
func TestExpTaskList_OwnerCanListOthers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Quiet logger to reduce noise.
|
|
quiet := slog.Make(sloghuman.Sink(io.Discard))
|
|
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{Logger: &quiet})
|
|
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
|
|
|
// Create two additional members in the owner's organization.
|
|
_, memberAUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
|
_, memberBUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
|
|
|
// Seed an AI task for member A and B.
|
|
_ = makeAITask(t, db, owner.OrganizationID, owner.UserID, memberAUser.ID, database.WorkspaceTransitionStart, "member-A-task")
|
|
_ = makeAITask(t, db, owner.OrganizationID, owner.UserID, memberBUser.ID, database.WorkspaceTransitionStart, "member-B-task")
|
|
|
|
t.Run("OwnerListsSpecificUserWithUserFlag_JSON", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// As the owner, list only member A tasks.
|
|
inv, root := clitest.New(t, "task", "list", "--user", memberAUser.Username, "--output=json")
|
|
//nolint:gocritic // Owner client is intended here to allow member tasks to be listed.
|
|
clitest.SetupConfig(t, ownerClient, root)
|
|
|
|
var stdout bytes.Buffer
|
|
inv.Stdout = &stdout
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
var tasks []codersdk.Task
|
|
require.NoError(t, json.Unmarshal(stdout.Bytes(), &tasks))
|
|
|
|
// At least one task to belong to member A.
|
|
require.NotEmpty(t, tasks, "expected at least one task for member A")
|
|
// All tasks should belong to member A.
|
|
for _, task := range tasks {
|
|
require.Equal(t, memberAUser.ID, task.OwnerID, "expected only member A tasks")
|
|
}
|
|
})
|
|
|
|
t.Run("OwnerListsAllWithAllFlag_JSON", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// As the owner, list all tasks to verify both member tasks are present.
|
|
// Use JSON output to reliably validate filtering.
|
|
inv, root := clitest.New(t, "task", "list", "--all", "--output=json")
|
|
//nolint:gocritic // Owner client is intended here to allow all tasks to be listed.
|
|
clitest.SetupConfig(t, ownerClient, root)
|
|
|
|
var stdout bytes.Buffer
|
|
inv.Stdout = &stdout
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
err := inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
var tasks []codersdk.Task
|
|
require.NoError(t, json.Unmarshal(stdout.Bytes(), &tasks))
|
|
|
|
// Expect at least two tasks and ensure both owners (member A and member B) are represented.
|
|
require.GreaterOrEqual(t, len(tasks), 2, "expected two or more tasks in --all listing")
|
|
|
|
// Use slice.Find for concise existence checks.
|
|
_, foundA := slice.Find(tasks, func(t codersdk.Task) bool { return t.OwnerID == memberAUser.ID })
|
|
_, foundB := slice.Find(tasks, func(t codersdk.Task) bool { return t.OwnerID == memberBUser.ID })
|
|
|
|
require.True(t, foundA, "expected at least one task for member A in --all listing")
|
|
require.True(t, foundB, "expected at least one task for member B in --all listing")
|
|
})
|
|
}
|