Skip to content

Commit 02e878c

Browse files
committed
chore: sort AI tasks by starting/running then created date
1 parent ab254ad commit 02e878c

File tree

4 files changed

+118
-44
lines changed

4 files changed

+118
-44
lines changed

coderd/database/dbmem/dbmem.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13637,6 +13637,10 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
1363713637
return job.CompletedAt.Valid && !job.CanceledAt.Valid && !job.Error.Valid && build.Transition == database.WorkspaceTransitionStart
1363813638
}
1363913639

13640+
isStarted := func(build database.WorkspaceBuild, job database.ProvisionerJob) bool {
13641+
return !job.CanceledAt.Valid && !job.Error.Valid && build.Transition == database.WorkspaceTransitionStart
13642+
}
13643+
1364013644
preloadedWorkspaceBuilds := map[uuid.UUID]database.WorkspaceBuild{}
1364113645
preloadedProvisionerJobs := map[uuid.UUID]database.ProvisionerJob{}
1364213646
preloadedUsers := map[uuid.UUID]database.User{}
@@ -13676,16 +13680,34 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
1367613680
return false
1367713681
}
1367813682

13679-
// Order by: running
13680-
w1IsRunning := isRunning(preloadedWorkspaceBuilds[w1.ID], preloadedProvisionerJobs[w1.ID])
13681-
w2IsRunning := isRunning(preloadedWorkspaceBuilds[w2.ID], preloadedProvisionerJobs[w2.ID])
13683+
// For tasks, order anything starting or running first, and then by created
13684+
// date.
13685+
if arg.HasAITask.Bool {
13686+
w1IsStarted := isStarted(preloadedWorkspaceBuilds[w1.ID], preloadedProvisionerJobs[w1.ID])
13687+
w2IsStarted := isStarted(preloadedWorkspaceBuilds[w2.ID], preloadedProvisionerJobs[w2.ID])
13688+
if w1IsStarted && !w2IsStarted {
13689+
return true
13690+
}
1368213691

13683-
if w1IsRunning && !w2IsRunning {
13684-
return true
13685-
}
13692+
if !w1IsStarted && w2IsStarted {
13693+
return false
13694+
}
1368613695

13687-
if !w1IsRunning && w2IsRunning {
13688-
return false
13696+
if w1.CreatedAt.After(w2.CreatedAt) {
13697+
return true
13698+
}
13699+
} else {
13700+
// Order by: running
13701+
w1IsRunning := isRunning(preloadedWorkspaceBuilds[w1.ID], preloadedProvisionerJobs[w1.ID])
13702+
w2IsRunning := isRunning(preloadedWorkspaceBuilds[w2.ID], preloadedProvisionerJobs[w2.ID])
13703+
13704+
if w1IsRunning && !w2IsRunning {
13705+
return true
13706+
}
13707+
13708+
if !w1IsRunning && w2IsRunning {
13709+
return false
13710+
}
1368913711
}
1369013712

1369113713
// Order by: usernames

coderd/database/queries.sql.go

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaces.sql

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -380,11 +380,21 @@ WHERE
380380
ORDER BY
381381
-- To ensure that 'favorite' workspaces show up first in the list only for their owner.
382382
CASE WHEN owner_id = @requester_id AND favorite THEN 0 ELSE 1 END ASC,
383-
(latest_build_completed_at IS NOT NULL AND
384-
latest_build_canceled_at IS NULL AND
385-
latest_build_error IS NULL AND
386-
latest_build_transition = 'start'::workspace_transition) DESC,
383+
-- For AI tasks, put anything running or starting first. Otherwise put
384+
-- running only first.
385+
CASE WHEN sqlc.narg('has_ai_task') :: boolean = true THEN
386+
(latest_build_canceled_at IS NULL AND
387+
latest_build_error IS NULL AND
388+
latest_build_transition = 'start'::workspace_transition)
389+
ELSE
390+
(latest_build_completed_at IS NOT NULL AND
391+
latest_build_canceled_at IS NULL AND
392+
latest_build_error IS NULL AND
393+
latest_build_transition = 'start'::workspace_transition)
394+
END DESC,
387395
LOWER(owner_username) ASC,
396+
-- AI tasks are additionally sorted by creation date before the name.
397+
CASE WHEN sqlc.narg('has_ai_task') :: boolean = true THEN created_at END DESC,
388398
LOWER(name) ASC
389399
LIMIT
390400
CASE

coderd/workspaces_test.go

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4508,17 +4508,30 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
45084508
ctx := testutil.Context(t, testutil.WaitLong)
45094509

45104510
// Helper function to create workspace with AI task configuration
4511-
createWorkspaceWithAIConfig := func(hasAITask sql.NullBool, jobCompleted bool, aiTaskPrompt *string) database.WorkspaceTable {
4511+
createWorkspaceWithAIConfig := func(hasAITask sql.NullBool, jobCompleted bool, aiTaskPrompt *string, conf *database.WorkspaceTable) database.WorkspaceTable {
45124512
// When a provisioner job uses these tags, no provisioner will match it.
45134513
// We do this so jobs will always be stuck in "pending", allowing us to exercise the intermediary state when
45144514
// has_ai_task is nil and we compensate by looking at pending provisioning jobs.
45154515
// See GetWorkspaces clauses.
45164516
unpickableTags := database.StringMap{"custom": "true"}
45174517

45184518
ws := dbgen.Workspace(t, db, database.WorkspaceTable{
4519-
OwnerID: user.UserID,
4520-
OrganizationID: user.OrganizationID,
4521-
TemplateID: template.ID,
4519+
OwnerID: user.UserID,
4520+
OrganizationID: user.OrganizationID,
4521+
TemplateID: template.ID,
4522+
ID: conf.ID,
4523+
CreatedAt: conf.CreatedAt,
4524+
UpdatedAt: conf.UpdatedAt,
4525+
Deleted: conf.Deleted,
4526+
Name: conf.Name,
4527+
AutostartSchedule: conf.AutostartSchedule,
4528+
Ttl: conf.Ttl,
4529+
LastUsedAt: conf.LastUsedAt,
4530+
DormantAt: conf.DormantAt,
4531+
DeletingAt: conf.DeletingAt,
4532+
AutomaticUpdates: conf.AutomaticUpdates,
4533+
Favorite: conf.Favorite,
4534+
NextStartAt: conf.NextStartAt,
45224535
})
45234536

45244537
jobConfig := database.ProvisionerJob{
@@ -4563,18 +4576,49 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
45634576
return ws
45644577
}
45654578

4579+
expectWorkspaces := func(workspaces []codersdk.Workspace, order []uuid.UUID) {
4580+
ids := make([]uuid.UUID, len(workspaces))
4581+
for i, ws := range workspaces {
4582+
t.Logf("Workspace %d: ID=%s, Name=%s, Status=%s", i, ws.ID, ws.Name, ws.LatestBuild.Status)
4583+
ids[i] = ws.ID
4584+
}
4585+
t.Logf("Expected IDs: %s", order)
4586+
require.Len(t, workspaces, len(order))
4587+
require.Equal(t, order, ids)
4588+
}
4589+
45664590
// Create test workspaces with different AI task configurations
4567-
wsWithAITask := createWorkspaceWithAIConfig(sql.NullBool{Bool: true, Valid: true}, true, nil)
4568-
wsWithoutAITask := createWorkspaceWithAIConfig(sql.NullBool{Bool: false, Valid: true}, false, nil)
4591+
wsWithAITask := createWorkspaceWithAIConfig(sql.NullBool{Bool: true, Valid: true}, true, nil, &database.WorkspaceTable{
4592+
Name: "alpha",
4593+
CreatedAt: time.Now().Add(-30 * time.Minute),
4594+
})
4595+
wsWithoutAITask := createWorkspaceWithAIConfig(sql.NullBool{Bool: false, Valid: true}, false, nil, &database.WorkspaceTable{
4596+
Name: "beta",
4597+
CreatedAt: time.Now().Add(-20 * time.Minute),
4598+
})
45694599

45704600
aiTaskPrompt := "Build me a web app"
4571-
wsWithAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, false, &aiTaskPrompt)
4601+
wsWithAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, false, &aiTaskPrompt, &database.WorkspaceTable{
4602+
Name: "gamma",
4603+
CreatedAt: time.Now().Add(-10 * time.Minute),
4604+
})
45724605

45734606
anotherTaskPrompt := "Another task"
4574-
wsCompletedWithAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, true, &anotherTaskPrompt)
4607+
wsCompletedWithAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, true, &anotherTaskPrompt, &database.WorkspaceTable{
4608+
Name: "delta",
4609+
CreatedAt: time.Now().Add(-5 * time.Minute),
4610+
})
45754611

45764612
emptyPrompt := ""
4577-
wsWithEmptyAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, false, &emptyPrompt)
4613+
wsWithEmptyAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, false, &emptyPrompt, &database.WorkspaceTable{
4614+
Name: "epsilon",
4615+
CreatedAt: time.Now(),
4616+
})
4617+
4618+
// Expected orders.
4619+
orderByWithAITask := []uuid.UUID{wsWithAITaskParam.ID, wsWithAITask.ID}
4620+
orderByWithoutAITask := []uuid.UUID{wsCompletedWithAITaskParam.ID, wsWithoutAITask.ID, wsWithEmptyAITaskParam.ID}
4621+
orderByAll := []uuid.UUID{wsWithAITask.ID, wsCompletedWithAITaskParam.ID, wsWithoutAITask.ID, wsWithEmptyAITaskParam.ID, wsWithAITaskParam.ID}
45784622

45794623
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
45804624
defer cancel()
@@ -4594,14 +4638,8 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
45944638
})
45954639
require.NoError(t, err)
45964640
t.Logf("Expected 2 workspaces for has-ai-task:true, got %d", len(res.Workspaces))
4597-
t.Logf("Expected workspaces: %s, %s", wsWithAITask.ID, wsWithAITaskParam.ID)
4598-
for i, ws := range res.Workspaces {
4599-
t.Logf("AI Task True Workspace %d: ID=%s, Name=%s", i, ws.ID, ws.Name)
4600-
}
4601-
require.Len(t, res.Workspaces, 2)
4602-
workspaceIDs := []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID}
4603-
require.Contains(t, workspaceIDs, wsWithAITask.ID)
4604-
require.Contains(t, workspaceIDs, wsWithAITaskParam.ID)
4641+
// Should be ordered by starting/running then created date.
4642+
expectWorkspaces(res.Workspaces, orderByWithAITask)
46054643

46064644
// Test filtering for workspaces without AI tasks
46074645
// Should include: wsWithoutAITask, wsCompletedWithAITaskParam, wsWithEmptyAITaskParam
@@ -4612,21 +4650,15 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
46124650

46134651
// Debug: print what we got
46144652
t.Logf("Expected 3 workspaces for has-ai-task:false, got %d", len(res.Workspaces))
4615-
for i, ws := range res.Workspaces {
4616-
t.Logf("Workspace %d: ID=%s, Name=%s", i, ws.ID, ws.Name)
4617-
}
4618-
t.Logf("Expected IDs: %s, %s, %s", wsWithoutAITask.ID, wsCompletedWithAITaskParam.ID, wsWithEmptyAITaskParam.ID)
4619-
4620-
require.Len(t, res.Workspaces, 3)
4621-
workspaceIDs = []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID, res.Workspaces[2].ID}
4622-
require.Contains(t, workspaceIDs, wsWithoutAITask.ID)
4623-
require.Contains(t, workspaceIDs, wsCompletedWithAITaskParam.ID)
4624-
require.Contains(t, workspaceIDs, wsWithEmptyAITaskParam.ID)
4653+
// Should be ordered by running then name.
4654+
expectWorkspaces(res.Workspaces, orderByWithoutAITask)
46254655

46264656
// Test no filter returns all
46274657
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
4658+
t.Logf("Expected 5 workspaces without filter, got %d", len(res.Workspaces))
46284659
require.NoError(t, err)
4629-
require.Len(t, res.Workspaces, 5)
4660+
// Should be ordered by running then name.
4661+
expectWorkspaces(res.Workspaces, orderByAll)
46304662
}
46314663

46324664
func TestWorkspaceAppUpsertRestart(t *testing.T) {

0 commit comments

Comments
 (0)