// @vitest-environment happy-dom import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { buildModuleUi } from "@ekklesia/module-ui-build"; import type { ModuleUiHostContext } from "@ekklesia/module-ui-sdk"; import { beforeAll, describe, expect, it } from "vitest"; import { loadModuleUi } from "../src/index.js"; const fixtureDirectory = path.join( path.dirname(fileURLToPath(import.meta.url)), "fixtures", "artifact-module", ); let artifactUrl: string; let artifactSource: string; /** An Ekklesia-shaped host: module routes live under a Core-owned prefix. */ function ekklesiaContext(requested: string[]): ModuleUiHostContext { return { moduleId: "example", api: { async request(pathname) { requested.push(`/api/v1/modules/example/${pathname.replace(/^\//u, "")}`); return new Response(null, { status: 200 }); }, }, principal: { kind: "authenticated", userId: "user-123" }, permissions: { has: () => true }, realtime: { subscribe: () => ({ close: () => undefined }) }, }; } /** A standalone host: one module, a different origin, a completely different URL shape, no Core. */ function standaloneContext(requested: string[]): ModuleUiHostContext { return { moduleId: "example", api: { async request(pathname) { requested.push(`https://example.test/api/${pathname.replace(/^\//u, "")}`); return new Response(null, { status: 200 }); }, }, principal: { kind: "anonymous" }, permissions: { has: () => false }, realtime: { subscribe: () => ({ close: () => undefined }) }, }; } /** * Proves the loader consumes exactly the browser ESM artifact `@ekklesia/module-ui-build` produces * (ADR 0023) — no second output format, no checked-in build output. The artifact is rebuilt from * source each run and served through a `data:` URL, a real module URL rather than a filesystem path. */ describe("loading a real compiled artifact", () => { beforeAll(async () => { const result = await buildModuleUi({ moduleDirectory: fixtureDirectory }); artifactSource = await readFile(result.outputFile, "utf8"); artifactUrl = `data:text/javascript;base64,${Buffer.from(artifactSource, "utf8").toString("base64")}`; }, 60_000); it("produces a self-contained artifact with no bare imports for a host to resolve", () => { expect(artifactSource).not.toMatch(/^import\s.*from\s*["'][^.]/mu); expect(artifactSource).toContain("export {"); expect(artifactSource).not.toContain("/api/v1/modules"); }); it("mounts the real artifact through the generic loader and unmounts it", async () => { const container = document.createElement("div"); const requested: string[] = []; const mounted = await loadModuleUi({ source: { moduleId: "example", version: "0.1.0", url: artifactUrl }, container, context: ekklesiaContext(requested), }); expect(container.textContent).toBe("example:200:user-123"); await mounted.unmount(); expect(container.textContent).toBe(""); expect(mounted.mounted).toBe(false); }); it("still rejects the real artifact when the host expects a different version", async () => { await expect( loadModuleUi({ source: { moduleId: "example", version: "0.2.0", url: artifactUrl }, container: document.createElement("div"), context: ekklesiaContext([]), }), ).rejects.toMatchObject({ code: "MODULE_UI_IDENTITY_MISMATCH" }); }); it("runs the same artifact under an Ekklesia host and a standalone host", async () => { const ekklesiaRequests: string[] = []; const standaloneRequests: string[] = []; const ekklesiaContainer = document.createElement("div"); const standaloneContainer = document.createElement("div"); const source = { moduleId: "example", version: "0.1.0", url: artifactUrl }; const inEkklesia = await loadModuleUi({ source, container: ekklesiaContainer, context: ekklesiaContext(ekklesiaRequests), }); const inStandalone = await loadModuleUi({ source, container: standaloneContainer, context: standaloneContext(standaloneRequests), }); // One artifact, one loader, two hosts. Every difference lives in the context the host supplied: // the module asked for "records" both times and resolved nothing itself. expect(ekklesiaRequests).toEqual(["/api/v1/modules/example/records"]); expect(standaloneRequests).toEqual(["https://example.test/api/records"]); expect(ekklesiaContainer.textContent).toBe("example:200:user-123"); expect(standaloneContainer.textContent).toBe("example:200:guest"); await inEkklesia.unmount(); await inStandalone.unmount(); }); });