diff --git a/server/src/__tests__/routines-routes.test.ts b/server/src/__tests__/routines-routes.test.ts index 1e8806a8029..35b6b355f0a 100644 --- a/server/src/__tests__/routines-routes.test.ts +++ b/server/src/__tests__/routines-routes.test.ts @@ -145,6 +145,7 @@ describe("routine routes", () => { registerModuleMocks(); vi.clearAllMocks(); mockGetTelemetryClient.mockReturnValue({ track: vi.fn() }); + mockRoutineService.list.mockResolvedValue([routine]); mockRoutineService.create.mockResolvedValue(routine); mockRoutineService.get.mockResolvedValue(routine); mockRoutineService.getTrigger.mockResolvedValue(trigger); @@ -158,6 +159,23 @@ describe("routine routes", () => { mockLogActivity.mockResolvedValue(undefined); }); + it("passes project filters to the routine list service", async () => { + const app = await createApp({ + type: "board", + userId: "board-user", + source: "session", + isInstanceAdmin: true, + companyIds: [companyId], + }); + + const res = await request(app) + .get(`/api/companies/${companyId}/routines`) + .query({ projectId }); + + expect(res.status).toBe(200); + expect(mockRoutineService.list).toHaveBeenCalledWith(companyId, { projectId }); + }); + it("requires tasks:assign permission for non-admin board routine creation", async () => { const app = await createApp({ type: "board", diff --git a/server/src/__tests__/routines-service.test.ts b/server/src/__tests__/routines-service.test.ts index e59dce51b23..45769877c09 100644 --- a/server/src/__tests__/routines-service.test.ts +++ b/server/src/__tests__/routines-service.test.ts @@ -178,6 +178,39 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => { return { companyId, agentId, issueSvc, projectId, routine, svc, wakeups }; } + it("filters listed routines by project", async () => { + const { companyId, agentId, projectId, routine, svc } = await seedFixture(); + const otherProjectId = randomUUID(); + await db.insert(projects).values({ + id: otherProjectId, + companyId, + name: "Other routines", + status: "in_progress", + }); + const otherRoutine = await svc.create( + companyId, + { + projectId: otherProjectId, + goalId: null, + parentIssueId: null, + title: "other project routine", + description: null, + assigneeAgentId: agentId, + priority: "medium", + status: "active", + concurrencyPolicy: "coalesce_if_active", + catchUpPolicy: "skip_missed", + }, + {}, + ); + + const projectRoutines = await svc.list(companyId, { projectId }); + const allRoutines = await svc.list(companyId); + + expect(projectRoutines.map((entry) => entry.id)).toEqual([routine.id]); + expect(allRoutines.map((entry) => entry.id)).toEqual(expect.arrayContaining([routine.id, otherRoutine.id])); + }); + it("creates a fresh execution issue when the previous routine issue is open but idle", async () => { const { companyId, issueSvc, routine, svc } = await seedFixture(); const previousRunId = randomUUID(); diff --git a/server/src/routes/routines.ts b/server/src/routes/routines.ts index 0d78c06629b..5bae6bdb45c 100644 --- a/server/src/routes/routines.ts +++ b/server/src/routes/routines.ts @@ -60,7 +60,8 @@ export function routineRoutes( router.get("/companies/:companyId/routines", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); - const result = await svc.list(companyId); + const projectId = typeof req.query.projectId === "string" ? req.query.projectId : undefined; + const result = await svc.list(companyId, { projectId }); res.json(result); }); diff --git a/server/src/services/routines.ts b/server/src/services/routines.ts index f12e275cc09..a632f7760ec 100644 --- a/server/src/services/routines.ts +++ b/server/src/services/routines.ts @@ -1071,11 +1071,17 @@ export function routineService( get: getRoutineById, getTrigger: getTriggerById, - list: async (companyId: string): Promise => { + list: async ( + companyId: string, + filters?: { projectId?: string | null }, + ): Promise => { + const conditions = [eq(routines.companyId, companyId)]; + if (filters?.projectId) conditions.push(eq(routines.projectId, filters.projectId)); + const rows = await db .select() .from(routines) - .where(eq(routines.companyId, companyId)) + .where(and(...conditions)) .orderBy(desc(routines.updatedAt), asc(routines.title)); const routineIds = rows.map((row) => row.id); const [triggersByRoutine, latestRunByRoutine, activeIssueByRoutine] = await Promise.all([ diff --git a/ui/src/api/routines.ts b/ui/src/api/routines.ts index f6e5099b3a3..0c6c0681c44 100644 --- a/ui/src/api/routines.ts +++ b/ui/src/api/routines.ts @@ -22,7 +22,12 @@ export interface RotateRoutineTriggerResponse { } export const routinesApi = { - list: (companyId: string) => api.get(`/companies/${companyId}/routines`), + list: (companyId: string, filters?: { projectId?: string | null }) => { + const params = new URLSearchParams(); + if (filters?.projectId) params.set("projectId", filters.projectId); + const query = params.toString(); + return api.get(`/companies/${companyId}/routines${query ? `?${query}` : ""}`); + }, create: (companyId: string, data: Record) => api.post(`/companies/${companyId}/routines`, data), get: (id: string) => api.get(`/routines/${id}`), diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index 7290c28a9ea..590af1067a1 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -64,7 +64,8 @@ export const queryKeys = { workProducts: (issueId: string) => ["issues", "work-products", issueId] as const, }, routines: { - list: (companyId: string) => ["routines", companyId] as const, + list: (companyId: string, filters?: { projectId?: string | null }) => + ["routines", companyId, filters?.projectId ?? "__all-projects__"] as const, detail: (id: string) => ["routines", "detail", id] as const, runs: (id: string) => ["routines", "runs", id] as const, activity: (companyId: string, id: string) => ["routines", "activity", companyId, id] as const, diff --git a/ui/src/pages/ExecutionWorkspaceDetail.tsx b/ui/src/pages/ExecutionWorkspaceDetail.tsx index fb374ef0c19..0201a224f90 100644 --- a/ui/src/pages/ExecutionWorkspaceDetail.tsx +++ b/ui/src/pages/ExecutionWorkspaceDetail.tsx @@ -384,8 +384,8 @@ function ExecutionWorkspaceRoutinesList({ const [runningRoutineId, setRunningRoutineId] = useState(null); const { data: routines, isLoading, error } = useQuery({ - queryKey: queryKeys.routines.list(workspace.companyId), - queryFn: () => routinesApi.list(workspace.companyId), + queryKey: queryKeys.routines.list(workspace.companyId, { projectId: workspace.projectId }), + queryFn: () => routinesApi.list(workspace.companyId, { projectId: workspace.projectId }), }); const { data: agents } = useQuery({