FN-9217: Fix project-scoped routine and automation listing
Project-scoped routine and automation reads now resolve the engine-backed project stores. - Preserve legacy empty-list behavior for omitted and global scopes. - Return project records through the same stores used for creation and surface unavailable stores as 503 errors. - Add route regressions for populated, duplicate, empty, and unavailable project-store cases. - Add a patch changeset for the published Fusion package. Files changed: .changeset/fn-9217-project-scoped-routine-list.md | 7 ++ .../src/__tests__/routes-automation.test.ts | 121 +++++++++++++++++++-- .../src/routes/register-plugins-automation.ts | 26 +++-- 3 files changed, 134 insertions(+), 20 deletions(-) Fusion-Task-Id: FN-9217 Fusion-Task-Lineage: bb2896d6-3e30-4b84-b86f-4a7c9394603a Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-9217-project-scoped-routine-list.md
Normal file
7
.changeset/fn-9217-project-scoped-routine-list.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Show project-scoped Automations and Routines instead of an empty list.
|
||||
category: fix
|
||||
dev: GET /routines and GET /automations now let scope=project bypass legacy global-store guards and resolve the project store.
|
||||
@@ -747,6 +747,26 @@ describe("Automation routes", () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
|
||||
it("reads project-scoped schedules from an engine-backed store without a global store", async () => {
|
||||
const automationStore = createMockAutomationStore();
|
||||
const projectSchedule = { ...FAKE_SCHEDULE, id: "sched-project-1", scope: "project" as const };
|
||||
const globalSchedule = { ...FAKE_SCHEDULE, id: "sched-global-1", scope: "global" as const };
|
||||
automationStore.listSchedules.mockResolvedValue([projectSchedule, globalSchedule]);
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(createMockStore(), {
|
||||
engineManager: {
|
||||
getEngine: vi.fn().mockReturnValue({ getAutomationStore: () => automationStore }),
|
||||
},
|
||||
} as any));
|
||||
|
||||
const res = await GET(app, "/api/automations?scope=project&projectId=proj-1");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([projectSchedule]);
|
||||
expect(automationStore.listSchedules).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /automations", () => {
|
||||
@@ -1900,16 +1920,33 @@ describe("Automation routes", () => {
|
||||
expect(res.body.some((s: any) => s.scope === "global")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns empty array when automation store unavailable (scope=project) - legacy fallback", async () => {
|
||||
// Build app WITHOUT automationStore option - routes return empty array for backward compatibility
|
||||
const store = createMockStore();
|
||||
it("returns 503 when no automation store is resolvable for scope=project", async () => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(store));
|
||||
app.use("/api", createApiRoutes(createMockStore()));
|
||||
|
||||
const res = await GET(app, "/api/automations?scope=project&projectId=proj-1");
|
||||
|
||||
expect(res.status).toBe(503);
|
||||
expect(res.body.error).toContain("Automation store not available");
|
||||
});
|
||||
|
||||
it("returns an empty project list from the engine-backed automation store", async () => {
|
||||
const automationStore = createMockAutomationStore();
|
||||
automationStore.listSchedules.mockResolvedValue([]);
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(createMockStore(), {
|
||||
engineManager: {
|
||||
getEngine: vi.fn().mockReturnValue({ getAutomationStore: () => automationStore }),
|
||||
},
|
||||
} as any));
|
||||
|
||||
const res = await GET(app, "/api/automations?scope=project&projectId=proj-1");
|
||||
|
||||
const res = await GET(app, "/api/automations?scope=project");
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
expect(automationStore.listSchedules).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns empty array when automation store unavailable (scope=global) - legacy fallback", async () => {
|
||||
@@ -2030,6 +2067,53 @@ describe("Routine routes", () => {
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
|
||||
it("reads duplicate project-scoped routines from an engine-backed store without a global store", async () => {
|
||||
const routineStore = createMockRoutineStore();
|
||||
const projectRoutineOne = { ...FAKE_ROUTINE, id: "routine-project-1", scope: "project" as const };
|
||||
const projectRoutineTwo = { ...FAKE_ROUTINE, id: "routine-project-2", scope: "project" as const };
|
||||
const globalRoutine = { ...FAKE_ROUTINE, id: "routine-global-1", scope: "global" as const };
|
||||
routineStore.listRoutines.mockResolvedValue([projectRoutineOne, projectRoutineTwo, globalRoutine]);
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(createMockStore(), {
|
||||
engineManager: {
|
||||
getEngine: vi.fn().mockReturnValue({ getRoutineStore: () => routineStore }),
|
||||
},
|
||||
} as any));
|
||||
|
||||
const res = await GET(app, "/api/routines?scope=project&projectId=proj-1");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([projectRoutineOne, projectRoutineTwo]);
|
||||
expect(routineStore.listRoutines).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses the same engine-backed routine store for project-scoped creation and listing", async () => {
|
||||
const routineStore = createMockRoutineStore();
|
||||
const createdRoutine = { ...FAKE_ROUTINE, id: "routine-created-1", name: "Created Project Routine" };
|
||||
routineStore.createRoutine.mockResolvedValue(createdRoutine);
|
||||
routineStore.listRoutines.mockResolvedValue([createdRoutine]);
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(createMockStore(), {
|
||||
engineManager: {
|
||||
getEngine: vi.fn().mockReturnValue({ getRoutineStore: () => routineStore }),
|
||||
},
|
||||
} as any));
|
||||
|
||||
const create = await REQUEST(app, "POST", "/api/routines?scope=project&projectId=proj-1", JSON.stringify({
|
||||
name: "Created Project Routine",
|
||||
trigger: { type: "cron", cronExpression: "0 * * * *" },
|
||||
}), { "Content-Type": "application/json" });
|
||||
const list = await GET(app, "/api/routines?scope=project&projectId=proj-1");
|
||||
|
||||
expect(create.status).toBe(201);
|
||||
expect(routineStore.createRoutine).toHaveBeenCalledTimes(1);
|
||||
expect(list.status).toBe(200);
|
||||
expect(list.body).toEqual([createdRoutine]);
|
||||
expect(routineStore.listRoutines).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /routines", () => {
|
||||
@@ -3022,16 +3106,33 @@ describe("Routine routes", () => {
|
||||
expect(res.body.some((r: any) => r.scope === "global")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns empty array when routine store unavailable (scope=project) - legacy fallback", async () => {
|
||||
// Build app WITHOUT routineStore option - routes return empty array for backward compatibility
|
||||
const store = createMockStore();
|
||||
it("returns 503 when no routine store is resolvable for scope=project", async () => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(store));
|
||||
app.use("/api", createApiRoutes(createMockStore()));
|
||||
|
||||
const res = await GET(app, "/api/routines?scope=project&projectId=proj-1");
|
||||
|
||||
expect(res.status).toBe(503);
|
||||
expect(res.body.error).toContain("Routine store not available");
|
||||
});
|
||||
|
||||
it("returns an empty project list from the engine-backed routine store", async () => {
|
||||
const routineStore = createMockRoutineStore();
|
||||
routineStore.listRoutines.mockResolvedValue([]);
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(createMockStore(), {
|
||||
engineManager: {
|
||||
getEngine: vi.fn().mockReturnValue({ getRoutineStore: () => routineStore }),
|
||||
},
|
||||
} as any));
|
||||
|
||||
const res = await GET(app, "/api/routines?scope=project&projectId=proj-1");
|
||||
|
||||
const res = await GET(app, "/api/routines?scope=project");
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
expect(routineStore.listRoutines).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns empty array when routine store unavailable (scope=global) - legacy fallback", async () => {
|
||||
|
||||
@@ -46,13 +46,16 @@ export function registerPluginsAutomationRoutes(ctx: ApiRoutesContext, deps: Plu
|
||||
|
||||
// GET /automations — list all scheduled tasks (optionally filtered by scope)
|
||||
router.get("/automations", async (req: Request, res: Response) => {
|
||||
// Return empty array when no store available (legacy backward-compatible behavior)
|
||||
if (!options?.automationStore) {
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
try {
|
||||
const scope = parseScopeParam(req);
|
||||
/*
|
||||
FNXC:PluginsAutomationRoutes 2026-08-27-14:40:
|
||||
The legacy empty-list fallback applies only to omitted/global reads. Project reads must
|
||||
resolve the engine-backed store used by creates, surfacing 503 when none is available.
|
||||
*/
|
||||
if (!options?.automationStore && scope !== "project") {
|
||||
return res.json([]);
|
||||
}
|
||||
const automationStore = resolveAutomationStore(req, scope);
|
||||
|
||||
// Get all schedules and filter by scope if specified
|
||||
@@ -403,13 +406,16 @@ export function registerPluginsAutomationRoutes(ctx: ApiRoutesContext, deps: Plu
|
||||
|
||||
// GET /routines — list all routines (optionally filtered by scope)
|
||||
router.get("/routines", async (req: Request, res: Response) => {
|
||||
// Return empty array when no store available (legacy backward-compatible behavior)
|
||||
if (!options?.routineStore) {
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
try {
|
||||
const scope = parseScopeParam(req);
|
||||
/*
|
||||
FNXC:PluginsAutomationRoutes 2026-08-27-14:40:
|
||||
The legacy empty-list fallback applies only to omitted/global reads. Project reads must
|
||||
resolve the engine-backed store used by creates, surfacing 503 when none is available.
|
||||
*/
|
||||
if (!options?.routineStore && scope !== "project") {
|
||||
return res.json([]);
|
||||
}
|
||||
const routineStore = resolveRoutineStore(req, scope);
|
||||
|
||||
// Get all routines and filter by scope if specified
|
||||
|
||||
Reference in New Issue
Block a user