import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { createModuleUiPublicationRegistry, type DiscoveredModule, discoverModules, } from "../src/modules/index.js"; const temporaryDirectories: string[] = []; afterEach(async () => { await Promise.all( temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true })), ); }); async function createModulesDirectory(): Promise { const directory = await mkdtemp(path.join(tmpdir(), "ekklesia-ui-publication-")); temporaryDirectories.push(directory); return directory; } interface ModuleFixture { readonly id: string; readonly version?: string; /** Omitted entirely for a backend-only module, so the manifest declares no `entrypoints.ui`. */ readonly ui?: string; /** A dedicated full-viewport artifact, independent of the primary UI. */ readonly display?: string; /** Capabilities this module provides — what another module's contributions can target. */ readonly capabilities?: readonly { readonly id: string; readonly version: string }[]; /** Browser renderers this module contributes to another module's capability. */ readonly contributions?: readonly { readonly localName: string; readonly version: string; readonly capability: string; readonly capabilityRange: string; }[]; } async function createModule(modulesDirectory: string, fixture: ModuleFixture): Promise { const version = fixture.version ?? "0.1.0"; const displayPermission = `${fixture.id}.display.view`; const moduleDirectory = path.join(modulesDirectory, fixture.id); await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true }); await writeFile( path.join(moduleDirectory, "package.json"), JSON.stringify({ ekklesia: { layer: "module" }, name: `@test/module-${fixture.id}`, type: "module", version, }), ); 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", ...(fixture.ui === undefined ? {} : { ui: "./dist/ui/index.js" }), ...(fixture.display === undefined ? {} : { display: { artifact: "./dist/display/index.js", permission: displayPermission, }, }), }, capabilities: fixture.capabilities ?? [], contributions: (fixture.contributions ?? []).map((contribution) => ({ artifact: `./dist/contributions/${contribution.localName}/index.js`, capability: contribution.capability, capabilityRange: contribution.capabilityRange, id: `${fixture.id}.${contribution.localName}`, version: contribution.version, })), id: fixture.id, manifestVersion: 7, name: `${fixture.id} module`, permissions: fixture.display === undefined ? [] : [ { baseline: "restricted", description: `View ${fixture.id}'s display`, id: displayPermission, }, ], version, }), ); await writeFile( path.join(moduleDirectory, "dist/server/index.js"), `export default { id: ${JSON.stringify(fixture.id)}, version: ${JSON.stringify(version)} };`, ); if (fixture.ui !== undefined) { await mkdir(path.join(moduleDirectory, "dist/ui"), { recursive: true }); await writeFile(path.join(moduleDirectory, "dist/ui/index.js"), fixture.ui); } if (fixture.display !== undefined) { await mkdir(path.join(moduleDirectory, "dist/display"), { recursive: true }); await writeFile(path.join(moduleDirectory, "dist/display/index.js"), fixture.display); } for (const contribution of fixture.contributions ?? []) { const contributionDirectory = path.join( moduleDirectory, "dist/contributions", contribution.localName, ); await mkdir(contributionDirectory, { recursive: true }); await writeFile(path.join(contributionDirectory, "index.js"), "export default {};"); } } /** Real discovery, never a hand-built `DiscoveredModule`: the whole point of the registry is that * it consumes only records that already passed manifest validation, compiled-artifact validation, * and package-containment/symlink resolution. */ async function discover(fixtures: readonly ModuleFixture[]): Promise { const modulesDirectory = await createModulesDirectory(); for (const fixture of fixtures) { await createModule(modulesDirectory, fixture); } return discoverModules({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }); } describe("module UI publication registry", () => { it("publishes a discovered module that declares entrypoints.ui", async () => { const modules = await discover([{ id: "announcements", ui: "export default {};" }]); const registry = createModuleUiPublicationRegistry(modules); const published = registry.resolve("announcements", "0.1.0"); expect(published).toMatchObject({ moduleId: "announcements", version: "0.1.0" }); expect(registry.list()).toHaveLength(1); }); it("exposes exactly the canonical artifact path discovery already validated", async () => { const modules = await discover([{ id: "announcements", ui: "export default {};" }]); const discovered = modules[0] as DiscoveredModule; const registry = createModuleUiPublicationRegistry(modules); // Identical to `entrypoints.ui` — not recomposed from the module directory plus a convention, // which is the failure mode a publisher must never reintroduce. expect(registry.resolve("announcements", "0.1.0")?.artifactPath).toBe( discovered.entrypoints.ui, ); expect(path.isAbsolute(discovered.entrypoints.ui as string)).toBe(true); }); it("omits a discovered module that declares no UI entrypoint", async () => { const modules = await discover([{ id: "calendar" }]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.list()).toEqual([]); expect(registry.resolve("calendar", "0.1.0")).toBeUndefined(); }); it("publishes only the modules that declare UI, from a mixed installation", async () => { const modules = await discover([ { id: "announcements", ui: "export default {};" }, { id: "calendar" }, { id: "signage", ui: "export default {};" }, ]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.list().map((entry) => entry.moduleId)).toEqual(["announcements", "signage"]); }); it("never resolves a module that is not part of the discovered set", async () => { const modules = await discover([{ id: "announcements", ui: "export default {};" }]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.resolve("calendar", "0.1.0")).toBeUndefined(); expect(registry.resolve("", "0.1.0")).toBeUndefined(); }); it("refuses a version other than the exact installed one", async () => { const modules = await discover([ { id: "announcements", ui: "export default {};", version: "1.4.2" }, ]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.resolve("announcements", "1.4.2")).toBeDefined(); for (const version of ["1.4.1", "1.4.3", "1.4", "^1.4.2", "latest", "1.4.2 ", ""]) { expect(registry.resolve("announcements", version)).toBeUndefined(); } }); it("fails closed on non-string lookup input rather than trusting its caller", async () => { const modules = await discover([{ id: "announcements", ui: "export default {};" }]); const registry = createModuleUiPublicationRegistry(modules); for (const value of [undefined, null, 0, {}, [], Symbol("announcements")]) { expect(registry.resolve(value, "0.1.0")).toBeUndefined(); expect(registry.resolve("announcements", value)).toBeUndefined(); } }); it("is an empty, usable registry when nothing is installed", () => { const registry = createModuleUiPublicationRegistry([]); expect(registry.list()).toEqual([]); expect(registry.listDisplays()).toEqual([]); expect(registry.resolve("announcements", "0.1.0")).toBeUndefined(); expect(registry.resolveDisplay("announcements", "0.1.0")).toBeUndefined(); }); it("publishes an immutable snapshot that no caller can extend", async () => { const modules = await discover([{ id: "announcements", ui: "export default {};" }]); const registry = createModuleUiPublicationRegistry(modules); expect(Object.isFrozen(registry)).toBe(true); expect(Object.isFrozen(registry.list())).toBe(true); expect(Object.isFrozen(registry.list()[0])).toBe(true); // The same array identity every time: nothing recomputes or re-reads the filesystem per call. expect(registry.list()).toBe(registry.list()); }); it("never reads or evaluates the artifact it publishes", async () => { // A top-level throw would surface immediately if anything imported this file. Registry // construction and resolution are pure metadata operations. const modules = await discover([ { id: "announcements", ui: 'throw new Error("module UI must never run on the server");' }, ]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.resolve("announcements", "0.1.0")?.artifactPath).toBeDefined(); }); }); describe("module display UI publication registry", () => { it("keeps primary UI and display publication independent for display-only, both, and neither", async () => { const modules = await discover([ { display: "export default { role: 'display' };", id: "announcements" }, { display: "export default { role: 'display' };", id: "signage", ui: "export default { role: 'primary' };", }, { id: "calendar" }, ]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.list().map((entry) => entry.moduleId)).toEqual(["signage"]); expect(registry.listDisplays().map((entry) => entry.moduleId)).toEqual([ "announcements", "signage", ]); expect(registry.resolve("announcements", "0.1.0")).toBeUndefined(); expect(registry.resolveDisplay("calendar", "0.1.0")).toBeUndefined(); const signage = registry.resolveDisplay("signage", "0.1.0"); expect(signage).toMatchObject({ displayPermission: "signage.display.view", moduleId: "signage", name: "signage module", version: "0.1.0", }); expect(signage?.artifactPath).not.toBe(registry.resolve("signage", "0.1.0")?.artifactPath); }); it("carries only discovery's canonical display artifact and declared permission metadata", async () => { const modules = await discover([ { display: 'throw new Error("display must remain inert");', id: "announcements" }, ]); const discovered = modules[0] as DiscoveredModule; const registry = createModuleUiPublicationRegistry(modules); const published = registry.resolveDisplay("announcements", "0.1.0"); expect(published?.artifactPath).toBe(discovered.entrypoints.display?.artifactPath); expect(path.isAbsolute(published?.artifactPath as string)).toBe(true); expect(published?.displayPermission).toBe(discovered.entrypoints.display?.permission); expect(published?.permissions.map((permission) => permission.id)).toEqual([ "announcements.display.view", ]); }); it("requires exact identity and fails closed on malformed lookup values", async () => { const modules = await discover([ { display: "export default {};", id: "announcements", version: "1.4.2" }, ]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.resolveDisplay("announcements", "1.4.2")).toBeDefined(); for (const version of ["1.4.1", "latest", "^1.4.2", "1.4.2 ", ""]) { expect(registry.resolveDisplay("announcements", version)).toBeUndefined(); } for (const value of [undefined, null, 0, {}, [], Symbol("announcements")]) { expect(registry.resolveDisplay(value, "1.4.2")).toBeUndefined(); expect(registry.resolveDisplay("announcements", value)).toBeUndefined(); } }); it("publishes a frozen snapshot and cannot expose a module omitted from its eligible input", async () => { const modules = await discover([ { display: "export default {};", id: "announcements" }, { display: "export default {};", id: "signage" }, ]); // Production passes Core's eligible/started set. Omitting a blocked module at this boundary is // sufficient to make both its metadata and artifact structurally unresolvable. const registry = createModuleUiPublicationRegistry( modules.filter((module) => module.manifest.id === "signage"), ); expect(registry.resolveDisplay("announcements", "0.1.0")).toBeUndefined(); expect(registry.listDisplays().map((entry) => entry.moduleId)).toEqual(["signage"]); expect(Object.isFrozen(registry.listDisplays())).toBe(true); expect(Object.isFrozen(registry.listDisplays()[0])).toBe(true); expect(Object.isFrozen(registry.listDisplays()[0]?.permissions)).toBe(true); expect(registry.listDisplays()).toBe(registry.listDisplays()); }); }); /** * The generic contribution mechanism (ADR 0041), exercised through the exact relationship it exists * for and with no platform knowledge of what any of it means: `signage` provides a capability, * `announcements` contributes a renderer to it, and neither module imports the other. */ describe("module contribution publication", () => { const signage = { capabilities: [{ id: "signage.slide-renderer", version: "1.2.0" }], id: "signage", }; const contributor = (capabilityRange: string) => ({ contributions: [ { capability: "signage.slide-renderer", capabilityRange, localName: "signage-slide", version: "1.0.0", }, ], id: "announcements", }); it("publishes a contribution whose target capability is provided within range", async () => { const modules = await discover([signage, contributor("^1.0.0")]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.listContributions()).toHaveLength(1); expect( registry.resolveContribution("announcements", "0.1.0", "announcements.signage-slide"), ).toMatchObject({ capability: "signage.slide-renderer", // The provider's declared version, so the range was checked against something real. capabilityVersion: "1.2.0", contributionId: "announcements.signage-slide", moduleId: "announcements", version: "1.0.0", }); }); it("serves exactly the canonical artifact path discovery validated", async () => { const modules = await discover([signage, contributor("^1.0.0")]); const announcements = modules.find((module) => module.manifest.id === "announcements"); const registry = createModuleUiPublicationRegistry(modules); const published = registry.resolveContribution( "announcements", "0.1.0", "announcements.signage-slide", ); expect(published?.artifactPath).toBe(announcements?.contributions[0]?.artifactPath); expect(path.isAbsolute(published?.artifactPath ?? "")).toBe(true); }); it("withholds a contribution whose capability no installed module provides", async () => { const modules = await discover([contributor("^1.0.0")]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.listContributions()).toEqual([]); expect( registry.resolveContribution("announcements", "0.1.0", "announcements.signage-slide"), ).toBeUndefined(); }); it("withholds a contribution whose provider is outside its declared compatible range", async () => { // Fails closed: an incompatible renderer is absent rather than mounted and broken. const modules = await discover([signage, contributor("^2.0.0")]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.listContributions()).toEqual([]); expect(registry.contributionsForModule("signage")).toEqual([]); }); it("gives a consuming module exactly the contributions aimed at its own capabilities", async () => { const modules = await discover([ signage, { capabilities: [{ id: "calendar.week-view", version: "1.0.0" }], id: "calendar" }, contributor("^1.0.0"), ]); const registry = createModuleUiPublicationRegistry(modules); expect(registry.contributionsForModule("signage").map((entry) => entry.contributionId)).toEqual( ["announcements.signage-slide"], ); // A module that provides a different capability sees nothing, and the *contributing* module // does not see its own contribution: it provides no capability it was aimed at. expect(registry.contributionsForModule("calendar")).toEqual([]); expect(registry.contributionsForModule("announcements")).toEqual([]); }); it("fails a lookup closed for an unknown, mismatched, or non-string identity", async () => { const modules = await discover([signage, contributor("^1.0.0")]); const registry = createModuleUiPublicationRegistry(modules); expect( registry.resolveContribution("announcements", "9.9.9", "announcements.signage-slide"), ).toBeUndefined(); expect( registry.resolveContribution("signage", "0.1.0", "announcements.signage-slide"), ).toBeUndefined(); expect( registry.resolveContribution("announcements", "0.1.0", "announcements.nope"), ).toBeUndefined(); expect(registry.resolveContribution(7, "0.1.0", "announcements.signage-slide")).toBeUndefined(); expect(registry.contributionsForCapability(undefined)).toEqual([]); }); it("carries no domain vocabulary at all", async () => { const modules = await discover([signage, contributor("^1.0.0")]); const registry = createModuleUiPublicationRegistry(modules); const published = registry.listContributions()[0]; // Core knows identity, version, compatibility, and one path. There is deliberately no payload // schema, slot, ordering, or rendering hint — what a renderer draws is not the platform's. expect(Object.keys(published ?? {}).sort()).toEqual([ "artifactPath", "capability", "capabilityVersion", "contributionId", "moduleId", "moduleVersion", "version", ]); }); });