diff --git a/.changeset/fix-planning-history-multitask.md b/.changeset/fix-planning-history-multitask.md new file mode 100644 index 0000000000..5bb2f58e90 --- /dev/null +++ b/.changeset/fix-planning-history-multitask.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Completed Planning Mode sessions that create multiple tasks now stay in planning history. +category: fix +dev: The multi-task create-tasks route now uses releaseSession instead of cleanupSession, retaining the persisted ai_sessions row like the single-task path. diff --git a/packages/dashboard/src/__tests__/routes-planning.test.ts b/packages/dashboard/src/__tests__/routes-planning.test.ts index 88d5147e65..c839680464 100644 --- a/packages/dashboard/src/__tests__/routes-planning.test.ts +++ b/packages/dashboard/src/__tests__/routes-planning.test.ts @@ -2594,6 +2594,77 @@ describe("Planning Mode Routes", () => { expect(res.body.tasks).toHaveLength(2); }); + it("keeps the completed planning session in history after multi-task creation", async () => { + // Bug C: /planning/create-tasks used cleanupSession() which deleted the + // persisted ai_sessions row, so a session that ran to completion AND + // created tasks vanished from the saved-sessions history. It must instead + // release only the in-memory runtime (like single-task create-task) and + // keep the persisted completed row. + const mockStore = new MockAiSessionStore(); + setAiSessionStore(mockStore as unknown as Parameters[0]); + + (store.createTask as ReturnType) + .mockResolvedValueOnce({ + id: "FN-270", + description: "First", + column: "triage", + dependencies: [], + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }) + .mockResolvedValueOnce({ + id: "FN-271", + description: "Second", + column: "triage", + dependencies: [], + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }); + (store.updateTask as ReturnType).mockResolvedValue({}); + (store.logEntry as ReturnType).mockResolvedValue(undefined); + + const planningSessionId = await createCompletedPlanningSession(); + + // Precondition: the completed planning session is persisted as history. + const persistedBefore = mockStore.get(planningSessionId); + expect(persistedBefore).not.toBeNull(); + expect(persistedBefore?.type).toBe("planning"); + expect(persistedBefore?.status).toBe("complete"); + + const breakdownRes = await REQUEST( + buildApp(), + "POST", + "/api/planning/start-breakdown", + JSON.stringify({ sessionId: planningSessionId }), + { "Content-Type": "application/json" } + ); + const generatedSubtasks = breakdownRes.body.subtasks as Array<{ id: string }>; + + const res = await REQUEST( + buildApp(), + "POST", + "/api/planning/create-tasks", + JSON.stringify({ + planningSessionId, + subtasks: [ + { id: generatedSubtasks[0]!.id, title: "Auth backend", description: "Implement backend", suggestedSize: "L", dependsOn: [] }, + { id: generatedSubtasks[1]!.id, title: "Auth frontend", description: "Implement frontend", dependsOn: [generatedSubtasks[0]!.id] }, + ], + }), + { "Content-Type": "application/json" } + ); + + expect(res.status).toBe(201); + expect(res.body.tasks).toHaveLength(2); + + // Regression assertion: the completed planning session row must survive + // task creation so it remains listable/restorable in history. + const persistedAfter = mockStore.get(planningSessionId); + expect(persistedAfter).not.toBeNull(); + expect(persistedAfter?.type).toBe("planning"); + expect(persistedAfter?.status).toBe("complete"); + }); + it("creates task with explicit summary priority", async () => { (store.createTask as ReturnType).mockResolvedValue({ id: "FN-100", diff --git a/packages/dashboard/src/routes/register-planning-subtask-routes.ts b/packages/dashboard/src/routes/register-planning-subtask-routes.ts index d73de7b701..d17464c254 100644 --- a/packages/dashboard/src/routes/register-planning-subtask-routes.ts +++ b/packages/dashboard/src/routes/register-planning-subtask-routes.ts @@ -1333,7 +1333,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann } const { store: scopedStore } = await getProjectContext(req); - const { getSession, cleanupSession, formatInterviewQA, mergePlanningSubtaskDrafts } = await import("../planning.js"); + const { getSession, releaseSession, formatInterviewQA, mergePlanningSubtaskDrafts } = await import("../planning.js"); const session = await getSession(planningSessionId); if (!session) { @@ -1494,9 +1494,15 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann ); } + // FNXC:PlanningMode 2026-07-13-00:00: release the live in-memory planning + // runtime but KEEP the persisted completed row so the multi-task path + // matches single-task create-task — completed planning sessions must remain + // listable/restorable in the saved-sessions history. Using cleanupSession + // here deleted the ai_sessions row, so a session that ran to completion and + // created tasks vanished from history (GET /ai-sessions returned it no more). await runPlanningCreateSideEffect( - "Planning create-tasks session cleanup failed", - () => cleanupSession(planningSessionId), + "Planning create-tasks session release failed", + () => releaseSession(planningSessionId), { planningSessionId }, );