From 1c79da70a69c8c889661a8a58fb748b1cb67572e Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 5 Feb 2026 17:50:18 -0600 Subject: [PATCH] fix(#338): Handle non-OK responses in ActiveProjectsWidget - Add error state tracking for both projects and agents API calls - Show error UI (amber alert icon + message) when fetch fails - Clear data on error to avoid showing stale information - Added tests for error handling: API failures, network errors Refs #338 Co-Authored-By: Claude Opus 4.5 --- .../widgets/ActiveProjectsWidget.tsx | 24 ++++ .../__tests__/ActiveProjectsWidget.test.tsx | 106 ++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/apps/web/src/components/widgets/ActiveProjectsWidget.tsx b/apps/web/src/components/widgets/ActiveProjectsWidget.tsx index cc64a5d..1db97d5 100644 --- a/apps/web/src/components/widgets/ActiveProjectsWidget.tsx +++ b/apps/web/src/components/widgets/ActiveProjectsWidget.tsx @@ -38,17 +38,23 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps): const [agentSessions, setAgentSessions] = useState([]); const [isLoadingProjects, setIsLoadingProjects] = useState(true); const [isLoadingAgents, setIsLoadingAgents] = useState(true); + const [projectsError, setProjectsError] = useState(null); + const [agentsError, setAgentsError] = useState(null); const [expandedSession, setExpandedSession] = useState(null); // Fetch active projects useEffect(() => { const fetchProjects = async (): Promise => { try { + setProjectsError(null); // Use API client to ensure CSRF token is included const data = await apiPost("/api/widgets/data/active-projects"); setProjects(data); } catch (error) { console.error("Failed to fetch active projects:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + setProjectsError(errorMessage); + setProjects([]); } finally { setIsLoadingProjects(false); } @@ -67,11 +73,15 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps): useEffect(() => { const fetchAgentSessions = async (): Promise => { try { + setAgentsError(null); // Use API client to ensure CSRF token is included const data = await apiPost("/api/widgets/data/agent-chains"); setAgentSessions(data); } catch (error) { console.error("Failed to fetch agent sessions:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + setAgentsError(errorMessage); + setAgentSessions([]); } finally { setIsLoadingAgents(false); } @@ -137,6 +147,13 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps):
{isLoadingProjects ? (
Loading projects...
+ ) : projectsError ? ( +
+
+ + Unable to load projects +
+
) : projects.length === 0 ? (
No active projects
) : ( @@ -185,6 +202,13 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps):
{isLoadingAgents ? (
Loading agents...
+ ) : agentsError ? ( +
+
+ + Unable to load agents +
+
) : agentSessions.length === 0 ? (
No running agents
) : ( diff --git a/apps/web/src/components/widgets/__tests__/ActiveProjectsWidget.test.tsx b/apps/web/src/components/widgets/__tests__/ActiveProjectsWidget.test.tsx index 094e059..fd73087 100644 --- a/apps/web/src/components/widgets/__tests__/ActiveProjectsWidget.test.tsx +++ b/apps/web/src/components/widgets/__tests__/ActiveProjectsWidget.test.tsx @@ -317,4 +317,110 @@ describe("ActiveProjectsWidget", (): void => { expect(screen.getByText("(2)")).toBeInTheDocument(); // Project count badge }); }); + + it("should display error state when projects API fails", async (): Promise => { + vi.mocked(global.fetch).mockImplementation((url: RequestInfo | URL) => { + const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : ""; + + // Return CSRF token + if (urlString.includes("csrf")) { + return Promise.resolve(mockCsrfResponse()); + } + if (urlString.includes("active-projects")) { + return Promise.resolve({ + ok: false, + status: 500, + statusText: "Internal Server Error", + json: () => + Promise.resolve({ code: "SERVER_ERROR", message: "Failed to fetch projects" }), + } as Response); + } + // Agent chains succeeds + return Promise.resolve({ + ok: true, + json: () => Promise.resolve([]), + } as Response); + }); + + render(); + + await waitFor(() => { + expect(screen.getByText(/unable to load projects/i)).toBeInTheDocument(); + }); + }); + + it("should display error state when agent chains API fails", async (): Promise => { + vi.mocked(global.fetch).mockImplementation((url: RequestInfo | URL) => { + const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : ""; + + // Return CSRF token + if (urlString.includes("csrf")) { + return Promise.resolve(mockCsrfResponse()); + } + if (urlString.includes("agent-chains")) { + return Promise.resolve({ + ok: false, + status: 500, + statusText: "Internal Server Error", + json: () => Promise.resolve({ code: "SERVER_ERROR", message: "Failed to fetch agents" }), + } as Response); + } + // Active projects succeeds + return Promise.resolve({ + ok: true, + json: () => Promise.resolve([]), + } as Response); + }); + + render(); + + await waitFor(() => { + expect(screen.getByText(/unable to load agents/i)).toBeInTheDocument(); + }); + }); + + it("should display error state when both APIs fail", async (): Promise => { + vi.mocked(global.fetch).mockImplementation((url: RequestInfo | URL) => { + const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : ""; + + // Return CSRF token + if (urlString.includes("csrf")) { + return Promise.resolve(mockCsrfResponse()); + } + // Both endpoints fail + return Promise.resolve({ + ok: false, + status: 500, + statusText: "Internal Server Error", + json: () => Promise.resolve({ code: "SERVER_ERROR", message: "Server error" }), + } as Response); + }); + + render(); + + await waitFor(() => { + expect(screen.getByText(/unable to load projects/i)).toBeInTheDocument(); + expect(screen.getByText(/unable to load agents/i)).toBeInTheDocument(); + }); + }); + + it("should handle network errors gracefully", async (): Promise => { + vi.mocked(global.fetch).mockImplementation((url: RequestInfo | URL) => { + const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : ""; + + // Return CSRF token + if (urlString.includes("csrf")) { + return Promise.resolve(mockCsrfResponse()); + } + // Network error + return Promise.reject(new Error("Network error")); + }); + + render(); + + await waitFor(() => { + expect(screen.getByText(/unable to load projects/i)).toBeInTheDocument(); + expect(screen.getByText(/unable to load agents/i)).toBeInTheDocument(); + }); + }); });