feat: add display kiosk host
This commit is contained in:
@@ -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());
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user