Skip to content

Commit 433f9c4

Browse files
refactor: modify task creation endpoint to return a task, not workspace (coder#19637)
Relates to coder/internal#898 Refactor the `POST /api/experimental/tasks/{user}` endpoint to return a codersdk.Task instead of a codersdk.Workspace
1 parent 6e55ed8 commit 433f9c4

File tree

9 files changed

+121
-111
lines changed

9 files changed

+121
-111
lines changed

cli/exp_taskcreate.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (r *RootCmd) taskCreate() *serpent.Command {
104104
templateVersionPresetID = preset.ID
105105
}
106106

107-
workspace, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
107+
task, err := expClient.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
108108
TemplateVersionID: templateVersionID,
109109
TemplateVersionPresetID: templateVersionPresetID,
110110
Prompt: taskInput,
@@ -116,8 +116,8 @@ func (r *RootCmd) taskCreate() *serpent.Command {
116116
_, _ = fmt.Fprintf(
117117
inv.Stdout,
118118
"The task %s has been created at %s!\n",
119-
cliui.Keyword(workspace.Name),
120-
cliui.Timestamp(workspace.CreatedAt),
119+
cliui.Keyword(task.Name),
120+
cliui.Timestamp(task.CreatedAt),
121121
)
122122

123123
return nil

coderd/aitasks.go

Lines changed: 60 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -188,15 +188,72 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
188188
WorkspaceOwner: owner.Username,
189189
},
190190
})
191-
192191
defer commitAudit()
193192
w, err := createWorkspace(ctx, aReq, apiKey.UserID, api, owner, createReq, r)
194193
if err != nil {
195194
httperror.WriteResponseError(ctx, rw, err)
196195
return
197196
}
198197

199-
httpapi.Write(ctx, rw, http.StatusCreated, w)
198+
task := taskFromWorkspace(w, req.Prompt)
199+
httpapi.Write(ctx, rw, http.StatusCreated, task)
200+
}
201+
202+
func taskFromWorkspace(ws codersdk.Workspace, initialPrompt string) codersdk.Task {
203+
// TODO(DanielleMaywood):
204+
// This just picks up the first agent it discovers.
205+
// This approach _might_ break when a task has multiple agents,
206+
// depending on which agent was found first.
207+
//
208+
// We explicitly do not have support for running tasks
209+
// inside of a sub agent at the moment, so we can be sure
210+
// that any sub agents are not the agent we're looking for.
211+
var taskAgentID uuid.NullUUID
212+
var taskAgentLifecycle *codersdk.WorkspaceAgentLifecycle
213+
var taskAgentHealth *codersdk.WorkspaceAgentHealth
214+
for _, resource := range ws.LatestBuild.Resources {
215+
for _, agent := range resource.Agents {
216+
if agent.ParentID.Valid {
217+
continue
218+
}
219+
220+
taskAgentID = uuid.NullUUID{Valid: true, UUID: agent.ID}
221+
taskAgentLifecycle = &agent.LifecycleState
222+
taskAgentHealth = &agent.Health
223+
break
224+
}
225+
}
226+
227+
var currentState *codersdk.TaskStateEntry
228+
if ws.LatestAppStatus != nil {
229+
currentState = &codersdk.TaskStateEntry{
230+
Timestamp: ws.LatestAppStatus.CreatedAt,
231+
State: codersdk.TaskState(ws.LatestAppStatus.State),
232+
Message: ws.LatestAppStatus.Message,
233+
URI: ws.LatestAppStatus.URI,
234+
}
235+
}
236+
237+
return codersdk.Task{
238+
ID: ws.ID,
239+
OrganizationID: ws.OrganizationID,
240+
OwnerID: ws.OwnerID,
241+
OwnerName: ws.OwnerName,
242+
Name: ws.Name,
243+
TemplateID: ws.TemplateID,
244+
TemplateName: ws.TemplateName,
245+
TemplateDisplayName: ws.TemplateDisplayName,
246+
TemplateIcon: ws.TemplateIcon,
247+
WorkspaceID: uuid.NullUUID{Valid: true, UUID: ws.ID},
248+
WorkspaceAgentID: taskAgentID,
249+
WorkspaceAgentLifecycle: taskAgentLifecycle,
250+
WorkspaceAgentHealth: taskAgentHealth,
251+
CreatedAt: ws.CreatedAt,
252+
UpdatedAt: ws.UpdatedAt,
253+
InitialPrompt: initialPrompt,
254+
Status: ws.LatestBuild.Status,
255+
CurrentState: currentState,
256+
}
200257
}
201258

202259
// tasksFromWorkspaces converts a slice of API workspaces into tasks, fetching
@@ -221,60 +278,7 @@ func (api *API) tasksFromWorkspaces(ctx context.Context, apiWorkspaces []codersd
221278

222279
tasks := make([]codersdk.Task, 0, len(apiWorkspaces))
223280
for _, ws := range apiWorkspaces {
224-
// TODO(DanielleMaywood):
225-
// This just picks up the first agent it discovers.
226-
// This approach _might_ break when a task has multiple agents,
227-
// depending on which agent was found first.
228-
//
229-
// We explicitly do not have support for running tasks
230-
// inside of a sub agent at the moment, so we can be sure
231-
// that any sub agents are not the agent we're looking for.
232-
var taskAgentID uuid.NullUUID
233-
var taskAgentLifecycle *codersdk.WorkspaceAgentLifecycle
234-
var taskAgentHealth *codersdk.WorkspaceAgentHealth
235-
for _, resource := range ws.LatestBuild.Resources {
236-
for _, agent := range resource.Agents {
237-
if agent.ParentID.Valid {
238-
continue
239-
}
240-
241-
taskAgentID = uuid.NullUUID{Valid: true, UUID: agent.ID}
242-
taskAgentLifecycle = &agent.LifecycleState
243-
taskAgentHealth = &agent.Health
244-
break
245-
}
246-
}
247-
248-
var currentState *codersdk.TaskStateEntry
249-
if ws.LatestAppStatus != nil {
250-
currentState = &codersdk.TaskStateEntry{
251-
Timestamp: ws.LatestAppStatus.CreatedAt,
252-
State: codersdk.TaskState(ws.LatestAppStatus.State),
253-
Message: ws.LatestAppStatus.Message,
254-
URI: ws.LatestAppStatus.URI,
255-
}
256-
}
257-
258-
tasks = append(tasks, codersdk.Task{
259-
ID: ws.ID,
260-
OrganizationID: ws.OrganizationID,
261-
OwnerID: ws.OwnerID,
262-
OwnerName: ws.OwnerName,
263-
Name: ws.Name,
264-
TemplateID: ws.TemplateID,
265-
TemplateName: ws.TemplateName,
266-
TemplateDisplayName: ws.TemplateDisplayName,
267-
TemplateIcon: ws.TemplateIcon,
268-
WorkspaceID: uuid.NullUUID{Valid: true, UUID: ws.ID},
269-
WorkspaceAgentID: taskAgentID,
270-
WorkspaceAgentLifecycle: taskAgentLifecycle,
271-
WorkspaceAgentHealth: taskAgentHealth,
272-
CreatedAt: ws.CreatedAt,
273-
UpdatedAt: ws.UpdatedAt,
274-
InitialPrompt: promptsByBuildID[ws.LatestBuild.ID],
275-
Status: ws.LatestBuild.Status,
276-
CurrentState: currentState,
277-
})
281+
tasks = append(tasks, taskFromWorkspace(ws, promptsByBuildID[ws.LatestBuild.ID]))
278282
}
279283

280284
return tasks, nil

coderd/aitasks_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -419,19 +419,23 @@ func TestTasksCreate(t *testing.T) {
419419
expClient := codersdk.NewExperimentalClient(client)
420420

421421
// When: We attempt to create a Task.
422-
workspace, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
422+
task, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
423423
TemplateVersionID: template.ActiveVersionID,
424424
Prompt: taskPrompt,
425425
})
426426
require.NoError(t, err)
427-
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
427+
require.True(t, task.WorkspaceID.Valid)
428+
429+
ws, err := client.Workspace(ctx, task.WorkspaceID.UUID)
430+
require.NoError(t, err)
431+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
428432

429433
// Then: We expect a workspace to have been created.
430-
assert.NotEmpty(t, workspace.Name)
431-
assert.Equal(t, template.ID, workspace.TemplateID)
434+
assert.NotEmpty(t, task.Name)
435+
assert.Equal(t, template.ID, task.TemplateID)
432436

433437
// And: We expect it to have the "AI Prompt" parameter correctly set.
434-
parameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
438+
parameters, err := client.WorkspaceBuildParameters(ctx, ws.LatestBuild.ID)
435439
require.NoError(t, err)
436440
require.Len(t, parameters, 1)
437441
assert.Equal(t, codersdk.AITaskPromptParameterName, parameters[0].Name)

codersdk/aitasks.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,23 @@ type CreateTaskRequest struct {
5353
Prompt string `json:"prompt"`
5454
}
5555

56-
func (c *ExperimentalClient) CreateTask(ctx context.Context, user string, request CreateTaskRequest) (Workspace, error) {
56+
func (c *ExperimentalClient) CreateTask(ctx context.Context, user string, request CreateTaskRequest) (Task, error) {
5757
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/experimental/tasks/%s", user), request)
5858
if err != nil {
59-
return Workspace{}, err
59+
return Task{}, err
6060
}
6161
defer res.Body.Close()
6262

6363
if res.StatusCode != http.StatusCreated {
64-
return Workspace{}, ReadBodyAsError(res)
64+
return Task{}, ReadBodyAsError(res)
6565
}
6666

67-
var workspace Workspace
68-
if err := json.NewDecoder(res.Body).Decode(&workspace); err != nil {
69-
return Workspace{}, err
67+
var task Task
68+
if err := json.NewDecoder(res.Body).Decode(&task); err != nil {
69+
return Task{}, err
7070
}
7171

72-
return workspace, nil
72+
return task, nil
7373
}
7474

7575
// TaskState represents the high-level lifecycle of a task.

site/src/api/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2686,8 +2686,8 @@ class ExperimentalApiMethods {
26862686
createTask = async (
26872687
user: string,
26882688
req: TypesGen.CreateTaskRequest,
2689-
): Promise<TypesGen.Workspace> => {
2690-
const response = await this.axios.post<TypesGen.Workspace>(
2689+
): Promise<TypesGen.Task> => {
2690+
const response = await this.axios.post<TypesGen.Task>(
26912691
`/api/experimental/tasks/${user}`,
26922692
req,
26932693
);

site/src/pages/TasksPage/TaskPrompt.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { API } from "api/api";
12
import { getErrorDetail, getErrorMessage } from "api/errors";
23
import { templateVersionPresets } from "api/queries/templates";
34
import type {
45
Preset,
6+
Task,
57
Template,
68
TemplateVersionExternalAuth,
79
} from "api/typesGenerated";
@@ -28,13 +30,12 @@ import {
2830
import { useAuthenticated } from "hooks/useAuthenticated";
2931
import { useExternalAuth } from "hooks/useExternalAuth";
3032
import { RedoIcon, RotateCcwIcon, SendIcon } from "lucide-react";
31-
import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks";
33+
import { AI_PROMPT_PARAMETER_NAME } from "modules/tasks/tasks";
3234
import { type FC, useEffect, useState } from "react";
3335
import { useMutation, useQuery, useQueryClient } from "react-query";
3436
import { useNavigate } from "react-router";
3537
import TextareaAutosize from "react-textarea-autosize";
3638
import { docs } from "utils/docs";
37-
import { data } from "./data";
3839

3940
const textareaPlaceholder = "Prompt your AI agent to start a task...";
4041

@@ -64,7 +65,7 @@ export const TaskPrompt: FC<TaskPromptProps> = ({
6465
<CreateTaskForm
6566
templates={templates}
6667
onSuccess={(task) => {
67-
navigate(`/tasks/${task.workspace.owner_name}/${task.workspace.name}`);
68+
navigate(`/tasks/${task.owner_name}/${task.name}`);
6869
}}
6970
/>
7071
);
@@ -188,12 +189,11 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
188189

189190
const createTaskMutation = useMutation({
190191
mutationFn: async ({ prompt }: CreateTaskMutationFnProps) =>
191-
data.createTask(
192+
API.experimental.createTask(user.id, {
192193
prompt,
193-
user.id,
194-
selectedTemplate.active_version_id,
195-
selectedPresetId,
196-
),
194+
template_version_id: selectedTemplate.active_version_id,
195+
template_version_preset_id: selectedPresetId,
196+
}),
197197
onSuccess: async (task) => {
198198
await queryClient.invalidateQueries({
199199
queryKey: ["tasks"],

site/src/pages/TasksPage/TasksPage.stories.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
MockAIPromptPresets,
33
MockNewTaskData,
44
MockPresets,
5+
MockTask,
56
MockTasks,
67
MockTemplate,
78
MockTemplateVersionExternalAuthGithub,
@@ -19,7 +20,6 @@ import { API } from "api/api";
1920
import { MockUsers } from "pages/UsersPage/storybookData/users";
2021
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
2122
import { reactRouterParameters } from "storybook-addon-remix-react-router";
22-
import { data } from "./data";
2323
import TasksPage from "./TasksPage";
2424

2525
const meta: Meta<typeof TasksPage> = {
@@ -248,7 +248,7 @@ export const CreateTaskSuccessfully: Story = {
248248
spyOn(API.experimental, "getTasks")
249249
.mockResolvedValueOnce(MockTasks)
250250
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
251-
spyOn(data, "createTask").mockResolvedValue(MockNewTaskData);
251+
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
252252
},
253253
play: async ({ canvasElement, step }) => {
254254
const canvas = within(canvasElement);
@@ -272,7 +272,7 @@ export const CreateTaskError: Story = {
272272
beforeEach: () => {
273273
spyOn(API, "getTemplates").mockResolvedValue([MockTemplate]);
274274
spyOn(API.experimental, "getTasks").mockResolvedValue(MockTasks);
275-
spyOn(data, "createTask").mockRejectedValue(
275+
spyOn(API.experimental, "createTask").mockRejectedValue(
276276
mockApiError({
277277
message: "Failed to create task",
278278
detail: "You don't have permission to create tasks.",
@@ -301,7 +301,7 @@ export const WithAuthenticatedExternalAuth: Story = {
301301
spyOn(API.experimental, "getTasks")
302302
.mockResolvedValueOnce(MockTasks)
303303
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
304-
spyOn(data, "createTask").mockResolvedValue(MockNewTaskData);
304+
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
305305
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
306306
MockTemplateVersionExternalAuthGithubAuthenticated,
307307
]);
@@ -327,7 +327,7 @@ export const MissingExternalAuth: Story = {
327327
spyOn(API.experimental, "getTasks")
328328
.mockResolvedValueOnce(MockTasks)
329329
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
330-
spyOn(data, "createTask").mockResolvedValue(MockNewTaskData);
330+
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
331331
spyOn(API, "getTemplateVersionExternalAuth").mockResolvedValue([
332332
MockTemplateVersionExternalAuthGithub,
333333
]);
@@ -353,7 +353,7 @@ export const ExternalAuthError: Story = {
353353
spyOn(API.experimental, "getTasks")
354354
.mockResolvedValueOnce(MockTasks)
355355
.mockResolvedValue([MockNewTaskData, ...MockTasks]);
356-
spyOn(data, "createTask").mockResolvedValue(MockNewTaskData);
356+
spyOn(API.experimental, "createTask").mockResolvedValue(MockTask);
357357
spyOn(API, "getTemplateVersionExternalAuth").mockRejectedValue(
358358
mockApiError({
359359
message: "Failed to load external auth",

site/src/pages/TasksPage/data.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

site/src/testHelpers/entities.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4903,6 +4903,32 @@ export const MockTasks = [
49034903
},
49044904
];
49054905

4906+
export const MockTask: TypesGen.Task = {
4907+
id: "test-task",
4908+
name: "task-wild-test-123",
4909+
organization_id: MockOrganization.id,
4910+
owner_id: MockUserOwner.id,
4911+
owner_name: MockUserOwner.username,
4912+
template_id: MockTemplate.id,
4913+
template_name: MockTemplate.name,
4914+
template_display_name: MockTemplate.display_name,
4915+
template_icon: MockTemplate.icon,
4916+
workspace_id: MockWorkspace.id,
4917+
workspace_agent_id: MockWorkspaceAgent.id,
4918+
workspace_agent_lifecycle: MockWorkspaceAgent.lifecycle_state,
4919+
workspace_agent_health: MockWorkspaceAgent.health,
4920+
initial_prompt: "Perform some task",
4921+
status: "running",
4922+
current_state: {
4923+
timestamp: "2022-05-17T17:39:01.382927298Z",
4924+
state: "idle",
4925+
message: "Should I continue?",
4926+
uri: "https://dev.coder.com",
4927+
},
4928+
created_at: "2022-05-17T17:39:01.382927298Z",
4929+
updated_at: "2022-05-17T17:39:01.382927298Z",
4930+
};
4931+
49064932
export const MockNewTaskData = {
49074933
prompt: "Create a new task",
49084934
workspace: {

0 commit comments

Comments
 (0)