test(dashboard): fix 30 pre-existing pnpm-test failures on main (#1765)

## What & why

`pnpm test` on `main` failed **30 tests** across 5 dashboard files. All
ran in the **non-blocking full-suite lane** (not the merge gate), so
they went unnoticed despite being deterministic. Every one is a **stale
test trailing an intentional product change** — fixes are **test-only**,
with **no product code changed** and **no appeasement** (no widened
timeouts, no `.skip`, no weakened assertions).

## Root causes & fixes

| File | Failures | Root cause | Fix |
|---|---:|---|---|
| `App.test.tsx` | 26 | Sidebar-destination tests (view switching, chat
unread, GitHub import, board branch filters) never set `leftSidebarNav:
true`; the shared `defaultSettings` keeps it `false` to preserve legacy
header-nav tests, so the sidebar never rendered. The backend-unreachable
recovery test asserted a setup wizard that **intentionally no longer
auto-opens** on zero projects (`useViewState` FNXC:Onboarding
2026-06-22-05:06) with `modelOnboardingComplete: true`. | Opt each
sidebar describe/test into `leftSidebarNav: true`; assert recovery to
the dashboard shell instead of the retired auto-wizard. |
| `board-workflows-route.test.ts` | 1 | `workflowColumns` flag
**graduated to always-on** (`isWorkflowColumnsEnabled` returns `true`;
stale persisted `false` treated as enabled) — the flag-OFF empty-shape
branch is retired. | Assert the graduation invariant (persisted `false`
→ `flagEnabled: true`). |
| `promote-route.test.ts` | 1 | Same graduation: the flag-OFF → `400`
branch is dead, so the route proceeds and 500s on the incomplete mock. |
Assert persisted `false` proceeds to the engine (no legacy 400). |
| `register-command-center-routes.auth.test.ts` | 1 |
`/command-center/tokens` now reads `modelPricingOverrides` via
`getGlobalSettingsStore()`; `MockStore` lacked it → 500. | Add the
`getGlobalSettingsStore()` stub. |
| `PlanningModeModal.initial.test.tsx` | 1 | The shared `.spin` loader
keyframe was renamed `spin` → `fusion-spinner-spin` for
collision-proofing. | Update the CSS regex to the current keyframe. |

## Verification
- All 5 files together: **161 passed / 0 failed** (`App.test.tsx`
128/128).
- ESLint on all 5 files: exit 0.
- `git diff` touches **test files only** — confirmed no product/source
file changed, and no `timeout:`/`.skip`/removed-`expect` introduced.

## Notes
- No changeset (test-only).
- Diagnosis confirmed by instrumenting `App.tsx` (reverted): after
settings load, `leftSidebarNavEnabled` flipped to `false` because the
fixture set `leftSidebarNav: false`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- stage-review-badge-begin -->

---

<a href="https://stagereview.app/Runfusion/Fusion/pull/1765">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg">
<img src="https://stagereview.app/assets/gh-open-in-stage-light.svg"
alt="Open in Stage">
  </picture>
</a>

<!-- stage-review-badge-end -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Dashboard navigation now consistently follows the new default sidebar
layout.
* Restored dashboard recovery flow after a backend outage so the normal
shell returns cleanly when service resumes.
* Workflow-related actions and board views now behave as enabled even
when older saved settings say otherwise.


<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-06-25 13:17:35 -07:00
committed by GitHub
5 changed files with 83 additions and 20 deletions

View File

@@ -1017,7 +1017,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 {
@@ -1050,7 +1050,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();
}
@@ -1283,6 +1293,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<typeof vi.fn>).mockResolvedValue({
...defaultSettings,
experimentalFeatures: { ...defaultSettings.experimentalFeatures, leftSidebarNav: true },
});
});
const getChatEvents = async () => {
render(<App />);
@@ -2110,6 +2128,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<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({
@@ -2117,6 +2143,7 @@ describe("App view switching", () => {
experimentalFeatures: {
...defaultSettings.experimentalFeatures,
researchView: true,
leftSidebarNav: true,
},
});
@@ -2417,7 +2444,7 @@ describe("App view switching", () => {
mockUseViewportMode.mockReturnValue("desktop");
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
...defaultSettings,
experimentalFeatures: { ...defaultSettings.experimentalFeatures, roadmap: true },
experimentalFeatures: { ...defaultSettings.experimentalFeatures, roadmap: true, leftSidebarNav: true },
});
(fetchPluginDashboardViews as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
{
@@ -2584,7 +2611,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(<App />);
@@ -2745,7 +2772,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 },
experimentalFeatures: { ...defaultSettings.experimentalFeatures, insights: false, leftSidebarNav: true },
});
render(<App />);
@@ -2791,7 +2818,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 },
experimentalFeatures: { ...defaultSettings.experimentalFeatures, memoryView: false, insights: true, leftSidebarNav: true },
});
render(<App />);
@@ -2863,6 +2890,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<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
@@ -4207,7 +4242,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"),

View File

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

View File

@@ -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<Task[]> {
return [];
}

View File

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

View File

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