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 { ModuleDiscoveryError, ModuleRuntimeError, type ModuleRuntimeIssueCode, startModuleRuntime, } from "../src/modules/index.js"; interface RuntimeModuleFixture { readonly directory?: string; readonly manifest?: Record; readonly packageJson?: Record; readonly serverSource: string; } const temporaryDirectories: string[] = []; const eventSymbols: symbol[] = []; afterEach(async () => { const globalValues = globalThis as unknown as Record; for (const symbol of eventSymbols.splice(0)) { delete globalValues[symbol]; } await Promise.all( temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true })), ); }); async function createModulesDirectory(): Promise { const directory = await mkdtemp(path.join(tmpdir(), "ekklesia-runtime-")); temporaryDirectories.push(directory); return directory; } function manifestFor( id: string, options: { capabilities?: Array<{ id: string; version: string }>; dependencies?: Record; } = {}, ): Record { return { manifestVersion: 2, id, name: `${id} module`, version: "0.1.0", compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0", }, capabilities: options.capabilities ?? [], permissions: [], ...(options.dependencies ? { dependencies: options.dependencies } : {}), entrypoints: { server: "./dist/server/index.js", }, }; } async function createRuntimeModule( modulesDirectory: string, id: string, fixture: RuntimeModuleFixture, ): Promise { const directoryName = fixture.directory ?? id; const moduleDirectory = path.join(modulesDirectory, directoryName); await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true }); await Promise.all([ writeFile( path.join(moduleDirectory, "package.json"), JSON.stringify( fixture.packageJson ?? { name: `@test/module-${id}`, version: "0.1.0", type: "module", ekklesia: { layer: "module" }, }, ), ), writeFile( path.join(moduleDirectory, "ekklesia.module.json"), JSON.stringify(fixture.manifest ?? manifestFor(id)), ), writeFile(path.join(moduleDirectory, "dist/server/index.js"), fixture.serverSource), ]); } function createEventRecorder(): { events: string[]; expression: string } { const key = `ekklesia.runtime.test.${eventSymbols.length}.${Date.now()}.${Math.random()}`; const symbol = Symbol.for(key); const events: string[] = []; (globalThis as unknown as Record)[symbol] = events; eventSymbols.push(symbol); return { events, expression: `globalThis[Symbol.for(${JSON.stringify(key)})]`, }; } function runtimeOptions(modulesDirectory: string) { return { moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }; } function runtimeIssueCodes(error: ModuleRuntimeError): ModuleRuntimeIssueCode[] { return error.issues.map((issue) => issue.code); } async function captureRuntimeError(modulesDirectory: string): Promise { try { await startModuleRuntime(runtimeOptions(modulesDirectory)); } catch (error) { if (error instanceof ModuleRuntimeError) { return error; } throw error; } throw new Error("Expected module runtime startup to fail."); } describe("module runtime", () => { it("discovers, loads, registers, starts, and stops dependency-first modules end to end", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "people", { manifest: manifestFor("people", { capabilities: [{ id: "people.records", version: "2.1.0" }], }), serverSource: ` export default { id: "people", version: "0.1.0", register({ capabilities }) { ${recorder.expression}.push("people:register"); capabilities.provide("people.records", { kind: "people-records" }); }, start() { ${recorder.expression}.push("people:start"); }, stop() { ${recorder.expression}.push("people:stop"); } }; `, }); await createRuntimeModule(modulesDirectory, "calendar", { manifest: manifestFor("calendar", { capabilities: [{ id: "calendar.events", version: "1.0.0" }], dependencies: { capabilities: { required: { "people.records": "^2.0.0" } }, }, }), serverSource: ` export default { id: "calendar", version: "0.1.0", register({ capabilities }) { ${recorder.expression}.push("calendar:register"); const people = capabilities.resolve("people.records"); if (!people || people.version !== "2.1.0") throw new Error("people unavailable"); capabilities.provide("calendar.events", { kind: "calendar-events" }); }, start({ capabilities }) { if (!capabilities.resolve("people.records")) throw new Error("people unavailable"); ${recorder.expression}.push("calendar:start"); }, stop() { ${recorder.expression}.push("calendar:stop"); } }; `, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect(recorder.events).toEqual([ "people:register", "calendar:register", "people:start", "calendar:start", ]); expect(runtime.modules.map((module) => module.id)).toEqual(["people", "calendar"]); expect(runtime.capabilities.resolve("people.records")).toMatchObject({ moduleId: "people", version: "2.1.0", }); expect(runtime.capabilities.list().map((capability) => capability.id)).toEqual([ "calendar.events", "people.records", ]); await Promise.all([runtime.stop(), runtime.stop()]); expect(recorder.events.slice(-2)).toEqual(["calendar:stop", "people:stop"]); }); it("completes discovery before importing any server entrypoint", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "valid", { serverSource: `${recorder.expression}.push("valid:import"); export default { id: "valid", version: "0.1.0" };`, }); await createRuntimeModule(modulesDirectory, "invalid", { manifest: { ...manifestFor("invalid"), manifestVersion: 1 }, serverSource: `${recorder.expression}.push("invalid:import"); export default { id: "invalid", version: "0.1.0" };`, }); await expect(startModuleRuntime(runtimeOptions(modulesDirectory))).rejects.toBeInstanceOf( ModuleDiscoveryError, ); expect(recorder.events).toEqual([]); }); it("validates every imported definition before running registration hooks", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "alpha", { serverSource: `export default { id: "alpha", version: "0.1.0", register() { ${recorder.expression}.push("alpha:register"); } };`, }); await createRuntimeModule(modulesDirectory, "beta", { serverSource: `export default { id: "wrong-id", version: "0.1.0" };`, }); await createRuntimeModule(modulesDirectory, "gamma", { serverSource: `export default { id: "gamma", version: "9.0.0" };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual([ "MODULE_RUNTIME_ID_MISMATCH", "MODULE_RUNTIME_VERSION_MISMATCH", ]); expect(recorder.events).toEqual([]); }); it("rejects incomplete and undeclared capability registration before startup", async () => { const missingDirectory = await createModulesDirectory(); await createRuntimeModule(missingDirectory, "missing", { manifest: manifestFor("missing", { capabilities: [{ id: "missing.records", version: "1.0.0" }], }), serverSource: `export default { id: "missing", version: "0.1.0", register() {} };`, }); let error = await captureRuntimeError(missingDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_CAPABILITY_UNREGISTERED"]); const undeclaredDirectory = await createModulesDirectory(); await createRuntimeModule(undeclaredDirectory, "undeclared", { serverSource: `export default { id: "undeclared", version: "0.1.0", register({ capabilities }) { capabilities.provide("undeclared.extra", {}); } };`, }); error = await captureRuntimeError(undeclaredDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_CAPABILITY_UNDECLARED"]); }); it("restricts module capability lookup to declared ownership and dependencies", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "snooper", { serverSource: `export default { id: "snooper", version: "0.1.0", register({ capabilities }) { capabilities.resolve("people.records"); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_CAPABILITY_ACCESS_UNDECLARED"]); }); it("rejects malformed runtime capability IDs without echoing their contents", async () => { const modulesDirectory = await createModulesDirectory(); const unsafeCapabilityId = "secret-token at /machine/private/path"; await createRuntimeModule(modulesDirectory, "unsafe", { serverSource: `export default { id: "unsafe", version: "0.1.0", register({ capabilities }) { capabilities.resolve(${JSON.stringify(unsafeCapabilityId)}); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_CAPABILITY_ID_INVALID"]); expect(`${error.message}\n${error.stack}\n${JSON.stringify(error.issues)}`).not.toContain( unsafeCapabilityId, ); expect(error.stack).not.toContain(modulesDirectory); }); it("rolls back started modules in reverse order and aggregates cleanup failures", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); for (const [id, dependency, stopFails] of [ ["one", undefined, false], ["two", "one", true], ["three", "two", false], ] as const) { await createRuntimeModule(modulesDirectory, id, { manifest: manifestFor(id, { ...(dependency ? { dependencies: { modules: { required: { [dependency]: "^0.1.0" } } } } : {}), }), serverSource: `export default { id: ${JSON.stringify(id)}, version: "0.1.0", start() { ${recorder.expression}.push(${JSON.stringify(`${id}:start`)}); ${id === "three" ? 'throw new Error("secret-token at /private/path");' : ""} }, stop() { ${recorder.expression}.push(${JSON.stringify(`${id}:stop`)}); ${stopFails ? 'throw new Error("rollback secret");' : ""} } };`, }); } const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_START_FAILED", "MODULE_ROLLBACK_FAILED"]); expect(recorder.events).toEqual([ "one:start", "two:start", "three:start", "two:stop", "one:stop", ]); expect(JSON.stringify(error.issues)).not.toContain("secret"); expect(JSON.stringify(error.issues)).not.toContain("/private/path"); }); it("continues reverse shutdown after a module stop failure", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "provider", { serverSource: `export default { id: "provider", version: "0.1.0", stop() { ${recorder.expression}.push("provider:stop"); } };`, }); await createRuntimeModule(modulesDirectory, "consumer", { manifest: manifestFor("consumer", { dependencies: { modules: { required: { provider: "^0.1.0" } } }, }), serverSource: `export default { id: "consumer", version: "0.1.0", stop() { ${recorder.expression}.push("consumer:stop"); throw new Error("hidden"); } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); await expect(runtime.stop()).rejects.toMatchObject({ issues: [expect.objectContaining({ code: "MODULE_STOP_FAILED", moduleId: "consumer" })], }); expect(recorder.events).toEqual(["consumer:stop", "provider:stop"]); }); it("reports import failures without leaking exception text or machine paths", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "broken", { serverSource: `throw new Error("database-password at /machine/private/path");`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_RUNTIME_IMPORT_FAILED"]); expect(JSON.stringify(error.issues)).not.toContain("database-password"); expect(JSON.stringify(error.issues)).not.toContain(modulesDirectory); }); });