fix(settings): show read failures on the MCP connector lists, not empty states

The connected-assistants and connector-token queries had no error or
loading branch, so a failed read rendered "no connected assistants" or
"no active tokens" on this security surface. Gate both lists with
QueryErrorCard and retry, a skeleton while loading. Test watched fail
first against the old empty-state fall-through.
This commit is contained in:
Marc-André Bombeck committed 2026-08-09 04:07:10 +02:00
1 parent ab2c5cb2aa
commit 48e4882400
2 files changed
+76 -7

No files matched your search

@@ -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(
<I18nProvider initialLocale="en">
<McpSection />
</I18nProvider>,
);
}
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");
});
});
+25 -7
View File
@@ -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<McpConnectionInfo[]>("/api/mcp/connections"),
enabled: isAuthenticated,
@@ -110,7 +117,11 @@ function McpConnectionsCard() {
<p className="text-sm">{t("settings.mcp.connectionsDetail")}</p>
<div className="space-y-4">
{list.length === 0 ? (
{isError ? (
<QueryErrorCard onRetry={() => refetch()} />
) : isLoading ? (
<Skeleton className="h-24 w-full rounded-lg" />
) : list.length === 0 ? (
<p className="text-muted-foreground rounded-lg border border-dashed px-3 py-4 text-center text-sm">
{t("settings.mcp.noConnections")}
</p>
@@ -264,7 +275,12 @@ function McpTokensCard() {
const [tokenMsg, setTokenMsg] = useState<string | null>(null);
const [tokenCopied, setTokenCopied] = useState(false);
const { data: tokens } = useQuery({
const {
data: tokens,
isError,
isLoading,
refetch,
} = useQuery({
queryKey: queryKeys.mcpTokens(),
queryFn: async () => apiGet<McpTokenInfo[]>("/api/mcp/tokens"),
enabled: isAuthenticated,
@@ -340,9 +356,7 @@ function McpTokensCard() {
<p className="text-sm">{t("settings.mcp.tokensDetail")}</p>
<div className="space-y-4">
<p className="text-sm leading-relaxed">
{t("settings.mcp.scopeNote")}
</p>
<p className="text-sm leading-relaxed">{t("settings.mcp.scopeNote")}</p>
<div className="border-border bg-muted/30 space-y-2 rounded-lg border p-3">
<div className="flex items-center justify-between gap-4">
@@ -421,7 +435,11 @@ function McpTokensCard() {
<p className="mb-2 text-sm font-medium">
{t("settings.activeTokensTitle")}
</p>
{activeTokens.length === 0 ? (
{isError ? (
<QueryErrorCard onRetry={() => refetch()} />
) : isLoading ? (
<Skeleton className="h-24 w-full rounded-lg" />
) : activeTokens.length === 0 ? (
<p className="text-muted-foreground rounded-lg border border-dashed px-3 py-4 text-center text-sm">
{t("settings.noActiveTokens")}
</p>