diff --git a/cli/exp_task_status_test.go b/cli/exp_task_status_test.go index 2ae31ad33c..d5b4e0a9d6 100644 --- a/cli/exp_task_status_test.go +++ b/cli/exp_task_status_test.go @@ -24,8 +24,6 @@ import ( func Test_TaskStatus(t *testing.T) { t.Parallel() - t.Skip("TODO(mafredri): Remove, fixed down-stack!") - for _, tc := range []struct { args []string expectOutput string @@ -90,7 +88,9 @@ func Test_TaskStatus(t *testing.T) { Healthy: true, }, WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady), + Status: codersdk.TaskStatusActive, }) + return default: t.Errorf("unexpected path: %s", r.URL.Path) } @@ -125,7 +125,9 @@ STATE CHANGED STATUS HEALTHY STATE MESSAGE Healthy: true, }, WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady), + Status: codersdk.TaskStatusPending, }) + return case 1: httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{ ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"), @@ -136,7 +138,9 @@ STATE CHANGED STATUS HEALTHY STATE MESSAGE }, WorkspaceAgentLifecycle: ptr.Ref(codersdk.WorkspaceAgentLifecycleReady), UpdatedAt: now.Add(-4 * time.Second), + Status: codersdk.TaskStatusActive, }) + return case 2: httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{ ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"), @@ -152,7 +156,9 @@ STATE CHANGED STATUS HEALTHY STATE MESSAGE Timestamp: now.Add(-3 * time.Second), Message: "Reticulating splines...", }, + Status: codersdk.TaskStatusActive, }) + return case 3: httpapi.Write(ctx, w, http.StatusOK, codersdk.Task{ ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"), @@ -168,13 +174,16 @@ STATE CHANGED STATUS HEALTHY STATE MESSAGE Timestamp: now.Add(-2 * time.Second), Message: "Splines reticulated successfully!", }, + Status: codersdk.TaskStatusActive, }) + return default: httpapi.InternalServerError(w, xerrors.New("too many calls!")) return } default: httpapi.InternalServerError(w, xerrors.Errorf("unexpected path: %q", r.URL.Path)) + return } } }, @@ -188,16 +197,18 @@ STATE CHANGED STATUS HEALTHY STATE MESSAGE "owner_name": "", "name": "", "template_id": "00000000-0000-0000-0000-000000000000", + "template_version_id": "00000000-0000-0000-0000-000000000000", "template_name": "", "template_display_name": "", "template_icon": "", "workspace_id": null, + "workspace_status": "running", "workspace_agent_id": null, "workspace_agent_lifecycle": null, "workspace_agent_health": null, "workspace_app_id": null, "initial_prompt": "", - "status": "running", + "status": "active", "current_state": { "timestamp": "2025-08-26T12:34:57Z", "state": "working", @@ -226,7 +237,9 @@ STATE CHANGED STATUS HEALTHY STATE MESSAGE Timestamp: ts.Add(time.Second), Message: "Thinking furiously...", }, + Status: codersdk.TaskStatusActive, }) + return default: t.Errorf("unexpected path: %s", r.URL.Path) } diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 2accd36712..2f3dd74ad2 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -25,7 +25,6 @@ import ( "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/searchquery" "github.com/coder/coder/v2/coderd/taskname" - "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" aiagentapi "github.com/coder/agentapi-sdk-go" @@ -604,25 +603,22 @@ func (api *API) tasksList(rw http.ResponseWriter, r *http.Request) { func (api *API) taskGet(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() apiKey := httpmw.APIKey(r) + task := httpmw.TaskParam(r) - idStr := chi.URLParam(r, "task") - taskID, err := uuid.Parse(idStr) - if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Invalid UUID %q for task ID.", idStr), + if !task.WorkspaceID.Valid { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching task.", + Detail: "Task workspace ID is invalid.", }) return } - // For now, taskID = workspaceID, once we have a task data model in - // the DB, we can change this lookup. - workspaceID := taskID - workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceID) - if httpapi.Is404Error(err) { - httpapi.ResourceNotFound(rw) - return - } + workspace, err := api.Database.GetWorkspaceByID(ctx, task.WorkspaceID.UUID) if err != nil { + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching workspace.", Detail: err.Error(), @@ -642,34 +638,6 @@ func (api *API) taskGet(rw http.ResponseWriter, r *http.Request) { httpapi.ResourceNotFound(rw) return } - if data.builds[0].HasAITask == nil || !*data.builds[0].HasAITask { - // TODO(DanielleMaywood): - // This is a temporary workaround. When a task has just been created, but - // not yet provisioned, the workspace build will not have `HasAITask` set. - // - // When we reach this code flow, it is _either_ because the workspace is - // not a task, or it is a task that has not yet been provisioned. This - // endpoint should rarely be called with a non-task workspace so we - // should be fine with this extra database call to check if it has the - // special "AI Task" parameter. - parameters, err := api.Database.GetWorkspaceBuildParameters(ctx, data.builds[0].ID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace build parameters.", - Detail: err.Error(), - }) - return - } - - _, hasAITask := slice.Find(parameters, func(t database.WorkspaceBuildParameter) bool { - return t.Name == codersdk.AITaskPromptParameterName - }) - - if !hasAITask { - httpapi.ResourceNotFound(rw) - return - } - } appStatus := codersdk.WorkspaceAppStatus{} if len(data.appStatuses) > 0 { @@ -692,16 +660,8 @@ func (api *API) taskGet(rw http.ResponseWriter, r *http.Request) { return } - tasks, err := api.tasksFromWorkspaces(ctx, []codersdk.Workspace{ws}) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching task prompt and state.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, tasks[0]) + taskResp := taskFromDBTaskAndWorkspace(task, ws) + httpapi.Write(ctx, rw, http.StatusOK, taskResp) } // @Summary Delete AI task by ID diff --git a/coderd/aitasks_test.go b/coderd/aitasks_test.go index 5623eb3076..c7db4f58c6 100644 --- a/coderd/aitasks_test.go +++ b/coderd/aitasks_test.go @@ -274,24 +274,27 @@ func TestTasks(t *testing.T) { t.Run("Get", func(t *testing.T) { t.Parallel() - t.Skip("TODO(mafredri): Remove, fixed down-stack!") - 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) - // Create a workspace (task) with a specific prompt. wantPrompt = "review my code" - workspace = coderdtest.CreateWorkspace(t, client, template.ID, func(req *codersdk.CreateWorkspaceRequest) { - req.RichParameterValues = []codersdk.WorkspaceBuildParameter{ - {Name: codersdk.AITaskPromptParameterName, Value: wantPrompt}, - } - }) + exp = codersdk.NewExperimentalClient(client) ) - coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) - ws := coderdtest.MustWorkspace(t, client, workspace.ID) + task, err := exp.CreateTask(ctx, "me", codersdk.CreateTaskRequest{ + TemplateVersionID: template.ActiveVersionID, + Input: wantPrompt, + }) + require.NoError(t, err) + require.True(t, task.WorkspaceID.Valid) + + // Get the workspace and wait for it to be ready. + ws, err := client.Workspace(ctx, task.WorkspaceID.UUID) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) + ws = coderdtest.MustWorkspace(t, client, task.WorkspaceID.UUID) // Assert invariant: the workspace has exactly one resource with one agent with one app. require.Len(t, ws.LatestBuild.Resources, 1) require.Len(t, ws.LatestBuild.Resources[0].Agents, 1) @@ -299,9 +302,9 @@ func TestTasks(t *testing.T) { taskAppID := ws.LatestBuild.Resources[0].Agents[0].Apps[0].ID // Insert an app status for the workspace - _, err := db.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{ + _, err = db.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{ ID: uuid.New(), - WorkspaceID: workspace.ID, + WorkspaceID: task.WorkspaceID.UUID, CreatedAt: dbtime.Now(), AgentID: agentID, AppID: taskAppID, @@ -311,31 +314,33 @@ func TestTasks(t *testing.T) { require.NoError(t, err) // Fetch the task by ID via experimental API and verify fields. - exp := codersdk.NewExperimentalClient(client) - task, err := exp.TaskByID(ctx, workspace.ID) + updated, err := exp.TaskByID(ctx, task.ID) require.NoError(t, err) - assert.Equal(t, workspace.ID, task.ID, "task ID should match workspace ID") - assert.Equal(t, workspace.Name, task.Name, "task name should map from workspace name") - assert.Equal(t, wantPrompt, task.InitialPrompt, "task prompt should match the AI Prompt parameter") - assert.Equal(t, workspace.ID, task.WorkspaceID.UUID, "workspace id should match") - assert.NotEmpty(t, task.WorkspaceStatus, "task status should not be empty") + assert.Equal(t, task.ID, updated.ID, "task ID should match") + assert.Equal(t, task.Name, updated.Name, "task name should match") + assert.Equal(t, wantPrompt, updated.InitialPrompt, "task prompt should match the AI Prompt parameter") + assert.Equal(t, task.WorkspaceID.UUID, updated.WorkspaceID.UUID, "workspace id should match") + assert.Equal(t, ws.LatestBuild.BuildNumber, updated.WorkspaceBuildNumber, "workspace build number should match") + assert.Equal(t, agentID, updated.WorkspaceAgentID.UUID, "workspace agent id should match") + assert.Equal(t, taskAppID, updated.WorkspaceAppID.UUID, "workspace app id should match") + assert.NotEmpty(t, updated.WorkspaceStatus, "task status should not be empty") // Stop the workspace - coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop) + coderdtest.MustTransitionWorkspace(t, client, task.WorkspaceID.UUID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop) // Verify that the previous status still remains - updated, err := exp.TaskByID(ctx, workspace.ID) + updated, err = exp.TaskByID(ctx, task.ID) require.NoError(t, err) assert.NotNil(t, updated.CurrentState, "current state should not be nil") assert.Equal(t, "all done", updated.CurrentState.Message) assert.Equal(t, codersdk.TaskStateComplete, updated.CurrentState.State) // Start the workspace again - coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStop, codersdk.WorkspaceTransitionStart) + coderdtest.MustTransitionWorkspace(t, client, task.WorkspaceID.UUID, codersdk.WorkspaceTransitionStop, codersdk.WorkspaceTransitionStart) // Verify that the status from the previous build is no longer present - updated, err = exp.TaskByID(ctx, workspace.ID) + updated, err = exp.TaskByID(ctx, task.ID) require.NoError(t, err) assert.Nil(t, updated.CurrentState, "current state should be nil") })