From ad246e8bd212872a3b1dcb59cff1d5f7c3efb223 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 25 Jun 2026 12:38:49 -0700 Subject: [PATCH] FN-7001: repair dashboard changed-test expectations Repair dashboard changed-test expectations for graduated navigation and workflow behavior. - Update App tests to use the left-sidebar default and keep graduated Todo/Goals destinations available when stale flags are false. - Align route tests with workflow-column graduation and live promote branches. - Adjust dashboard test fixtures for command-center pricing settings and current spinner/viewport listener contracts. Files changed: .../app/components/__tests__/App.test.tsx | 43 +++++++++++++++++++--- .../__tests__/PlanningModeModal.initial.test.tsx | 6 ++- .../__tests__/board-mobile-initial-render.test.tsx | 12 +++++- .../register-command-center-routes.auth.test.ts | 10 +++++ .../routes/__tests__/board-workflows-route.test.ts | 19 ++++++---- .../src/routes/__tests__/promote-route.test.ts | 13 ++----- 6 files changed, 77 insertions(+), 26 deletions(-) Fusion-Task-Id: FN-7001 Fusion-Task-Lineage: 23c0b18d-6895-4a94-a77a-17c7438f477a --- .../app/components/__tests__/App.test.tsx | 90 +++++++++---------- .../PlanningModeModal.initial.test.tsx | 7 +- .../board-mobile-initial-render.test.tsx | 12 ++- ...egister-command-center-routes.auth.test.ts | 10 +-- .../__tests__/board-workflows-route.test.ts | 21 +++-- .../routes/__tests__/promote-route.test.ts | 20 +---- 6 files changed, 74 insertions(+), 86 deletions(-) diff --git a/packages/dashboard/app/components/__tests__/App.test.tsx b/packages/dashboard/app/components/__tests__/App.test.tsx index ee3fe608f2..1a59edd5e3 100644 --- a/packages/dashboard/app/components/__tests__/App.test.tsx +++ b/packages/dashboard/app/components/__tests__/App.test.tsx @@ -26,11 +26,10 @@ const defaultSettings: Settings = { capacityRiskBannerEnabled: false, capacityRiskTodoThreshold: 20, /* - * FNXC:DashboardTests 2026-06-22-03:38: - * App.test.tsx keeps legacy Header view-toggle coverage unless a test explicitly opts into the left-sidebar default. - * The product now enables leftSidebarNav by default, so the test fixture must set the flag false instead of accidentally hiding Header controls. + * FNXC:DashboardTests 2026-06-25-10:52: + * App.test.tsx now mirrors the shipped left-sidebar navigation default because graduated destinations (Insights, Memory, Todo, Goals, Agents) must stay visible even when stale experimental flags are false. Individual legacy header-toggle tests opt out explicitly instead of making the whole fixture hide sidebar controls. */ - experimentalFeatures: { insights: true, skillsView: true, agentsView: true, memoryView: true, evalsView: true, leftSidebarNav: false }, + experimentalFeatures: { insights: true, skillsView: true, agentsView: true, memoryView: true, evalsView: true, leftSidebarNav: true }, }; const mockAgentStats = { @@ -1017,7 +1016,7 @@ describe("App backend-unreachable first-run flow", () => { expect(screen.queryByText("Welcome to Fusion")).toBeNull(); }); - it("retries project loading and recovers from the backend error page after connectivity recovers", async () => { + it("retries project loading and resumes the empty-project dashboard after connectivity recovers", async () => { vi.useFakeTimers(); try { @@ -1051,16 +1050,10 @@ describe("App backend-unreachable first-run flow", () => { }); /* - 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(); + * FNXC:BackendRecovery 2026-06-25-10:58: + * Recovering from an unreachable backend should resume the current no-project dashboard state, not force the setup wizard. The retry assertion pins the user-visible recovery surface after the navigation-default cleanup. + */ + expect(screen.getByText("No Projects Found")).toBeTruthy(); } finally { vi.useRealTimers(); } @@ -1293,14 +1286,6 @@ 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(); @@ -2128,14 +2113,6 @@ 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({ @@ -2143,7 +2120,6 @@ describe("App view switching", () => { experimentalFeatures: { ...defaultSettings.experimentalFeatures, researchView: true, - leftSidebarNav: true, }, }); @@ -2444,7 +2420,7 @@ describe("App view switching", () => { mockUseViewportMode.mockReturnValue("desktop"); (fetchSettings as ReturnType).mockResolvedValueOnce({ ...defaultSettings, - experimentalFeatures: { ...defaultSettings.experimentalFeatures, roadmap: true, leftSidebarNav: true }, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, roadmap: true }, }); (fetchPluginDashboardViews as ReturnType).mockResolvedValueOnce([ { @@ -2611,7 +2587,7 @@ describe("App view switching", () => { // Override the default mock to exclude agentsView vi.mocked(fetchSettings).mockResolvedValue({ ...defaultSettings, - experimentalFeatures: { ...defaultSettings.experimentalFeatures, insights: true, skillsView: true, leftSidebarNav: true }, // no agentsView + experimentalFeatures: { ...defaultSettings.experimentalFeatures, insights: true, skillsView: true }, // no agentsView }); render(); @@ -2772,7 +2748,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, leftSidebarNav: true }, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, insights: false }, }); render(); @@ -2818,7 +2794,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, leftSidebarNav: true }, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, memoryView: false, insights: true }, }); render(); @@ -2884,20 +2860,40 @@ describe("App view switching", () => { expect(screen.getByTestId("goals-view")).toBeTruthy(); }); + expect(screen.getByTestId("sidebar-nav-goals")).toBeTruthy(); expect(document.querySelector(".board")).toBeNull(); localStorage.removeItem(taskViewStorageKey()); }); + + it("keeps todo view selected after graduation from experimental flags", async () => { + localStorage.setItem("kb-dashboard-view-mode", "project"); + localStorage.setItem(taskViewStorageKey(), "todos"); + (fetchSettings as ReturnType).mockResolvedValueOnce({ + ...defaultSettings, + experimentalFeatures: { ...defaultSettings.experimentalFeatures, todoView: false }, + }); + + render(); + + await waitFor(() => { + expect(fetchSettings).toHaveBeenCalled(); + }); + + await waitFor(() => { + expect(screen.getByTestId("todo-view")).toBeTruthy(); + }); + + /* + * FNXC:DefaultNavigation 2026-06-25-11:03: + * Todo graduated with the other primary destinations; stale `todoView:false` settings must not redirect a saved Todo view back to Board. Todos intentionally stay out of the left sidebar because that destination lives in the right dock and mobile More sheet. + */ + expect(document.querySelector(".board")).toBeNull(); + localStorage.removeItem(taskViewStorageKey()); + localStorage.removeItem("kb-dashboard-view-mode"); + }); }); 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 @@ -4242,11 +4238,7 @@ 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"); - /* 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 }, - }); + vi.mocked(fetchSettings).mockResolvedValue({ ...defaultSettings }); 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 5500faf2ed..a1dc8dedc8 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.initial.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.initial.test.tsx @@ -671,9 +671,10 @@ describe("PlanningModeModal", () => { const { loadAllAppCss } = await import("../../test/cssFixture"); const css = loadAllAppCss(); - // 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`. + /* + FNXC:LoadingIndicators 2026-06-25-12:05: + Planning's first-paint spinner contract follows the shared collision-proof dashboard keyframe name. Do not require the obsolete global `spin` keyframe here; lazy CSS chunks can redefine generic keyframes. + */ expect(css).toMatch(/\.spin\s*\{[^}]*animation:\s*fusion-spinner-spin\s+1s\s+linear\s+infinite;/); expect(screen.getByRole("button", { name: "Hide thinking" })).toBeDefined(); diff --git a/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx b/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx index 68979ac333..f8046bd4b2 100644 --- a/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx +++ b/packages/dashboard/app/components/__tests__/board-mobile-initial-render.test.tsx @@ -202,10 +202,18 @@ describe("Board mobile initial render stabilization (FN-4574)", () => { render(); - expect(visualViewportResizeListeners).toHaveLength(1); + /* + * FNXC:MobileBoard 2026-06-25-11:24: + * Board owns more than one legitimate visualViewport resize subscriber (responsive mode plus mobile re-anchor). The Android seam is the missing `removeEventListener`, so assert every registered listener is safe instead of pinning an incidental listener count. + */ + expect(visualViewportResizeListeners.length).toBeGreaterThan(0); expect(() => { - visualViewportResizeListeners[0](); + act(() => { + for (const listener of visualViewportResizeListeners) { + listener(); + } + }); }).not.toThrow(); viewportSpy.mockRestore(); 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 015c30e781..a001c1e9ab 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,13 +53,13 @@ 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: {} }), + /* + * FNXC:CommandCenter 2026-06-25-11:10: + * The authenticated `/command-center/tokens` route reads global pricing overrides through the scoped store. Auth coverage must provide that collaborator so a valid bearer token verifies authorization instead of failing on missing test-store plumbing. + */ + getSettings: vi.fn(async () => ({})), }; } 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 5cd82e2138..c2c79ea10d 100644 --- a/packages/dashboard/src/routes/__tests__/board-workflows-route.test.ts +++ b/packages/dashboard/src/routes/__tests__/board-workflows-route.test.ts @@ -6,8 +6,7 @@ // 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: -// - 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 OFF → { flagEnabled: false } (the legacy single-lane shape) // - 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). @@ -85,20 +84,20 @@ describe("GET /tasks/board-workflows", () => { const get = (path: string) => REQUEST(app, "GET", path); - // 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 () => { + it("stale flag OFF still returns the graduated workflow payload", async () => { + /* + * FNXC:WorkflowColumns 2026-06-25-11:12: + * Workflow columns graduated from the experimental flag. Persisted `workflowColumns:false` settings are stale and must not reactivate the empty legacy board-workflows payload. + */ await store.updateGlobalSettings({ experimentalFeatures: { workflowColumns: false } }); - const card = await store.createTask({ description: "card" }); + const task = 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 }; + const body = res.body as { flagEnabled: boolean; defaultWorkflowId: string; workflows: unknown[]; taskWorkflowIds: Record }; expect(body.flagEnabled).toBe(true); - expect(body.taskWorkflowIds[card.id]).toBe(DEFAULT_LANE); + expect(body.defaultWorkflowId).toBeTruthy(); expect(body.workflows.length).toBeGreaterThan(0); + expect(body.taskWorkflowIds[task.id]).toBe(body.defaultWorkflowId); }); 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 d697482f8b..cc9b34f7d2 100644 --- a/packages/dashboard/src/routes/__tests__/promote-route.test.ts +++ b/packages/dashboard/src/routes/__tests__/promote-route.test.ts @@ -2,15 +2,16 @@ // // FN-1404: route-level integration coverage for POST /tasks/:id/promote. // -// The promote endpoint has four error branches plus a success path, none of +// The promote endpoint has three live error branches plus a success path, none of // which were exercised at the HTTP layer: -// - 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" // - TransitionRejectionError → 409 carrying the rejection's code/messageKey // +// FNXC:WorkflowColumns 2026-06-25-11:14: +// Workflow columns graduated, so the historical flag-OFF 400 guard is unreachable; route coverage should pin only the live promote branches rather than reintroducing retired feature-gate behavior. +// // promoteHeldTask is engine-internal cross-package logic; we mock it so the // route's branch-to-HTTP mapping is what's under test (the documented incident // class is route tests not matching real engine shapes — so we assert the @@ -61,19 +62,6 @@ describe("POST /tasks/:id/promote", () => { promoteHeldTask.mockReset(); }); - // 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(200); - expect(promoteHeldTask).toHaveBeenCalledTimes(1); - }); - it("success → 200 and returns the promoted task", async () => { const { app } = buildApp({ flagEnabled: true }); promoteHeldTask.mockResolvedValue({ released: true, toColumn: "in-progress" });