feat(coderd): use task data model when fetching a task (#20380)

Updates coder/internal#976
This commit is contained in:
Mathias Fredriksson
2025-10-23 19:58:47 +03:00
committed by GitHub
parent 9855460524
commit c6f63990cf
3 changed files with 56 additions and 78 deletions
+16 -3
View File
@@ -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)
}
+12 -52
View File
@@ -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
+28 -23
View File
@@ -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")
})