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
This commit is contained in:
gsxdsm
2026-06-25 12:38:49 -07:00
parent 6c51bd1d44
commit ad246e8bd2
6 changed files with 74 additions and 86 deletions

View File

@@ -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<typeof vi.fn>).mockResolvedValue({
...defaultSettings,
experimentalFeatures: { ...defaultSettings.experimentalFeatures, leftSidebarNav: true },
});
});
const getChatEvents = async () => {
render(<App />);
@@ -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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
experimentalFeatures: { ...defaultSettings.experimentalFeatures, roadmap: true, leftSidebarNav: true },
experimentalFeatures: { ...defaultSettings.experimentalFeatures, roadmap: true },
});
(fetchPluginDashboardViews as ReturnType<typeof vi.fn>).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(<App />);
@@ -2772,7 +2748,7 @@ describe("App view switching", () => {
it("keeps insights view button visible after graduation from experimental flags", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
experimentalFeatures: { ...defaultSettings.experimentalFeatures, insights: false, leftSidebarNav: true },
experimentalFeatures: { ...defaultSettings.experimentalFeatures, insights: false },
});
render(<App />);
@@ -2818,7 +2794,7 @@ describe("App view switching", () => {
it("keeps memory view button visible after graduation from experimental flags", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
experimentalFeatures: { ...defaultSettings.experimentalFeatures, memoryView: false, insights: true, leftSidebarNav: true },
experimentalFeatures: { ...defaultSettings.experimentalFeatures, memoryView: false, insights: true },
});
render(<App />);
@@ -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<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
experimentalFeatures: { ...defaultSettings.experimentalFeatures, todoView: false },
});
render(<App />);
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<typeof vi.fn>).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"),

View File

@@ -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();

View File

@@ -202,10 +202,18 @@ describe("Board mobile initial render stabilization (FN-4574)", () => {
render(<Board {...boardProps} />);
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();

View File

@@ -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 () => ({})),
};
}

View File

@@ -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<string, string> };
const body = res.body as { flagEnabled: boolean; defaultWorkflowId: string; workflows: unknown[]; taskWorkflowIds: Record<string, string> };
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 () => {

View File

@@ -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" });