import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { ANONYMOUS_PRINCIPAL, createAuthenticatedPrincipal, type EkklesiaPrincipal, parseEkklesiaUserId, } from "@ekklesia/contracts"; import { type AuthorizationStore, createAuthorizationEvaluator, } from "@ekklesia/core/authorization"; import type { EkklesiaIdentityRuntime } from "@ekklesia/core/identity"; import { composePlatformPermissionRegistry, createModuleUiPublicationRegistry, discoverModules, type LegacyModuleRuntimeServices, type ModuleDatabase, startDiscoveredModuleRuntime, } from "@ekklesia/core/modules"; import { loadModuleUi, MODULE_UI_ISOLATION_ATTRIBUTE, MODULE_UI_ISOLATION_ROOT_ATTRIBUTE, } from "@ekklesia/module-ui-host"; import { MODULE_UI_ABI_VERSION } from "@ekklesia/module-ui-sdk"; import { Window } from "happy-dom"; import { bootstrapDisplayHost, bootstrapHost, createModuleUiContext, type EkklesiaHostConfig, moduleUiSourceUrl, } from "@ekklesia/shell"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createInMemoryDashboardStore } from "./support/in-memory-dashboard-store.js"; import { testDashboardHostAssets } from "./support/dashboard-host-assets.js"; import { readyHealth } from "./support/ready-health.js"; import { createApiApp } from "../src/app.js"; import { createDisplayHostAssets } from "../src/display-host-assets.js"; import { createInMemoryModuleLifecycleStore, moduleLifecycleSnapshotFor, } from "./support/in-memory-module-lifecycle.js"; import { moduleConfigurationOptionFor } from "./support/in-memory-module-configuration.js"; import { moduleStorageOptionFor } from "./support/in-memory-module-storage.js"; /** * The whole chain, with nothing between the halves faked: * * ```text * module fixture on disk -> discovery -> module runtime started -> publication registry * -> inventory endpoint -> the shell's own bootstrap -> server-produced artifact URL * -> the generic loader -> mount -> the module calls its own API relatively * -> the shell resolves it to /api/v1/modules//... -> server permission enforcement * ``` * * Every step runs real code. The module is discovered and *started* by Core's real runtime, its * routes are mounted by the real `createApiApp`, its artifact is published by the real ADR 0025 * route, its descriptor comes from the real inventory endpoint, and the browser half is * `@ekklesia/shell`'s own exported protocol — the same functions the React shell calls — rather * than a reimplementation of them here. * * Two things are deliberately *not* faked-around and are worth naming. The shell's `fetch` is the * Hono app's own `request`, so requests really traverse routing, CORS, authorization middleware, and * handlers without needing a socket. And Node cannot `import()` an `http:` URL, so the artifact is * fetched over that same in-process HTTP path and the **bytes it returned** are handed to the * generic loader through a `data:` URL — a genuine module URL, the same separation ADR 0025 used. * No test here reads the artifact off the filesystem and calls that a proof of publication. */ const viewerId = parseEkklesiaUserId("22222222-2222-4222-8222-222222222222"); const temporaryDirectories: string[] = []; afterEach(async () => { await Promise.all( temporaryDirectories .splice(0) .map((directory) => rm(directory, { force: true, recursive: true })), ); }); /** * A real, complete module: a manifest declaring a restricted permission and a UI entrypoint, a * server entrypoint that registers a permission-gated route, and a compiled UI artifact that reads * its host context and calls its own API relatively. * * The UI artifact is written as plain ESM rather than compiled from TSX here: ADR 0023's pipeline is * already proven end to end by `templates/module`'s own tests, and what this test needs is a byte * sequence whose journey through publication, inventory, and the loader can be asserted exactly. */ async function createFixtureModule( modulesDirectory: string, id = "example", name = "Example module", withDisplay = false, ): Promise { const moduleDirectory = path.join(modulesDirectory, id); await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true }); await mkdir(path.join(moduleDirectory, "dist/ui"), { recursive: true }); if (withDisplay) { await mkdir(path.join(moduleDirectory, "dist/display"), { recursive: true }); } await writeFile( path.join(moduleDirectory, "package.json"), JSON.stringify({ ekklesia: { layer: "module" }, name: `@test/module-${id}`, type: "module", version: "0.1.0", }), ); await writeFile( path.join(moduleDirectory, "ekklesia.module.json"), JSON.stringify({ compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" }, entrypoints: { server: "./dist/server/index.js", ui: "./dist/ui/index.js", ...(withDisplay ? { display: { artifact: "./dist/display/index.js", permission: `${id}.display.view`, }, } : {}), }, id, manifestVersion: 8, name, permissions: [ { baseline: "restricted", description: "Read example records.", id: `${id}.records.read`, }, ...(withDisplay ? [ { baseline: "restricted", description: "View the example display.", id: `${id}.display.view`, }, ] : []), ], version: "0.1.0", }), ); await writeFile( path.join(moduleDirectory, "dist/server/index.js"), `export default { id: ${JSON.stringify(id)}, version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/records", permission: ${JSON.stringify(`${id}.records.read`)}, maxBodyBytes: 0, async handler() { return Response.json([{ id: "a" }, { id: "b" }]); }, }); }, };\n`, ); // The module UI: host-neutral, asks for a relative path, and renders from the host context. It // hardcodes no origin, no prefix, and no permission decision of its own. await writeFile( path.join(moduleDirectory, "dist/ui/index.js"), `export default { abi: ${MODULE_UI_ABI_VERSION}, id: ${JSON.stringify(id)}, version: "0.1.0", async mount(container, context) { if (!context.permissions.has(${JSON.stringify(`${id}.records.read`)})) { container.textContent = context.moduleId + ":denied"; return () => { container.textContent = ""; }; } const response = await context.api.request("records"); const records = response.ok ? await response.json() : []; container.textContent = context.moduleId + ":" + response.status + ":" + records.length; return () => { container.textContent = ""; }; }, };\n`, ); if (withDisplay) { await writeFile( path.join(moduleDirectory, "dist/display/index.js"), `export default { abi: ${MODULE_UI_ABI_VERSION}, id: ${JSON.stringify(id)}, version: "0.1.0", async mount(container, context) { const response = await context.api.request("records"); const records = response.ok ? await response.json() : []; container.textContent = context.moduleId + ":display:" + response.status + ":" + records.length; return () => { container.textContent = ""; }; }, };\n`, ); } } interface HarnessOptions { readonly display?: boolean; readonly principal?: EkklesiaPrincipal; readonly grants?: readonly string[]; /** Extra fixture modules, so grouping can be exercised with more than one — a folder containing a * single module proves very little. */ readonly extraModules?: ReadonlyArray<{ readonly id: string; readonly name: string }>; readonly platformOwner?: boolean; } interface Harness { readonly config: EkklesiaHostConfig; readonly moduleApiCalls: readonly string[]; stop(): Promise; } /** * Starts a real API over a real discovered-and-started module, and returns the `EkklesiaHostConfig` * a browser would use — with `fetch` wired to the app itself. */ async function startHarness(options: HarnessOptions = {}): Promise { const modulesDirectory = await mkdtemp(path.join(tmpdir(), "ekklesia-host-e2e-")); temporaryDirectories.push(modulesDirectory); await createFixtureModule(modulesDirectory, "example", "Example module", options.display); for (const extra of options.extraModules ?? []) { await createFixtureModule(modulesDirectory, extra.id, extra.name); } const modules = await discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); const services: LegacyModuleRuntimeServices = Object.freeze({ database: Object.freeze({}) as unknown as ModuleDatabase, }); // The module's real runtime: its server entrypoint is imported, registered, and started, and the // route snapshot below is the one it actually published. const moduleRuntime = await startDiscoveredModuleRuntime(modules, services); const granted = new Set(options.grants ?? []); const evaluator = createAuthorizationEvaluator({ grants: { hasActiveGrant: async (query) => granted.has(query.permission) }, platformOwners: { isPlatformOwner: async () => options.platformOwner === true }, }); const runtime: EkklesiaIdentityRuntime = { handleAuthRequest: vi.fn().mockResolvedValue(new Response(null, { status: 200 })), resolvePrincipal: vi.fn().mockResolvedValue(options.principal ?? ANONYMOUS_PRINCIPAL), }; const store = { createGrant: vi.fn(), grantPlatformOwner: vi.fn(), hasActiveGrant: vi.fn().mockResolvedValue(false), isPlatformOwner: vi.fn().mockResolvedValue(false), listGrantsForUser: vi.fn().mockResolvedValue({ grants: [] }), revokeGrant: vi.fn(), revokePlatformOwner: vi.fn(), } as unknown as AuthorizationStore; const moduleUi = createModuleUiPublicationRegistry(modules); const app = createApiApp({ authorization: { evaluator, registry: composePlatformPermissionRegistry(modules), store }, moduleRoutes: moduleRuntime.routes, moduleRealtime: moduleRuntime.realtime, moduleServices: services, moduleUi, ...(moduleUi.listDisplays().length === 0 ? {} : { displayHostAssets: createDisplayHostAssets( '
', "export const displayHost = true;\n", "export const displayWorker = true;\n", ), }), moduleLifecycle: { snapshot: moduleLifecycleSnapshotFor(modules), store: createInMemoryModuleLifecycleStore(), }, moduleConfiguration: moduleConfigurationOptionFor(modules), moduleStorage: moduleStorageOptionFor(modules), dashboard: createInMemoryDashboardStore(), dashboardHostAssets: testDashboardHostAssets(), health: readyHealth(), runtime, trustedOrigins: ["https://dashboard.example.test"], }); const moduleApiCalls: string[] = []; const hostFetch = (async (input: Parameters[0], init?: RequestInit) => { const url = new URL(String(input)); if (url.pathname.startsWith("/api/v1/modules/")) { moduleApiCalls.push(`${init?.method ?? "GET"} ${url.pathname}${url.search}`); } return app.request(url.href, init); }) as unknown as typeof fetch; return { config: { apiBaseUrl: "https://ekklesia.example.test", fetch: hostFetch }, moduleApiCalls, stop: () => moduleRuntime.stop(), }; } /** Node cannot `import()` an `http:` URL, so the artifact is fetched over the real route and its * exact bytes become a `data:` URL. The loader still performs a genuine dynamic import of a real * module URL; only the transport differs, and the transport itself is asserted separately. */ async function publishedArtifactUrl(harness: Harness, sourceUrl: string): Promise { const response = await (harness.config.fetch as typeof fetch)(sourceUrl); expect(response.status).toBe(200); expect(response.headers.get("content-type")).toBe("text/javascript; charset=utf-8"); const bytes = Buffer.from(await response.arrayBuffer()); return `data:text/javascript;base64,${bytes.toString("base64")}`; } /** * A real host-owned container, built from `happy-dom` **as a library** rather than as this suite's * test environment. * * Since ADR 0040 the generic loader builds an isolated Shadow DOM root inside whatever container it * is given, so a plain object sink is no longer a faithful stand-in — and the isolation is exactly * what an end-to-end proof should exercise rather than stub out. Installing happy-dom as the * *environment* is not an option here: it replaces the module runner's resolution, and this suite * dynamically imports real module server entrypoints from temporary directories, which then fail to * resolve. Constructing one `Window` gives the DOM these few assertions need and leaves every global * this server-side suite depends on exactly as Node provides it. */ function containerSink(): HostContainer { return new Window().document.createElement("div") as unknown as HostContainer; } interface HostContainer { readonly children: { readonly length: number }; querySelector(selectors: string): HostContainer | null; readonly shadowRoot: { querySelector(selectors: string): { textContent: string } | null } | null; } /** What the mounted artifact rendered, read through its open isolated root. */ function mountedText(container: HostContainer): string { const shadowHost = container.querySelector(`[${MODULE_UI_ISOLATION_ATTRIBUTE}]`); return ( shadowHost?.shadowRoot?.querySelector(`[${MODULE_UI_ISOLATION_ROOT_ATTRIBUTE}]`)?.textContent ?? "" ); } type LoaderContainer = Parameters[0]["container"]; describe("end to end: a discovered module's UI mounts in the Ekklesia host", () => { it("bootstraps, publishes, loads, mounts, and reaches the module's own API", async () => { const harness = await startHarness({ grants: ["example.records.read"], principal: createAuthenticatedPrincipal(viewerId), }); try { // 1. The shell's own bootstrap: principal, then inventory. const bootstrap = await bootstrapHost(harness.config); expect(bootstrap.principal).toEqual({ kind: "authenticated", userId: viewerId }); // A module nobody has arranged yet is ungrouped and visible — no initialization write. expect(bootstrap.navigation.groups).toEqual([]); expect(bootstrap.navigation.ungrouped).toHaveLength(1); expect(bootstrap.navigation.ungrouped[0]?.name).toBe("Example module"); const descriptor = bootstrap.navigation.ungrouped[0]?.descriptor; if (!descriptor) { throw new Error("the inventory returned no module"); } // 2. The URLs came from the server, not from anything the browser composed. expect(descriptor).toEqual({ apiBaseUrl: "/api/v1/modules/example", contributions: [], id: "example", permissions: ["example.records.read"], realtimeBaseUrl: "/api/v1/module-realtime/example", uiUrl: "/api/v1/module-ui/example/0.1.0", version: "0.1.0", }); // 3. The artifact really is served by the real publication route at that URL. const sourceUrl = moduleUiSourceUrl(harness.config, descriptor); expect(sourceUrl).toBe("https://ekklesia.example.test/api/v1/module-ui/example/0.1.0"); const artifactUrl = await publishedArtifactUrl(harness, sourceUrl); // 4. The generic loader mounts it with the context the shell built. const container = containerSink(); const mounted = await loadModuleUi({ container: container as unknown as LoaderContainer, context: createModuleUiContext({ config: harness.config, descriptor, principal: bootstrap.principal, }), source: { moduleId: descriptor.id, url: artifactUrl, version: descriptor.version }, }); // 5. The module rendered, having reached its own backend through the host. expect(mountedText(container)).toBe("example:200:2"); // 6. It asked for "records"; the host resolved that onto Ekklesia's module mount prefix. expect(harness.moduleApiCalls).toEqual(["GET /api/v1/modules/example/records"]); // 7. And it tears down cleanly. await mounted.unmount(); expect(mountedText(container)).toBe(""); expect(mounted.mounted).toBe(false); } finally { await harness.stop(); } }); it("opens the stable display route, resolves its real descriptor, and mounts the published display bytes", async () => { const harness = await startHarness({ display: true, grants: ["example.records.read", "example.display.view"], principal: createAuthenticatedPrincipal(viewerId), }); try { // 1. A device keeps this version-independent host URL. It is a real permission-gated API route, // not a test-only HTML wrapper around the browser functions below. const page = await (harness.config.fetch as typeof fetch)( "https://ekklesia.example.test/display/example", ); expect(page.status).toBe(200); expect(page.headers.get("content-type")).toBe("text/html; charset=utf-8"); expect(await page.text()).toContain("/assets/ekklesia-display/app.js"); // 2. The display surface's own bootstrap resolves principal first, then the singular protected // descriptor. The browser constructs no artifact, API, or realtime URL itself. const bootstrap = await bootstrapDisplayHost(harness.config, "example"); expect(bootstrap.status).toBe("ready"); if (bootstrap.status !== "ready") { throw new Error("the authenticated display did not resolve"); } expect(bootstrap.descriptor).toEqual({ apiBaseUrl: "/api/v1/modules/example", contributions: [], displayPageUrl: "/display/example", id: "example", permissions: ["example.records.read", "example.display.view"], realtimeBaseUrl: "/api/v1/module-realtime/example", uiUrl: "/api/v1/module-ui/example/0.1.0/display", version: "0.1.0", }); // 3. Fetch the exact role-specific bytes from the real publication route, then genuinely import // those returned bytes. Reading the fixture file directly would not prove publication. const sourceUrl = moduleUiSourceUrl(harness.config, bootstrap.descriptor); expect(sourceUrl).toBe( "https://ekklesia.example.test/api/v1/module-ui/example/0.1.0/display", ); const artifactUrl = await publishedArtifactUrl(harness, sourceUrl); // 4. The generic loader receives the host-owned browser context, including the canonical // display page, and mounts the display artifact independently of the Dashboard artifact. const context = createModuleUiContext({ config: harness.config, descriptor: bootstrap.descriptor, principal: bootstrap.principal, }); expect(Object.keys(context).sort()).toEqual([ "api", "contributions", "display", "moduleId", "permissions", "principal", "realtime", ]); const container = containerSink(); const mounted = await loadModuleUi({ container: container as unknown as LoaderContainer, context, source: { moduleId: bootstrap.descriptor.id, url: artifactUrl, version: bootstrap.descriptor.version, }, }); expect(mountedText(container)).toBe("example:display:200:2"); expect(harness.moduleApiCalls).toEqual(["GET /api/v1/modules/example/records"]); await mounted.unmount(); expect(mountedText(container)).toBe(""); expect(mounted.mounted).toBe(false); } finally { await harness.stop(); } }); it("keeps server authorization authoritative when the UI ignores its own hint", async () => { // Signed in, but holding no grant for the module's restricted permission. const harness = await startHarness({ grants: [], principal: createAuthenticatedPrincipal(viewerId), }); try { const bootstrap = await bootstrapHost(harness.config); const descriptor = bootstrap.navigation.ungrouped[0]?.descriptor; if (!descriptor) { throw new Error("the inventory returned no module"); } // The hint is honest about it... expect(descriptor.permissions).toEqual([]); const context = createModuleUiContext({ config: harness.config, descriptor, principal: bootstrap.principal, }); expect(context.permissions.has("example.records.read")).toBe(false); // ...and the hint is not what protects the route. Calling anyway — exactly what a buggy or // hostile UI would do — is still refused by the server. const response = await context.api.request("records"); expect(response.status).toBe(403); expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" }); expect(harness.moduleApiCalls).toEqual(["GET /api/v1/modules/example/records"]); } finally { await harness.stop(); } }); it("renders the module's own denied state from the hint, without calling the API", async () => { const harness = await startHarness({ grants: [], principal: createAuthenticatedPrincipal(viewerId), }); try { const bootstrap = await bootstrapHost(harness.config); const descriptor = bootstrap.navigation.ungrouped[0]?.descriptor; if (!descriptor) { throw new Error("the inventory returned no module"); } const artifactUrl = await publishedArtifactUrl( harness, moduleUiSourceUrl(harness.config, descriptor), ); const container = containerSink(); await loadModuleUi({ container: container as unknown as LoaderContainer, context: createModuleUiContext({ config: harness.config, descriptor, principal: bootstrap.principal, }), source: { moduleId: descriptor.id, url: artifactUrl, version: descriptor.version }, }); // Hiding a control is a usability improvement: the module simply did not ask. expect(mountedText(container)).toBe("example:denied"); expect(harness.moduleApiCalls).toEqual([]); } finally { await harness.stop(); } }); it("renders a configured arrangement, and a move changes presentation only", async () => { const harness = await startHarness({ extraModules: [ { id: "video", name: "Video" }, { id: "calendar", name: "Calendar" }, ], grants: ["example.records.read"], platformOwner: true, principal: createAuthenticatedPrincipal(viewerId), }); const organize = (url: string, body: unknown) => (harness.config.fetch as typeof fetch)(`https://ekklesia.example.test${url}`, { body: JSON.stringify(body), headers: { "content-type": "application/json", origin: "https://dashboard.example.test" }, method: "PUT", }); try { // An administrator organizes the Dashboard, through the real mutation API. await organize("/api/v1/dashboard/navigation/groups/media", { label: "Media", order: 10 }); await organize("/api/v1/dashboard/navigation/groups/planning", { label: "Planning", order: 20, }); await organize("/api/v1/dashboard/navigation/placements/video", { groupId: "media", order: 10, }); await organize("/api/v1/dashboard/navigation/placements/example", { groupId: "media", order: 20, }); await organize("/api/v1/dashboard/navigation/placements/calendar", { groupId: "planning", order: 10, }); // The shell's own bootstrap sees exactly that arrangement. const before = await bootstrapHost(harness.config); expect( before.navigation.groups.map((group) => [ group.label, group.modules.map((entry) => entry.name), ]), ).toEqual([ ["Media", ["Video", "Example module"]], ["Planning", ["Calendar"]], ]); expect(before.navigation.ungrouped).toEqual([]); // Mounting from inside a group uses the descriptor the group carried, untouched. const groupedEntry = before.navigation.groups[0]?.modules[1]; if (!groupedEntry) { throw new Error("the arrangement did not contain the expected module"); } const container = containerSink(); const mounted = await loadModuleUi({ container: container as unknown as LoaderContainer, context: createModuleUiContext({ config: harness.config, descriptor: groupedEntry.descriptor, principal: before.principal, }), source: { moduleId: groupedEntry.descriptor.id, url: await publishedArtifactUrl( harness, moduleUiSourceUrl(harness.config, groupedEntry.descriptor), ), version: groupedEntry.descriptor.version, }, }); expect(mountedText(container)).toBe("example:200:2"); await mounted.unmount(); // Now move it somewhere else entirely. await organize("/api/v1/dashboard/navigation/groups/sunday-production", { label: "Sunday Production", order: 5, }); await organize("/api/v1/dashboard/navigation/placements/example", { groupId: "sunday-production", order: 10, }); const after = await bootstrapHost(harness.config); expect( after.navigation.groups.map((group) => [ group.label, group.modules.map((entry) => entry.name), ]), ).toEqual([ ["Sunday Production", ["Example module"]], ["Media", ["Video"]], ["Planning", ["Calendar"]], ]); const movedEntry = after.navigation.groups[0]?.modules[0]; if (!movedEntry) { throw new Error("the moved module was not found in its new group"); } // The whole point of the pass: the module is byte-identical across the move — same artifact // URL, same API root, same permissions, same version, same name. expect(movedEntry).toEqual(groupedEntry); // And it still mounts the same artifact and reaches the same API. const callsBeforeRemount = harness.moduleApiCalls.length; const movedContainer = containerSink(); const remounted = await loadModuleUi({ container: movedContainer as unknown as LoaderContainer, context: createModuleUiContext({ config: harness.config, descriptor: movedEntry.descriptor, principal: after.principal, }), source: { moduleId: movedEntry.descriptor.id, url: await publishedArtifactUrl( harness, moduleUiSourceUrl(harness.config, movedEntry.descriptor), ), version: movedEntry.descriptor.version, }, }); expect(mountedText(movedContainer)).toBe("example:200:2"); expect(harness.moduleApiCalls.slice(callsBeforeRemount)).toEqual([ "GET /api/v1/modules/example/records", ]); await remounted.unmount(); } finally { await harness.stop(); } }); it("keeps a newly installed module visible beside an existing arrangement", async () => { const harness = await startHarness({ extraModules: [{ id: "video", name: "Video" }], platformOwner: true, principal: createAuthenticatedPrincipal(viewerId), }); try { await (harness.config.fetch as typeof fetch)( "https://ekklesia.example.test/api/v1/dashboard/navigation/groups/media", { body: JSON.stringify({ label: "Media", order: 10 }), headers: { "content-type": "application/json", origin: "https://dashboard.example.test", }, method: "PUT", }, ); await (harness.config.fetch as typeof fetch)( "https://ekklesia.example.test/api/v1/dashboard/navigation/placements/video", { body: JSON.stringify({ groupId: "media", order: 10 }), headers: { "content-type": "application/json", origin: "https://dashboard.example.test", }, method: "PUT", }, ); const bootstrap = await bootstrapHost(harness.config); expect(bootstrap.navigation.groups[0]?.modules.map((entry) => entry.name)).toEqual(["Video"]); // `example` was never placed, and is still reachable rather than invisible. expect(bootstrap.navigation.ungrouped.map((entry) => entry.name)).toEqual(["Example module"]); } finally { await harness.stop(); } }); it("gives an anonymous visitor no inventory and therefore nothing to mount", async () => { const harness = await startHarness(); try { const bootstrap = await bootstrapHost(harness.config); expect(bootstrap.principal).toEqual({ kind: "anonymous" }); expect(bootstrap.navigation).toEqual({ groups: [], ungrouped: [] }); } finally { await harness.stop(); } }); it("refuses a module UI that tries to address another module's API through the host", async () => { const harness = await startHarness({ grants: ["example.records.read"], principal: createAuthenticatedPrincipal(viewerId), }); try { const bootstrap = await bootstrapHost(harness.config); const descriptor = bootstrap.navigation.ungrouped[0]?.descriptor; if (!descriptor) { throw new Error("the inventory returned no module"); } const context = createModuleUiContext({ config: harness.config, descriptor, principal: bootstrap.principal, }); await expect(context.api.request("../../grants?userId=someone")).rejects.toMatchObject({ code: "MODULE_API_PATH_INVALID", }); // It never became a request at all. expect(harness.moduleApiCalls).toEqual([]); } finally { await harness.stop(); } }); });