From 5c5ffb6f481e86d6f1155688c952fc4ca51fc07b Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 15 Aug 2026 22:15:06 -0700 Subject: [PATCH] test(dashboard): update component expectations to intentional post-FN-87xx behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dashboard bare-run repair, component-drift cluster. All nine suites were stale expectations behind cited intentional commits — no UI regressions found: FN-8826's always-on WIP lifecycle badge (TaskCard.oversight now asserts no overseer element instead of an empty header), FN-8762's Todo Lists plugin extraction (MobileNavBar, MainContent.graph-popout, useAppSettings repurposed to the pluginDashboardViews contract, with a regression pin that the removed todosEnabled field never returns), FN-8796's clock-arbitrated mergeTaskSnapshot, FN-8797's prompt-only planning refresh, FN-8702's 767.98px phone-sheet boundary, FN-8764's primary-role split, and FN-8947's added touch-target selector. Every update carries an FNXC comment citing the causing commit. Verified 9 files / 293 tests green, tsconfig.app.json typecheck clean. Co-Authored-By: Claude Fable 5 --- .../mission-planning-modals-mobile.test.ts | 9 +++- ...AgentDetailView.advanced-settings.test.tsx | 11 ++++- .../__tests__/MobileNavBar.test.tsx | 33 +++++++++---- .../__tests__/ScheduledTasksModal.test.tsx | 9 +++- .../__tests__/TaskCard.oversight.test.tsx | 46 +++++++++++++------ .../TaskDetailModal.gitlab-tracking.test.tsx | 8 +++- ...skDetailModal.popup-hidden-gating.test.tsx | 21 ++++++--- .../MainContent.graph-popout.test.tsx | 19 +++++++- .../hooks/__tests__/useAppSettings.test.ts | 12 ++++- 9 files changed, 127 insertions(+), 41 deletions(-) diff --git a/packages/dashboard/app/__tests__/mission-planning-modals-mobile.test.ts b/packages/dashboard/app/__tests__/mission-planning-modals-mobile.test.ts index 7632bea78b..3c688a47ba 100644 --- a/packages/dashboard/app/__tests__/mission-planning-modals-mobile.test.ts +++ b/packages/dashboard/app/__tests__/mission-planning-modals-mobile.test.ts @@ -59,7 +59,14 @@ function findMobileBlockContaining(needle: string): string { describe("mission + planning modal mobile CSS", () => { it("MissionManager: mission icon button touch targets are 36px", () => { const missionBlock = findMobileBlockContaining(".mission-manager-overlay"); - expect(missionBlock).toMatch(/\.mission-list__item-actions \.mission-icon-btn,[\s\S]*?\.mission-feature__actions \.mission-icon-btn\s*\{[\s\S]*?min-width:\s*36px;[\s\S]*?min-height:\s*36px;/s); + /* + FNXC:Missions 2026-08-15-22:40: + FN-8947 (b82f1a41f3) appended `.mission-fix-feature__actions .mission-icon-btn` to this + touch-target selector list, so `.mission-feature__actions .mission-icon-btn` is no longer + the final selector before `{`. Match the selectors up to the fix-feature entry that now + closes the list, keeping the same 36px touch-target invariant. + */ + expect(missionBlock).toMatch(/\.mission-list__item-actions \.mission-icon-btn,[\s\S]*?\.mission-feature__actions \.mission-icon-btn,[\s\S]*?\.mission-fix-feature__actions \.mission-icon-btn\s*\{[\s\S]*?min-width:\s*36px;[\s\S]*?min-height:\s*36px;/s); }); it("MissionManager: body prevents horizontal overflow", () => { diff --git a/packages/dashboard/app/components/__tests__/AgentDetailView.advanced-settings.test.tsx b/packages/dashboard/app/components/__tests__/AgentDetailView.advanced-settings.test.tsx index 2a909e07ff..8e85dd51a2 100644 --- a/packages/dashboard/app/components/__tests__/AgentDetailView.advanced-settings.test.tsx +++ b/packages/dashboard/app/components/__tests__/AgentDetailView.advanced-settings.test.tsx @@ -84,7 +84,12 @@ describe("Advanced Settings", () => { expect((screen.getByLabelText("Name") as HTMLInputElement).value).toBe("Interviewed Agent"); expect((screen.getByLabelText("Title") as HTMLInputElement).value).toBe("Draft Title"); expect((screen.getByLabelText("Icon") as HTMLInputElement).value).toBe("🧠"); - expect((screen.getByLabelText("Role") as HTMLSelectElement).value).toBe("reviewer"); + /* + FNXC:Agents 2026-08-15-22:45: + FN-8764 (eaadd153b1) split agent roles into a primary role plus additional workflow + roles, renaming this select's label from "Role" to "Primary role". + */ + expect((screen.getByLabelText("Primary role") as HTMLSelectElement).value).toBe("reviewer"); }); expect(mockUpdateAgent).not.toHaveBeenCalled(); @@ -94,7 +99,9 @@ describe("Advanced Settings", () => { "agent-001", expect.objectContaining({ name: "Interviewed Agent", - role: "reviewer", + // FNXC:Agents 2026-08-15-22:45: FN-8764 (eaadd153b1) saves the role list as `roles` + // (primary first) instead of a scalar `role`. + roles: ["reviewer"], title: "Draft Title", reportsTo: "agent-002", runtimeConfig: expect.objectContaining({ model: "openai/gpt-4o" }), diff --git a/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx b/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx index d9a29762b8..aec6c358c6 100644 --- a/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx +++ b/packages/dashboard/app/components/__tests__/MobileNavBar.test.tsx @@ -288,19 +288,32 @@ describe("MobileNavBar", () => { expect(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-spacing-check-wide")).toBeDefined(); }); - it("keeps Todos in the mobile More sheet and routes to the todos view", () => { + /* + FNXC:Navigation 2026-08-15-22:15: + FN-8762 (5b2b31d2c9) extracted Todo Lists into the bundled `fusion-plugin-todos` + plugin: the host `todos` More-sheet item and `experimentalFeatures.todoView` gate + are gone, and Todos now surfaces via `pluginDashboardViews` with overflow placement. + These tests keep the same coverage (More-sheet only, routing, More-tab active) in + the plugin form. + */ + const todosPluginView = { + pluginId: "fusion-plugin-todos", + view: { viewId: "todos", label: "Todos", componentPath: "./dashboard-view", icon: "CheckSquare", placement: "overflow" as const, order: 70 }, + }; + + it("keeps Todos in the mobile More sheet and routes to the todos plugin view", () => { const props = createDefaultProps(); render( , ); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); - fireEvent.click(screen.getByTestId("mobile-more-item-todos")); + fireEvent.click(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-todos-todos")); - expect(props.onChangeView).toHaveBeenCalledWith("todos"); + expect(props.onChangeView).toHaveBeenCalledWith("plugin:fusion-plugin-todos:todos"); }); it("Mailbox is a primary tab and is not duplicated in the More sheet", () => { @@ -316,22 +329,22 @@ describe("MobileNavBar", () => { render( , ); - expect(screen.queryByTestId("mobile-nav-tab-todos")).toBeNull(); + expect(screen.queryByTestId("mobile-nav-tab-plugin-fusion-plugin-todos-todos")).toBeNull(); fireEvent.click(screen.getByTestId("mobile-nav-tab-more")); - expect(screen.getByTestId("mobile-more-item-todos")).toBeInTheDocument(); + expect(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-todos-todos")).toBeInTheDocument(); }); - it("marks the mobile More tab active for the todos view", () => { + it("marks the mobile More tab active for the todos plugin view", () => { render( , ); diff --git a/packages/dashboard/app/components/__tests__/ScheduledTasksModal.test.tsx b/packages/dashboard/app/components/__tests__/ScheduledTasksModal.test.tsx index 098ab5316c..2a66ef5628 100644 --- a/packages/dashboard/app/components/__tests__/ScheduledTasksModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/ScheduledTasksModal.test.tsx @@ -209,8 +209,15 @@ describe("ScheduledTasksModal", () => { it("keeps mobile Automations full-screen and hides resize handles by CSS contract", () => { const source = readFileSync(resolve(__dirname, "../ScriptsModal.css"), "utf8"); + /* + FNXC:GitManager 2026-08-15-22:35: + FN-8702 (1e67e87321) moved ScriptsModal.css's standalone phone-sheet blocks from + `max-width: 768px` to `max-width: 767.98px` so the takeover applies only BELOW the + 768px boundary (768px-wide tablets keep desktop geometry). The Automations + full-screen contract rides in that same block, so match the phone boundary. + */ const mobileBlock = source - .match(/@media \(max-width: 768px\)\s*\{[\s\S]*?\n\}/g) + .match(/@media \(max-width: 767\.98px\)\s*\{[\s\S]*?\n\}/g) ?.find((block) => block.includes(".floating-window--automation")) ?? ""; expect(mobileBlock).toContain(".floating-window--automation"); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.oversight.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.oversight.test.tsx index 156fffebae..3d96cc2e6e 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.oversight.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.oversight.test.tsx @@ -102,6 +102,22 @@ function makeTask(overrides: Partial = {}): Task { const noop = () => {}; +/* +FNXC:PlannerOversight 2026-08-15-22:10: +FN-8826 (1f5c44ac71) intentionally restored an always-on WIP lifecycle badge for +empty-status cards in WIP lanes, so `card-header-badges` now legitimately mounts +on the stale-snapshot fixtures below. The oversight invariant these tests guard is +narrower: the header wrapper must never contain an overseer eye element while the +effective oversight level is off/unresolved/inherited-default. Assert that instead +of wrapper absence. +*/ +function expectHeaderBadgesFreeOfOverseerEye() { + const header = screen.queryByTestId("card-header-badges"); + if (header) { + expect(header.querySelector(".card-planner-overseer-state")).toBeNull(); + } +} + function renderCard( overrides: Partial = {}, cardProps: { @@ -272,14 +288,14 @@ describe("TaskCard workflow-effective oversight level (FN-7516 code-review fix)" // unresolved on first render. The stale runtime snapshot must not leak an // Eye badge or header wrapper before the configured-off response arrives. expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); await waitFor(() => { expect(fetchWorkflowSettingValues).toHaveBeenCalledWith(`wf-stale-snapshot-off-${column}`, undefined); }); await waitFor(() => { expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); }); }); @@ -425,13 +441,13 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => { renderCard(staleSnapshot(column), { planningWorkflowId: " selected-workflow-off ", projectId: "project-8251" }); expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); await waitFor(() => { expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("selected-workflow-off", "project-8251"); }); await waitFor(() => { expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); }); }); @@ -447,13 +463,13 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => { // The selected workflow's declaration default is not a meaningful oversight // configuration, so a stale runtime snapshot cannot create an eye or shell. expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); await waitFor(() => { expect(fetchWorkflowSettingValues).toHaveBeenCalledWith(`selected-inherited-default-${column}`, "project-8255"); }); await waitFor(() => { expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); }); }); @@ -475,12 +491,12 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => { }); expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); await waitFor(() => { expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("aggregate-inherited-default", undefined); }); expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); expect(container.querySelector(".card-planner-overseer-state[title][aria-label]")).toBeNull(); }); @@ -495,7 +511,7 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => { await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalled()); await waitFor(() => { expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); expect(screen.queryByLabelText(/overseer/i)).toBeNull(); }); }); @@ -503,7 +519,7 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => { it("fails closed without workflow identity but keeps a valid active task override authoritative", () => { const { rerender } = renderCard(staleSnapshot("in-progress")); expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); rerender( { await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalledTimes(2)); await waitFor(() => { expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); expect(container.querySelector(".card-planner-overseer-state[title][aria-label]")).toBeNull(); }); }); @@ -596,12 +612,12 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => { act(() => notifyWorkflowSettingValuesUpdated(workflowId, "project-cache-fix")); expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); expect(container.querySelector(".card-planner-overseer-state[title][aria-label]")).toBeNull(); await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalledTimes(2)); await waitFor(() => { expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); }); }); @@ -626,7 +642,7 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => { resolveActive?.({ stored: { plannerOversightLevel: "steer" }, effective: { plannerOversightLevel: "steer" }, orphaned: [] }); }); expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); }); it("hides the eye synchronously when an active selected workflow changes to an unresolved off workflow", async () => { @@ -661,7 +677,7 @@ describe("TaskCard selected-workflow oversight identity (FN-8251)", () => { expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); await waitFor(() => expect(fetchWorkflowSettingValues).toHaveBeenCalledWith("mobile-inherited-default", undefined)); expect(screen.queryByTestId("planner-overseer-state-badge")).toBeNull(); - expect(screen.queryByTestId("card-header-badges")).toBeNull(); + expectHeaderBadgesFreeOfOverseerEye(); expect(container.querySelector(".card-planner-overseer-state[title][aria-label]")).toBeNull(); }); }); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.gitlab-tracking.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.gitlab-tracking.test.tsx index 6d44d10aae..ec7eaa19c7 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.gitlab-tracking.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.gitlab-tracking.test.tsx @@ -84,7 +84,13 @@ describe("TaskDetailModal GitLab tracking", () => { rerender( { it("keeps Definition prompt refresh request-free while hidden and resumes one planning chain on reveal", async () => { vi.useFakeTimers(); - const mockDetail = vi.mocked(dashboardApi.fetchTaskDetail); - mockDetail.mockReset(); - mockDetail.mockResolvedValue(makeTask({ id: "FN-9001", column: "triage", status: "planning", prompt: "# Fresh plan" })); + /* + FNXC:TaskDetailPlan 2026-08-15-22:30: + FN-8797 (35810666be) made the Definition tab's 5s planning refresh prompt-only via + `fetchTaskPrompt` — the old `fetchTaskDetail` chain rolled queued cards back to Todo, + so the periodic read no longer touches the detail endpoint. The hidden/reveal gating + invariant is unchanged; only the polled request boundary moved. + */ + const mockPrompt = vi.mocked(dashboardApi.fetchTaskPrompt); + mockPrompt.mockReset(); + mockPrompt.mockResolvedValue({ id: "FN-9001", prompt: "# Fresh plan" }); const { rerenderWithActive } = renderContent(false, makeTask({ id: "FN-9001", column: "triage", status: "planning" })); await act(async () => { await vi.advanceTimersByTimeAsync(16_000); }); - expect(mockDetail).not.toHaveBeenCalled(); + expect(mockPrompt).not.toHaveBeenCalled(); rerenderWithActive(true); await act(async () => { await vi.advanceTimersByTimeAsync(0); }); - expect(mockDetail).toHaveBeenCalledTimes(1); + expect(mockPrompt).toHaveBeenCalledTimes(1); await act(async () => { await vi.advanceTimersByTimeAsync(5_000); }); - expect(mockDetail).toHaveBeenCalledTimes(2); + expect(mockPrompt).toHaveBeenCalledTimes(2); rerenderWithActive(false); await act(async () => { await vi.advanceTimersByTimeAsync(16_000); }); - expect(mockDetail).toHaveBeenCalledTimes(2); + expect(mockPrompt).toHaveBeenCalledTimes(2); }); it("suspends the 5s verification polling while hidden and resumes it on reveal", async () => { diff --git a/packages/dashboard/app/components/dashboard/__tests__/MainContent.graph-popout.test.tsx b/packages/dashboard/app/components/dashboard/__tests__/MainContent.graph-popout.test.tsx index 8e2806b8dd..421c1b3e76 100644 --- a/packages/dashboard/app/components/dashboard/__tests__/MainContent.graph-popout.test.tsx +++ b/packages/dashboard/app/components/dashboard/__tests__/MainContent.graph-popout.test.tsx @@ -104,6 +104,23 @@ function mainContentProps(overrides: Partial = {}): MainConten handleResumeProject: vi.fn(), handleRemoveProject: vi.fn(), nodes: [], + /* + FNXC:TodoPluginEnablement 2026-08-15-22:20: + FN-8762 (5b2b31d2c9) gated plugin task views on the project-scoped + `pluginDashboardViews` roster (MainContent.isEnabledPluginTaskView), so this + harness must enroll the plugin views it renders or MainContent treats them as + disabled. The same commit removed the host `todosEnabled`/`TodoView` props. + */ + pluginDashboardViews: [ + { + pluginId: "fusion-plugin-dependency-graph", + view: { viewId: "graph", label: "Graph", componentPath: "./dashboard-view", icon: "Workflow", placement: "primary", order: 1 }, + }, + { + pluginId: "example", + view: { viewId: "dashboard", label: "Example", componentPath: "./dashboard-view", icon: "Workflow", placement: "overflow", order: 2 }, + }, + ] as MainContentProps["pluginDashboardViews"], graphPluginTaskView: "plugin:fusion-plugin-dependency-graph:graph", graphWorkflowSelection: null, setGraphWorkflowSelection: vi.fn(), @@ -145,7 +162,6 @@ function mainContentProps(overrides: Partial = {}): MainConten memoryEnabled: true, goalsEnabled: true, handleOpenMission: vi.fn(), - todosEnabled: true, openPlanningWithInitialPlanWithNav: vi.fn(), ingestCreatedTasks: vi.fn(), nodesEnabled: true, @@ -211,7 +227,6 @@ function mainContentProps(overrides: Partial = {}): MainConten ResearchView: LazyStub as MainContentProps["ResearchView"], SecretsView: LazyStub as MainContentProps["SecretsView"], SkillsView: LazyStub as MainContentProps["SkillsView"], - TodoView: LazyStub as MainContentProps["TodoView"], _AutomationsView: LazyStub as MainContentProps["_AutomationsView"], _ImportTasksView: LazyStub as MainContentProps["_ImportTasksView"], _SettingsView: LazyStub as MainContentProps["_SettingsView"], diff --git a/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts b/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts index bfacc365c7..e7bc8f9a04 100644 --- a/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts +++ b/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts @@ -523,7 +523,14 @@ describe("useAppSettings", () => { }); }); - it("derives todosEnabled from experimentalFeatures.todoView", async () => { + /* + FNXC:Navigation 2026-08-15-22:15: + FN-8762 (5b2b31d2c9) extracted Todo Lists into the bundled `fusion-plugin-todos` + plugin and removed the host `todosEnabled` derivation from useAppSettings; the + `experimentalFeatures.todoView` gate no longer exists. Regression-pin that the + removed field never reappears on the hook result. + */ + it("no longer derives a host todosEnabled flag (Todos is the fusion-plugin-todos plugin, FN-8762)", async () => { mockFetchSettings.mockResolvedValueOnce({ autoMerge: false, globalPause: false, @@ -539,8 +546,9 @@ describe("useAppSettings", () => { const { result } = renderHook(() => useAppSettings("proj_123")); await waitFor(() => { - expect(result.current.todosEnabled).toBe(true); + expect(result.current.taskStuckTimeoutMs).toBe(600000); }); + expect("todosEnabled" in result.current).toBe(false); }); it("derives goalsEnabled from experimentalFeatures.goalsView", async () => {