From c7960d8a59bfbcbd16b28a9574df811a6f556a71 Mon Sep 17 00:00:00 2001 From: Fusion Agent Date: Wed, 8 Jul 2026 16:47:34 -0400 Subject: [PATCH] fix(dashboard): wrap ActiveAgentsPanel/AgentsView tests in ToastProvider RuntimeFallbackBadge.tsx calls useToast() unconditionally, but the ActiveAgentsPanel.test.tsx and AgentsView.test.tsx render helpers never wrapped the component under test in a ToastProvider, causing every mount that transitively renders RuntimeFallbackBadge to throw 'useToast must be used within ToastProvider'. Adds renderPanel()/renderView() helpers (matching the existing RuntimeFallbackBadge.test.tsx pattern) that wrap render(...) calls in ToastProvider, and routes every render call site in both files through them. No test assertions or fixtures were changed. Fixes 12/15 ActiveAgentsPanel.test.tsx failures and ~96-108/132 AgentsView.test.tsx failures. --- .../__tests__/ActiveAgentsPanel.test.tsx | 41 +-- .../components/__tests__/AgentsView.test.tsx | 249 +++++++++--------- 2 files changed, 154 insertions(+), 136 deletions(-) diff --git a/packages/dashboard/app/components/__tests__/ActiveAgentsPanel.test.tsx b/packages/dashboard/app/components/__tests__/ActiveAgentsPanel.test.tsx index 641b1aaafb..0dfe172e5b 100644 --- a/packages/dashboard/app/components/__tests__/ActiveAgentsPanel.test.tsx +++ b/packages/dashboard/app/components/__tests__/ActiveAgentsPanel.test.tsx @@ -1,7 +1,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; +import type { ReactElement } from "react"; import i18next from "i18next"; import { ActiveAgentsPanel } from "../ActiveAgentsPanel"; +import { ToastProvider } from "../../hooks/useToast"; import type { Agent } from "../../api"; import { useLiveTranscript } from "../../hooks/useLiveTranscript"; import esApp from "../../../../i18n/locales/es/app.json"; @@ -20,6 +22,13 @@ vi.mock("../../hooks/useLiveTranscript", () => ({ const mockUseLiveTranscript = vi.mocked(useLiveTranscript); +// RuntimeFallbackBadge (rendered inside LiveAgentCard) calls useToast() unconditionally, +// so every mount must be wrapped in a real ToastProvider (see RuntimeFallbackBadge.test.tsx +// for the reference pattern this replicates). +function renderPanel(ui: ReactElement) { + return render({ui}); +} + const nonEnglishAppCatalogs = [ ["es", esApp], ["fr", frApp], @@ -65,7 +74,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - render(); + renderPanel(); expect(screen.getByText("Processing request...")).toBeInTheDocument(); expect(screen.getByText("Analyzing code...")).toBeInTheDocument(); @@ -81,7 +90,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - render(); + renderPanel(); // Verify the hook was called with the projectId expect(mockUseLiveTranscript).toHaveBeenCalledWith("FN-001", "my-project"); @@ -97,7 +106,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - render(); + renderPanel(); // Verify the hook was called without projectId expect(mockUseLiveTranscript).toHaveBeenCalledWith("FN-001", undefined); @@ -119,7 +128,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - const futureRender = render(); + const futureRender = renderPanel(); const futureBadge = futureRender.container.querySelector(".live-agent-card-next-heartbeat"); expect(futureBadge, `${locale} next-heartbeat badge`).toBeInTheDocument(); expect(futureBadge?.textContent?.trim(), `${locale} next-heartbeat text`).not.toBe(""); @@ -135,7 +144,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), } as Agent; - const overdueRender = render(); + const overdueRender = renderPanel(); const overdueBadge = overdueRender.container.querySelector(".live-agent-card-next-heartbeat"); expect(overdueBadge, `${locale} heartbeat-overdue badge`).toBeInTheDocument(); expect(overdueBadge?.textContent?.trim(), `${locale} heartbeat-overdue text`).not.toBe(""); @@ -161,7 +170,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - render(); + renderPanel(); expect(screen.getByText("Connecting...")).toBeInTheDocument(); }); @@ -181,7 +190,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - render(); + renderPanel(); expect(screen.getByText("Waiting for output...")).toBeInTheDocument(); }); @@ -201,7 +210,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - render(); + renderPanel(); expect(screen.getByText("Idle — no task assigned")).toBeInTheDocument(); expect(screen.queryByText("Connecting...")).toBeNull(); @@ -222,7 +231,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - render(); + renderPanel(); expect(screen.getByText("Starting...")).toBeInTheDocument(); expect(screen.queryByText("Connecting...")).toBeNull(); @@ -257,7 +266,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - render(); + renderPanel(); expect(screen.getByText("Agent 1 output")).toBeInTheDocument(); expect(screen.getByText("Agent 2 output")).toBeInTheDocument(); @@ -287,7 +296,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - render(); + renderPanel(); // Should show the first 20 entries (most recent first) // With reversed entries, slice(0, 20) gives us Line 24 through Line 5 @@ -296,7 +305,7 @@ describe("ActiveAgentsPanel", () => { }); it("returns null when agents array is empty", async () => { - const { container } = render(); + const { container } = renderPanel(); expect(container.firstChild).toBeNull(); }); @@ -343,7 +352,7 @@ describe("ActiveAgentsPanel", () => { } as Agent, ]; - const { container } = render(); + const { container } = renderPanel(); expect(screen.getByText("Triage Agent")).toBeInTheDocument(); expect(screen.getByText((_, el) => el?.textContent === "FN-TRIAGE · Planning")).toBeInTheDocument(); @@ -368,7 +377,7 @@ describe("ActiveAgentsPanel", () => { } as Agent; const handleSelect = vi.fn(); - render(); + renderPanel(); fireEvent.click(screen.getByRole("button", { name: /select agent test agent/i })); @@ -390,7 +399,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - const { container } = render(); + const { container } = renderPanel(); // The streaming dot should be present when connected const streamingDot = container.querySelector(".live-agent-streaming-dot"); @@ -421,7 +430,7 @@ describe("ActiveAgentsPanel", () => { lastHeartbeatAt: new Date().toISOString(), } as Agent; - render(); + renderPanel(); // Both agents should receive the same projectId expect(mockUseLiveTranscript).toHaveBeenCalledWith("FN-001", "shared-project"); diff --git a/packages/dashboard/app/components/__tests__/AgentsView.test.tsx b/packages/dashboard/app/components/__tests__/AgentsView.test.tsx index 135bc969a7..6b847e66e1 100644 --- a/packages/dashboard/app/components/__tests__/AgentsView.test.tsx +++ b/packages/dashboard/app/components/__tests__/AgentsView.test.tsx @@ -1,8 +1,10 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, waitFor, within } from "@testing-library/react"; +import type { ReactElement } from "react"; import i18next from "i18next"; import { loadAllAppCss } from "../../test/cssFixture"; import { AgentsView } from "../AgentsView"; +import { ToastProvider } from "../../hooks/useToast"; import * as apiModule from "../../api"; import type { Agent, AgentState, AgentCapability, OrgTreeNode } from "../../api"; import { scopedKey } from "../../utils/projectStorage"; @@ -102,6 +104,13 @@ const mockClipboardWriteText = vi.fn(); const mockResizeObserverObserve = vi.fn(); const mockResizeObserverDisconnect = vi.fn(); +// RuntimeFallbackBadge (rendered on both board and list agent cards) calls useToast() +// unconditionally, so every AgentsView mount must be wrapped in a real ToastProvider +// (see RuntimeFallbackBadge.test.tsx for the reference pattern this replicates). +function renderView(ui: ReactElement) { + return render({ui}); +} + describe("AgentsView", () => { const mockAddToast = vi.fn(); const projectId = "proj_123"; @@ -214,14 +223,14 @@ describe("AgentsView", () => { describe("rendering", () => { it("renders the agents view header", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("Agents")).toBeTruthy(); }); }); it("renders agent list on mount", async () => { - render(); + renderView(); await waitFor(() => { // Active agents may appear in both ActiveAgentsPanel and main list expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThanOrEqual(1); @@ -235,7 +244,7 @@ describe("AgentsView", () => { ]); mockFetchAgentStats.mockResolvedValueOnce({ total: 1, byState: {}, byRole: {} }); - render(); + renderView(); await waitFor(() => { expect(screen.getByTitle("Pending approvals")).toBeInTheDocument(); @@ -256,7 +265,7 @@ describe("AgentsView", () => { ]); mockFetchAgentStats.mockResolvedValueOnce({ total: 1, byState: {}, byRole: {} }); - render(); + renderView(); await waitFor(() => { expect(screen.getByText("review")).toBeInTheDocument(); @@ -295,7 +304,7 @@ describe("AgentsView", () => { mockFetchAgents.mockResolvedValueOnce(modelAgents); mockFetchAgentStats.mockResolvedValueOnce({ total: 4, byState: {}, byRole: {} }); - const { container } = render(); + const { container } = renderView(); await waitFor(() => { expect(screen.getByText("Provider Model Agent")).toBeInTheDocument(); @@ -316,7 +325,7 @@ describe("AgentsView", () => { }); it("renders cross-pane overview above split layout", async () => { - const { container } = render(); + const { container } = renderView(); await waitFor(() => { expect(container.querySelector(".agents-overview-bar")).toBeTruthy(); @@ -341,7 +350,7 @@ describe("AgentsView", () => { }); it("opens inline detail pane and marks selected card", async () => { - const { container } = render(); + const { container } = renderView(); const detailButton = await screen.findByRole("button", { name: "View details for Test Agent 1" }); fireEvent.click(detailButton); @@ -354,7 +363,7 @@ describe("AgentsView", () => { }); it("keeps desktop selection in detail pane without rendering sidebar quick-controls strip", async () => { - const { container } = render(); + const { container } = renderView(); const detailButton = await screen.findByRole("button", { name: "View details for Test Agent 2" }); fireEvent.click(detailButton); @@ -369,7 +378,7 @@ describe("AgentsView", () => { it.each(["desktop", "tablet"] as const)("renders an accessible resize handle on %s split layouts", async (mode) => { mockViewportMode.mockReturnValue(mode); - const { container } = render(); + const { container } = renderView(); const handle = await screen.findByTestId("agents-sidebar-resize-handle"); expect(handle).toHaveAttribute("role", "separator"); @@ -382,7 +391,7 @@ describe("AgentsView", () => { it("does not render the resize handle or inline split width on mobile", async () => { mockViewportMode.mockReturnValue("mobile"); - const { container } = render(); + const { container } = renderView(); await waitFor(() => { expect(screen.getByText("Agents")).toBeTruthy(); @@ -403,7 +412,7 @@ describe("AgentsView", () => { localStorage.setItem(scopedKey(agentsSidebarWidthKey, projectId), stored); } - const { container } = render(); + const { container } = renderView(); const handle = await screen.findByTestId("agents-sidebar-resize-handle"); expect(handle).toHaveAttribute("aria-valuenow", String(expected)); @@ -412,7 +421,7 @@ describe("AgentsView", () => { it("supports keyboard resizing with project-scoped persistence and clamping", async () => { localStorage.setItem(scopedKey(agentsSidebarWidthKey, projectId), "515"); - render(); + renderView(); const handle = await screen.findByTestId("agents-sidebar-resize-handle"); @@ -433,7 +442,7 @@ describe("AgentsView", () => { it("clamps keyboard resizing at the minimum width", async () => { localStorage.setItem(scopedKey(agentsSidebarWidthKey, projectId), "260"); - render(); + renderView(); const handle = await screen.findByTestId("agents-sidebar-resize-handle"); fireEvent.keyDown(handle, { key: "ArrowLeft", shiftKey: true }); @@ -444,7 +453,7 @@ describe("AgentsView", () => { it("supports pointer drag resizing with capture, cleanup, persistence, and max clamping", async () => { localStorage.setItem(scopedKey(agentsSidebarWidthKey, projectId), "500"); - const { container } = render(); + const { container } = renderView(); const handle = await screen.findByTestId("agents-sidebar-resize-handle"); const setPointerCapture = vi.fn(); const releasePointerCapture = vi.fn(); @@ -469,7 +478,7 @@ describe("AgentsView", () => { it("supports pointer drag resizing with min clamping", async () => { localStorage.setItem(scopedKey(agentsSidebarWidthKey, projectId), "300"); - render(); + renderView(); const handle = await screen.findByTestId("agents-sidebar-resize-handle"); fireEvent.pointerDown(handle, { pointerId: 2, clientX: 300 }); @@ -484,7 +493,7 @@ describe("AgentsView", () => { it("supports mobile drill-in detail with back navigation", async () => { mockViewportMode.mockReturnValue("mobile"); - const { container } = render(); + const { container } = renderView(); expect(container.querySelector(".agents-split-layout")).toBeTruthy(); expect(container.querySelector(".agents-view-content")).toBeTruthy(); @@ -526,7 +535,7 @@ describe("AgentsView", () => { }, ]); - render(); + renderView(); fireEvent.click(await screen.findByRole("button", { name: "View details for Test Agent 1" })); await waitFor(() => { @@ -544,7 +553,7 @@ describe("AgentsView", () => { it("collapses mobile overview after selecting an active agent card", async () => { mockViewportMode.mockReturnValue("mobile"); - render(); + renderView(); const overviewToggle = await openOverviewPanel(); fireEvent.click(await screen.findByRole("button", { name: /select agent test agent 2/i })); @@ -557,7 +566,7 @@ describe("AgentsView", () => { it("keeps desktop overview open after selecting an active agent card", async () => { mockViewportMode.mockReturnValue("desktop"); - render(); + renderView(); const overviewToggle = await openOverviewPanel(); fireEvent.click(await screen.findByRole("button", { name: /select agent test agent 2/i })); @@ -577,7 +586,7 @@ describe("AgentsView", () => { }), ); - render(); + renderView(); const loadingStatus = await screen.findByRole("status"); expect(loadingStatus).toHaveTextContent("Loading agents..."); @@ -598,7 +607,7 @@ describe("AgentsView", () => { }), ); - render(); + renderView(); expect(await screen.findByText("Loading agents...")).toBeTruthy(); @@ -611,7 +620,7 @@ describe("AgentsView", () => { }); it("keeps existing agents visible during refresh loads", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThan(0); @@ -639,7 +648,7 @@ describe("AgentsView", () => { }); it("keeps New Agent directly accessible on desktop while controls live in popup", async () => { - render(); + renderView(); expect(screen.getByRole("button", { name: "New Agent" })).toBeTruthy(); expect(screen.queryByRole("dialog", { name: "Agent controls" })).toBeNull(); @@ -654,7 +663,7 @@ describe("AgentsView", () => { it("moves import and new-agent actions into the controls popup on mobile", async () => { mockViewportMode.mockReturnValue("mobile"); - render(); + renderView(); expect(screen.queryByRole("button", { name: "Import" })).toBeNull(); expect(screen.queryByRole("button", { name: "New Agent" })).toBeNull(); @@ -665,7 +674,7 @@ describe("AgentsView", () => { }); it("closes controls popup on Escape and outside click", async () => { - render(); + renderView(); const trigger = await openControlsPanel(); fireEvent.keyDown(document, { key: "Escape" }); @@ -686,7 +695,7 @@ describe("AgentsView", () => { }); it("keeps metrics and active agents collapsed behind overview disclosure by default", async () => { - const { container } = render(); + const { container } = renderView(); await waitFor(() => { expect(container.querySelector(".agent-list")).toBeTruthy(); @@ -707,7 +716,7 @@ describe("AgentsView", () => { }); it("fetches agents only once on mount (regression: no duplicate initial load path)", async () => { - render(); + renderView(); await waitFor(() => { expect(mockFetchAgents).toHaveBeenCalledTimes(1); @@ -720,7 +729,7 @@ describe("AgentsView", () => { }); it("renders token stats derived from the currently displayed agents", async () => { - render(); + renderView(); // Token-usage panel now lives inside the controls popup, not in the // main view body — open the controls panel before asserting. @@ -743,7 +752,7 @@ describe("AgentsView", () => { }); it("passes projectId to agent fetches", async () => { - render(); + renderView(); await waitFor(() => { expect(mockFetchAgents).toHaveBeenCalledWith({ includeEphemeral: false }, projectId); }); @@ -751,7 +760,7 @@ describe("AgentsView", () => { it("renders empty state when no agents", async () => { mockFetchAgents.mockResolvedValue([]); - render(); + renderView(); await waitFor(() => { expect(screen.getByText("No agents found")).toBeTruthy(); expect(screen.getByText("Create an agent to get started")).toBeTruthy(); @@ -761,7 +770,7 @@ describe("AgentsView", () => { it("opens the create dialog from the empty state CTA", async () => { mockFetchAgents.mockResolvedValue([]); - render(); + renderView(); const cta = await screen.findByRole("button", { name: "Create Agent" }); fireEvent.click(cta); @@ -772,7 +781,7 @@ describe("AgentsView", () => { }); it("displays agent states", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getAllByText("idle").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("active").length).toBeGreaterThanOrEqual(1); @@ -797,7 +806,7 @@ describe("AgentsView", () => { mockFetchAgentStats.mockResolvedValue({ total: 1, byState: { [state]: 1 }, byRole: { executor: 1 } }); mockFetchOrgTree.mockResolvedValue([{ agent: highlightAgent, children: [] }]); - render(); + renderView(); await waitFor(() => { expect(document.querySelector(`.agent-card--${state}`)).toBeTruthy(); @@ -815,7 +824,7 @@ describe("AgentsView", () => { }); it("keeps paused agents out of active highlight classes", async () => { - render(); + renderView(); const pausedAgentCard = await screen.findByText("Test Agent 3"); const pausedCard = pausedAgentCard.closest(".agent-card"); @@ -835,7 +844,7 @@ describe("AgentsView", () => { ]); mockFetchAgentStats.mockResolvedValue({ total: 4, byState: { active: 1, running: 1 }, byRole: { executor: 2 } }); - const { container } = render(); + const { container } = renderView(); await waitFor(() => { expect(screen.getAllByText((_, el) => el?.textContent === "FN-TRIAGE · Planning").length).toBeGreaterThanOrEqual(1); @@ -846,14 +855,14 @@ describe("AgentsView", () => { }); it("displays unresolved context when task column is missing", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getAllByText((_, el) => el?.textContent === "FN-001 · Unresolved task").length).toBeGreaterThanOrEqual(1); }); }); it("renders explicit View Details button on list cards", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByRole("button", { name: "View details for Test Agent 1" })).toBeTruthy(); @@ -864,7 +873,7 @@ describe("AgentsView", () => { }); it("keeps a visible icon affordance on split-sidebar action buttons when labels are compacted", async () => { - const { container } = render(); + const { container } = renderView(); const sidebarCard = await waitFor(() => { const card = container.querySelector(".agents-split-sidebar .agent-card"); @@ -897,7 +906,7 @@ describe("AgentsView", () => { }); it("opens matching detail view when clicking View Details button", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByRole("button", { name: "View details for Test Agent 3" })).toBeTruthy(); @@ -918,7 +927,7 @@ describe("AgentsView", () => { ...mockAgents.slice(1), ]); - render(); + renderView(); await waitFor(() => { expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThan(0); @@ -934,7 +943,7 @@ describe("AgentsView", () => { }); it("opens detail view when clicking anywhere on the agent card body", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThan(0); @@ -975,7 +984,7 @@ describe("AgentsView", () => { mockFetchAgents.mockResolvedValue([runningAgent]); mockFetchAgentStats.mockResolvedValue({ total: 1, byState: { running: 1 }, byRole: { executor: 1 } }); - render(); + renderView(); const runningButton = await screen.findByRole("button", { name: "View live run details for Runner" }); fireEvent.click(runningButton); @@ -990,7 +999,7 @@ describe("AgentsView", () => { }); it("shows heartbeat interval control on agent cards with 5m minimum presets", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByLabelText("Set heartbeat interval for Test Agent 2")).toBeTruthy(); @@ -1033,7 +1042,7 @@ describe("AgentsView", () => { ]); mockFetchAgentStats.mockResolvedValueOnce({ total: 1, byState: { active: 1 }, byRole: { triage: 1 } }); - const { container } = render(); + const { container } = renderView(); const lastAt = new Date(lastHeartbeatAt); const nextAt = new Date(lastAt.getTime() + 300000); @@ -1064,7 +1073,7 @@ describe("AgentsView", () => { }, ]); - render(); + renderView(); await waitFor(() => { expect(screen.getByLabelText("Set heartbeat interval for Test Agent 2")).toBeTruthy(); @@ -1076,7 +1085,7 @@ describe("AgentsView", () => { }); it("updates agent heartbeat interval from preset dropdown", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByLabelText("Set heartbeat interval for Test Agent 2")).toBeTruthy(); @@ -1098,7 +1107,7 @@ describe("AgentsView", () => { }); it("shows Custom... option in dropdown that reveals typed input", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByLabelText("Set heartbeat interval for Test Agent 2")).toBeTruthy(); @@ -1118,7 +1127,7 @@ describe("AgentsView", () => { }); it("can enter custom minutes value and save it", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByLabelText("Set heartbeat interval for Test Agent 2")).toBeTruthy(); @@ -1152,7 +1161,7 @@ describe("AgentsView", () => { }); it("clamps custom value 1-4 minutes to 5 minutes with info toast", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByLabelText("Set heartbeat interval for Test Agent 2")).toBeTruthy(); @@ -1191,7 +1200,7 @@ describe("AgentsView", () => { }); it("does not save when custom input is empty", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByLabelText("Set heartbeat interval for Test Agent 2")).toBeTruthy(); @@ -1227,7 +1236,7 @@ describe("AgentsView", () => { }); it("does not save when custom input is non-numeric", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByLabelText("Set heartbeat interval for Test Agent 2")).toBeTruthy(); @@ -1263,7 +1272,7 @@ describe("AgentsView", () => { }); it("does not save when custom input is zero or negative", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByLabelText("Set heartbeat interval for Test Agent 2")).toBeTruthy(); @@ -1309,7 +1318,7 @@ describe("AgentsView", () => { mockFetchAgents.mockResolvedValueOnce([errorAgent]); mockFetchAgentStats.mockResolvedValueOnce({ total: 1, byState: { error: 1 }, byRole: { executor: 1 } }); - render(); + renderView(); await waitFor(() => { expect(screen.getByRole("button", { name: "Open error details" })).toBeTruthy(); @@ -1345,7 +1354,7 @@ describe("AgentsView", () => { ]); mockFetchAgentStats.mockResolvedValueOnce({ total: 2, byState: { error: 1, active: 1 }, byRole: { executor: 2 } }); - render(); + renderView(); await waitFor(() => { expect(screen.getByText("Error No Text")).toBeTruthy(); @@ -1357,7 +1366,7 @@ describe("AgentsView", () => { }); it("shows refresh button", async () => { - render(); + renderView(); // Use findBy to ensure React has flushed all pending state updates before asserting. // This prevents act(...) warnings from any async effects triggered during render. const refreshBtn = await screen.findByTitle("Refresh"); @@ -1367,7 +1376,7 @@ describe("AgentsView", () => { describe("view toggle (list/board)", () => { it("can toggle between list and board view", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThanOrEqual(1); @@ -1392,7 +1401,7 @@ describe("AgentsView", () => { }); it("board view shows compact cards", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("Agents")).toBeTruthy(); @@ -1407,7 +1416,7 @@ describe("AgentsView", () => { }); it("persists view toggle preference to project-scoped localStorage", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("Agents")).toBeTruthy(); @@ -1421,7 +1430,7 @@ describe("AgentsView", () => { }); it("defaults to list view when no localStorage preference exists", async () => { - render(); + renderView(); await waitFor(() => { const listContainer = document.querySelector(".agent-list"); @@ -1430,7 +1439,7 @@ describe("AgentsView", () => { }); it("marks board view button as active when in board mode", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("Agents")).toBeTruthy(); @@ -1519,7 +1528,7 @@ describe("AgentsView", () => { it("renders org chart toggle with aria attributes and activates org view", async () => { mockFetchOrgTree.mockResolvedValue(orgTree); - render(); + renderView(); const orgButton = screen.getByRole("button", { name: "Org Chart view" }); expect(orgButton.getAttribute("aria-pressed")).toBe("false"); @@ -1538,7 +1547,7 @@ describe("AgentsView", () => { it("does not render the split resize handle in org chart view", async () => { mockFetchOrgTree.mockResolvedValue(orgTree); - const { container } = render(); + const { container } = renderView(); expect(await screen.findByTestId("agents-sidebar-resize-handle")).toBeTruthy(); @@ -1554,7 +1563,7 @@ describe("AgentsView", () => { it("renders org chart nodes and opens detail view when clicking a node", async () => { mockFetchOrgTree.mockResolvedValue(orgTree); - const { container } = render(); + const { container } = renderView(); fireEvent.click(screen.getByRole("button", { name: "Org Chart view" })); @@ -1589,7 +1598,7 @@ describe("AgentsView", () => { it("keeps org chart node metadata compact without skill badges", async () => { mockFetchOrgTree.mockResolvedValue(orgTree); - render(); + renderView(); fireEvent.click(screen.getByRole("button", { name: "Org Chart view" })); @@ -1635,7 +1644,7 @@ describe("AgentsView", () => { it("switches org chart to vertical layout mode when estimated width exceeds viewport", async () => { const clientWidthSpy = vi.spyOn(window.HTMLElement.prototype, "clientWidth", "get").mockReturnValue(320); mockFetchOrgTree.mockResolvedValue(orgTree); - render(); + renderView(); fireEvent.click(screen.getByRole("button", { name: "Org Chart view" })); @@ -1655,7 +1664,7 @@ describe("AgentsView", () => { it("keeps org chart horizontal layout mode when viewport is wide enough", async () => { const clientWidthSpy = vi.spyOn(window.HTMLElement.prototype, "clientWidth", "get").mockReturnValue(1920); mockFetchOrgTree.mockResolvedValue(orgTree); - render(); + renderView(); fireEvent.click(screen.getByRole("button", { name: "Org Chart view" })); @@ -1672,7 +1681,7 @@ describe("AgentsView", () => { localStorage.setItem(scopedKey("fn-agent-view", projectId), "org"); localStorage.setItem(scopedKey(ORG_CHART_LAYOUT_STORAGE_KEY, projectId), "vertical"); mockFetchOrgTree.mockResolvedValue(orgTree); - render(); + renderView(); const chart = await screen.findByTestId("agent-org-chart"); const toggle = screen.getByTestId("agent-org-chart-layout-toggle"); @@ -1716,7 +1725,7 @@ describe("AgentsView", () => { it("shows org chart empty state when API returns no nodes", async () => { mockFetchOrgTree.mockResolvedValue([]); - render(); + renderView(); fireEvent.click(screen.getByRole("button", { name: "Org Chart view" })); @@ -1736,7 +1745,7 @@ describe("AgentsView", () => { }), ); - render(); + renderView(); fireEvent.click(screen.getByRole("button", { name: "Org Chart view" })); await waitFor(() => { @@ -1753,7 +1762,7 @@ describe("AgentsView", () => { describe("filter agents by state", () => { it("renders the state filter with styled container", async () => { - render(); + renderView(); await openControlsPanel(); // Styled filter container exists @@ -1766,7 +1775,7 @@ describe("AgentsView", () => { }); it("can filter agents by state", async () => { - render(); + renderView(); await openControlsPanel(); const filterSelect = screen.getByLabelText("Filter agents by state"); @@ -1778,7 +1787,7 @@ describe("AgentsView", () => { }); it("clears filter when selecting 'all'", async () => { - render(); + renderView(); await openControlsPanel(); const filterSelect = screen.getByLabelText("Filter agents by state"); @@ -1798,7 +1807,7 @@ describe("AgentsView", () => { describe("show system agents toggle", () => { it("renders the system agents checkbox", async () => { - render(); + renderView(); await openControlsPanel(); // Checkbox should be unchecked by default @@ -1807,7 +1816,7 @@ describe("AgentsView", () => { }); it("passes includeEphemeral: false by default to fetchAgents", async () => { - render(); + renderView(); // Default call should include includeEphemeral: false await waitFor(() => { @@ -1816,7 +1825,7 @@ describe("AgentsView", () => { }); it("toggles system agents visibility when checkbox is clicked", async () => { - render(); + renderView(); await openControlsPanel(); const checkbox = screen.getByLabelText("Show system agents"); @@ -1828,7 +1837,7 @@ describe("AgentsView", () => { }); it("combines system agents toggle with state filter", async () => { - render(); + renderView(); await openControlsPanel(); // First enable system agents toggle @@ -1865,7 +1874,7 @@ describe("AgentsView", () => { // client-side filtering still hides it unless the toggle is enabled. mockFetchAgents.mockResolvedValue([...mockAgents.slice(0, 3), ...systemAgents]); - render(); + renderView(); await waitFor(() => { expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThan(0); @@ -1886,7 +1895,7 @@ describe("AgentsView", () => { describe("create new agent", () => { it("can create new agent via multi-step dialog", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("New Agent")).toBeTruthy(); @@ -1921,7 +1930,7 @@ describe("AgentsView", () => { }); it("shows create dialog when clicking New Agent button", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("New Agent")).toBeTruthy(); @@ -1938,7 +1947,7 @@ describe("AgentsView", () => { }); it("keeps legacy dialog launch when agent onboarding flag is disabled", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("New Agent")).toBeTruthy(); @@ -1953,7 +1962,7 @@ describe("AgentsView", () => { }); it("keeps New Agent launch on the standard dialog when agent onboarding flag is enabled", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("New Agent")).toBeTruthy(); @@ -1968,7 +1977,7 @@ describe("AgentsView", () => { }); it("launches interview from AgentsView and only applies draft after review confirmation", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("New Agent")).toBeTruthy(); @@ -2003,7 +2012,7 @@ describe("AgentsView", () => { }); it("does not allow proceeding with empty name", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("New Agent")).toBeTruthy(); @@ -2025,7 +2034,7 @@ describe("AgentsView", () => { it("handles creation error gracefully", async () => { mockCreateAgent.mockRejectedValue(new Error("Creation failed")); - render(); + renderView(); await waitFor(() => { expect(screen.getByText("New Agent")).toBeTruthy(); @@ -2053,7 +2062,7 @@ describe("AgentsView", () => { describe("change agent state", () => { it("can change agent state - activate idle agent", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByTitle("Activate")).toBeTruthy(); @@ -2073,7 +2082,7 @@ describe("AgentsView", () => { }); it("can pause active agent", async () => { - render(); + renderView(); await waitFor(() => { const agentCards = document.querySelectorAll(".agent-card"); @@ -2096,7 +2105,7 @@ describe("AgentsView", () => { }); it("can resume paused agent without manual run trigger", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByTitle("Resume")).toBeTruthy(); @@ -2118,7 +2127,7 @@ describe("AgentsView", () => { }); mockUpdateAgentState.mockImplementationOnce(() => transitionPromise); - render(); + renderView(); await waitFor(() => { expect(screen.getByTitle("Activate")).toBeTruthy(); @@ -2146,7 +2155,7 @@ describe("AgentsView", () => { }); mockUpdateAgentState.mockImplementationOnce(() => transitionPromise); - render(); + renderView(); await waitFor(() => { expect(screen.getByTitle("Activate")).toBeTruthy(); @@ -2181,7 +2190,7 @@ describe("AgentsView", () => { }); mockUpdateAgentState.mockImplementationOnce(() => transitionPromise); - render(); + renderView(); await waitFor(() => { expect(screen.getByTitle("Activate")).toBeTruthy(); @@ -2215,7 +2224,7 @@ describe("AgentsView", () => { it("handles state change error gracefully", async () => { mockUpdateAgentState.mockRejectedValue(new Error("State change failed")); - render(); + renderView(); await waitFor(() => { expect(screen.getByTitle("Activate")).toBeTruthy(); @@ -2232,7 +2241,7 @@ describe("AgentsView", () => { }); it("does not start run when pausing agent", async () => { - render(); + renderView(); await waitFor(() => { const agentCards = document.querySelectorAll(".agent-card"); @@ -2267,7 +2276,7 @@ describe("AgentsView", () => { mockAgents[3], ]); - render(); + renderView(); const runNowButton = await screen.findByTitle("Run Now"); const pauseButton = await screen.findByTitle("Pause"); @@ -2291,7 +2300,7 @@ describe("AgentsView", () => { mockAgents[3], ]); - render(); + renderView(); await waitFor(() => { expect(screen.getByTitle("Run Now")).toBeTruthy(); @@ -2308,7 +2317,7 @@ describe("AgentsView", () => { mockAgents[3], ]); - render(); + renderView(); await waitFor(() => { expect(screen.getByTitle("Run Now")).toBeTruthy(); @@ -2331,7 +2340,7 @@ describe("AgentsView", () => { describe("delete agent", () => { it("shows Delete button for idle, paused, and error agents in default view", async () => { - render(); + renderView(); await waitFor(() => { const deleteButtons = screen.getAllByTitle("Delete"); @@ -2340,7 +2349,7 @@ describe("AgentsView", () => { }); it("does not show Delete button for active agents", async () => { - render(); + renderView(); await waitFor(() => { const allCards = Array.from(document.querySelectorAll(".agent-card")); @@ -2351,7 +2360,7 @@ describe("AgentsView", () => { }); it("shows Delete button for paused agents", async () => { - render(); + renderView(); await waitFor(() => { const allCards = Array.from(document.querySelectorAll(".agent-card")); @@ -2362,7 +2371,7 @@ describe("AgentsView", () => { }); it("shows Delete button for error agents in board view", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("Agents")).toBeTruthy(); @@ -2380,7 +2389,7 @@ describe("AgentsView", () => { it("deletes idle agent after confirmation (from default view)", async () => { - render(); + renderView(); await waitFor(() => { const deleteButtons = screen.getAllByTitle("Delete"); @@ -2405,7 +2414,7 @@ describe("AgentsView", () => { }); it("deletes paused agent after confirmation (from default view)", async () => { - render(); + renderView(); await waitFor(() => { const pausedCard = Array.from(document.querySelectorAll(".agent-card")).find( @@ -2422,7 +2431,7 @@ describe("AgentsView", () => { }); it("deletes error agent after confirmation (from board view)", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByText("Agents")).toBeTruthy(); @@ -2447,7 +2456,7 @@ describe("AgentsView", () => { describe("refresh functionality", () => { it("refreshes agent list when clicking refresh button", async () => { - render(); + renderView(); await waitFor(() => { expect(screen.getByTitle("Refresh")).toBeTruthy(); @@ -2465,7 +2474,7 @@ describe("AgentsView", () => { describe("active agents panel selection", () => { it("renders active agents panel when agents are active", async () => { // agent-002 is active with taskId FN-001 - render(); + renderView(); await openOverviewPanel(); await waitFor(() => { @@ -2478,7 +2487,7 @@ describe("AgentsView", () => { }); it("opens AgentDetailView when clicking an active agent card", async () => { - render(); + renderView(); await openOverviewPanel(); await waitFor(() => { @@ -2498,7 +2507,7 @@ describe("AgentsView", () => { }); it("opens AgentDetailView when pressing Enter on an active agent card", async () => { - render(); + renderView(); await openOverviewPanel(); await waitFor(() => { @@ -2519,7 +2528,7 @@ describe("AgentsView", () => { }); it("opens AgentDetailView when pressing Space on an active agent card", async () => { - render(); + renderView(); await openOverviewPanel(); await waitFor(() => { @@ -2540,7 +2549,7 @@ describe("AgentsView", () => { }); it("live agent cards have proper accessibility attributes", async () => { - render(); + renderView(); await openOverviewPanel(); await waitFor(() => { @@ -2571,7 +2580,7 @@ describe("AgentsView", () => { ]; mockFetchAgents.mockResolvedValue(inactiveAgents); - render(); + renderView(); await openOverviewPanel(); await waitFor(() => { @@ -2597,7 +2606,7 @@ describe("AgentsView", () => { ]; mockFetchAgents.mockResolvedValue(spawnedAgents); - render(); + renderView(); await openOverviewPanel(); await waitFor(() => { @@ -2673,7 +2682,7 @@ describe("AgentsView", () => { it("loads bulk eligibility when controls open and shows count hints", async () => { mockFetchAgents.mockResolvedValue(bulkAgents); - render(); + renderView(); const initialFetchCount = mockFetchAgents.mock.calls.length; await openControlsPanel(); @@ -2690,7 +2699,7 @@ describe("AgentsView", () => { { ...bulkAgents[3] }, { ...bulkAgents[4], state: "paused" as AgentState }, ]); - render(); + renderView(); await openControlsPanel(); await waitFor(() => { @@ -2703,7 +2712,7 @@ describe("AgentsView", () => { it("pauses eligible non-ephemeral agents after confirmation", async () => { mockFetchAgents.mockResolvedValue(bulkAgents); - render(); + renderView(); await openControlsPanel(); fireEvent.click(screen.getByRole("menuitem", { name: /Pause All Agents/i })); @@ -2727,7 +2736,7 @@ describe("AgentsView", () => { it("resumes paused agents only", async () => { mockFetchAgents.mockResolvedValue(bulkAgents); - render(); + renderView(); await openControlsPanel(); fireEvent.click(screen.getByRole("menuitem", { name: /Resume All Agents/i })); @@ -2751,7 +2760,7 @@ describe("AgentsView", () => { } return { ...bulkAgents[0], id: agentId, state: newState }; }); - render(); + renderView(); await openControlsPanel(); fireEvent.click(screen.getByRole("menuitem", { name: /Pause All Agents/i })); @@ -2768,7 +2777,7 @@ describe("AgentsView", () => { describe("global heartbeat multiplier", () => { it("renders the global heartbeat speed control", async () => { mockFetchSettings.mockResolvedValue({ heartbeatMultiplier: 1 }); - render(); + renderView(); await openControlsPanel(); // Check the slider and preset are rendered @@ -2781,7 +2790,7 @@ describe("AgentsView", () => { it("loads heartbeat multiplier from settings", async () => { mockFetchSettings.mockResolvedValue({ heartbeatMultiplier: 2.5 }); - render(); + renderView(); await openControlsPanel(); const slider = screen.getByRole("slider", { name: "Heartbeat Speed" }) as HTMLInputElement; @@ -2790,7 +2799,7 @@ describe("AgentsView", () => { it("saves heartbeat multiplier when slider changes", async () => { mockFetchSettings.mockResolvedValue({ heartbeatMultiplier: 1 }); - render(); + renderView(); await openControlsPanel(); // Change the slider @@ -2805,7 +2814,7 @@ describe("AgentsView", () => { it("saves heartbeat multiplier when preset is selected", async () => { mockFetchSettings.mockResolvedValue({ heartbeatMultiplier: 1 }); - render(); + renderView(); await openControlsPanel(); // Change the preset @@ -2820,7 +2829,7 @@ describe("AgentsView", () => { it("disables control while saving", async () => { mockFetchSettings.mockResolvedValue({ heartbeatMultiplier: 1 }); mockUpdateSettings.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100))); - render(); + renderView(); await openControlsPanel(); // Change the slider - this should start the save