Files
Ekklesia/core/tests/module-ui-publication.test.ts
ekklesia 49b4d4165d
CI / verify (pull_request) Successful in 1m0s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 23s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s
feat: deliver module database ABI by manifest
2026-10-07 17:29:32 -05:00

448 lines
18 KiB
TypeScript

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<string> {
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<void> {
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: 8,
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<readonly DiscoveredModule[]> {
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",
]);
});
});