diff --git a/clients/web/src/App.tsx b/clients/web/src/App.tsx index 68a8af159..1d089d602 100644 --- a/clients/web/src/App.tsx +++ b/clients/web/src/App.tsx @@ -1504,6 +1504,11 @@ function App() { // any connect (legacy included) between the `connected` status dispatch and the // `serverInfo` dispatch, which land in a single React render. The modal uses // `serverInfoReported` (above), not this name, to stay faithful. + // + // This `??` only covers an *absent* serverInfo. A server that *reports* a + // blank name (`{ name: "" }`) is degraded for display a layer below, in + // InspectorView's `resolveHeaderServerInfo` (#1774) — kept there so this + // faithful object (and thus the modal) never carries a borrowed name. const initializeResult = useMemo(() => { if (connectionStatus !== "connected") return undefined; const resolvedServerInfo = serverInfo ?? { diff --git a/clients/web/src/components/groups/ConnectionInfoContent/ConnectionInfoContent.test.tsx b/clients/web/src/components/groups/ConnectionInfoContent/ConnectionInfoContent.test.tsx index a17e380f6..9f9a8ea4a 100644 --- a/clients/web/src/components/groups/ConnectionInfoContent/ConnectionInfoContent.test.tsx +++ b/clients/web/src/components/groups/ConnectionInfoContent/ConnectionInfoContent.test.tsx @@ -96,6 +96,42 @@ describe("ConnectionInfoContent", () => { expect(screen.getAllByText("—")).toHaveLength(3); }); + it("renders an em-dash when the reported name is missing", () => { + renderWithMantine( + , + ); + expect(screen.getByText("1.0.0")).toBeInTheDocument(); + expect(screen.getAllByText("—")).toHaveLength(3); + }); + + it("renders an em-dash when the reported name is whitespace-only", () => { + renderWithMantine( + , + ); + // A whitespace-only reported name is the same non-conforming class as an + // empty one (#1774): it must read as unknown ("—"), not a visually blank + // row. Stays faithful — never borrows the catalog name. + expect(screen.getByText("1.0.0")).toBeInTheDocument(); + expect(screen.getAllByText("—")).toHaveLength(3); + }); + it("shows 'not reported' for name and version when serverInfo was not reported", () => { renderWithMantine( { expect(screen.getByRole("switch")).toBeChecked(); }); + it("falls back to the catalog name in the header when the reported serverInfo name is empty (#1774)", () => { + // A non-conforming server reports serverInfo with an empty name string. + // `App`'s `??` fallback only fires when the whole object is absent, so the + // header would otherwise render a nameless title. The view degrades to the + // active server's catalog name ("Alpha") so the header still identifies the + // server. Scoped to the header (role="banner") to exclude the ServerCard, + // which shows the catalog name unconditionally. + renderWithMantine( + , + ); + expect( + within(screen.getByRole("banner")).getByText("Alpha"), + ).toBeInTheDocument(); + }); + + it("falls back to the catalog name in the header when the reported serverInfo name is missing (#1774)", () => { + // Non-conforming server: `serverInfo` omits `name` entirely (the field is + // typed non-null). Pins the `?.trim()` tolerance in resolveHeaderServerInfo + // — a runtime-absent name degrades to the catalog name, it doesn't throw. + renderWithMantine( + , + ); + expect( + within(screen.getByRole("banner")).getByText("Alpha"), + ).toBeInTheDocument(); + }); + + it("falls back to the catalog name in the header when the reported serverInfo name is whitespace-only (#1774)", () => { + // A whitespace-only reported name (" ") is the same non-conforming class + // as an empty string — truthy, so a naive `if (serverInfo.name)` guard would + // let it through and render a blank-looking title. The `.trim()` guard + // degrades it to the catalog name like the empty case. + renderWithMantine( + , + ); + expect( + within(screen.getByRole("banner")).getByText("Alpha"), + ).toBeInTheDocument(); + }); + + it("still renders the connected header when the reported name is blank and no catalog entry matches (#1774)", () => { + // Blank reported name AND no catalog server to borrow from: the connected + // header must still render (the connection is live) rather than crash or + // invent a label — it just shows no server name. Asserting the Disconnect + // control inside the banner makes this a real regression guard for the + // no-catalog-match branch, not merely a coverage-only test. + renderWithMantine( + , + ); + const header = screen.getByRole("banner"); + expect( + within(header).getByRole("button", { name: "Disconnect from server" }), + ).toBeInTheDocument(); + // No reported name and nothing to borrow, so the header shows no catalog name. + expect(within(header).queryByText("ghost")).not.toBeInTheDocument(); + }); + it("surfaces the negotiated protocol version on the active connected card", () => { renderWithMantine( s.id === activeServerId)?.name; + return catalogName ? { ...serverInfo, name: catalogName } : serverInfo; +} + const ALL_TABS: string[] = [ SERVERS_TAB, "Apps", @@ -1274,7 +1296,11 @@ export function InspectorView({ {connectionStatus === "connected" && initializeResult ? (