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