diff --git a/packages/dashboard/app/components/__tests__/App.test.tsx b/packages/dashboard/app/components/__tests__/App.test.tsx index 390d12bb95..923b040e61 100644 --- a/packages/dashboard/app/components/__tests__/App.test.tsx +++ b/packages/dashboard/app/components/__tests__/App.test.tsx @@ -1010,7 +1010,7 @@ describe("App backend-unreachable first-run flow", () => { expect(screen.queryByText("Welcome to Fusion")).toBeNull(); }); - it("retries project loading and resumes setup wizard flow after connectivity recovers", async () => { + it("retries project loading and recovers from the backend error page after connectivity recovers", async () => { vi.useFakeTimers(); try { @@ -1043,7 +1043,17 @@ describe("App backend-unreachable first-run flow", () => { await vi.advanceTimersByTimeAsync(1000); }); - expect(screen.getByText("Set Up AI")).toBeTruthy(); + /* + FNXC:Onboarding 2026-06-25-11:50: After connectivity recovers the backend-error + page is replaced by the normal dashboard shell. The setup wizard no longer + auto-resumes here: zero projects no longer force-open the project wizard + (useViewState FNXC:Onboarding 2026-06-22-05:06) and modelOnboardingComplete is + true in this fixture, so the AI-setup wizard ("Set Up AI") does not auto-open. + Assert recovery (error page gone, shell rendered) instead of the retired wizard. + */ + expect(screen.queryByText("Can't reach the Fusion backend")).toBeNull(); + expect(screen.queryByRole("button", { name: "Retry Connection" })).toBeNull(); + expect(screen.getByTestId("dashboard-project-shell")).toBeTruthy(); } finally { vi.useRealTimers(); } @@ -1276,6 +1286,14 @@ describe("App approval notification banner", () => { }); describe("App chat unread response indicator", () => { + /* FNXC:Navigation 2026-06-25-11:00: Sidebar-destination tests must opt into leftSidebarNav:true; the shared defaultSettings keeps it false so legacy header-nav tests stay valid. */ + beforeEach(() => { + (fetchSettings as ReturnType).mockResolvedValue({ + ...defaultSettings, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, leftSidebarNav: true }, + }); + }); + const getChatEvents = async () => { render(); @@ -2020,6 +2038,14 @@ describe("OnboardingResumeCard", () => { describe("App view switching", () => { // FNXC:Navigation 2026-06-22-09:30: Research/Evals/Insights/Memory are now left-sidebar // destinations (sidebar-nav-*), not header More-views overflow items, on desktop. + /* FNXC:Navigation 2026-06-25-11:00: Sidebar-destination tests must opt into leftSidebarNav:true; the shared defaultSettings keeps it false so legacy header-nav tests stay valid. Tests that set their own fetchSettings mock add leftSidebarNav:true to their spread. */ + beforeEach(() => { + (fetchSettings as ReturnType).mockResolvedValue({ + ...defaultSettings, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, leftSidebarNav: true }, + }); + }); + it("opens research view from the sidebar and persists view selection", async () => { localStorage.setItem("kb-dashboard-view-mode", "project"); (fetchSettings as ReturnType).mockResolvedValue({ @@ -2027,6 +2053,7 @@ describe("App view switching", () => { experimentalFeatures: { ...defaultSettings.experimentalFeatures, researchView: true, + leftSidebarNav: true, }, }); @@ -2327,7 +2354,7 @@ describe("App view switching", () => { mockUseViewportMode.mockReturnValue("desktop"); (fetchSettings as ReturnType).mockResolvedValueOnce({ ...defaultSettings, - experimentalFeatures: { ...defaultSettings.experimentalFeatures, roadmap: true }, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, roadmap: true, leftSidebarNav: true }, }); (fetchPluginDashboardViews as ReturnType).mockResolvedValueOnce([ { @@ -2494,7 +2521,7 @@ describe("App view switching", () => { // Override the default mock to exclude agentsView vi.mocked(fetchSettings).mockResolvedValue({ ...defaultSettings, - experimentalFeatures: { ...defaultSettings.experimentalFeatures, insights: true, skillsView: true }, // no agentsView + experimentalFeatures: { ...defaultSettings.experimentalFeatures, insights: true, skillsView: true, leftSidebarNav: true }, // no agentsView }); render(); @@ -2655,7 +2682,7 @@ describe("App view switching", () => { it("keeps insights view button visible after graduation from experimental flags", async () => { (fetchSettings as ReturnType).mockResolvedValueOnce({ ...defaultSettings, - experimentalFeatures: { ...defaultSettings.experimentalFeatures, insights: false }, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, insights: false, leftSidebarNav: true }, }); render(); @@ -2701,7 +2728,7 @@ describe("App view switching", () => { it("keeps memory view button visible after graduation from experimental flags", async () => { (fetchSettings as ReturnType).mockResolvedValueOnce({ ...defaultSettings, - experimentalFeatures: { ...defaultSettings.experimentalFeatures, memoryView: false, insights: true }, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, memoryView: false, insights: true, leftSidebarNav: true }, }); render(); @@ -2773,6 +2800,14 @@ describe("App view switching", () => { }); describe("App GitHub import", () => { + /* FNXC:Navigation 2026-06-25-11:00: Sidebar-destination tests must opt into leftSidebarNav:true; the shared defaultSettings keeps it false so legacy header-nav tests stay valid. */ + beforeEach(() => { + (fetchSettings as ReturnType).mockResolvedValue({ + ...defaultSettings, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, leftSidebarNav: true }, + }); + }); + // FNXC:Navigation 2026-06-22-09:30: GitHub import is now the left-sidebar "Import Tasks" // destination rendering the GitHubImportModal embedded in main content (presentation="embedded"), // not a header-button modal overlay. Navigation in/out goes through the sidebar; embedded mode @@ -4117,7 +4152,11 @@ describe("App board branch filters", () => { it("composes with search and does not affect list view tasks", async () => { localStorage.setItem("kb-dashboard-view-mode", "project"); localStorage.setItem(taskViewStorageKey(), "board"); - vi.mocked(fetchSettings).mockResolvedValue({ ...defaultSettings }); + /* FNXC:Navigation 2026-06-25-11:00: This test navigates via sidebar-nav-list, so it must load leftSidebarNav:true; the shared defaultSettings keeps it false for legacy header-nav tests. */ + vi.mocked(fetchSettings).mockResolvedValue({ + ...defaultSettings, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, leftSidebarNav: true }, + }); mockUseTasks.mockImplementation(() => ({ tasks: [ makeTask("FN-4", "Alpha Search", "feature/a", "main"), diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.initial.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.initial.test.tsx index 0675fd0274..5500faf2ed 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.initial.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.initial.test.tsx @@ -671,7 +671,10 @@ describe("PlanningModeModal", () => { const { loadAllAppCss } = await import("../../test/cssFixture"); const css = loadAllAppCss(); - expect(css).toMatch(/\.spin\s*\{[^}]*animation:\s*spin\s+1s\s+linear\s+infinite;/); + // FNXC:PlanningModeModal 2026-06-25-11:30: The shared `.spin` loader keyframe was + // renamed `spin` → `fusion-spinner-spin` for collision-proofing (see styles.css), + // so assert the current keyframe name instead of the retired generic `spin`. + expect(css).toMatch(/\.spin\s*\{[^}]*animation:\s*fusion-spinner-spin\s+1s\s+linear\s+infinite;/); expect(screen.getByRole("button", { name: "Hide thinking" })).toBeDefined(); expect(document.querySelector(".planning-thinking-output")?.textContent).toContain("Analyzing requirements..."); diff --git a/packages/dashboard/src/__tests__/register-command-center-routes.auth.test.ts b/packages/dashboard/src/__tests__/register-command-center-routes.auth.test.ts index 52d65475f1..015c30e781 100644 --- a/packages/dashboard/src/__tests__/register-command-center-routes.auth.test.ts +++ b/packages/dashboard/src/__tests__/register-command-center-routes.auth.test.ts @@ -53,6 +53,16 @@ class MockStore extends EventEmitter { }; } + // FNXC:CommandCenter 2026-06-25-11:35: The /command-center/tokens handler now reads + // settings.modelPricingOverrides via getGlobalSettingsStore().getSettings() for USD + // cost derivation. Stub it so the auth check reaches a 200 instead of a 500 from a + // missing store accessor. + getGlobalSettingsStore() { + return { + getSettings: async () => ({ modelPricingOverrides: {} }), + }; + } + async listTasks(): Promise { return []; } diff --git a/packages/dashboard/src/routes/__tests__/board-workflows-route.test.ts b/packages/dashboard/src/routes/__tests__/board-workflows-route.test.ts index 69c6ace262..5cd82e2138 100644 --- a/packages/dashboard/src/routes/__tests__/board-workflows-route.test.ts +++ b/packages/dashboard/src/routes/__tests__/board-workflows-route.test.ts @@ -6,7 +6,8 @@ // route registration, the flag-gated early-return shape, and the deduped // flag-ON payload were untested. This exercises the route end-to-end against a // REAL TaskStore via createApiRoutes: -// - flag OFF → { flagEnabled: false } (the legacy single-lane shape) +// - workflowColumns graduated → a stale persisted `false` is treated as enabled, +// so the route returns the full payload (the legacy empty single-lane shape is retired) // - flag ON, mixed default + custom selections → correct taskWorkflowIds and a // DEDUPED workflows array (two cards on the same default lane collapse to one // workflow entry). @@ -84,17 +85,20 @@ describe("GET /tasks/board-workflows", () => { const get = (path: string) => REQUEST(app, "GET", path); - it("flag OFF → { flagEnabled: false } legacy shape", async () => { - // Even with tasks on the board, an explicitly-disabled flag returns the empty - // single-lane shape regardless of the project's default feature policy. + // FNXC:WorkflowColumns 2026-06-25-11:40: workflowColumns graduated from the + // experimental flag (isWorkflowColumnsEnabled always returns true; stale persisted + // `false` must resolve as enabled). The retired flag-OFF empty single-lane shape no + // longer exists; assert the graduation invariant — a persisted `false` still yields + // the full enabled payload with the card mapped to the default lane. + it("persisted workflowColumns:false is treated as enabled (graduated)", async () => { await store.updateGlobalSettings({ experimentalFeatures: { workflowColumns: false } }); - await store.createTask({ description: "card" }); + const card = await store.createTask({ description: "card" }); const res = await get("/api/tasks/board-workflows"); expect(res.status).toBe(200); const body = res.body as { flagEnabled: boolean; workflows: unknown[]; taskWorkflowIds: Record }; - expect(body.flagEnabled).toBe(false); - expect(body.workflows).toEqual([]); - expect(body.taskWorkflowIds).toEqual({}); + expect(body.flagEnabled).toBe(true); + expect(body.taskWorkflowIds[card.id]).toBe(DEFAULT_LANE); + expect(body.workflows.length).toBeGreaterThan(0); }); it("flag ON, mixed default + custom → correct taskWorkflowIds and deduped workflows", async () => { diff --git a/packages/dashboard/src/routes/__tests__/promote-route.test.ts b/packages/dashboard/src/routes/__tests__/promote-route.test.ts index f2cfd71066..d697482f8b 100644 --- a/packages/dashboard/src/routes/__tests__/promote-route.test.ts +++ b/packages/dashboard/src/routes/__tests__/promote-route.test.ts @@ -4,7 +4,8 @@ // // The promote endpoint has four error branches plus a success path, none of // which were exercised at the HTTP layer: -// - flag OFF → 400 (workflow columns not enabled) +// - workflowColumns graduated → a stale persisted `false` is treated as +// enabled, so promote proceeds (no legacy 400) // - promoteHeldTask success → 200 (returns the promoted task) // - capacity-exhausted-or-no-slot → 409 code:"capacity-exhausted" // - other engine rejection → 409 code:"guard-rejected" @@ -60,11 +61,17 @@ describe("POST /tasks/:id/promote", () => { promoteHeldTask.mockReset(); }); - it("flag OFF → 400 and never calls the engine", async () => { + // FNXC:WorkflowColumns 2026-06-25-11:40: workflowColumns graduated from the + // experimental flag (isWorkflowColumnsEnabled always returns true and stale + // persisted `false` must be treated as enabled). The retired flag-OFF → 400 + // branch no longer exists; assert the graduation invariant instead — a persisted + // `false` still lets promote proceed and reach the engine. + it("persisted workflowColumns:false is treated as enabled (graduated) → proceeds to engine", async () => { const { app } = buildApp({ flagEnabled: false }); + promoteHeldTask.mockResolvedValue({ released: true, toColumn: "in-progress" }); const res = await promote(app); - expect(res.status).toBe(400); - expect(promoteHeldTask).not.toHaveBeenCalled(); + expect(res.status).toBe(200); + expect(promoteHeldTask).toHaveBeenCalledTimes(1); }); it("success → 200 and returns the promoted task", async () => {