mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd): add task prompt modification endpoint (#20811)
This PR adds the backend implementation for modifying task prompts. Part of https://github.com/coder/internal/issues/1084 ## Changes - New `UpdateTaskPrompt` database query to update task prompts - New PATCH `/api/v2/tasks/{task}/prompt` endpoint ## Notes This is part 1 of a 2-part PR stack. The frontend UI will be added in a follow-up PR based on this branch (https://github.com/coder/coder/pull/20812). --- 🤖 PR was written by Claude Sonnet 4.5 Thinking using [Coder Mux](https://github.com/coder/cmux) and reviewed by a human 👩
This commit is contained in:
@@ -5130,6 +5130,21 @@ func (q *querier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg
|
||||
return q.db.UpdateTailnetPeerStatusByCoordinator(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) {
|
||||
// An actor is allowed to update the prompt of a task if they have
|
||||
// permission to update the task (same as UpdateTaskWorkspaceID).
|
||||
task, err := q.db.GetTaskByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.TaskTable{}, err
|
||||
}
|
||||
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, task.RBACObject()); err != nil {
|
||||
return database.TaskTable{}, err
|
||||
}
|
||||
|
||||
return q.db.UpdateTaskPrompt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) {
|
||||
// An actor is allowed to update the workspace ID of a task if they are the
|
||||
// owner of the task and workspace or have the appropriate permissions.
|
||||
|
||||
@@ -2457,6 +2457,22 @@ func (s *MethodTestSuite) TestTasks() {
|
||||
|
||||
check.Args(arg).Asserts(task, policy.ActionUpdate, ws, policy.ActionUpdate).Returns(database.TaskTable{})
|
||||
}))
|
||||
s.Run("UpdateTaskPrompt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
task := testutil.Fake(s.T(), faker, database.Task{})
|
||||
arg := database.UpdateTaskPromptParams{
|
||||
ID: task.ID,
|
||||
Prompt: "Updated prompt text",
|
||||
}
|
||||
|
||||
// Create a copy of the task with the updated prompt
|
||||
updatedTask := task
|
||||
updatedTask.Prompt = arg.Prompt
|
||||
|
||||
dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateTaskPrompt(gomock.Any(), arg).Return(updatedTask.TaskTable(), nil).AnyTimes()
|
||||
|
||||
check.Args(arg).Asserts(task, policy.ActionUpdate).Returns(updatedTask.TaskTable())
|
||||
}))
|
||||
s.Run("GetTaskByWorkspaceID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
task := testutil.Fake(s.T(), faker, database.Task{})
|
||||
task.WorkspaceID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
|
||||
|
||||
@@ -3154,6 +3154,13 @@ func (m queryMetricsStore) UpdateTailnetPeerStatusByCoordinator(ctx context.Cont
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateTaskPrompt(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateTaskPrompt").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateTaskWorkspaceID(ctx, arg)
|
||||
|
||||
@@ -6770,6 +6770,21 @@ func (mr *MockStoreMockRecorder) UpdateTailnetPeerStatusByCoordinator(ctx, arg a
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTailnetPeerStatusByCoordinator", reflect.TypeOf((*MockStore)(nil).UpdateTailnetPeerStatusByCoordinator), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateTaskPrompt mocks base method.
|
||||
func (m *MockStore) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateTaskPrompt", ctx, arg)
|
||||
ret0, _ := ret[0].(database.TaskTable)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateTaskPrompt indicates an expected call of UpdateTaskPrompt.
|
||||
func (mr *MockStoreMockRecorder) UpdateTaskPrompt(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskPrompt", reflect.TypeOf((*MockStore)(nil).UpdateTaskPrompt), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateTaskWorkspaceID mocks base method.
|
||||
func (m *MockStore) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -132,11 +132,28 @@ func (w ConnectionLog) RBACObject() rbac.Object {
|
||||
return obj
|
||||
}
|
||||
|
||||
// TaskTable converts a Task to it's reduced version.
|
||||
// A more generalized solution is to use json marshaling to
|
||||
// consistently keep these two structs in sync.
|
||||
// That would be a lot of overhead, and a more costly unit test is
|
||||
// written to make sure these match up.
|
||||
func (t Task) TaskTable() TaskTable {
|
||||
return TaskTable{
|
||||
ID: t.ID,
|
||||
OrganizationID: t.OrganizationID,
|
||||
OwnerID: t.OwnerID,
|
||||
Name: t.Name,
|
||||
WorkspaceID: t.WorkspaceID,
|
||||
TemplateVersionID: t.TemplateVersionID,
|
||||
TemplateParameters: t.TemplateParameters,
|
||||
Prompt: t.Prompt,
|
||||
CreatedAt: t.CreatedAt,
|
||||
DeletedAt: t.DeletedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Task) RBACObject() rbac.Object {
|
||||
return rbac.ResourceTask.
|
||||
WithID(t.ID).
|
||||
WithOwner(t.OwnerID.String()).
|
||||
InOrg(t.OrganizationID)
|
||||
return t.TaskTable().RBACObject()
|
||||
}
|
||||
|
||||
func (t TaskTable) RBACObject() rbac.Object {
|
||||
|
||||
@@ -58,6 +58,45 @@ func TestWorkspaceTableConvert(t *testing.T) {
|
||||
"To resolve this, go to the 'func (w Workspace) WorkspaceTable()' and ensure all fields are converted.")
|
||||
}
|
||||
|
||||
// TestTaskTableConvert verifies all task fields are converted
|
||||
// when reducing a `Task` to a `TaskTable`.
|
||||
// This test is a guard rail to prevent developer oversight mistakes.
|
||||
func TestTaskTableConvert(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
staticRandoms := &testutil.Random{
|
||||
String: func() string { return "foo" },
|
||||
Bool: func() bool { return true },
|
||||
Int: func() int64 { return 500 },
|
||||
Uint: func() uint64 { return 126 },
|
||||
Float: func() float64 { return 3.14 },
|
||||
Complex: func() complex128 { return 6.24 },
|
||||
Time: func() time.Time {
|
||||
return time.Date(2020, 5, 2, 5, 19, 21, 30, time.UTC)
|
||||
},
|
||||
}
|
||||
|
||||
// Copies the approach taken by TestWorkspaceTableConvert.
|
||||
//
|
||||
// If you use 'PopulateStruct' to create 2 tasks, using the same
|
||||
// "random" values for each type. Then they should be identical.
|
||||
//
|
||||
// So if 'task.TaskTable()' was missing any fields in its
|
||||
// conversion, the comparison would fail.
|
||||
|
||||
var task Task
|
||||
err := testutil.PopulateStruct(&task, staticRandoms)
|
||||
require.NoError(t, err)
|
||||
|
||||
var subset TaskTable
|
||||
err = testutil.PopulateStruct(&subset, staticRandoms)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, task.TaskTable(), subset,
|
||||
"'task.TaskTable()' is not missing at least 1 field when converting to 'TaskTable'. "+
|
||||
"To resolve this, go to the 'func (t Task) TaskTable()' and ensure all fields are converted.")
|
||||
}
|
||||
|
||||
// TestAuditLogsQueryConsistency ensures that GetAuditLogsOffset and CountAuditLogs
|
||||
// have identical WHERE clauses to prevent filtering inconsistencies.
|
||||
// This test is a guard rail to prevent developer oversight mistakes.
|
||||
|
||||
@@ -686,6 +686,7 @@ type sqlcQuerier interface {
|
||||
UpdateProvisionerJobWithCompleteWithStartedAtByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteWithStartedAtByIDParams) error
|
||||
UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error)
|
||||
UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) error
|
||||
UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error)
|
||||
UpdateTaskWorkspaceID(ctx context.Context, arg UpdateTaskWorkspaceIDParams) (TaskTable, error)
|
||||
UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error
|
||||
UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error
|
||||
|
||||
@@ -13425,6 +13425,40 @@ func (q *sqlQuerier) ListTasks(ctx context.Context, arg ListTasksParams) ([]Task
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateTaskPrompt = `-- name: UpdateTaskPrompt :one
|
||||
UPDATE
|
||||
tasks
|
||||
SET
|
||||
prompt = $1::text
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
AND deleted_at IS NULL
|
||||
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at
|
||||
`
|
||||
|
||||
type UpdateTaskPromptParams struct {
|
||||
Prompt string `db:"prompt" json:"prompt"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateTaskPrompt, arg.Prompt, arg.ID)
|
||||
var i TaskTable
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.OrganizationID,
|
||||
&i.OwnerID,
|
||||
&i.Name,
|
||||
&i.WorkspaceID,
|
||||
&i.TemplateVersionID,
|
||||
&i.TemplateParameters,
|
||||
&i.Prompt,
|
||||
&i.CreatedAt,
|
||||
&i.DeletedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateTaskWorkspaceID = `-- name: UpdateTaskWorkspaceID :one
|
||||
UPDATE
|
||||
tasks
|
||||
|
||||
@@ -64,3 +64,14 @@ WHERE
|
||||
id = @id::uuid
|
||||
AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
|
||||
-- name: UpdateTaskPrompt :one
|
||||
UPDATE
|
||||
tasks
|
||||
SET
|
||||
prompt = @prompt::text
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
Reference in New Issue
Block a user