Files
Ekklesia/core/tests/module-realtime-runtime.test.ts
T
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

241 lines
8.0 KiB
TypeScript

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<PropertyKey, unknown>;
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<string, string>[] = [],
): Record<string, unknown> {
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<string, string>[] = [],
): Promise<string> {
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<PropertyKey, unknown>)[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"]);
});
});