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