From f5d74c897782c1278bed7daa64c4bd2f82383cb7 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 20 Aug 2026 22:47:05 -0700 Subject: [PATCH 1/2] Add regression tests for MCP preset catalog rendering (CL-6472) Covers the owner's live-repro scenario (fresh bench, zero connections, presets route returns all 10 curated apps) end to end through PluginsGallery, plus a guard that the presets section never renders nothing on a failed load. --- .../plugins-ui/test/mcp-preset-cards.test.tsx | 53 +++++++++++++++++++ .../plugins-ui/test/plugins-gallery.test.tsx | 46 ++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/packages/plugins-ui/test/mcp-preset-cards.test.tsx b/packages/plugins-ui/test/mcp-preset-cards.test.tsx index 2baa2d5d9..4751f2044 100644 --- a/packages/plugins-ui/test/mcp-preset-cards.test.tsx +++ b/packages/plugins-ui/test/mcp-preset-cards.test.tsx @@ -8,6 +8,8 @@ import { act } from "react"; import { createRoot } from "react-dom/client"; import type { Root } from "react-dom/client"; +import { MCP_PRESETS } from "@workbench/connections/mcp-presets"; + import { McpPresetCardsSection } from "../src/mcp-preset-cards"; const realFetch = globalThis.fetch; @@ -267,4 +269,55 @@ describe("McpPresetCardsSection", () => { expect(deleteCall).not.toBeUndefined(); expect(deleteCall?.url).toBe("/api/tenants/tenant_test/mcp-servers/exa"); }); + + // CL-6472: a fresh bench with zero connections still owns the same + // curated catalog — the route returns all 10 presets regardless, so + // every one of them must reach the page. + test("every preset the route returns reaches the page on a fresh bench", async () => { + globalThis.fetch = (async () => + new Response( + JSON.stringify({ + data: MCP_PRESETS.map((preset) => ({ + slug: preset.slug, + displayName: preset.displayName, + description: preset.description, + url: preset.url, + connectionMode: preset.connectionMode, + docsUrl: preset.docsUrl, + ...(preset.icon === undefined ? {} : { icon: preset.icon }), + ...(preset.tokenSteps === undefined + ? {} + : { tokenSteps: preset.tokenSteps }), + connected: false, + })), + }), + )) as unknown as typeof fetch; + + const container = mountSection(); + await settle(); + + expect(container.querySelectorAll("[data-plugin-slug]")).toHaveLength( + MCP_PRESETS.length, + ); + for (const preset of MCP_PRESETS) { + expect(container.textContent).toContain(preset.displayName); + } + }); + + // CL-6472: this section must never silently disappear. Before this + // fix it `return null`ed for any load that resolved with zero presets + // (including one still in flight), which is indistinguishable from the + // whole "Connect apps" catalog vanishing. + test("never renders nothing — a failed load surfaces an error, not silence", async () => { + globalThis.fetch = (async () => + new Response("Internal Server Error", { + status: 500, + })) as unknown as typeof fetch; + + const container = mountSection(); + await settle(); + + expect(container.textContent).toContain("Connect apps"); + expect(container.querySelector('[role="alert"]')).not.toBeNull(); + }); }); diff --git a/packages/plugins-ui/test/plugins-gallery.test.tsx b/packages/plugins-ui/test/plugins-gallery.test.tsx index 087c80819..377f78ac3 100644 --- a/packages/plugins-ui/test/plugins-gallery.test.tsx +++ b/packages/plugins-ui/test/plugins-gallery.test.tsx @@ -11,6 +11,7 @@ import type { Root } from "react-dom/client"; import type { ConnectorDescriptor } from "@workbench/connections/registry"; import type { ResolvedPlugin } from "@workbench/connections/plugins"; +import { MCP_PRESETS } from "@workbench/connections/mcp-presets"; import { PluginsGallery } from "../src/plugins-gallery"; import type { SkillCardData } from "../src/skill-card"; @@ -269,4 +270,49 @@ describe("PluginsGallery", () => { } expect(container.textContent).not.toContain("Add MCP server"); }); + + // CL-6472: the owner's live repro found the presets route returns all 10 + // curated MCP presets on a fresh bench with zero connections, but the + // gallery showed none of them. Reproduce that exact fetch shape here so + // a regression in how the gallery composes `McpPresetCardsSection` + // (as opposed to a bug in that component alone) gets caught too. + test("every MCP preset the route returns reaches the gallery on a fresh bench (CL-6472)", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (input: string | URL | Request) => { + const url = String(input); + if (url.includes("/mcp-servers/presets")) { + return new Response( + JSON.stringify({ + data: MCP_PRESETS.map((preset) => ({ + slug: preset.slug, + displayName: preset.displayName, + description: preset.description, + url: preset.url, + connectionMode: preset.connectionMode, + docsUrl: preset.docsUrl, + ...(preset.icon === undefined ? {} : { icon: preset.icon }), + ...(preset.tokenSteps === undefined + ? {} + : { tokenSteps: preset.tokenSteps }), + connected: false, + })), + }), + ); + } + return new Response(JSON.stringify({ data: [] })); + }) as unknown as typeof fetch; + + try { + const { container } = renderGallery([]); + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 20)); + }); + + for (const preset of MCP_PRESETS) { + expect(container.textContent).toContain(preset.displayName); + } + } finally { + globalThis.fetch = originalFetch; + } + }); }); From 3cefbbb33fccde4175f68b36d1b899dca2b03f0d Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 20 Aug 2026 22:47:05 -0700 Subject: [PATCH 2/2] Stop the MCP presets section from ever rendering nothing (CL-6472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit McpPresetCardsSection returned null whenever its own list came back empty and had no error — indistinguishable from a load still in flight. Track whether the fetch has resolved and always render the 'Connect apps' heading with an explicit loading, empty, or error state instead of silently disappearing. --- packages/plugins-ui/src/mcp-preset-cards.tsx | 60 +++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/packages/plugins-ui/src/mcp-preset-cards.tsx b/packages/plugins-ui/src/mcp-preset-cards.tsx index e14a8bd31..8f4fedb64 100644 --- a/packages/plugins-ui/src/mcp-preset-cards.tsx +++ b/packages/plugins-ui/src/mcp-preset-cards.tsx @@ -210,6 +210,14 @@ export function McpPresetCardsSection({ new Map(), ); const [loadError, setLoadError] = useState(null); + // Whether the presets fetch has resolved at least once — distinct from + // "zero presets": this catalog is the same ~10 curated apps for every + // tenant, so an empty `presets` array before this flips true is a + // loading gap, never a real "nothing to connect" state (CL-6472). The + // section used to `return null` whenever presets were empty regardless + // of why, which let a load-in-progress render as if the whole catalog + // had vanished — this component must never go quiet like that again. + const [loaded, setLoaded] = useState(false); function reload() { listMcpPresets(tenantId) @@ -217,7 +225,8 @@ export function McpPresetCardsSection({ setPresets(data); setLoadError(null); }) - .catch((cause: unknown) => setLoadError(messageOf(cause))); + .catch((cause: unknown) => setLoadError(messageOf(cause))) + .finally(() => setLoaded(true)); } useEffect(() => { @@ -233,8 +242,6 @@ export function McpPresetCardsSection({ .includes(needle), ); - if (visiblePresets.length === 0 && loadError === null) return null; - return (

@@ -244,25 +251,34 @@ export function McpPresetCardsSection({

{loadError}

- ) : null} -
- {visiblePresets.map((preset) => ( - { - if (toolCount !== undefined) { - setToolCounts((prev) => - new Map(prev).set(preset.slug, toolCount), - ); - } - reload(); - }} - /> - ))} -
+ ) : !loaded ? ( +

Loading…

+ ) : visiblePresets.length === 0 ? ( +

+ {presets.length === 0 + ? "No apps to connect right now." + : `No app matches "${query.trim()}".`} +

+ ) : ( +
+ {visiblePresets.map((preset) => ( + { + if (toolCount !== undefined) { + setToolCounts((prev) => + new Map(prev).set(preset.slug, toolCount), + ); + } + reload(); + }} + /> + ))} +
+ )}

); }