feat: add display kiosk host
CI / verify (pull_request) Successful in 38s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Successful in 59s

This commit is contained in:
ekklesia
2026-08-23 20:50:56 -05:00
parent be5ed7853d
commit 2e0fe059c3
80 changed files with 4868 additions and 397 deletions
+111 -1
View File
@@ -27,10 +27,13 @@ interface ModuleFixture {
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;
}
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(
@@ -49,10 +52,28 @@ async function createModule(modulesDirectory: string, fixture: ModuleFixture): P
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,
},
}),
},
id: fixture.id,
manifestVersion: 5,
manifestVersion: 6,
name: `${fixture.id} module`,
permissions:
fixture.display === undefined
? []
: [
{
baseline: "restricted",
description: `View ${fixture.id}'s display`,
id: displayPermission,
},
],
version,
}),
);
@@ -64,6 +85,10 @@ async function createModule(modulesDirectory: string, fixture: ModuleFixture): P
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);
}
}
/** Real discovery, never a hand-built `DiscoveredModule`: the whole point of the registry is that
@@ -158,7 +183,9 @@ describe("module UI publication registry", () => {
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 () => {
@@ -184,3 +211,86 @@ describe("module UI publication registry", () => {
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());
});
});