fix(FN-2510): harden skills catalog fallback for short and invalid queries

- Update skills adapter fallback logic to handle empty and short catalog queries without surfacing 400 failures
- Handle ApiRequestError paths in SkillsView so invalid-query responses degrade gracefully in the UI
- Add regression coverage for adapter, routes, and SkillsView behavior across empty/short query cases
- Document short-query catalog behavior in the dashboard guide and include a patch changeset for @runfusion/fusion
This commit is contained in:
Fusion
2026-04-25 12:40:32 -07:00
committed by gsxdsm
parent 1d3d7438ff
commit ff6a68bac4
7 changed files with 195 additions and 29 deletions

View File

@@ -63,15 +63,34 @@ export function SkillsView({ projectId, addToast, onClose }: SkillsViewProps) {
// Fetch catalog
const loadCatalog = useCallback(async (query: string) => {
const isCatalogUnavailableError = (error: unknown): boolean => {
if (!error || typeof error !== "object") {
return false;
}
const withStatus = error as { status?: unknown; details?: unknown };
if (typeof withStatus.status === "number" && withStatus.status >= 500) {
return true;
}
if (withStatus.details && typeof withStatus.details === "object") {
const details = withStatus.details as { code?: unknown };
if (typeof details.code === "string" && details.code.startsWith("upstream_")) {
return true;
}
}
const legacy = error as { error?: unknown; code?: unknown };
return typeof legacy.error === "string" && typeof legacy.code === "string";
};
setIsLoadingCatalog(true);
setCatalogError(null);
try {
const result = await fetchSkillsCatalog(query, 20, projectId);
setCatalogEntries(result.entries);
} catch (err) {
// Check for upstream error with code (502 etc.)
const error = err as { error?: string; code?: string };
if (error.error && error.code) {
if (isCatalogUnavailableError(err)) {
setCatalogError("Catalog is temporarily unavailable. Please try again later.");
} else {
const message = err instanceof Error ? err.message : "Failed to load catalog";

View File

@@ -336,11 +336,13 @@ describe("SkillsView", () => {
});
describe("error handling", () => {
it("shows error message for catalog fetch with upstream error", async () => {
mockFetchSkillsCatalog.mockRejectedValue({
error: "Service unavailable",
code: "upstream_http_error",
});
it("shows friendly error message for catalog fetch with upstream ApiRequestError", async () => {
mockFetchSkillsCatalog.mockRejectedValue(
Object.assign(new Error("Upstream returned 400: Bad Request"), {
status: 502,
details: { code: "upstream_http_error" },
})
);
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);
@@ -363,10 +365,12 @@ describe("SkillsView", () => {
});
it("shows Try Again button for catalog error", async () => {
mockFetchSkillsCatalog.mockRejectedValue({
error: "Service unavailable",
code: "upstream_http_error",
});
mockFetchSkillsCatalog.mockRejectedValue(
Object.assign(new Error("Upstream returned 400: Bad Request"), {
status: 502,
details: { code: "upstream_http_error" },
})
);
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);
@@ -569,10 +573,12 @@ describe("SkillsView", () => {
describe("error-state retry", () => {
it("retry button is displayed when catalog fetch fails", async () => {
mockFetchSkillsCatalog.mockRejectedValue({
error: "Service unavailable",
code: "upstream_http_error",
});
mockFetchSkillsCatalog.mockRejectedValue(
Object.assign(new Error("Upstream returned 400: Bad Request"), {
status: 502,
details: { code: "upstream_http_error" },
})
);
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);