From 57c58dd2f403afb6fe550e8ef73ee838e56c55fc Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sat, 28 Feb 2026 21:10:00 -0600 Subject: [PATCH] feat(api): add assigned_agent to Task model (MS22-DB-003, MS22-API-003) --- .../migration.sql | 2 + apps/api/prisma/schema.prisma | 1 + apps/api/src/tasks/dto/create-task.dto.ts | 6 +++ apps/api/src/tasks/dto/update-task.dto.ts | 6 +++ apps/api/src/tasks/tasks.service.spec.ts | 43 +++++++++++++++++++ apps/api/src/tasks/tasks.service.ts | 6 +++ 6 files changed, 64 insertions(+) create mode 100644 apps/api/prisma/migrations/20260301211000_ms22_task_assigned_agent/migration.sql diff --git a/apps/api/prisma/migrations/20260301211000_ms22_task_assigned_agent/migration.sql b/apps/api/prisma/migrations/20260301211000_ms22_task_assigned_agent/migration.sql new file mode 100644 index 0000000..e698d9c --- /dev/null +++ b/apps/api/prisma/migrations/20260301211000_ms22_task_assigned_agent/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "tasks" ADD COLUMN "assigned_agent" TEXT; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 04f37a8..936538b 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -379,6 +379,7 @@ model Task { creatorId String @map("creator_id") @db.Uuid projectId String? @map("project_id") @db.Uuid parentId String? @map("parent_id") @db.Uuid + assignedAgent String? @map("assigned_agent") domainId String? @map("domain_id") @db.Uuid sortOrder Int @default(0) @map("sort_order") metadata Json @default("{}") diff --git a/apps/api/src/tasks/dto/create-task.dto.ts b/apps/api/src/tasks/dto/create-task.dto.ts index 6b8d752..c53675f 100644 --- a/apps/api/src/tasks/dto/create-task.dto.ts +++ b/apps/api/src/tasks/dto/create-task.dto.ts @@ -50,6 +50,12 @@ export class CreateTaskDto { @IsUUID("4", { message: "parentId must be a valid UUID" }) parentId?: string; + @IsOptional() + @IsString({ message: "assignedAgent must be a string" }) + @MinLength(1, { message: "assignedAgent must not be empty" }) + @MaxLength(255, { message: "assignedAgent must not exceed 255 characters" }) + assignedAgent?: string; + @IsOptional() @IsInt({ message: "sortOrder must be an integer" }) @Min(0, { message: "sortOrder must be at least 0" }) diff --git a/apps/api/src/tasks/dto/update-task.dto.ts b/apps/api/src/tasks/dto/update-task.dto.ts index 1e0ce47..3db35d1 100644 --- a/apps/api/src/tasks/dto/update-task.dto.ts +++ b/apps/api/src/tasks/dto/update-task.dto.ts @@ -52,6 +52,12 @@ export class UpdateTaskDto { @IsUUID("4", { message: "parentId must be a valid UUID" }) parentId?: string | null; + @IsOptional() + @IsString({ message: "assignedAgent must be a string" }) + @MinLength(1, { message: "assignedAgent must not be empty" }) + @MaxLength(255, { message: "assignedAgent must not exceed 255 characters" }) + assignedAgent?: string | null; + @IsOptional() @IsInt({ message: "sortOrder must be an integer" }) @Min(0, { message: "sortOrder must be at least 0" }) diff --git a/apps/api/src/tasks/tasks.service.spec.ts b/apps/api/src/tasks/tasks.service.spec.ts index e751af5..5995a2c 100644 --- a/apps/api/src/tasks/tasks.service.spec.ts +++ b/apps/api/src/tasks/tasks.service.spec.ts @@ -48,6 +48,7 @@ describe("TasksService", () => { creatorId: mockUserId, projectId: null, parentId: null, + assignedAgent: null, sortOrder: 0, metadata: {}, createdAt: new Date(), @@ -158,6 +159,28 @@ describe("TasksService", () => { }) ); }); + + it("should include assignedAgent when provided", async () => { + const createDto = { + title: "Agent-owned Task", + assignedAgent: "fleet-worker-1", + }; + + mockPrismaService.task.create.mockResolvedValue({ + ...mockTask, + assignedAgent: createDto.assignedAgent, + }); + + await service.create(mockWorkspaceId, mockUserId, createDto); + + expect(prisma.task.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + assignedAgent: createDto.assignedAgent, + }), + }) + ); + }); }); describe("findAll", () => { @@ -469,6 +492,26 @@ describe("TasksService", () => { service.update(mockTaskId, mockWorkspaceId, mockUserId, { title: "Test" }) ).rejects.toThrow(NotFoundException); }); + + it("should update assignedAgent when provided", async () => { + const updateDto = { assignedAgent: "fleet-worker-2" }; + + mockPrismaService.task.findUnique.mockResolvedValue(mockTask); + mockPrismaService.task.update.mockResolvedValue({ + ...mockTask, + assignedAgent: updateDto.assignedAgent, + }); + + await service.update(mockTaskId, mockWorkspaceId, mockUserId, updateDto); + + expect(prisma.task.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + assignedAgent: updateDto.assignedAgent, + }), + }) + ); + }); }); describe("remove", () => { diff --git a/apps/api/src/tasks/tasks.service.ts b/apps/api/src/tasks/tasks.service.ts index aecf1b0..5e3af96 100644 --- a/apps/api/src/tasks/tasks.service.ts +++ b/apps/api/src/tasks/tasks.service.ts @@ -67,6 +67,9 @@ export class TasksService { metadata: createTaskDto.metadata ? (createTaskDto.metadata as unknown as Prisma.InputJsonValue) : {}, + ...(createTaskDto.assignedAgent !== undefined && { + assignedAgent: createTaskDto.assignedAgent, + }), ...(assigneeConnection && { assignee: assigneeConnection }), ...(projectConnection && { project: projectConnection }), ...(parentConnection && { parent: parentConnection }), @@ -291,6 +294,9 @@ export class TasksService { if (updateTaskDto.parentId !== undefined && updateTaskDto.parentId !== null) { data.parent = { connect: { id: updateTaskDto.parentId } }; } + if (updateTaskDto.assignedAgent !== undefined) { + data.assignedAgent = updateTaskDto.assignedAgent; + } // Handle completedAt based on status changes if (updateTaskDto.status) {