Skip to content
53 changes: 18 additions & 35 deletions cli/exp_task_delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,14 @@ func TestExpTaskDelete(t *testing.T) {
taskID := uuid.MustParse(id1)
return func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks" && r.URL.Query().Get("q") == "owner:\"me\"":
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks/me/exists":
c.nameResolves.Add(1)
httpapi.Write(r.Context(), w, http.StatusOK, struct {
Tasks []codersdk.Task `json:"tasks"`
Count int `json:"count"`
}{
Tasks: []codersdk.Task{{
httpapi.Write(r.Context(), w, http.StatusOK,
codersdk.Task{
ID: taskID,
Name: "exists",
OwnerName: "me",
}},
Count: 1,
})
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/experimental/tasks/me/"+id1:
c.deleteCalls.Add(1)
w.WriteHeader(http.StatusAccepted)
Expand Down Expand Up @@ -107,27 +102,21 @@ func TestExpTaskDelete(t *testing.T) {
name: "Multiple_YesFlag",
args: []string{"--yes", "first", id4},
buildHandler: func(c *testCounters) http.HandlerFunc {
firstID := uuid.MustParse(id3)
return func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks" && r.URL.Query().Get("q") == "owner:\"me\"":
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks/me/first":
c.nameResolves.Add(1)
httpapi.Write(r.Context(), w, http.StatusOK, struct {
Tasks []codersdk.Task `json:"tasks"`
Count int `json:"count"`
}{
Tasks: []codersdk.Task{{
ID: firstID,
Name: "first",
OwnerName: "me",
}},
Count: 1,
httpapi.Write(r.Context(), w, http.StatusOK, codersdk.Task{
ID: uuid.MustParse(id3),
Name: "first",
OwnerName: "me",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks/me/"+id4:
c.nameResolves.Add(1)
httpapi.Write(r.Context(), w, http.StatusOK, codersdk.Task{
ID: uuid.MustParse(id4),
OwnerName: "me",
Name: "uuid-task-2",
Name: "uuid-task-4",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/experimental/tasks/me/"+id3:
c.deleteCalls.Add(1)
Expand All @@ -141,7 +130,7 @@ func TestExpTaskDelete(t *testing.T) {
}
},
wantDeleteCalls: 2,
wantNameResolves: 1,
wantNameResolves: 2,
wantDeletedMessage: 2,
},
{
Expand Down Expand Up @@ -174,20 +163,14 @@ func TestExpTaskDelete(t *testing.T) {
taskID := uuid.MustParse(id5)
return func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks" && r.URL.Query().Get("q") == "owner:\"me\"":
case r.Method == http.MethodGet && r.URL.Path == "/api/experimental/tasks/me/bad":
c.nameResolves.Add(1)
httpapi.Write(r.Context(), w, http.StatusOK, struct {
Tasks []codersdk.Task `json:"tasks"`
Count int `json:"count"`
}{
Tasks: []codersdk.Task{{
ID: taskID,
Name: "bad",
OwnerName: "me",
}},
Count: 1,
httpapi.Write(r.Context(), w, http.StatusOK, codersdk.Task{
ID: taskID,
Name: "bad",
OwnerName: "me",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/experimental/tasks/me/"+id5:
case r.Method == http.MethodDelete && r.URL.Path == "/api/experimental/tasks/me/bad":
httpapi.InternalServerError(w, xerrors.New("boom"))
default:
httpapi.InternalServerError(w, xerrors.New("unwanted path: "+r.Method+" "+r.URL.Path))
Expand Down
127 changes: 30 additions & 97 deletions cli/exp_task_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,9 @@ func Test_TaskStatus(t *testing.T) {
hf: func(ctx context.Context, _ time.Time) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/experimental/tasks":
if r.URL.Query().Get("q") == "owner:\"me\"" {
httpapi.Write(ctx, w, http.StatusOK, struct {
Tasks []codersdk.Task `json:"tasks"`
Count int `json:"count"`
}{
Tasks: []codersdk.Task{},
Count: 0,
})
return
}
case "/api/experimental/tasks/me/doesnotexist":
httpapi.ResourceNotFound(w)
return
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
Expand All @@ -60,35 +52,7 @@ func Test_TaskStatus(t *testing.T) {
hf: func(ctx context.Context, now time.Time) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/experimental/tasks":
if r.URL.Query().Get("q") == "owner:\"me\"" {
httpapi.Write(ctx, w, http.StatusOK, struct {
Tasks []codersdk.Task `json:"tasks"`
Count int `json:"count"`
}{
Tasks: []codersdk.Task{{
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
Name: "exists",
OwnerName: "me",
WorkspaceStatus: codersdk.WorkspaceStatusRunning,
CreatedAt: now,
UpdatedAt: now,
CurrentState: &codersdk.TaskStateEntry{
State: codersdk.TaskStateWorking,
Timestamp: now,
Message: "Thinking furiously...",
},
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
Healthy: true,
},
WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
Status: codersdk.TaskStatusActive,
}},
Count: 1,
})
return
}
case "/api/experimental/tasks/me/11111111-1111-1111-1111-111111111111":
case "/api/experimental/tasks/me/exists":
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
WorkspaceStatus: codersdk.WorkspaceStatusRunning,
Expand Down Expand Up @@ -124,30 +88,21 @@ func Test_TaskStatus(t *testing.T) {
var calls atomic.Int64
return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/experimental/tasks":
if r.URL.Query().Get("q") == "owner:\"me\"" {
// Return initial task state for --watch test
httpapi.Write(ctx, w, http.StatusOK, struct {
Tasks []codersdk.Task `json:"tasks"`
Count int `json:"count"`
}{
Tasks: []codersdk.Task{{
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
Name: "exists",
OwnerName: "me",
WorkspaceStatus: codersdk.WorkspaceStatusPending,
CreatedAt: now.Add(-5 * time.Second),
UpdatedAt: now.Add(-5 * time.Second),
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
Healthy: true,
},
WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
Status: codersdk.TaskStatusPending,
}},
Count: 1,
})
return
}
case "/api/experimental/tasks/me/exists":
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
Name: "exists",
OwnerName: "me",
WorkspaceStatus: codersdk.WorkspaceStatusPending,
CreatedAt: now.Add(-5 * time.Second),
UpdatedAt: now.Add(-5 * time.Second),
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
Healthy: true,
},
WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
Status: codersdk.TaskStatusPending,
})
return
case "/api/experimental/tasks/me/11111111-1111-1111-1111-111111111111":
defer calls.Add(1)
switch calls.Load() {
Expand Down Expand Up @@ -263,40 +218,18 @@ func Test_TaskStatus(t *testing.T) {
ts := time.Date(2025, 8, 26, 12, 34, 56, 0, time.UTC)
return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/experimental/tasks":
if r.URL.Query().Get("q") == "owner:\"me\"" {
httpapi.Write(ctx, w, http.StatusOK, struct {
Tasks []codersdk.Task `json:"tasks"`
Count int `json:"count"`
}{
Tasks: []codersdk.Task{{
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
Name: "exists",
OwnerName: "me",
WorkspaceStatus: codersdk.WorkspaceStatusRunning,
CreatedAt: ts,
UpdatedAt: ts,
CurrentState: &codersdk.TaskStateEntry{
State: codersdk.TaskStateWorking,
Timestamp: ts.Add(time.Second),
Message: "Thinking furiously...",
},
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
Healthy: true,
},
WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
Status: codersdk.TaskStatusActive,
}},
Count: 1,
})
return
}
case "/api/experimental/tasks/me/11111111-1111-1111-1111-111111111111":
case "/api/experimental/tasks/me/exists":
httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
WorkspaceStatus: codersdk.WorkspaceStatusRunning,
CreatedAt: ts,
UpdatedAt: ts,
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
Name: "exists",
OwnerName: "me",
WorkspaceAgentHealth: &codersdk.WorkspaceAgentHealth{
Healthy: true,
},
WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady),
WorkspaceStatus: codersdk.WorkspaceStatusRunning,
CreatedAt: ts,
UpdatedAt: ts,
CurrentState: &codersdk.TaskStateEntry{
State: codersdk.TaskStateWorking,
Timestamp: ts.Add(time.Second),
Expand Down
33 changes: 26 additions & 7 deletions coderd/aitasks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,13 @@ func TestTasks(t *testing.T) {
t.Parallel()

var (
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
ctx = testutil.Context(t, testutil.WaitLong)
user = coderdtest.CreateFirstUser(t, client)
template = createAITemplate(t, client, user)
wantPrompt = "review my code"
exp = codersdk.NewExperimentalClient(client)
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
ctx = testutil.Context(t, testutil.WaitLong)
user = coderdtest.CreateFirstUser(t, client)
anotherUser, _ = coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
template = createAITemplate(t, client, user)
wantPrompt = "review my code"
exp = codersdk.NewExperimentalClient(client)
)

task, err := exp.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
Expand Down Expand Up @@ -211,6 +212,24 @@ func TestTasks(t *testing.T) {
assert.Equal(t, taskAppID, updated.WorkspaceAppID.UUID, "workspace app id should match")
assert.NotEmpty(t, updated.WorkspaceStatus, "task status should not be empty")

// Fetch the task by name and verify the same result
byName, err := exp.TaskByOwnerAndName(ctx, codersdk.Me, task.Name)
require.NoError(t, err)
require.Equal(t, byName, updated)

// Another member user should not be able to fetch the task
otherClient := codersdk.NewExperimentalClient(anotherUser)
_, err = otherClient.TaskByID(ctx, task.ID)
require.Error(t, err, "fetching task should fail by ID for another member user")
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
// Also test by name
_, err = otherClient.TaskByOwnerAndName(ctx, task.OwnerName, task.Name)
require.Error(t, err, "fetching task should fail by name for another member user")
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())

// Stop the workspace
coderdtest.MustTransitionWorkspace(t, client, task.WorkspaceID.UUID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)

Expand Down Expand Up @@ -654,7 +673,7 @@ func TestTasks(t *testing.T) {
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)

// Fetch the task by ID via experimental API and verify fields.
task, err = exp.TaskByID(ctx, task.ID)
task, err = exp.TaskByIdentifier(ctx, task.ID.String())
require.NoError(t, err)
require.NotZero(t, task.WorkspaceBuildNumber)
require.True(t, task.WorkspaceAgentID.Valid)
Expand Down
4 changes: 4 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -2989,6 +2989,10 @@ func (q *querier) GetTaskByID(ctx context.Context, id uuid.UUID) (database.Task,
return fetch(q.log, q.auth, q.db.GetTaskByID)(ctx, id)
}

func (q *querier) GetTaskByOwnerIDAndName(ctx context.Context, arg database.GetTaskByOwnerIDAndNameParams) (database.Task, error) {
return fetch(q.log, q.auth, q.db.GetTaskByOwnerIDAndName)(ctx, arg)
}

func (q *querier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.Task, error) {
return fetch(q.log, q.auth, q.db.GetTaskByWorkspaceID)(ctx, workspaceID)
}
Expand Down
11 changes: 11 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2375,6 +2375,17 @@ func (s *MethodTestSuite) TestTasks() {
dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes()
check.Args(task.ID).Asserts(task, policy.ActionRead).Returns(task)
}))
s.Run("GetTaskByOwnerIDAndName", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
task := testutil.Fake(s.T(), faker, database.Task{})
dbm.EXPECT().GetTaskByOwnerIDAndName(gomock.Any(), database.GetTaskByOwnerIDAndNameParams{
OwnerID: task.OwnerID,
Name: task.Name,
}).Return(task, nil).AnyTimes()
check.Args(database.GetTaskByOwnerIDAndNameParams{
OwnerID: task.OwnerID,
Name: task.Name,
}).Asserts(task, policy.ActionRead).Returns(task)
}))
s.Run("DeleteTask", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
task := testutil.Fake(s.T(), faker, database.Task{})
arg := database.DeleteTaskParams{
Expand Down
7 changes: 7 additions & 0 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading