import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import type { ModuleDatabase, ModuleRealtimePublisher, ModuleRuntimeServices, } from "@ekklesia/contracts"; import { afterEach, describe, expect, it } from "vitest"; import { ModuleRuntimeError, startModuleRuntime } from "../src/modules/runtime.js"; const temporaryDirectories: string[] = []; const globalSymbols: symbol[] = []; afterEach(async () => { const globals = globalThis as unknown as Record; for (const symbol of globalSymbols.splice(0)) { delete globals[symbol]; } await Promise.all( temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true })), ); }); function fakeServices(): ModuleRuntimeServices { return Object.freeze({ database: Object.freeze({}) as ModuleDatabase }); } function manifest( id: string, permissions: readonly Record[] = [], ): Record { return { manifestVersion: 8, id, name: `${id} module`, version: "0.1.0", compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0" }, permissions, entrypoints: { server: "./dist/server/index.js" }, }; } async function createRuntimeFixture( id: string, serverSource: string, permissions: readonly Record[] = [], ): Promise { const modulesDirectory = await mkdtemp(path.join(tmpdir(), "ekklesia-realtime-runtime-")); temporaryDirectories.push(modulesDirectory); const moduleDirectory = path.join(modulesDirectory, id); await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true }); await Promise.all([ writeFile( path.join(moduleDirectory, "package.json"), JSON.stringify({ name: `@test/module-${id}`, version: "0.1.0", type: "module", ekklesia: { layer: "module" }, }), ), writeFile( path.join(moduleDirectory, "ekklesia.module.json"), JSON.stringify(manifest(id, permissions)), ), writeFile(path.join(moduleDirectory, "dist/server/index.js"), serverSource), ]); return modulesDirectory; } function runtimeOptions(modulesDirectory: string) { return { moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", services: fakeServices(), }; } function recorder(): { readonly expression: string; readonly value: { events: string[]; publishers: ModuleRealtimePublisher[] }; } { const symbol = Symbol.for(`ekklesia.realtime.${Date.now()}.${Math.random()}`); const value = { events: [] as string[], publishers: [] as ModuleRealtimePublisher[] }; (globalThis as unknown as Record)[symbol] = value; globalSymbols.push(symbol); return { expression: `globalThis[Symbol.for(${JSON.stringify(Symbol.keyFor(symbol))})]`, value }; } describe("module runtime realtime lifecycle", () => { it("stages declarations, requires startup state, delivers live state, and closes before stop", async () => { const recorded = recorder(); const objectUrl = "https://delivery.invalid/slides/welcome.png?signature=unchanged"; const modulesDirectory = await createRuntimeFixture( "announcements", ` export default { id: "announcements", version: "0.1.0", register({ realtime }) { realtime.channel({ channel: "state", permission: "announcements.slideshow.view" }); ${recorded.expression}.events.push("register"); }, start({ realtime }) { ${recorded.expression}.publishers.push(realtime); realtime.publish("state", { currentSlide: 0, previousSlide: -1, slides: [${JSON.stringify(objectUrl)}], slideSeconds: 7, mode: "on" }); ${recorded.expression}.events.push("start"); }, stop({ realtime }) { try { realtime.publish("state", { mode: "off" }); } catch (error) { ${recorded.expression}.events.push("stop:" + error.code); } } }; `, [ { id: "announcements.slideshow.view", baseline: "restricted", description: "View the announcement slideshow.", }, ], ); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect(recorded.value.events).toEqual(["register", "start"]); expect(runtime.realtime.channels).toEqual([ { moduleId: "announcements", channel: "state", permission: "announcements.slideshow.view", }, ]); const subscription = runtime.realtime.subscribe("announcements", "state"); expect(subscription.snapshot.sequence).toBe(1); expect(subscription.snapshot.serializedPayload).toContain(objectUrl); const next = subscription.read(); recorded.value.publishers[0]?.publish("state", { currentSlide: 1, previousSlide: 0, slides: [objectUrl], slideSeconds: 7, mode: "on", }); await expect(next).resolves.toMatchObject({ sequence: 2 }); const pendingClose = subscription.read(); await runtime.stop(); await expect(pendingClose).resolves.toBeUndefined(); expect(recorded.value.events).toEqual(["register", "start", "stop:REALTIME_PUBLISHER_STOPPED"]); expect(() => recorded.value.publishers[0]?.publish("state", null)).toThrowError( expect.objectContaining({ code: "REALTIME_PUBLISHER_STOPPED" }), ); }); it("fails atomically and rolls back when a declared channel has no initial snapshot", async () => { const recorded = recorder(); const modulesDirectory = await createRuntimeFixture( "announcements", ` export default { id: "announcements", version: "0.1.0", register({ realtime }) { realtime.channel({ channel: "state", permission: "announcements.slideshow.view" }); }, start() { ${recorded.expression}.events.push("start"); }, stop() { ${recorded.expression}.events.push("rollback"); } }; `, [ { id: "announcements.slideshow.view", baseline: "restricted", description: "View the announcement slideshow.", }, ], ); await expect(startModuleRuntime(runtimeOptions(modulesDirectory))).rejects.toMatchObject({ issues: [expect.objectContaining({ code: "MODULE_REALTIME_SNAPSHOT_MISSING" })], }); expect(recorded.value.events).toEqual(["start", "rollback"]); }); it("reports invalid owned-permission registration with a deterministic sanitized issue", async () => { const secret = "/machine/private/permission-token"; const modulesDirectory = await createRuntimeFixture( "announcements", ` export default { id: "announcements", version: "0.1.0", register({ realtime }) { realtime.channel({ channel: "state", permission: "announcements.not-owned" }); } }; // ${secret} `, ); let failure: unknown; try { await startModuleRuntime(runtimeOptions(modulesDirectory)); } catch (error) { failure = error; } expect(failure).toBeInstanceOf(ModuleRuntimeError); expect(failure).toMatchObject({ issues: [expect.objectContaining({ code: "MODULE_REALTIME_PERMISSION_UNDECLARED" })], }); expect(JSON.stringify(failure)).not.toContain(secret); }); it("starts and stops modules that declare no realtime channels unchanged", async () => { const recorded = recorder(); const modulesDirectory = await createRuntimeFixture( "calendar", ` export default { id: "calendar", version: "0.1.0", start() { ${recorded.expression}.events.push("start"); }, stop() { ${recorded.expression}.events.push("stop"); } }; `, ); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect(runtime.realtime.channels).toEqual([]); await runtime.stop(); expect(recorded.value.events).toEqual(["start", "stop"]); }); });