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 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-05 17:50:18 -06:00
parent 1a15c12c56
commit 1c79da70a6
2 changed files with 130 additions and 0 deletions

View File

@@ -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<void> => {
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(<ActiveProjectsWidget id="active-projects-1" />);
await waitFor(() => {
expect(screen.getByText(/unable to load projects/i)).toBeInTheDocument();
});
});
it("should display error state when agent chains API fails", async (): Promise<void> => {
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(<ActiveProjectsWidget id="active-projects-1" />);
await waitFor(() => {
expect(screen.getByText(/unable to load agents/i)).toBeInTheDocument();
});
});
it("should display error state when both APIs fail", async (): Promise<void> => {
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(<ActiveProjectsWidget id="active-projects-1" />);
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<void> => {
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(<ActiveProjectsWidget id="active-projects-1" />);
await waitFor(() => {
expect(screen.getByText(/unable to load projects/i)).toBeInTheDocument();
expect(screen.getByText(/unable to load agents/i)).toBeInTheDocument();
});
});
});