fix(FN-1895): improve companies.sh import error handling and add retry behavior

- Surface companies.sh API errors to frontend instead of returning empty list
- Add 30s timeout to GitHub archive download to prevent hanging
- Prevent infinite retry loop in browse catalog useEffect
- Update retry handler to call fetch directly for better control
- Fix TypeScript error with Response type in timeout code
- Add comprehensive tests for error handling and retry behavior
This commit is contained in:
Fusion
2026-04-16 13:31:47 -07:00
committed by gsxdsm
parent 222a807dc2
commit 0bbdb87e55
6 changed files with 307 additions and 17 deletions

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import { render, screen, waitFor, fireEvent, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AgentImportModal } from "../AgentImportModal";
@@ -285,4 +285,147 @@ describe("AgentImportModal", () => {
fireEvent.click(overlay!);
expect(onClose).toHaveBeenCalledTimes(1);
});
describe("Browse Catalog Mode", () => {
function createMockResponse(body: unknown): Response {
return {
ok: true,
status: 200,
headers: new globalThis.Headers({ "content-type": "application/json" }),
text: async () => JSON.stringify(body),
json: async () => body,
} as unknown as Response;
}
it("shows companies when fetch succeeds", async () => {
const mockCompanies = [
{ slug: "test-company", name: "Test Company", tagline: "A great company" },
{ slug: "another-co", name: "Another Company" },
];
globalThis.fetch = vi.fn().mockResolvedValue(createMockResponse({ companies: mockCompanies }));
renderModal(true);
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "Browse Catalog" }));
await waitFor(() => {
expect(screen.getByText("Test Company")).toBeInTheDocument();
expect(screen.getByText("Another Company")).toBeInTheDocument();
});
});
it("shows error message when companies.sh returns error", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(createMockResponse({
companies: [],
error: "Failed to fetch companies.sh catalog: Network unreachable",
}));
renderModal(true);
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "Browse Catalog" }));
await waitFor(() => {
expect(screen.getByText(/Failed to fetch companies.sh catalog/)).toBeInTheDocument();
});
});
it("shows Retry button when error occurs", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(createMockResponse({
companies: [],
error: "Failed to fetch companies.sh catalog: Network unreachable",
}));
renderModal(true);
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "Browse Catalog" }));
await waitFor(() => {
expect(screen.getByRole("button", { name: /Retry/i })).toBeInTheDocument();
});
});
it("does not retry infinitely after error", async () => {
let fetchCallCount = 0;
globalThis.fetch = vi.fn().mockImplementation(() => {
fetchCallCount++;
return Promise.resolve(createMockResponse({
companies: [],
error: "Network unreachable",
}));
});
renderModal(true);
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "Browse Catalog" }));
// Wait for the error to appear
await waitFor(() => {
expect(screen.getByText(/Network unreachable/)).toBeInTheDocument();
});
// Allow a bit of time for any potential extra fetches
await new Promise((resolve) => setTimeout(resolve, 500));
// Should have exactly 1 fetch call (no infinite retry)
expect(fetchCallCount).toBe(1);
});
it("retry button allows re-fetching after error", async () => {
// Use a ref to track call count
const callCountRef = { current: 0 };
globalThis.fetch = vi.fn().mockImplementation(() => {
callCountRef.current++;
const callNum = callCountRef.current;
// First call returns error, subsequent calls return success
if (callNum === 1) {
return Promise.resolve(createMockResponse({
companies: [],
error: "Network unreachable",
}));
}
return Promise.resolve(createMockResponse({
companies: [{ slug: "test-co", name: "Test Company" }],
}));
});
renderModal(true);
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: "Browse Catalog" }));
// Wait for error
await waitFor(() => {
expect(screen.getByText(/Network unreachable/)).toBeInTheDocument();
});
// Verify only 1 fetch happened
expect(callCountRef.current).toBe(1);
// Click retry using act to ensure state updates are processed
const retryButton = screen.getByRole("button", { name: /Retry/i });
await act(async () => {
await user.click(retryButton);
});
// Wait for the error to be cleared (indicating retry is in progress)
await waitFor(() => {
expect(screen.queryByText(/Network unreachable/)).not.toBeInTheDocument();
});
// The mock should return success on second call, so we should see the companies
await waitFor(() => {
expect(screen.getByText("Test Company")).toBeInTheDocument();
});
// Verify a second fetch happened
expect(callCountRef.current).toBe(2);
});
});
});