feat(FN-1152): add retry flows for failed AI planning sessions

- Keep errored AI sessions retryable in the session store and add backend retry handlers for planning, subtask breakdown, and mission interview flows
- Add retry API routes and dashboard API client helpers for planning, subtask, and mission interview session retries
- Update PlanningModeModal, SubtaskBreakdownModal, MissionInterviewModal, and background session handling to show error states with retry/cancel UX
- Expand unit and integration tests across store, services, routes, and modal components to cover retry success and failure paths
This commit is contained in:
gsxdsm
2026-04-08 15:10:46 -07:00
parent 5f53949e2a
commit e80fb448d4
20 changed files with 1368 additions and 234 deletions

View File

@@ -3,12 +3,14 @@ import { act, render, screen, fireEvent, waitFor } from "@testing-library/react"
import { SubtaskBreakdownModal } from "./SubtaskBreakdownModal";
const mockStartSubtaskBreakdown = vi.fn();
const mockRetrySubtaskSession = vi.fn();
const mockConnectSubtaskStream = vi.fn();
const mockCreateTasksFromBreakdown = vi.fn();
const mockCancelSubtaskBreakdown = vi.fn();
vi.mock("../api", () => ({
startSubtaskBreakdown: (...args: any[]) => mockStartSubtaskBreakdown(...args),
retrySubtaskSession: (...args: any[]) => mockRetrySubtaskSession(...args),
connectSubtaskStream: (...args: any[]) => mockConnectSubtaskStream(...args),
createTasksFromBreakdown: (...args: any[]) => mockCreateTasksFromBreakdown(...args),
cancelSubtaskBreakdown: (...args: any[]) => mockCancelSubtaskBreakdown(...args),
@@ -40,6 +42,7 @@ describe("SubtaskBreakdownModal", () => {
vi.clearAllMocks();
streamHandlers = undefined;
mockStartSubtaskBreakdown.mockResolvedValue({ sessionId: "session-123" });
mockRetrySubtaskSession.mockResolvedValue({ success: true, sessionId: "session-123" });
mockConnectSubtaskStream.mockImplementation((_sessionId, _projectId, handlers) => {
streamHandlers = handlers;
return { close: vi.fn(), isConnected: () => true };
@@ -483,6 +486,20 @@ describe("SubtaskBreakdownModal", () => {
expect(await screen.findByText("Something went wrong")).toBeInTheDocument();
});
it("retries after an error and reconnects stream", async () => {
renderModal();
await waitFor(() => expect(streamHandlers).toBeDefined());
streamHandlers.onError("Something went wrong");
const retryButton = await screen.findByRole("button", { name: "Retry" });
fireEvent.click(retryButton);
await waitFor(() => {
expect(mockRetrySubtaskSession).toHaveBeenCalledWith("session-123", undefined);
});
expect(mockConnectSubtaskStream).toHaveBeenCalledTimes(2);
});
it("shows Stream error fallback when receiving empty error", async () => {
renderModal();
await waitFor(() => expect(streamHandlers).toBeDefined());