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:
@@ -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"),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 () => ({})),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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" });
|
||||
|
||||
Reference in New Issue
Block a user