Filter workspace routines by project in API

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-01 10:55:15 -05:00
1 parent 462ea100e4
commit e55b161d56
7 files changed
+71 -7

No files matched your search

@@ -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",
@@ -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();
+2 -1
View File
@@ -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);
});
+8 -2
View File
@@ -1071,11 +1071,17 @@ export function routineService(
get: getRoutineById,
getTrigger: getTriggerById,
list: async (companyId: string): Promise<RoutineListItem[]> => {
list: async (
companyId: string,
filters?: { projectId?: string | null },
): Promise<RoutineListItem[]> => {
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([
+6 -1
View File
@@ -22,7 +22,12 @@ export interface RotateRoutineTriggerResponse {
}
export const routinesApi = {
list: (companyId: string) => api.get<RoutineListItem[]>(`/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<RoutineListItem[]>(`/companies/${companyId}/routines${query ? `?${query}` : ""}`);
},
create: (companyId: string, data: Record<string, unknown>) =>
api.post<Routine>(`/companies/${companyId}/routines`, data),
get: (id: string) => api.get<RoutineDetail>(`/routines/${id}`),
+2 -1
View File
@@ -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,
+2 -2
View File
@@ -384,8 +384,8 @@ function ExecutionWorkspaceRoutinesList({
const [runningRoutineId, setRunningRoutineId] = useState<string | null>(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({