mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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:
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");
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user