diff --git a/src/components/settings/__tests__/mcp-section-error-state.test.tsx b/src/components/settings/__tests__/mcp-section-error-state.test.tsx new file mode 100644 index 000000000..3511d6890 --- /dev/null +++ b/src/components/settings/__tests__/mcp-section-error-state.test.tsx @@ -0,0 +1,51 @@ +/** + * A read failure on the MCP connector cards (connected assistants, connector + * tokens) must not read as "nothing connected / no tokens". Both queries are + * forced into their error state; each card has to render the `query-error-card` + * slot with retry and never fall through to its empty copy — the §6 + * fall-through on another security surface. + */ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +vi.mock("@tanstack/react-query", () => ({ + useQuery: () => ({ + data: undefined, + isLoading: false, + isError: true, + refetch: vi.fn(), + }), + useQueryClient: () => ({ invalidateQueries: vi.fn() }), + useMutation: () => ({ mutate: vi.fn(), isPending: false }), +})); + +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { id: "u1", role: "USER", modules: { mcp: true } }, + isAuthenticated: true, + isLoading: false, + refetch: vi.fn(), + }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { McpSection } from "../mcp-section"; + +function render() { + return renderToStaticMarkup( + + + , + ); +} + +describe("McpSection — a read failure is honest, not empty", () => { + it("renders query-error cards with retry and never the empty copy on error", () => { + const html = render(); + expect(html).toContain('data-slot="query-error-card"'); + expect(html).toContain('data-slot="query-error-retry"'); + // Neither empty copy may appear when the reads failed. + expect(html).not.toContain("No connected assistants"); + expect(html).not.toContain("No active tokens"); + }); +}); diff --git a/src/components/settings/mcp-section.tsx b/src/components/settings/mcp-section.tsx index 9c0692e1c..3d6099963 100644 --- a/src/components/settings/mcp-section.tsx +++ b/src/components/settings/mcp-section.tsx @@ -35,6 +35,8 @@ import { import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { QueryErrorCard } from "@/components/ui/query-error-card"; +import { Skeleton } from "@/components/ui/skeleton"; import { Switch } from "@/components/ui/switch"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; @@ -77,7 +79,12 @@ function McpConnectionsCard() { const { isAuthenticated } = useAuth(); const queryClient = useQueryClient(); - const { data: connections } = useQuery({ + const { + data: connections, + isError, + isLoading, + refetch, + } = useQuery({ queryKey: queryKeys.mcpConnections(), queryFn: async () => apiGet("/api/mcp/connections"), enabled: isAuthenticated, @@ -110,7 +117,11 @@ function McpConnectionsCard() {

{t("settings.mcp.connectionsDetail")}

- {list.length === 0 ? ( + {isError ? ( + refetch()} /> + ) : isLoading ? ( + + ) : list.length === 0 ? (

{t("settings.mcp.noConnections")}

@@ -264,7 +275,12 @@ function McpTokensCard() { const [tokenMsg, setTokenMsg] = useState(null); const [tokenCopied, setTokenCopied] = useState(false); - const { data: tokens } = useQuery({ + const { + data: tokens, + isError, + isLoading, + refetch, + } = useQuery({ queryKey: queryKeys.mcpTokens(), queryFn: async () => apiGet("/api/mcp/tokens"), enabled: isAuthenticated, @@ -340,9 +356,7 @@ function McpTokensCard() {

{t("settings.mcp.tokensDetail")}

-

- {t("settings.mcp.scopeNote")} -

+

{t("settings.mcp.scopeNote")}

@@ -421,7 +435,11 @@ function McpTokensCard() {

{t("settings.activeTokensTitle")}

- {activeTokens.length === 0 ? ( + {isError ? ( + refetch()} /> + ) : isLoading ? ( + + ) : activeTokens.length === 0 ? (

{t("settings.noActiveTokens")}