diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index e1009066cf..6e76d28383 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -13553,7 +13553,10 @@ const docTemplate = `{ "cli", "ssh_connection", "vscode_connection", - "jetbrains_connection" + "jetbrains_connection", + "task_auto_pause", + "task_manual_pause", + "task_resume" ], "x-enum-varnames": [ "BuildReasonInitiator", @@ -13564,7 +13567,10 @@ const docTemplate = `{ "BuildReasonCLI", "BuildReasonSSHConnection", "BuildReasonVSCodeConnection", - "BuildReasonJetbrainsConnection" + "BuildReasonJetbrainsConnection", + "BuildReasonTaskAutoPause", + "BuildReasonTaskManualPause", + "BuildReasonTaskResume" ] }, "codersdk.CORSBehavior": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index dfd4472b2c..b90ad878e1 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -12148,7 +12148,10 @@ "cli", "ssh_connection", "vscode_connection", - "jetbrains_connection" + "jetbrains_connection", + "task_auto_pause", + "task_manual_pause", + "task_resume" ], "x-enum-varnames": [ "BuildReasonInitiator", @@ -12159,7 +12162,10 @@ "BuildReasonCLI", "BuildReasonSSHConnection", "BuildReasonVSCodeConnection", - "BuildReasonJetbrainsConnection" + "BuildReasonJetbrainsConnection", + "BuildReasonTaskAutoPause", + "BuildReasonTaskManualPause", + "BuildReasonTaskResume" ] }, "codersdk.CORSBehavior": { diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index d26e9f47ca..d212a25331 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -525,10 +525,18 @@ func getNextTransition( ) { switch { case isEligibleForAutostop(user, ws, latestBuild, latestJob, currentTick): + // Use task-specific reason for AI task workspaces. + if ws.TaskID.Valid { + return database.WorkspaceTransitionStop, database.BuildReasonTaskAutoPause, nil + } return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil case isEligibleForAutostart(user, ws, latestBuild, latestJob, templateSchedule, currentTick): return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil case isEligibleForFailedStop(latestBuild, latestJob, templateSchedule, currentTick): + // Use task-specific reason for AI task workspaces. + if ws.TaskID.Valid { + return database.WorkspaceTransitionStop, database.BuildReasonTaskAutoPause, nil + } return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil case isEligibleForDormantStop(ws, templateSchedule, currentTick): // Only stop started workspaces. diff --git a/coderd/autobuild/lifecycle_executor_internal_test.go b/coderd/autobuild/lifecycle_executor_internal_test.go index 2d556d58a2..cde61a18d1 100644 --- a/coderd/autobuild/lifecycle_executor_internal_test.go +++ b/coderd/autobuild/lifecycle_executor_internal_test.go @@ -5,12 +5,113 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/schedule" ) +func Test_getNextTransition_TaskAutoPause(t *testing.T) { + t.Parallel() + + // Set up a workspace that is eligible for autostop (past deadline). + now := time.Now() + pastDeadline := now.Add(-time.Hour) + + okUser := database.User{Status: database.UserStatusActive} + okBuild := database.WorkspaceBuild{ + Transition: database.WorkspaceTransitionStart, + Deadline: pastDeadline, + } + okJob := database.ProvisionerJob{ + JobStatus: database.ProvisionerJobStatusSucceeded, + } + okTemplateSchedule := schedule.TemplateScheduleOptions{} + + // Failed build setup for failedstop tests. + failedBuild := database.WorkspaceBuild{ + Transition: database.WorkspaceTransitionStart, + } + failedJob := database.ProvisionerJob{ + JobStatus: database.ProvisionerJobStatusFailed, + CompletedAt: sql.NullTime{Time: now.Add(-time.Hour), Valid: true}, + } + failedTemplateSchedule := schedule.TemplateScheduleOptions{ + FailureTTL: time.Minute, // TTL already elapsed since job completed an hour ago. + } + + testCases := []struct { + Name string + Workspace database.Workspace + Build database.WorkspaceBuild + Job database.ProvisionerJob + TemplateSchedule schedule.TemplateScheduleOptions + ExpectedReason database.BuildReason + }{ + { + Name: "RegularWorkspace_Autostop", + Workspace: database.Workspace{ + DormantAt: sql.NullTime{Valid: false}, + }, + Build: okBuild, + Job: okJob, + TemplateSchedule: okTemplateSchedule, + ExpectedReason: database.BuildReasonAutostop, + }, + { + Name: "TaskWorkspace_Autostop_UsesTaskAutoPause", + Workspace: database.Workspace{ + DormantAt: sql.NullTime{Valid: false}, + TaskID: uuid.NullUUID{UUID: uuid.New(), Valid: true}, + }, + Build: okBuild, + Job: okJob, + TemplateSchedule: okTemplateSchedule, + ExpectedReason: database.BuildReasonTaskAutoPause, + }, + { + Name: "RegularWorkspace_FailedStop", + Workspace: database.Workspace{ + DormantAt: sql.NullTime{Valid: false}, + }, + Build: failedBuild, + Job: failedJob, + TemplateSchedule: failedTemplateSchedule, + ExpectedReason: database.BuildReasonAutostop, + }, + { + Name: "TaskWorkspace_FailedStop_UsesTaskAutoPause", + Workspace: database.Workspace{ + DormantAt: sql.NullTime{Valid: false}, + TaskID: uuid.NullUUID{UUID: uuid.New(), Valid: true}, + }, + Build: failedBuild, + Job: failedJob, + TemplateSchedule: failedTemplateSchedule, + ExpectedReason: database.BuildReasonTaskAutoPause, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + transition, reason, err := getNextTransition( + okUser, + tc.Workspace, + tc.Build, + tc.Job, + tc.TemplateSchedule, + now, + ) + require.NoError(t, err) + require.Equal(t, database.WorkspaceTransitionStop, transition) + require.Equal(t, tc.ExpectedReason, reason) + }) + } +} + func Test_isEligibleForAutostart(t *testing.T) { t.Parallel() diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go index 630bbe14d8..37ae651c67 100644 --- a/coderd/autobuild/lifecycle_executor_test.go +++ b/coderd/autobuild/lifecycle_executor_test.go @@ -2019,5 +2019,11 @@ func TestExecutorTaskWorkspace(t *testing.T) { assert.Contains(t, stats.Transitions, workspace.ID, "task workspace should be in transitions") assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID], "should autostop the workspace") require.Empty(t, stats.Errors, "should have no errors when managing task workspaces") + + // Then: The build reason should be TaskAutoPause (not regular Autostop) + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + assert.Equal(t, codersdk.BuildReasonTaskAutoPause, workspace.LatestBuild.Reason, "task workspace should use TaskAutoPause build reason") }) } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 78efbb4eaa..6206539da0 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -59,6 +59,15 @@ const ( BuildReasonVSCodeConnection BuildReason = "vscode_connection" // BuildReasonJetbrainsConnection "jetbrains_connection" is used when a build to start a workspace is triggered by a JetBrains connection. BuildReasonJetbrainsConnection BuildReason = "jetbrains_connection" + // BuildReasonTaskAutoPause "task_auto_pause" is used when a build to stop + // a task workspace is triggered by the lifecycle executor. + BuildReasonTaskAutoPause BuildReason = "task_auto_pause" + // BuildReasonTaskManualPause "task_manual_pause" is used when a build to + // stop a task workspace is triggered by a user. + BuildReasonTaskManualPause BuildReason = "task_manual_pause" + // BuildReasonTaskResume "task_resume" is used when a build to + // start a task workspace is triggered by a user. + BuildReasonTaskResume BuildReason = "task_resume" ) // WorkspaceBuild is an at-point representation of a workspace state. diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 155611a461..efd14c2a68 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1511,9 +1511,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in #### Enumerated Values -| Value(s) | -|-------------------------------------------------------------------------------------------------------------------------------------| -| `autostart`, `autostop`, `cli`, `dashboard`, `dormancy`, `initiator`, `jetbrains_connection`, `ssh_connection`, `vscode_connection` | +| Value(s) | +|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `autostart`, `autostop`, `cli`, `dashboard`, `dormancy`, `initiator`, `jetbrains_connection`, `ssh_connection`, `task_auto_pause`, `task_manual_pause`, `task_resume`, `vscode_connection` | ## codersdk.CORSBehavior diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index be0c0f964e..01156b827d 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -963,6 +963,9 @@ export type BuildReason = | "initiator" | "jetbrains_connection" | "ssh_connection" + | "task_auto_pause" + | "task_manual_pause" + | "task_resume" | "vscode_connection"; export const BuildReasons: BuildReason[] = [ @@ -974,6 +977,9 @@ export const BuildReasons: BuildReason[] = [ "initiator", "jetbrains_connection", "ssh_connection", + "task_auto_pause", + "task_manual_pause", + "task_resume", "vscode_connection", ]; diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index 7c4edaaf24..f18b427c37 100644 --- a/site/src/pages/TaskPage/TaskPage.stories.tsx +++ b/site/src/pages/TaskPage/TaskPage.stories.tsx @@ -220,7 +220,7 @@ export const TaskPausedTimeout: Story = { latest_build: { ...MockWorkspaceBuildStop, status: "stopped", - reason: "autostop", + reason: "task_auto_pause", }, }); }, diff --git a/site/src/utils/workspace.test.ts b/site/src/utils/workspace.test.ts index b534a4b367..1bc8d2de99 100644 --- a/site/src/utils/workspace.test.ts +++ b/site/src/utils/workspace.test.ts @@ -93,6 +93,13 @@ describe("util > workspace", () => { }, "Coder", ], + [ + { + ...Mocks.MockWorkspaceBuild, + reason: "task_auto_pause", + }, + "Coder", + ], ])( "getDisplayWorkspaceBuildInitiatedBy(%p) returns %p", (build, initiatedBy) => { diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 3c89ddce6d..225530218b 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -87,16 +87,26 @@ export const getDisplayWorkspaceBuildInitiatedBy = ( case "ssh_connection": case "vscode_connection": case "jetbrains_connection": + case "task_manual_pause": + case "task_resume": return build.initiator_name; case "autostart": case "autostop": case "dormancy": + case "task_auto_pause": return "Coder"; } return undefined; }; -export const systemBuildReasons = ["autostart", "autostop", "dormancy"]; +export const systemBuildReasons = [ + "autostart", + "autostop", + "dormancy", + "task_auto_pause", + "task_manual_pause", + "task_resume", +]; export const buildReasonLabels: Record = { // User build reasons @@ -111,6 +121,9 @@ export const buildReasonLabels: Record = { autostart: "Autostart", autostop: "Autostop", dormancy: "Dormancy", + task_auto_pause: "Task Auto-Pause", + task_manual_pause: "Task Manual Pause", + task_resume: "Task Resume", }; const getWorkspaceBuildDurationInSeconds = (