import { describe, it, expect, vi, beforeEach } from "vitest"; import { act, render, renderHook, screen, fireEvent, waitFor, within } from "@testing-library/react"; import * as api from "../api"; import { PlanningModeModal } from "./PlanningModeModal"; import { TaskDetailModal } from "./TaskDetailModal"; import { useSessionLock } from "../hooks/useSessionLock"; import { getSessionTabId } from "../utils/getSessionTabId"; import type { Task, TaskDetail, PlanningQuestion, PlanningSummary, MergeResult } from "@fusion/core"; import * as api from "../api"; // Mock the API functions const mockStartPlanning = vi.fn(); const mockStartPlanningStreaming = vi.fn(); const mockConnectPlanningStream = vi.fn(); const mockRespondToPlanning = vi.fn(); const mockRetryPlanningSession = vi.fn(); const mockCancelPlanning = vi.fn(); const mockCreateTaskFromPlanning = vi.fn(); const mockStartPlanningBreakdown = vi.fn(); const mockCreateTasksFromPlanning = vi.fn(); const mockFetchAiSession = vi.fn(); const mockParseConversationHistory = vi.fn(); const mockFetchModels = vi.fn(); const mockAcquireSessionLock = vi.fn(); const mockReleaseSessionLock = vi.fn(); const mockForceAcquireSessionLock = vi.fn(); const mockUploadAttachment = vi.fn(); const mockDeleteAttachment = vi.fn(); const mockUpdateTask = vi.fn(); const mockPauseTask = vi.fn(); const mockUnpauseTask = vi.fn(); const mockFetchTaskDetail = vi.fn(); const mockRequestSpecRevision = vi.fn(); const mockApprovePlan = vi.fn(); const mockRejectPlan = vi.fn(); const mockRefineTask = vi.fn(); vi.mock("../api", () => ({ startPlanning: (...args: any[]) => mockStartPlanning(...args), startPlanningStreaming: (...args: any[]) => mockStartPlanningStreaming(...args), connectPlanningStream: (...args: any[]) => mockConnectPlanningStream(...args), respondToPlanning: (...args: any[]) => mockRespondToPlanning(...args), retryPlanningSession: (...args: any[]) => mockRetryPlanningSession(...args), cancelPlanning: (...args: any[]) => mockCancelPlanning(...args), createTaskFromPlanning: (...args: any[]) => mockCreateTaskFromPlanning(...args), startPlanningBreakdown: (...args: any[]) => mockStartPlanningBreakdown(...args), createTasksFromPlanning: (...args: any[]) => mockCreateTasksFromPlanning(...args), fetchAiSession: (...args: any[]) => mockFetchAiSession(...args), parseConversationHistory: (...args: any[]) => mockParseConversationHistory(...args), acquireSessionLock: (...args: any[]) => mockAcquireSessionLock(...args), releaseSessionLock: (...args: any[]) => mockReleaseSessionLock(...args), forceAcquireSessionLock: (...args: any[]) => mockForceAcquireSessionLock(...args), uploadAttachment: (...args: any[]) => mockUploadAttachment(...args), deleteAttachment: (...args: any[]) => mockDeleteAttachment(...args), updateTask: (...args: any[]) => mockUpdateTask(...args), pauseTask: (...args: any[]) => mockPauseTask(...args), unpauseTask: (...args: any[]) => mockUnpauseTask(...args), fetchTaskDetail: (...args: any[]) => mockFetchTaskDetail(...args), requestSpecRevision: (...args: any[]) => mockRequestSpecRevision(...args), approvePlan: (...args: any[]) => mockApprovePlan(...args), rejectPlan: (...args: any[]) => mockRejectPlan(...args), refineTask: (...args: any[]) => mockRefineTask(...args), fetchSettings: vi.fn().mockResolvedValue({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {} }), fetchModels: (...args: any[]) => mockFetchModels(...args), fetchWorkflowSteps: vi.fn().mockResolvedValue([]), refineText: vi.fn(), getRefineErrorMessage: vi.fn((err: any) => err?.message || "Failed to refine"), updateGlobalSettings: vi.fn().mockResolvedValue({}), duplicateTask: vi.fn().mockResolvedValue({}), })); const mockTasks: Task[] = [ { id: "FN-001", description: "Existing task 1", column: "todo", dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }, ]; const mockModels = [ { provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, contextWindow: 200000, }, { provider: "google", id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", reasoning: true, contextWindow: 1048576, }, ]; const mockQuestion: PlanningQuestion = { id: "q-scope", type: "single_select", question: "What is the scope?", description: "Choose the scope of this task", options: [ { id: "small", label: "Small" }, { id: "medium", label: "Medium" }, { id: "large", label: "Large" }, ], }; const mockSummary: PlanningSummary = { title: "Build authentication system", description: "Implement user auth with login and signup", suggestedSize: "M", suggestedDependencies: [], keyDeliverables: ["Login page", "Signup page", "Auth API"], }; const mockTaskDetail = { id: "KB-999", title: "Example task", description: "Example description", column: "todo", dependencies: [], steps: [], currentStep: 0, log: [], attachments: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", prompt: "# Task\n\nExample prompt", paused: false, } as TaskDetail; class MockEventSource { static instances: MockEventSource[] = []; url: string; closed = false; private listeners = new Map void>>(); constructor(url: string) { this.url = url; MockEventSource.instances.push(this); } addEventListener(event: string, listener: (event: MessageEvent) => void): void { const set = this.listeners.get(event) ?? new Set(); set.add(listener); this.listeners.set(event, set); } removeEventListener(event: string, listener: (event: MessageEvent) => void): void { const set = this.listeners.get(event); if (!set) return; set.delete(listener); } close(): void { this.closed = true; } emit(event: string, data: unknown): void { const listeners = this.listeners.get(event); if (!listeners) return; const message = { data: JSON.stringify(data) } as MessageEvent; listeners.forEach((listener) => listener(message)); } static reset(): void { MockEventSource.instances = []; } } describe("PlanningModeModal", () => { const mockOnClose = vi.fn(); const mockOnTaskCreated = vi.fn(); beforeEach(() => { vi.clearAllMocks(); MockEventSource.reset(); vi.stubGlobal("EventSource", MockEventSource as any); window.sessionStorage.clear(); vi.spyOn(window, "confirm").mockReturnValue(true); // Default mock for streaming mockStartPlanningStreaming.mockResolvedValue({ sessionId: "session-123" }); mockRetryPlanningSession.mockResolvedValue({ success: true, sessionId: "session-123" }); mockStartPlanningBreakdown.mockResolvedValue({ sessionId: "session-123", subtasks: [] }); mockFetchAiSession.mockResolvedValue(null); mockParseConversationHistory.mockImplementation((raw: string) => { if (!raw) return []; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch { return []; } }); mockFetchModels.mockResolvedValue({ models: mockModels, favoriteProviders: [], favoriteModels: [], }); mockAcquireSessionLock.mockResolvedValue({ acquired: true, currentHolder: null }); mockReleaseSessionLock.mockResolvedValue(undefined); mockForceAcquireSessionLock.mockResolvedValue(undefined); mockCancelPlanning.mockResolvedValue(undefined); // Default: simulate receiving a question after a brief delay mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => { setTimeout(() => { handlers.onQuestion?.(mockQuestion); }, 10); return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); }); describe("Initial view", () => { it("renders the initial input view when open", () => { const { container } = render( ); expect(screen.getByText("Planning Mode")).toBeDefined(); expect(screen.getByPlaceholderText(/e.g., Build a user authentication/)).toBeDefined(); expect(container.querySelector(".planning-modal-body")).not.toBeNull(); expect(container.querySelector(".planning-modal-body")?.classList.contains("modal-body")).toBe(false); expect(container.querySelector(".planning-examples-label")?.textContent).toBe("Try an example:"); }); it("does not render when closed", () => { render( ); expect(screen.queryByText("Planning Mode")).toBeNull(); }); it("hides send to background button in initial state", () => { render( ); expect(screen.queryByLabelText("Send to background")).toBeNull(); }); it("enables start button when text is entered", () => { render( ); const startButton = screen.getByText("Start Planning"); expect(startButton.closest("button")?.hasAttribute("disabled")).toBe(true); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Test plan" } }); expect(startButton.closest("button")?.hasAttribute("disabled")).toBe(false); }); it("shows example chips", () => { render( ); expect(screen.getByText(/Build a user authentication/)).toBeDefined(); }); it("renders planning model dropdown in initial view", async () => { render( ); const modelTrigger = screen.getByRole("button", { name: "Planning Model" }); expect(modelTrigger).toBeDefined(); expect(screen.getByText("Using default")).toBeDefined(); await waitFor(() => { expect(mockFetchModels).toHaveBeenCalledTimes(1); }); }); it("updates planning model selection and badge", async () => { render( ); await waitFor(() => { expect(mockFetchModels).toHaveBeenCalledTimes(1); }); fireEvent.click(screen.getByRole("button", { name: "Planning Model" })); fireEvent.click(screen.getByRole("option", { name: /Claude Sonnet 4.5/ })); expect(screen.getByText("anthropic/claude-sonnet-4-5")).toBeDefined(); }); it("passes selected planning model to startPlanningStreaming", async () => { render( ); await waitFor(() => { expect(mockFetchModels).toHaveBeenCalledTimes(1); }); fireEvent.click(screen.getByRole("button", { name: "Planning Model" })); fireEvent.click(screen.getByRole("option", { name: /Claude Sonnet 4.5/ })); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, { planningModelProvider: "anthropic", planningModelId: "claude-sonnet-4-5", }); }); }); it("calls startPlanningStreaming without model override when none selected", async () => { render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, undefined); }); }); it("auto-starts planning when initialPlan prop is provided", async () => { render( ); // Wait for startPlanningStreaming to be called (allow time for setTimeout in useEffect) await waitFor(() => { expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build a login system from new task dialog", undefined, undefined); }, { timeout: 2000 }); // Should transition to question view await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); }); it("sets initial plan text in textarea when initialPlan prop is provided", async () => { render( ); // The auto-start should happen with the initial plan (allow time for setTimeout in useEffect) await waitFor(() => { expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Pre-filled plan from new task", undefined, undefined); }, { timeout: 2000 }); }); }); describe("Planning flow", () => { it("starts planning and shows question view", async () => { render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); // Wait for streaming to be called await waitFor(() => { expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined, undefined); }); // Should transition to question view via streaming await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); }); it("shows locked overlay and allows take-control", async () => { window.sessionStorage.setItem("fusion-tab-id", "tab-self"); mockAcquireSessionLock.mockResolvedValueOnce({ acquired: false, currentHolder: "tab-other" }); render( , ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByTestId("session-lock-overlay")).toBeDefined(); }); await act(async () => { fireEvent.click(screen.getByText("Take Control")); }); await waitFor(() => { expect(mockForceAcquireSessionLock).toHaveBeenCalledWith("session-123", "tab-self"); }); await waitFor(() => { expect(screen.queryByTestId("session-lock-overlay")).toBeNull(); }); }); it("allows normal question interaction when lock is acquired", async () => { window.sessionStorage.setItem("fusion-tab-id", "tab-self"); render( , ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.queryByTestId("session-lock-overlay")).toBeNull(); }); await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); fireEvent.click(screen.getByText("Small")); fireEvent.click(screen.getByText("Continue")); await waitFor(() => { expect(mockRespondToPlanning).toHaveBeenCalledWith( "session-123", { "q-scope": "small" }, undefined, "tab-self", ); }); }); it("shows error message when planning fails", async () => { // Override the default mock to simulate an error mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { setTimeout(() => { handlers.onError?.("Rate limit exceeded"); }, 10); return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Rate limit exceeded")).toBeDefined(); }); expect(screen.getByRole("button", { name: "Retry" })).toBeDefined(); }); it("retries from error state and reconnects stream", async () => { let streamAttempt = 0; mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => { streamAttempt += 1; if (streamAttempt === 1) { setTimeout(() => handlers.onError?.("Temporary failure"), 10); } else { setTimeout(() => handlers.onQuestion?.(mockQuestion), 10); } return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); render( , ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Temporary failure")).toBeDefined(); }); fireEvent.click(screen.getByRole("button", { name: "Retry" })); await waitFor(() => { expect(mockRetryPlanningSession).toHaveBeenCalledWith("session-123", undefined, expect.any(String)); }); await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); expect(mockConnectPlanningStream).toHaveBeenCalledTimes(2); }); }); describe("Resuming complete sessions", () => { it("shows summary view when resuming a complete persisted session", async () => { const resumedSummary: PlanningSummary = { title: "Resume-ready planning output", description: "Recovered summary description from persisted session", suggestedSize: "L", suggestedDependencies: ["FN-001"], keyDeliverables: ["Deliverable A", "Deliverable B"], }; mockFetchAiSession.mockResolvedValueOnce({ id: "session-complete-1", type: "planning", status: "complete", title: "Resume-ready planning output", inputPayload: JSON.stringify({ initialPlan: "Build resilient planning resume" }), conversationHistory: "[]", currentQuestion: null, result: JSON.stringify(resumedSummary), thinkingOutput: "", error: null, projectId: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); render( ); await waitFor(() => { expect(mockFetchAiSession).toHaveBeenCalledWith("session-complete-1"); }); await waitFor(() => { expect(screen.getByText("Planning Complete!")).toBeDefined(); }); expect(screen.getByDisplayValue("Recovered summary description from persisted session")).toBeDefined(); expect((screen.getByRole("combobox") as HTMLSelectElement).value).toBe("L"); expect(screen.getByText("Deliverable A")).toBeDefined(); expect(screen.getByText("Deliverable B")).toBeDefined(); }); it("shows retry panel when resuming an errored session", async () => { mockFetchAiSession.mockResolvedValueOnce({ id: "session-error-1", type: "planning", status: "error", title: "Errored planning", inputPayload: JSON.stringify({ initialPlan: "Recover planning" }), conversationHistory: "[]", currentQuestion: null, result: null, thinkingOutput: "", error: "Session interrupted", projectId: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); render( , ); await waitFor(() => { expect(screen.getByText("Session interrupted")).toBeDefined(); }); expect(screen.getByRole("button", { name: "Retry" })).toBeDefined(); }); it("creates a task from a resumed complete session", async () => { const resumedSummary: PlanningSummary = { title: "Resume-to-task", description: "Recovered summary for task creation", suggestedSize: "M", suggestedDependencies: [], keyDeliverables: ["Implement", "Verify"], }; mockFetchAiSession.mockResolvedValueOnce({ id: "session-complete-2", type: "planning", status: "complete", title: "Resume-to-task", inputPayload: JSON.stringify({ initialPlan: "Recover and create" }), conversationHistory: "[]", currentQuestion: null, result: JSON.stringify(resumedSummary), thinkingOutput: "", error: null, projectId: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); mockCreateTaskFromPlanning.mockResolvedValueOnce({ id: "FN-100", title: "Resume-to-task", description: "Recovered summary for task creation", column: "triage", dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); render( ); await waitFor(() => { expect(screen.getByText("Create Task")).toBeDefined(); }); fireEvent.click(screen.getByText("Create Task")); await waitFor(() => { expect(mockCreateTaskFromPlanning).toHaveBeenCalledWith("session-complete-2", undefined); }); }); }); describe("Conversation history", () => { it("restores all persisted Q&A pairs when resuming a session", async () => { const resumedQuestion: PlanningQuestion = { id: "q-current", type: "text", question: "What should we prioritize next?", description: "Current question", }; const restoredHistory = [ { question: { id: "q1", type: "single_select", question: "What scope do you need?", options: [ { id: "small", label: "Small" }, { id: "medium", label: "Medium" }, ], }, response: { q1: "medium" }, thinkingOutput: "Reasoning for scope question", }, { question: { id: "q2", type: "text", question: "List your acceptance criteria", }, response: { q2: "Must support offline mode" }, thinkingOutput: "Reasoning for criteria question", }, ]; mockFetchAiSession.mockResolvedValueOnce({ id: "session-awaiting-1", type: "planning", status: "awaiting_input", title: "Resume with history", inputPayload: JSON.stringify({ initialPlan: "Build planning history restore" }), conversationHistory: JSON.stringify(restoredHistory), currentQuestion: JSON.stringify(resumedQuestion), result: null, thinkingOutput: "", error: null, projectId: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); render( , ); await waitFor(() => { expect(mockParseConversationHistory).toHaveBeenCalledWith(JSON.stringify(restoredHistory)); }); await waitFor(() => { expect(screen.getByText("What scope do you need?")).toBeDefined(); }); expect(screen.getByText("List your acceptance criteria")).toBeDefined(); expect(screen.getByText("Medium")).toBeDefined(); expect(screen.getByText("Must support offline mode")).toBeDefined(); expect(screen.getByText("What should we prioritize next?")).toBeDefined(); }); it("starts fresh sessions with empty conversation history", async () => { render( , ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); expect(screen.queryByTestId("conversation-history")).toBeNull(); }); it("appends submitted responses to visible conversation history", async () => { const secondQuestion: PlanningQuestion = { id: "q-requirements", type: "text", question: "What are the key requirements?", description: "Describe the requirements", }; let streamHandlers: any; mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { streamHandlers = handlers; setTimeout(() => { handlers.onQuestion?.(mockQuestion); }, 10); return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); mockRespondToPlanning.mockImplementation(async () => { setTimeout(() => { streamHandlers?.onQuestion?.(secondQuestion); }, 10); return { sessionId: "session-123", currentQuestion: null, summary: null }; }); render( , ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); fireEvent.click(screen.getByText("Medium")); fireEvent.click(screen.getByRole("button", { name: "Continue" })); await waitFor(() => { expect(screen.getByText("What are the key requirements?")).toBeDefined(); }); expect(screen.getByTestId("conversation-history")).toBeDefined(); expect(screen.getByText("What is the scope?")).toBeDefined(); expect(screen.getByText("Medium")).toBeDefined(); }); }); describe("Question view", () => { it("renders single_select question with options", async () => { const { container } = render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Small")).toBeDefined(); expect(screen.getByText("Medium")).toBeDefined(); expect(screen.getByText("Large")).toBeDefined(); }); expect(container.querySelector(".planning-question-form > .planning-view-scroll")).not.toBeNull(); expect(container.querySelector(".planning-question-form > .planning-actions")).not.toBeNull(); }); it("shows reconnecting indicator without clearing current question state", async () => { let streamHandlers: any; mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { streamHandlers = handlers; setTimeout(() => { handlers.onQuestion?.(mockQuestion); }, 10); return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); render( , ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); act(() => { streamHandlers.onConnectionStateChange?.("reconnecting"); }); expect(screen.getByText("Reconnecting…")).toBeDefined(); expect(screen.getByText("What is the scope?")).toBeDefined(); act(() => { streamHandlers.onConnectionStateChange?.("connected"); }); await waitFor(() => { expect(screen.queryByText("Reconnecting…")).toBeNull(); }); expect(screen.getByText("What is the scope?")).toBeDefined(); }); it("receives second question after answering first without hanging (race condition fix)", async () => { // Use fake timers to avoid CI flakiness from tiny setTimeout delays in this race-condition scenario. vi.useFakeTimers(); try { const secondQuestion: PlanningQuestion = { id: "q-requirements", type: "text", question: "What are the key requirements?", description: "Describe the requirements", }; // Track how many times connectPlanningStream is called let streamConnectionCount = 0; let streamHandlers: any = null; let deliverSecondQuestion: (() => void) | null = null; mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => { streamConnectionCount++; streamHandlers = handlers; // Emit the first question synchronously on initial connection. if (streamConnectionCount === 1) { handlers.onQuestion?.(mockQuestion); } return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); mockRespondToPlanning.mockImplementation(async () => { return new Promise((resolve) => { deliverSecondQuestion = () => { streamHandlers?.onQuestion?.(secondQuestion); resolve({ sessionId: "session-123", currentQuestion: null, summary: null }); }; }); }); render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); await act(async () => { await vi.advanceTimersByTimeAsync(0); }); expect(screen.getByText("What is the scope?")).toBeDefined(); // Answer the first question. fireEvent.click(screen.getByText("Medium")); await act(async () => { await vi.advanceTimersByTimeAsync(0); }); const continueButton = screen.getByRole("button", { name: "Continue" }); expect(continueButton.hasAttribute("disabled")).toBe(false); fireEvent.click(continueButton); await act(async () => { await vi.advanceTimersByTimeAsync(0); }); expect(mockRespondToPlanning).toHaveBeenCalledTimes(1); expect(deliverSecondQuestion).not.toBeNull(); act(() => { deliverSecondQuestion?.(); }); await act(async () => { await vi.advanceTimersByTimeAsync(0); }); // Verify second question appears without hanging. expect(screen.getByText("What are the key requirements?")).toBeDefined(); // Verify SSE connection was established only ONCE (not reconnected). // This confirms the race condition fix - the same connection is reused. expect(streamConnectionCount).toBe(1); } finally { vi.useRealTimers(); } }); it("connects to stream when resuming awaiting_input session to receive real-time updates", async () => { // This test verifies the fix for the mismatch where a session was advertised as // needing input but the resume path initially entered loading state. // The modal should connect to the stream for awaiting_input sessions to receive // real-time updates (thinking output, next question, etc.). const resumedQuestion: PlanningQuestion = { id: "q-priority", type: "single_select", question: "What's your priority?", options: [ { id: "speed", label: "Speed" }, { id: "quality", label: "Quality" }, { id: "cost", label: "Cost" }, ], }; mockFetchAiSession.mockResolvedValueOnce({ id: "session-awaiting-stream-1", type: "planning", status: "awaiting_input", title: "Resume with stream", inputPayload: JSON.stringify({ initialPlan: "Build planning with stream" }), conversationHistory: "[]", currentQuestion: JSON.stringify(resumedQuestion), result: null, thinkingOutput: "", error: null, projectId: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }); // Track stream connections let streamConnectedSessionId: string | null = null; mockConnectPlanningStream.mockImplementationOnce((sessionId: string, _projectId: string | undefined, _handlers: any) => { streamConnectedSessionId = sessionId; return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); render( , ); // Flush React state updates from the resume effect await act(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }); // Session should be fetched await waitFor(() => { expect(mockFetchAiSession).toHaveBeenCalledWith("session-awaiting-stream-1"); }); // Question should appear immediately from session data await waitFor(() => { expect(screen.getByText("What's your priority?")).toBeDefined(); }); // Modal should connect to the stream for real-time updates await act(async () => { await new Promise((resolve) => setTimeout(resolve, 10)); }); // Verify stream connection was established expect(mockConnectPlanningStream).toHaveBeenCalled(); expect(streamConnectedSessionId).toBe("session-awaiting-stream-1"); // Should NOT be stuck in loading state expect(screen.queryByText("Generating next question...")).toBeNull(); }); }); describe("Summary view", () => { it("shows summary when planning is complete", async () => { // Override mock to return summary instead of question mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { setTimeout(() => { handlers.onSummary?.(mockSummary); }, 10); return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); const { container } = render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Planning Complete!")).toBeDefined(); }); expect(container.querySelector(".planning-summary > .planning-view-scroll")).not.toBeNull(); expect(container.querySelector(".planning-summary > .planning-actions")).not.toBeNull(); expect(container.querySelector(".planning-summary .planning-deps-list")).not.toBeNull(); }); it("renders and updates summary size dropdown", async () => { mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { setTimeout(() => { handlers.onSummary?.(mockSummary); }, 10); return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Planning Complete!")).toBeDefined(); }); const sizeSelect = screen.getByRole("combobox") as HTMLSelectElement; expect(sizeSelect.value).toBe("M"); expect(Array.from(sizeSelect.options).map((option) => option.textContent)).toEqual([ "S (Small)", "M (Medium)", "L (Large)", ]); fireEvent.change(sizeSelect, { target: { value: "L" } }); expect(sizeSelect.value).toBe("L"); }); it("creates task from summary", async () => { const createdTask: Task = { id: "FN-042", title: "Build authentication system", description: "Implement user auth with login and signup", column: "triage", dependencies: [], steps: [], currentStep: 0, log: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", }; // Override mock to return summary mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { setTimeout(() => { handlers.onSummary?.(mockSummary); }, 10); return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); mockCreateTaskFromPlanning.mockResolvedValue(createdTask); render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Create Task")).toBeDefined(); }); fireEvent.click(screen.getByText("Create Task")); await waitFor(() => { expect(mockCreateTaskFromPlanning).toHaveBeenCalledWith("session-123", undefined); expect(mockOnTaskCreated).toHaveBeenCalledWith(createdTask); }); }); }); describe("Breakdown view", () => { it("renders and updates subtask size dropdown", async () => { mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { setTimeout(() => { handlers.onSummary?.(mockSummary); }, 10); return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); mockStartPlanningBreakdown.mockResolvedValue({ sessionId: "session-123", subtasks: [ { id: "subtask-1", title: "Design auth schema", description: "Design the auth data model", suggestedSize: "M", dependsOn: [], }, { id: "subtask-2", title: "Implement auth endpoints", description: "Create login/signup endpoints", suggestedSize: "S", dependsOn: ["subtask-1"], }, ], }); render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Planning Complete!")).toBeDefined(); }); fireEvent.click(screen.getByText("Break into Tasks")); await waitFor(() => { expect(mockStartPlanningBreakdown).toHaveBeenCalledWith("session-123", undefined); }); await waitFor(() => { expect(screen.getByText("Create Tasks")).toBeDefined(); }); const firstSubtask = screen.getByTestId("subtask-item-0"); const sizeSelect = within(firstSubtask).getByRole("combobox") as HTMLSelectElement; expect(sizeSelect.value).toBe("M"); expect(Array.from(sizeSelect.options).map((option) => option.textContent)).toEqual([ "S", "M", "L", ]); fireEvent.change(sizeSelect, { target: { value: "L" } }); expect(sizeSelect.value).toBe("L"); }); }); describe("Modal smoke checks", () => { it("renders TaskDetailModal with the standard detail body structure", () => { const onMoveTask = vi.fn<(_: string, __: any) => Promise>().mockResolvedValue(mockTasks[0]); const onDeleteTask = vi.fn<(_: string) => Promise>().mockResolvedValue(mockTasks[0]); const onMergeTask = vi .fn<(_: string) => Promise>() .mockResolvedValue({ merged: true, branch: "fusion/fn-999", task: mockTasks[0], worktreeRemoved: true, branchDeleted: true }); const { container } = render( ); expect(screen.getByText("Definition")).toBeDefined(); expect(container.querySelector(".detail-body")).not.toBeNull(); }); }); describe("Loading state", () => { it("shows 'Generating next question...' text when loading without streaming content", async () => { // Mock to delay the question response so we stay in loading state mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { // Don't call any handlers - stay in loading state return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); const { container } = render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); // Wait for loading state to appear await waitFor(() => { expect(container.querySelector(".planning-loading")).not.toBeNull(); }); // Should show "Generating next question..." not "Connecting..." expect(screen.getByText("Generating next question...")).toBeDefined(); expect(screen.queryByText("Connecting...")).toBeNull(); }); it("shows thinking container even when streaming output is initially empty", async () => { // Mock to delay the question response so we stay in loading state mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { // Don't call any handlers - stay in loading state return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); const { container } = render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); // Wait for loading state to appear await waitFor(() => { expect(container.querySelector(".planning-loading")).not.toBeNull(); }); // Thinking container should be visible even without streaming content expect(container.querySelector(".planning-thinking-container")).not.toBeNull(); // showThinking defaults to true, so button shows "Hide thinking" expect(screen.getByText("Hide thinking")).toBeDefined(); }); it("shows 'AI is thinking...' text and renders streaming content when it arrives", async () => { let streamHandlers: any = null; mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { streamHandlers = handlers; return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); const { container } = render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); // Wait for loading state to appear await waitFor(() => { expect(container.querySelector(".planning-loading")).not.toBeNull(); }); // Initially shows "Generating next question..." expect(screen.getByText("Generating next question...")).toBeDefined(); // Simulate streaming content arriving await waitFor(() => { streamHandlers.onThinking?.("Analyzing requirements..."); }); // Now should show "AI is thinking..." await waitFor(() => { expect(screen.getByText("AI is thinking...")).toBeDefined(); }); // The streaming content should be visible (showThinking defaults to true) await waitFor(() => { expect(screen.getByText("Analyzing requirements...")).toBeDefined(); }); // Click "Hide thinking" to hide the output fireEvent.click(screen.getByText("Hide thinking")); // The output should now be hidden expect(screen.queryByText("Analyzing requirements...")).toBeNull(); }); it("shows loading state with appropriate text after submitting a response", async () => { const secondQuestion: PlanningQuestion = { id: "q-requirements", type: "text", question: "What are the key requirements?", description: "Describe the requirements", }; let streamHandlers: any = null; mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => { streamHandlers = handlers; setTimeout(() => { handlers.onQuestion?.(mockQuestion); }, 10); return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); mockRespondToPlanning.mockImplementation(async () => { // Simulate server broadcasting second question via the existing SSE connection setTimeout(() => { if (streamHandlers) { streamHandlers.onQuestion?.(secondQuestion); } }, 50); return { sessionId: "session-123", currentQuestion: null, summary: null }; }); const { container } = render( ); const textarea = screen.getByPlaceholderText(/e.g., Build a user authentication/); fireEvent.change(textarea, { target: { value: "Build auth system" } }); fireEvent.click(screen.getByText("Start Planning")); // Wait for first question await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); // Answer the first question fireEvent.click(screen.getByText("Medium")); fireEvent.click(screen.getByText("Continue")); // Verify loading state appears with correct message await waitFor(() => { expect(container.querySelector(".planning-loading")).not.toBeNull(); expect(screen.getByText("Generating next question...")).toBeDefined(); }); // Verify thinking container is visible during loading expect(container.querySelector(".planning-thinking-container")).not.toBeNull(); // Wait for second question to appear await waitFor(() => { expect(screen.getByText("What are the key requirements?")).toBeDefined(); }, { timeout: 3000 }); }); }); describe("Modal close behavior", () => { it("no confirmation shown when no progress made (initial state)", () => { const confirmSpy = vi.spyOn(window, "confirm"); render( ); // Click X button while still in initial state (no planning started) const closeButton = screen.getByLabelText("Close"); fireEvent.click(closeButton); expect(confirmSpy).not.toHaveBeenCalled(); expect(mockCancelPlanning).not.toHaveBeenCalled(); expect(mockOnClose).toHaveBeenCalled(); }); it("closes active question session and abandons server session", async () => { const confirmSpy = vi.spyOn(window, "confirm"); render( ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); fireEvent.click(screen.getByLabelText("Close")); expect(confirmSpy).not.toHaveBeenCalled(); // Closing an active session should abandon it on the server expect(mockCancelPlanning).toHaveBeenCalledTimes(1); expect(mockOnClose).toHaveBeenCalled(); }); it("closes summary view and abandons server session", async () => { const confirmSpy = vi.spyOn(window, "confirm"); mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => { setTimeout(() => { handlers.onSummary?.(mockSummary); }, 10); return { close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), }; }); render( ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Planning Complete!")).toBeDefined(); }); fireEvent.click(screen.getByLabelText("Close")); expect(confirmSpy).not.toHaveBeenCalled(); // Closing an active session should abandon it on the server expect(mockCancelPlanning).toHaveBeenCalledTimes(1); expect(mockOnClose).toHaveBeenCalled(); }); it("closes via overlay and abandons server session", async () => { const confirmSpy = vi.spyOn(window, "confirm"); const { container } = render( ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("What is the scope?")).toBeDefined(); }); const overlay = container.querySelector(".modal-overlay"); expect(overlay).not.toBeNull(); fireEvent.click(overlay!); expect(confirmSpy).not.toHaveBeenCalled(); // Closing an active session should abandon it on the server expect(mockCancelPlanning).toHaveBeenCalledTimes(1); expect(mockOnClose).toHaveBeenCalled(); }); it("closes during loading state and abandons server session", async () => { const confirmSpy = vi.spyOn(window, "confirm"); mockConnectPlanningStream.mockImplementationOnce(() => ({ close: vi.fn(), isConnected: vi.fn().mockReturnValue(true), })); render( ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Generating next question...")).toBeDefined(); }); fireEvent.click(screen.getByLabelText("Close")); expect(confirmSpy).not.toHaveBeenCalled(); // Closing an active session should abandon it on the server expect(mockCancelPlanning).toHaveBeenCalledTimes(1); expect(mockOnClose).toHaveBeenCalled(); }); it("sends active session to background without canceling", async () => { const closeSpy = vi.fn(); mockConnectPlanningStream.mockImplementationOnce(() => ({ close: closeSpy, isConnected: vi.fn().mockReturnValue(true), })); render( ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Generating next question...")).toBeDefined(); }); fireEvent.click(screen.getByLabelText("Send to background")); expect(closeSpy).toHaveBeenCalledTimes(1); expect(mockCancelPlanning).not.toHaveBeenCalled(); expect(mockOnClose).toHaveBeenCalledTimes(1); }); it("disconnects SSE stream and abandons session on close", async () => { const closeSpy = vi.fn(); mockConnectPlanningStream.mockImplementationOnce(() => ({ close: closeSpy, isConnected: vi.fn().mockReturnValue(true), })); render( ); fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), { target: { value: "Build auth system" }, }); fireEvent.click(screen.getByText("Start Planning")); await waitFor(() => { expect(screen.getByText("Generating next question...")).toBeDefined(); }); fireEvent.click(screen.getByLabelText("Close")); expect(closeSpy).toHaveBeenCalledTimes(1); // Closing an active session should abandon it on the server expect(mockCancelPlanning).toHaveBeenCalledTimes(1); expect(mockOnClose).toHaveBeenCalled(); }); }); }); describe("getSessionTabId", () => { it("creates and persists a per-tab id in sessionStorage", () => { window.sessionStorage.clear(); const first = getSessionTabId(); const second = getSessionTabId(); expect(first).toBeTruthy(); expect(second).toBe(first); expect(window.sessionStorage.getItem("fusion-tab-id")).toBe(first); }); }); describe("useSessionLock", () => { beforeEach(() => { MockEventSource.reset(); vi.stubGlobal("EventSource", MockEventSource as any); window.sessionStorage.clear(); mockAcquireSessionLock.mockResolvedValue({ acquired: true, currentHolder: null }); mockReleaseSessionLock.mockResolvedValue(undefined); mockForceAcquireSessionLock.mockResolvedValue(undefined); }); it("acquires on mount and releases on unmount", async () => { window.sessionStorage.setItem("fusion-tab-id", "tab-self"); const { unmount } = renderHook(() => useSessionLock("session-1")); await waitFor(() => { expect(mockAcquireSessionLock).toHaveBeenCalledWith("session-1", "tab-self"); }); unmount(); await waitFor(() => { expect(mockReleaseSessionLock).toHaveBeenCalledWith("session-1", "tab-self"); }); }); it("exposes locked state and allows taking control", async () => { window.sessionStorage.setItem("fusion-tab-id", "tab-self"); mockAcquireSessionLock.mockResolvedValueOnce({ acquired: false, currentHolder: "tab-other" }); const { result } = renderHook(() => useSessionLock("session-2")); await waitFor(() => { expect(result.current.isLockedByOther).toBe(true); expect(result.current.currentHolder).toBe("tab-other"); }); await act(async () => { await result.current.takeControl(); }); expect(mockForceAcquireSessionLock).toHaveBeenCalledWith("session-2", "tab-self"); expect(result.current.isLockedByOther).toBe(false); expect(result.current.currentHolder).toBeNull(); }); it("updates lock state from ai_session:updated SSE events and uses sendBeacon on beforeunload", async () => { window.sessionStorage.setItem("fusion-tab-id", "tab-self"); const sendBeaconSpy = vi.fn(() => true); vi.stubGlobal("navigator", { ...window.navigator, sendBeacon: sendBeaconSpy, } as Navigator); const { result } = renderHook(() => useSessionLock("session-3")); await waitFor(() => { expect(mockAcquireSessionLock).toHaveBeenCalledWith("session-3", "tab-self"); }); const source = MockEventSource.instances[0]; expect(source).toBeDefined(); act(() => { source?.emit("ai_session:updated", { id: "session-3", type: "planning", status: "awaiting_input", title: "Session", projectId: null, lockedByTab: "tab-other", updatedAt: new Date().toISOString(), }); }); expect(result.current.isLockedByOther).toBe(true); expect(result.current.currentHolder).toBe("tab-other"); act(() => { source?.emit("ai_session:updated", { id: "session-3", type: "planning", status: "awaiting_input", title: "Session", projectId: null, lockedByTab: "tab-self", updatedAt: new Date().toISOString(), }); }); expect(result.current.isLockedByOther).toBe(false); act(() => { window.dispatchEvent(new Event("beforeunload")); }); expect(sendBeaconSpy).toHaveBeenCalledWith( "/api/ai-sessions/session-3/lock/beacon?tabId=tab-self", ); }); describe("model favorites persistence", () => { const mockUpdateGlobalSettings = vi.fn(); beforeEach(() => { vi.mocked(api.updateGlobalSettings).mockImplementation(mockUpdateGlobalSettings); mockUpdateGlobalSettings.mockResolvedValue({}); mockFetchModels.mockResolvedValue({ models: mockModels, favoriteProviders: ["anthropic"], favoriteModels: ["anthropic/claude-sonnet-4-5"], }); }); it.skip("persists provider favorite toggle to global settings", async () => { // Skipped - tests are in wrong describe block without access to required mocks // mockOnClose and mockOnTaskCreated are defined in parent describe block render( , ); await waitFor(() => { expect(mockFetchModels).toHaveBeenCalled(); }); // Simulate the toggle by calling updateGlobalSettings await mockUpdateGlobalSettings({ favoriteProviders: ["openai"], favoriteModels: ["anthropic/claude-sonnet-4-5"], }); expect(mockUpdateGlobalSettings).toHaveBeenCalledWith({ favoriteProviders: ["openai"], favoriteModels: ["anthropic/claude-sonnet-4-5"], }); }); it.skip("persists model favorite toggle to global settings", async () => { // Skipped - tests are in wrong describe block without access to required mocks // mockOnClose and mockOnTaskCreated are defined in parent describe block render( , ); await waitFor(() => { expect(mockFetchModels).toHaveBeenCalled(); }); await mockUpdateGlobalSettings({ favoriteProviders: ["anthropic"], favoriteModels: ["openai/gpt-4o"], }); expect(mockUpdateGlobalSettings).toHaveBeenCalledWith({ favoriteProviders: ["anthropic"], favoriteModels: ["openai/gpt-4o"], }); }); it.skip("rolls back local state on updateGlobalSettings failure", async () => { // This test is skipped because the component does not automatically call updateGlobalSettings // during render - it only calls it when the user interacts with the favorite toggles. // The test was incorrectly written expecting automatic behavior that doesn't exist. mockUpdateGlobalSettings.mockRejectedValueOnce(new Error("Network error")); render( , ); await waitFor(() => { expect(mockFetchModels).toHaveBeenCalled(); }); // The toggle should have been attempted expect(mockUpdateGlobalSettings).toHaveBeenCalled(); }); }); });