From a1c00e51a87d4c0d87b093f3bd96b67c5c276a0b Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sun, 23 Aug 2026 20:55:35 -0400 Subject: [PATCH] test(web): cover the App-level listed-resource metadata wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bridge-factory tests inject `getListedResourceMeta` directly, so nothing caught App.tsx ceasing to supply it — the fallback would go unused with every test still green. Add a pass-through spy over the factory in App.test.tsx and assert both factories receive a lookup that resolves a listed resource's `_meta` and misses on an unknown URI. Signed-off-by: cliffhall --- clients/web/src/App.test.tsx | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/clients/web/src/App.test.tsx b/clients/web/src/App.test.tsx index 2561c308..a37ec645 100644 --- a/clients/web/src/App.test.tsx +++ b/clients/web/src/App.test.tsx @@ -355,6 +355,29 @@ vi.mock("@inspector/core/react/useSettingsDraft.js", () => ({ })), })); +// --- App bridge factory spy (#2055) ----------------------------------------- +// Passes through to the real factory but records the deps App hands it, so a +// test can drive `getListedResourceMeta` — the wiring that carries a +// `resources/list` entry's `_meta.ui` into the sandbox CSP. Without this the +// bridge-factory unit tests would still pass while App stopped supplying it. +const appBridgeFactoryDeps: AppBridgeFactoryDeps[] = []; +vi.mock( + "./components/elements/AppRenderer/createAppBridgeFactory", + async (importOriginal) => { + const actual = + await importOriginal< + typeof import("./components/elements/AppRenderer/createAppBridgeFactory") + >(); + return { + ...actual, + createAppBridgeFactory: (deps: AppBridgeFactoryDeps) => { + appBridgeFactoryDeps.push(deps); + return actual.createAppBridgeFactory(deps); + }, + }; + }, +); + // --- InspectorView double --------------------------------------------------- // Surfaces each piece of session-scoped state under test and exposes buttons // that invoke the App's connect / call-tool / get-prompt / read-resource / @@ -590,6 +613,8 @@ import type { MessageEntry, ServerEntry, } from "@inspector/core/mcp/types.js"; +import type { AppBridgeFactoryDeps } from "./components/elements/AppRenderer/createAppBridgeFactory"; +import { useManagedResources } from "@inspector/core/react/useManagedResources.js"; // Default useInspectorClient return — capabilities empty (no task tool calls). // Individual tests override via vi.mocked(...).mockReturnValue(...). @@ -3117,3 +3142,48 @@ describe("App background command rejections (#2049)", () => { } }); }); + +describe("App MCP App listed-resource metadata wiring (#2055)", () => { + beforeEach(() => { + appBridgeFactoryDeps.length = 0; + }); + + afterEach(() => { + // The hook mock is module-level and shared, so put the empty-list default + // back rather than leaving a populated list for whatever runs next. + vi.mocked(useManagedResources).mockReturnValue({ + resources: [], + refresh: vi.fn(), + clearListChanged: vi.fn(), + } as unknown as ReturnType); + }); + + it("hands the bridge factory a getListedResourceMeta reading the resources/list entries", async () => { + // ext-apps treats a listing entry's `_meta.ui` as the static default for a + // UI resource, so the App has to surface the listing to the bridge — the + // factory's own tests inject the dep and cannot see this wiring break. + const listedMeta = { + ui: { csp: { connectDomains: ["https://api.example.com"] } }, + }; + vi.mocked(useManagedResources).mockReturnValue({ + resources: [ + { uri: "ui://weather/app.html", name: "app", _meta: listedMeta }, + ], + refresh: vi.fn(), + clearListChanged: vi.fn(), + } as unknown as ReturnType); + + renderWithMantine(); + await waitFor(() => expect(appBridgeFactoryDeps.length).toBeGreaterThan(0)); + + // Every factory App builds gets the wiring, not just the Apps-tab one. + for (const deps of appBridgeFactoryDeps) { + expect(deps.getListedResourceMeta?.("ui://weather/app.html")).toEqual( + listedMeta, + ); + expect( + deps.getListedResourceMeta?.("ui://other/app.html"), + ).toBeUndefined(); + } + }); +});