feat(FN-1768): merge fusion/fn-1768

This commit is contained in:
gsxdsm
2026-04-16 00:08:06 -07:00
parent 23ca8bd3ef
commit c020764107
4 changed files with 1453 additions and 15 deletions

View File

@@ -502,3 +502,782 @@ describe("Terminal WebSocket heartbeat", () => {
);
});
});
/**
* Scoped Scheduling Resolver Regression Tests
* ===========================================
*
* These tests verify the scoped scheduling resolver invariants across automation and routine routes.
*
* Scope Resolution Precedence:
* 1. scope=global → Uses the default AutomationStore/RoutineStore from options (process-level)
* 2. scope=project → Uses the same store with scope filtering at query time
* 3. Omitted scope (legacy) → Defaults to project for POST, returns all for GET
*
* Error Contracts:
* - 400: Invalid scope value ("invalid" is not "global" or "project")
* - 503: Store unavailable when scope is specified
* - 200: Empty array when store is unavailable (legacy fallback for backward compatibility)
*
* Cross-Project Isolation:
* - scope=project requests never leak global-scoped items into results
* - scope=global requests never include project-scoped items
* - No opportunistic lane hopping: when scope=project has no results, it returns empty,
* NOT the global lane results
*
* Fallback Behavior:
* - When no AutomationStore/RoutineStore is configured, GET endpoints return []
* (This is a legacy backward-compatible behavior)
* - POST endpoints requiring a store will throw if no store is configured
*/
describe("createServer scoped scheduling resolver regressions", () => {
// ── Mock factory helpers ─────────────────────────────────────────
function createMockAutomationStore(name = "mock-automation-store") {
return {
listSchedules: vi.fn().mockResolvedValue([]),
getSchedule: vi.fn(),
createSchedule: vi.fn(),
updateSchedule: vi.fn(),
deleteSchedule: vi.fn(),
recordRun: vi.fn(),
reorderSteps: vi.fn(),
isValidCron: vi.fn().mockReturnValue(true),
};
}
function createMockRoutineStore(name = "mock-routine-store") {
return {
listRoutines: vi.fn().mockResolvedValue([]),
getRoutine: vi.fn(),
createRoutine: vi.fn(),
updateRoutine: vi.fn(),
deleteRoutine: vi.fn(),
isValidCron: vi.fn().mockReturnValue(true),
};
}
function createMockRoutineRunner() {
return {
triggerManual: vi.fn().mockResolvedValue({ success: true }),
triggerWebhook: vi.fn().mockResolvedValue({ success: true }),
};
}
// ── Mock ProjectEngineManager ───────────────────────────────────
function createMockEngineManager() {
return {
getEngine: vi.fn(),
ensureEngine: vi.fn(),
startReconciliation: vi.fn(),
};
}
// ── Test fixtures ───────────────────────────────────────────────
const FAKE_GLOBAL_SCHEDULE = {
id: "sched-global-1",
name: "Global Schedule",
scope: "global" as const,
scheduleType: "hourly" as const,
command: "echo global",
enabled: true,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
const FAKE_PROJECT_SCHEDULE = {
id: "sched-proj-a-1",
name: "Project A Schedule",
scope: "project" as const,
scheduleType: "daily" as const,
command: "echo proj-a",
enabled: true,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
const FAKE_GLOBAL_ROUTINE = {
id: "routine-global-1",
name: "Global Routine",
scope: "global" as const,
trigger: { type: "manual" as const },
enabled: true,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
const FAKE_PROJECT_ROUTINE = {
id: "routine-proj-a-1",
name: "Project A Routine",
scope: "project" as const,
trigger: { type: "manual" as const },
enabled: true,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
// ── Automation lane selection tests ──────────────────────────────────────
//
// Automation lane selection: The automation store resolves based on scope parameter.
// Precedence: global lane uses process-level store, project lane uses same store
// with scope filtering. No lane switching occurs — scope=project with no project
// schedules returns empty array, NOT global schedules.
describe("Automation lane selection", () => {
it("GET /api/automations?scope=global calls only global automation store", async () => {
const globalStore = createMockAutomationStore("global");
const projectStore = createMockAutomationStore("project");
globalStore.listSchedules.mockResolvedValue([FAKE_GLOBAL_SCHEDULE]);
const engineManager = createMockEngineManager();
// engineManager returns undefined for all projects (no engine available)
engineManager.getEngine.mockReturnValue(undefined);
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
engineManager: engineManager as any,
});
const res = await GET(app, "/api/automations?scope=global");
expect(res.status).toBe(200);
expect(res.body).toHaveLength(1);
expect(res.body[0].scope).toBe("global");
expect(globalStore.listSchedules).toHaveBeenCalledTimes(1);
});
it("GET /api/automations?scope=global returns only global schedules (no project leakage)", async () => {
const globalStore = createMockAutomationStore("global");
const projectStore = createMockAutomationStore("project");
// Global store returns mixed results, but route filters by scope
globalStore.listSchedules.mockResolvedValue([FAKE_GLOBAL_SCHEDULE, FAKE_PROJECT_SCHEDULE]);
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
const res = await GET(app, "/api/automations?scope=global");
expect(res.status).toBe(200);
// Route should filter by scope so only global schedules are returned
expect(res.body.every((s: any) => s.scope === "global")).toBe(true);
});
it("GET /api/automations?scope=project returns only project-scoped schedules", async () => {
const globalStore = createMockAutomationStore("global");
globalStore.listSchedules.mockResolvedValue([FAKE_GLOBAL_SCHEDULE, FAKE_PROJECT_SCHEDULE]);
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
const res = await GET(app, "/api/automations?scope=project");
expect(res.status).toBe(200);
// Route should filter by scope so only project schedules are returned
expect(res.body.every((s: any) => s.scope === "project")).toBe(true);
});
it("POST /api/automations with scope=global creates in global lane", async () => {
const globalStore = createMockAutomationStore("global");
globalStore.createSchedule.mockResolvedValue({ ...FAKE_GLOBAL_SCHEDULE, scope: "global" });
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
const res = await REQUEST(app, "POST", "/api/automations", JSON.stringify({
name: "Test Global Schedule",
command: "echo test",
scheduleType: "hourly",
scope: "global",
}), { "Content-Type": "application/json" });
expect(res.status).toBe(201);
expect(globalStore.createSchedule).toHaveBeenCalledWith(
expect.objectContaining({ scope: "global" }),
);
});
it("POST /api/automations with scope=project creates in project lane", async () => {
const globalStore = createMockAutomationStore("global");
globalStore.createSchedule.mockResolvedValue({ ...FAKE_PROJECT_SCHEDULE, scope: "project" });
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
const res = await REQUEST(app, "POST", "/api/automations", JSON.stringify({
name: "Test Project Schedule",
command: "echo test",
scheduleType: "daily",
scope: "project",
}), { "Content-Type": "application/json" });
expect(res.status).toBe(201);
expect(globalStore.createSchedule).toHaveBeenCalledWith(
expect.objectContaining({ scope: "project" }),
);
});
it("GET /api/automations?scope=invalid returns 400 when automation store is configured", async () => {
const globalStore = createMockAutomationStore("global");
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
const res = await GET(app, "/api/automations?scope=invalid");
expect(res.status).toBe(400);
expect(res.body.error).toContain('Invalid scope value "invalid"');
});
it("GET /api/automations?scope=invalid returns empty array when no automation store configured (early exit)", async () => {
const store = createMockStore();
// When no automationStore is configured, route returns empty array BEFORE scope validation
const app = createServer(store);
const res = await GET(app, "/api/automations?scope=invalid");
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
});
it("GET /api/automations without scope returns all (legacy default)", async () => {
const globalStore = createMockAutomationStore("global");
globalStore.listSchedules.mockResolvedValue([FAKE_GLOBAL_SCHEDULE, FAKE_PROJECT_SCHEDULE]);
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
const res = await GET(app, "/api/automations");
expect(res.status).toBe(200);
expect(res.body).toHaveLength(2);
});
it("GET /api/automations when no automation store configured returns empty array", async () => {
const store = createMockStore();
const app = createServer(store); // No automationStore option
const res = await GET(app, "/api/automations");
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
});
it("POST /api/automations/:id/run with scope=global runs global schedule", async () => {
const globalStore = createMockAutomationStore("global");
globalStore.getSchedule.mockResolvedValue({ ...FAKE_GLOBAL_SCHEDULE });
globalStore.recordRun.mockResolvedValue({ ...FAKE_GLOBAL_SCHEDULE });
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
const res = await REQUEST(app, "POST", "/api/automations/sched-global-1/run?scope=global");
expect(res.status).toBe(200);
expect(res.body.schedule).toBeDefined();
expect(res.body.result).toBeDefined();
expect(globalStore.getSchedule).toHaveBeenCalledWith("sched-global-1");
});
it("POST /api/automations/:id/run with scope=project for global schedule returns 404", async () => {
const globalStore = createMockAutomationStore("global");
// Schedule is global-scoped
globalStore.getSchedule.mockResolvedValue({ ...FAKE_GLOBAL_SCHEDULE });
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
// Request with scope=project but schedule is global
const res = await REQUEST(app, "POST", "/api/automations/sched-global-1/run?scope=project");
expect(res.status).toBe(404);
expect(res.body.error).toContain("Schedule not found");
});
it("POST /api/automations/:id/toggle with scope=global toggles global schedule", async () => {
const globalStore = createMockAutomationStore("global");
globalStore.getSchedule.mockResolvedValue({ ...FAKE_GLOBAL_SCHEDULE, enabled: true });
globalStore.updateSchedule.mockResolvedValue({ ...FAKE_GLOBAL_SCHEDULE, enabled: false });
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
const res = await REQUEST(app, "POST", "/api/automations/sched-global-1/toggle?scope=global");
expect(res.status).toBe(200);
expect(globalStore.updateSchedule).toHaveBeenCalled();
});
it("Cross-project isolation: request for proj-a never touches proj-b dependencies", async () => {
const globalStore = createMockAutomationStore("global");
// Returns only project A's schedule
globalStore.listSchedules.mockResolvedValue([FAKE_PROJECT_SCHEDULE]);
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
// Request for project scope (would be proj-a in real scenario)
const res = await GET(app, "/api/automations?scope=project");
expect(res.status).toBe(200);
// All returned schedules should be project-scoped
expect(res.body.every((s: any) => s.scope === "project")).toBe(true);
});
});
// ── Routine lane selection tests ────────────────────────────────────────
//
// Routine lane selection: The routine store and routine runner resolve based on scope.
// Precedence: global lane uses process-level store/runner, project lane uses same
// with scope filtering. RoutineRunner is invoked for /run and /trigger endpoints
// when scope matches and routine is enabled.
describe("Routine lane selection", () => {
it("GET /api/routines?scope=global returns only global routines", async () => {
const globalStore = createMockRoutineStore("global");
globalStore.listRoutines.mockResolvedValue([FAKE_GLOBAL_ROUTINE, FAKE_PROJECT_ROUTINE]);
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
});
const res = await GET(app, "/api/routines?scope=global");
expect(res.status).toBe(200);
expect(res.body.every((r: any) => r.scope === "global")).toBe(true);
});
it("GET /api/routines?scope=project returns only project-scoped routines", async () => {
const globalStore = createMockRoutineStore("global");
globalStore.listRoutines.mockResolvedValue([FAKE_GLOBAL_ROUTINE, FAKE_PROJECT_ROUTINE]);
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
});
const res = await GET(app, "/api/routines?scope=project");
expect(res.status).toBe(200);
expect(res.body.every((r: any) => r.scope === "project")).toBe(true);
});
it("POST /api/routines with scope=global creates in global lane", async () => {
const globalStore = createMockRoutineStore("global");
globalStore.createRoutine.mockResolvedValue({ ...FAKE_GLOBAL_ROUTINE });
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
});
const res = await REQUEST(app, "POST", "/api/routines", JSON.stringify({
name: "Test Global Routine",
trigger: { type: "cron", cronExpression: "0 * * * *" },
scope: "global",
}), { "Content-Type": "application/json" });
expect(res.status).toBe(201);
expect(globalStore.createRoutine).toHaveBeenCalledWith(
expect.objectContaining({ scope: "global" }),
);
});
it("POST /api/routines with scope=project creates in project lane", async () => {
const globalStore = createMockRoutineStore("global");
globalStore.createRoutine.mockResolvedValue({ ...FAKE_PROJECT_ROUTINE });
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
});
const res = await REQUEST(app, "POST", "/api/routines", JSON.stringify({
name: "Test Project Routine",
trigger: { type: "cron", cronExpression: "0 * * * *" },
scope: "project",
}), { "Content-Type": "application/json" });
expect(res.status).toBe(201);
expect(globalStore.createRoutine).toHaveBeenCalledWith(
expect.objectContaining({ scope: "project" }),
);
});
it("GET /api/routines?scope=invalid returns 400 when routine store is configured", async () => {
const globalStore = createMockRoutineStore("global");
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
});
const res = await GET(app, "/api/routines?scope=invalid");
expect(res.status).toBe(400);
expect(res.body.error).toContain('Invalid scope value "invalid"');
});
it("GET /api/routines?scope=invalid returns empty array when no routine store configured (early exit)", async () => {
const store = createMockStore();
// When no routineStore is configured, route returns empty array BEFORE scope validation
const app = createServer(store);
const res = await GET(app, "/api/routines?scope=invalid");
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
});
it("GET /api/routines without scope returns all (legacy default)", async () => {
const globalStore = createMockRoutineStore("global");
globalStore.listRoutines.mockResolvedValue([FAKE_GLOBAL_ROUTINE, FAKE_PROJECT_ROUTINE]);
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
});
const res = await GET(app, "/api/routines");
expect(res.status).toBe(200);
expect(res.body).toHaveLength(2);
});
it("GET /api/routines when no routine store configured returns empty array", async () => {
const store = createMockStore();
const app = createServer(store); // No routineStore option
const res = await GET(app, "/api/routines");
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
});
it("POST /api/routines/:id/run with scope=global runs global routine via RoutineRunner", async () => {
const globalStore = createMockRoutineStore("global");
const routineRunner = createMockRoutineRunner();
globalStore.getRoutine.mockResolvedValue({ ...FAKE_GLOBAL_ROUTINE });
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
routineRunner: routineRunner as any,
});
const res = await REQUEST(app, "POST", "/api/routines/routine-global-1/run?scope=global");
expect(res.status).toBe(200);
expect(res.body.routine).toBeDefined();
expect(res.body.result).toBeDefined();
expect(routineRunner.triggerManual).toHaveBeenCalledWith("routine-global-1");
});
it("POST /api/routines/:id/run with scope=project for global routine returns 404", async () => {
const globalStore = createMockRoutineStore("global");
globalStore.getRoutine.mockResolvedValue({ ...FAKE_GLOBAL_ROUTINE });
const routineRunner = createMockRoutineRunner();
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
routineRunner: routineRunner as any,
});
// Request with scope=project but routine is global
const res = await REQUEST(app, "POST", "/api/routines/routine-global-1/run?scope=project");
expect(res.status).toBe(404);
expect(res.body.error).toContain("Routine not found");
// RoutineRunner should NOT be called for mismatched scope
expect(routineRunner.triggerManual).not.toHaveBeenCalled();
});
it("POST /api/routines/:id/trigger with scope=global triggers global routine", async () => {
const globalStore = createMockRoutineStore("global");
const routineRunner = createMockRoutineRunner();
globalStore.getRoutine.mockResolvedValue({ ...FAKE_GLOBAL_ROUTINE });
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
routineRunner: routineRunner as any,
});
const res = await REQUEST(app, "POST", "/api/routines/routine-global-1/trigger?scope=global");
expect(res.status).toBe(200);
expect(routineRunner.triggerManual).toHaveBeenCalledWith("routine-global-1");
});
it("GET /api/routines/:id/runs with scope=global returns runs for global routine", async () => {
const globalStore = createMockRoutineStore("global");
const runHistory = [
{ routineId: "routine-global-1", startedAt: "2026-01-01T00:00:00.000Z", completedAt: "2026-01-01T00:01:00.000Z", success: true, output: "Done" },
];
globalStore.getRoutine.mockResolvedValue({ ...FAKE_GLOBAL_ROUTINE, runHistory });
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
});
const res = await GET(app, "/api/routines/routine-global-1/runs?scope=global");
expect(res.status).toBe(200);
expect(res.body).toHaveLength(1);
});
it("Cross-project isolation: request for proj-a never touches proj-b dependencies", async () => {
const globalStore = createMockRoutineStore("global");
// Returns only project-scoped routines (would be proj-b in real multi-project scenario)
globalStore.listRoutines.mockResolvedValue([FAKE_PROJECT_ROUTINE]);
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
});
// Request for project scope
const res = await GET(app, "/api/routines?scope=project");
expect(res.status).toBe(200);
// All returned routines should be project-scoped
expect(res.body.every((r: any) => r.scope === "project")).toBe(true);
});
it("POST /api/routines/:id/webhook is scope-independent (uses routine's own scope)", async () => {
const globalStore = createMockRoutineStore("global");
const routineRunner = createMockRoutineRunner();
globalStore.getRoutine.mockResolvedValue({
...FAKE_PROJECT_ROUTINE,
trigger: { type: "webhook" as const, webhookPath: "/trigger/test" },
});
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
routineRunner: routineRunner as any,
});
// Webhook without scope param - should work regardless of scope
const res = await REQUEST(app, "POST", "/api/routines/routine-proj-a-1/webhook", JSON.stringify({}), { "Content-Type": "application/json" });
expect(res.status).toBe(200);
expect(routineRunner.triggerWebhook).toHaveBeenCalled();
});
it("POST /api/routines/:id/run when routine is disabled returns 400", async () => {
const globalStore = createMockRoutineStore("global");
globalStore.getRoutine.mockResolvedValue({ ...FAKE_GLOBAL_ROUTINE, enabled: false });
const routineRunner = createMockRoutineRunner();
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
routineRunner: routineRunner as any,
});
const res = await REQUEST(app, "POST", "/api/routines/routine-global-1/run?scope=global");
expect(res.status).toBe(400);
expect(res.body.error).toContain("disabled");
expect(routineRunner.triggerManual).not.toHaveBeenCalled();
});
it("POST /api/routines/:id/run when no RoutineRunner returns 503", async () => {
const globalStore = createMockRoutineStore("global");
globalStore.getRoutine.mockResolvedValue({ ...FAKE_GLOBAL_ROUTINE });
const store = createMockStore();
// No routineRunner configured
const app = createServer(store, {
routineStore: globalStore as any,
});
const res = await REQUEST(app, "POST", "/api/routines/routine-global-1/run?scope=global");
expect(res.status).toBe(503);
expect(res.body.error).toContain("not available");
});
});
// ── Fallback and error contract tests ────────────────────────────────
//
// Backward-compatible defaults: omitted scope defaults to "project" for mutations.
// This preserves existing behavior while adding explicit scope selection.
// Store unavailability: when no store is configured, GET returns [] for
// backward compatibility (legacy behavior).
describe("Fallback and error contracts", () => {
it("omitted scope defaults to project for backward compatibility (POST automations)", async () => {
const globalStore = createMockAutomationStore("global");
globalStore.createSchedule.mockResolvedValue({ ...FAKE_PROJECT_SCHEDULE, scope: "project" });
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
});
// No scope specified - should default to "project"
const res = await REQUEST(app, "POST", "/api/automations", JSON.stringify({
name: "Test Schedule",
command: "echo test",
scheduleType: "hourly",
// scope omitted
}), { "Content-Type": "application/json" });
expect(res.status).toBe(201);
expect(globalStore.createSchedule).toHaveBeenCalledWith(
expect.objectContaining({ scope: "project" }),
);
});
it("omitted scope defaults to project for backward compatibility (POST routines)", async () => {
const globalStore = createMockRoutineStore("global");
globalStore.createRoutine.mockResolvedValue({ ...FAKE_PROJECT_ROUTINE, scope: "project" });
const store = createMockStore();
const app = createServer(store, {
routineStore: globalStore as any,
});
// No scope specified - should default to "project"
const res = await REQUEST(app, "POST", "/api/routines", JSON.stringify({
name: "Test Routine",
trigger: { type: "cron", cronExpression: "0 * * * *" },
// scope omitted
}), { "Content-Type": "application/json" });
expect(res.status).toBe(201);
expect(globalStore.createRoutine).toHaveBeenCalledWith(
expect.objectContaining({ scope: "project" }),
);
});
it("scope=project with no automation store returns empty array (legacy fallback)", async () => {
const store = createMockStore();
// No automationStore configured - routes return empty array for backward compatibility
const app = createServer(store);
const res = await GET(app, "/api/automations?scope=project");
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
});
it("scope=global with no automation store returns empty array (legacy fallback)", async () => {
const store = createMockStore();
// No automationStore configured - routes return empty array for backward compatibility
const app = createServer(store);
const res = await GET(app, "/api/automations?scope=global");
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
});
it("scope=project with no routine store returns empty array (legacy fallback)", async () => {
const store = createMockStore();
// No routineStore configured - routes return empty array for backward compatibility
const app = createServer(store);
const res = await GET(app, "/api/routines?scope=project");
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
});
it("scope=global with no routine store returns empty array (legacy fallback)", async () => {
const store = createMockStore();
// No routineStore configured - routes return empty array for backward compatibility
const app = createServer(store);
const res = await GET(app, "/api/routines?scope=global");
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
});
});
// ── No-opportunistic-lane-hopping assertions ───────────────────────────
//
// Critical invariant: scope selection is deterministic and deterministic.
// When scope=project returns no results, the resolver must NOT fall back to
// global lane results. This prevents cross-project data leakage and ensures
// automation/routine isolation between global and project contexts.
describe("No opportunistic lane hopping", () => {
it("scope=project request never falls back to global when engine is unavailable", async () => {
const globalStore = createMockAutomationStore("global");
const engineManager = createMockEngineManager();
// No engine available for any project
engineManager.getEngine.mockReturnValue(undefined);
// Only global-scoped schedules exist
globalStore.listSchedules.mockResolvedValue([FAKE_GLOBAL_SCHEDULE]);
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
engineManager: engineManager as any,
});
// Request for project scope - should filter by scope, NOT switch to global
const res = await GET(app, "/api/automations?scope=project");
expect(res.status).toBe(200);
// Should return empty (no project-scoped schedules) not global schedules
expect(res.body).toHaveLength(0);
});
it("scope=global request never switches to project lane", async () => {
const globalStore = createMockAutomationStore("global");
const engineManager = createMockEngineManager();
// Engine IS available for some project
const mockEngine = { getTaskStore: vi.fn() };
engineManager.getEngine.mockReturnValue(mockEngine as any);
// Both global and project schedules exist
globalStore.listSchedules.mockResolvedValue([FAKE_GLOBAL_SCHEDULE, FAKE_PROJECT_SCHEDULE]);
const store = createMockStore();
const app = createServer(store, {
automationStore: globalStore as any,
engineManager: engineManager as any,
});
// Request for global scope - should only return global schedules
const res = await GET(app, "/api/automations?scope=global");
expect(res.status).toBe(200);
// All results should be global-scoped
expect(res.body.every((s: any) => s.scope === "global")).toBe(true);
// Should not have fallen back to project
expect(res.body.some((s: any) => s.scope === "project")).toBe(false);
});
});
});