import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import type { ModuleConfiguration, ModuleDatabaseExecutor } from "@ekklesia/contracts"; import type { AnyModuleRuntimeServices, LegacyModuleRuntimeServices, ModuleDatabase, } from "../src/modules/database.js"; import { afterEach, describe, expect, it, vi } from "vitest"; import { discoverModules } from "../src/modules/discovery.js"; import { ModuleDiscoveryError, ModuleRuntimeError, type ModuleRuntimeIssueCode, type ModuleRuntimeServicesLookup, startDiscoveredModuleRuntime, startModuleRuntime, } from "../src/modules/index.js"; import { resolveModuleEligibility } from "../src/modules/lifecycle.js"; vi.mock("../src/modules/discovery.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, discoverModules: vi.fn(actual.discoverModules) }; }); 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 }>; database?: { abi: 1 }; dependencies?: Record; permissions?: Array<{ id: string; baseline: string; description: string }>; } = {}, ): Record { return { manifestVersion: 8, id, name: `${id} module`, version: "0.1.0", compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0", }, capabilities: options.capabilities ?? [], permissions: options.permissions ?? [], ...(options.database ? { database: options.database } : {}), ...(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)})]`, }; } /** Same globalThis-symbol technique as `createEventRecorder`, but for capturing an actual object * reference (such as `context.services`) pushed from inside a dynamically imported fixture module, * rather than a string label — dynamic `import()` of a file:// URL still shares this process's * heap, so a reference pushed from the fixture and read back here is the exact same object. */ function createReferenceRecorder(): { values: unknown[]; expression: string } { const key = `ekklesia.runtime.test.ref.${eventSymbols.length}.${Date.now()}.${Math.random()}`; const symbol = Symbol.for(key); const values: unknown[] = []; (globalThis as unknown as Record)[symbol] = values; eventSymbols.push(symbol); return { values, expression: `globalThis[Symbol.for(${JSON.stringify(key)})]`, }; } /** A small typed fake `ModuleDatabase` — no PostgreSQL required for these unit tests, which only * ever assert on context wiring and object identity, never on actual query behavior. */ function fakeModuleDatabase(): ModuleDatabase { return Object.freeze({ delete: vi.fn(), execute: vi.fn(), insert: vi.fn(), select: vi.fn(), selectDistinct: vi.fn(), selectDistinctOn: vi.fn(), transaction: vi.fn(), update: vi.fn(), }) as unknown as ModuleDatabase; } function fakeModuleRuntimeServices(): ModuleRuntimeServicesLookup { const services = new Map(); const database = fakeModuleDatabase(); return new Proxy(services, { get(target, property, receiver) { if (property === "get") { return (moduleId: string) => { let value = target.get(moduleId); if (!value) { value = Object.freeze({ database }); target.set(moduleId, value); } return value; }; } return Reflect.get(target, property, receiver); }, }); } function runtimeOptions( modulesDirectory: string, services: ModuleRuntimeServicesLookup = fakeModuleRuntimeServices(), ) { return { moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", services, }; } 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"]); // The module whose `start` threw is rolled back *first*, then the previously started modules in // reverse order — its own hook may have partial initialization to release before it threw. expect(recorder.events).toEqual([ "one:start", "two:start", "three:start", "three:stop", "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); }); }); function noopHandlerSource(): string { return "() => new Response(null, { status: 204 })"; } function permissionDeclaration( id: string, baseline: "authenticated" | "public" | "restricted" = "restricted", ): { id: string; baseline: string; description: string } { return { id, baseline, description: `Test permission ${id}.` }; } describe("module HTTP routes", () => { it("registers one route and publishes it on the started runtime", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "single", { manifest: manifestFor("single", { permissions: [permissionDeclaration("single.items.read")], }), serverSource: `export default { id: "single", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "single.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()}, }); } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect(runtime.routes).toHaveLength(1); expect(runtime.routes[0]).toMatchObject({ moduleId: "single", method: "GET", path: "/items", permission: "single.items.read", }); await runtime.stop(); }); it("retains registration order within a module and discovery order across modules", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "alpha", { manifest: manifestFor("alpha", { permissions: [ permissionDeclaration("alpha.first.read"), permissionDeclaration("alpha.second.read"), ], }), serverSource: `export default { id: "alpha", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/first", permission: "alpha.first.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "GET", path: "/second", permission: "alpha.second.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); await createRuntimeModule(modulesDirectory, "beta", { manifest: manifestFor("beta", { permissions: [permissionDeclaration("beta.third.read")], }), serverSource: `export default { id: "beta", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/third", permission: "beta.third.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect( runtime.routes.map((route) => `${route.moduleId}:${route.method}:${route.path}`), ).toEqual(["alpha:GET:/first", "alpha:GET:/second", "beta:GET:/third"]); await runtime.stop(); }); it("allows the same path under different methods", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "same-path", { manifest: manifestFor("same-path", { permissions: [ permissionDeclaration("same-path.items.read"), permissionDeclaration("same-path.items.write"), ], }), serverSource: `export default { id: "same-path", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "same-path.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "POST", path: "/items", permission: "same-path.items.write", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect(runtime.routes.map((route) => `${route.method} ${route.path}`)).toEqual([ "GET /items", "POST /items", ]); await runtime.stop(); }); it("rejects a duplicate method/path route registered by the same module", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "dup", { manifest: manifestFor("dup", { permissions: [permissionDeclaration("dup.items.read")], }), serverSource: `export default { id: "dup", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "dup.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "GET", path: "/items", permission: "dup.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_ROUTE_DUPLICATE"]); }); it("collides parameter-name-only path variants under the same method", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "param-shape", { manifest: manifestFor("param-shape", { permissions: [permissionDeclaration("param-shape.events.read")], }), serverSource: `export default { id: "param-shape", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/events/:eventId", permission: "param-shape.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "GET", path: "/events/:id", permission: "param-shape.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_ROUTE_DUPLICATE"]); }); it.each([ ["parameter registered first", "/events/:id", "/events/new"], ["static registered first", "/events/new", "/events/:id"], ["multi-position, parameter first", "/events/:id/edit", "/events/new/:action"], ["multi-position, static first", "/events/new/:action", "/events/:id/edit"], ["parameters on both sides", "/a/:left/c", "/a/b/:right"], ])("rejects routes that can both match one request path (%s) regardless of registration order", async (_label, firstPath, secondPath) => { const modulesDirectory = await createModulesDirectory(); // Deliberately *different* permissions: the defect this rejects is that whichever route // registered first would decide which permission governs the ambiguous path. await createRuntimeModule(modulesDirectory, "overlap", { manifest: manifestFor("overlap", { permissions: [ permissionDeclaration("overlap.events.read"), permissionDeclaration("overlap.events.create"), ], }), serverSource: `export default { id: "overlap", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "${firstPath}", permission: "overlap.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "GET", path: "${secondPath}", permission: "overlap.events.create", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_ROUTE_OVERLAP"]); }); it.each([ ["two different static literals", "/events/new", "/events/archive"], ["different segment counts", "/events/:id", "/events/:id/edit"], ["a different static prefix", "/events/:id", "/people/:id"], ["module root against a one-segment path", "/", "/events"], ])("still allows disjoint routes (%s) and preserves their registration order", async (_label, firstPath, secondPath) => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "disjoint", { manifest: manifestFor("disjoint", { permissions: [permissionDeclaration("disjoint.events.read")], }), serverSource: `export default { id: "disjoint", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "${firstPath}", permission: "disjoint.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "GET", path: "${secondPath}", permission: "disjoint.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const runtime = await startModuleRuntime({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", services: fakeModuleRuntimeServices(), }); expect(runtime.routes.map((route) => route.path)).toEqual([firstPath, secondPath]); await runtime.stop(); }); it("allows the same overlapping local path under different methods", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "methods", { manifest: manifestFor("methods", { permissions: [permissionDeclaration("methods.events.read")], }), serverSource: `export default { id: "methods", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/events/:id", permission: "methods.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "POST", path: "/events/new", permission: "methods.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const runtime = await startModuleRuntime({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", services: fakeModuleRuntimeServices(), }); expect(runtime.routes).toHaveLength(2); await runtime.stop(); }); it("allows two different modules to declare the same overlapping local paths", async () => { const modulesDirectory = await createModulesDirectory(); for (const moduleId of ["alpha", "beta"]) { await createRuntimeModule(modulesDirectory, moduleId, { manifest: manifestFor(moduleId, { permissions: [permissionDeclaration(`${moduleId}.events.read`)], }), serverSource: `export default { id: "${moduleId}", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/events/:id", permission: "${moduleId}.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); } // Each module has its own platform-owned mount prefix, so these can never collide. const runtime = await startModuleRuntime({ moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", services: fakeModuleRuntimeServices(), }); expect(runtime.routes.map((route) => route.moduleId).sort()).toEqual(["alpha", "beta"]); await runtime.stop(); }); it("publishes no route from a module whose registration was rejected for an overlap", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "partial", { manifest: manifestFor("partial", { permissions: [permissionDeclaration("partial.events.read")], }), serverSource: `export default { id: "partial", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/healthy", permission: "partial.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "GET", path: "/events/:id", permission: "partial.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "GET", path: "/events/new", permission: "partial.events.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); }, start() { throw new Error("start must never run for a rejected registration"); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_ROUTE_OVERLAP"]); // The earlier, perfectly valid `/healthy` route is not published either: registration is // all-or-nothing, and the start hook above would have thrown had it ever run. }); it("fails a malformed route with its specific structured issue code", async () => { const badMethodDirectory = await createModulesDirectory(); await createRuntimeModule(badMethodDirectory, "bad-method", { manifest: manifestFor("bad-method", { permissions: [permissionDeclaration("bad-method.items.read")], }), serverSource: `export default { id: "bad-method", version: "0.1.0", register({ http }) { http.route({ method: "get", path: "/items", permission: "bad-method.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); expect(runtimeIssueCodes(await captureRuntimeError(badMethodDirectory))).toEqual([ "MODULE_ROUTE_METHOD_INVALID", ]); const badPathDirectory = await createModulesDirectory(); await createRuntimeModule(badPathDirectory, "bad-path", { manifest: manifestFor("bad-path", { permissions: [permissionDeclaration("bad-path.items.read")], }), serverSource: `export default { id: "bad-path", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "items", permission: "bad-path.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); expect(runtimeIssueCodes(await captureRuntimeError(badPathDirectory))).toEqual([ "MODULE_ROUTE_PATH_INVALID", ]); const badMaxBodyBytesDirectory = await createModulesDirectory(); await createRuntimeModule(badMaxBodyBytesDirectory, "bad-max-body-bytes", { manifest: manifestFor("bad-max-body-bytes", { permissions: [permissionDeclaration("bad-max-body-bytes.items.read")], }), serverSource: `export default { id: "bad-max-body-bytes", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "bad-max-body-bytes.items.read", maxBodyBytes: -1, handler: ${noopHandlerSource()} }); } };`, }); expect(runtimeIssueCodes(await captureRuntimeError(badMaxBodyBytesDirectory))).toEqual([ "MODULE_ROUTE_MAX_BODY_BYTES_INVALID", ]); const badDefinitionDirectory = await createModulesDirectory(); await createRuntimeModule(badDefinitionDirectory, "bad-shape", { manifest: manifestFor("bad-shape", { permissions: [permissionDeclaration("bad-shape.items.read")], }), serverSource: `export default { id: "bad-shape", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "bad-shape.items.read", baseline: "public", handler: ${noopHandlerSource()}, }); } };`, }); expect(runtimeIssueCodes(await captureRuntimeError(badDefinitionDirectory))).toEqual([ "MODULE_ROUTE_DEFINITION_INVALID", ]); }); it("rejects a route permission the registering module does not itself declare", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "undeclared", { manifest: manifestFor("undeclared"), serverSource: `export default { id: "undeclared", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "undeclared.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_ROUTE_PERMISSION_UNDECLARED"]); }); it("rejects a module gating a route with a Core-owned permission it cannot declare", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "coreattempt", { manifest: manifestFor("coreattempt"), serverSource: `export default { id: "coreattempt", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "core.authorization.grants.manage", maxBodyBytes: 1024, handler: ${noopHandlerSource()}, }); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_ROUTE_PERMISSION_UNDECLARED"]); }); it("rejects a module gating a route with another module's permission", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "alpha", { manifest: manifestFor("alpha", { permissions: [permissionDeclaration("alpha.items.read")], }), serverSource: `export default { id: "alpha", version: "0.1.0" };`, }); await createRuntimeModule(modulesDirectory, "beta", { manifest: manifestFor("beta"), serverSource: `export default { id: "beta", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "alpha.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_ROUTE_PERMISSION_UNDECLARED"]); }); it("accepts a declared public, authenticated, or restricted permission without interpreting its baseline", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "baselines", { manifest: manifestFor("baselines", { permissions: [ permissionDeclaration("baselines.public.read", "public"), permissionDeclaration("baselines.authenticated.read", "authenticated"), permissionDeclaration("baselines.restricted.read", "restricted"), ], }), serverSource: `export default { id: "baselines", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/public", permission: "baselines.public.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "GET", path: "/authenticated", permission: "baselines.authenticated.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); http.route({ method: "GET", path: "/restricted", permission: "baselines.restricted.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect(runtime.routes).toHaveLength(3); for (const route of runtime.routes) { expect(Object.keys(route).sort()).toEqual([ "bodyMode", "handler", "maxBodyBytes", "method", "moduleId", "path", "permission", ]); } await runtime.stop(); }); it("never invokes a route handler while registering or starting", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "never-invoked", { manifest: manifestFor("never-invoked", { permissions: [permissionDeclaration("never-invoked.items.read")], }), serverSource: `export default { id: "never-invoked", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "never-invoked.items.read", maxBodyBytes: 1024, handler: () => { ${recorder.expression}.push("handler:called"); return new Response(null, { status: 204 }); }, }); } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect(recorder.events).toEqual([]); await runtime.stop(); expect(recorder.events).toEqual([]); }); it("publishes frozen, immutable routes as a frozen route list", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "frozen", { manifest: manifestFor("frozen", { permissions: [permissionDeclaration("frozen.items.read")], }), serverSource: `export default { id: "frozen", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "frozen.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect(Object.isFrozen(runtime.routes)).toBe(true); expect(Object.isFrozen(runtime.routes[0])).toBe(true); await runtime.stop(); }); it("does not let a module mutate the published route by mutating its own definition afterward", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "mutate", { manifest: manifestFor("mutate", { permissions: [ permissionDeclaration("mutate.items.read"), permissionDeclaration("mutate.other.read"), ], }), serverSource: `export default { id: "mutate", version: "0.1.0", register({ http }) { const definition = { method: "GET", path: "/items", permission: "mutate.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()}, }; http.route(definition); definition.method = "POST"; definition.path = "/mutated"; definition.permission = "mutate.other.read"; } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect(runtime.routes[0]).toMatchObject({ method: "GET", path: "/items", permission: "mutate.items.read", }); await runtime.stop(); }); it("publishes no runtime when a later module's route registration fails", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "first-ok", { manifest: manifestFor("first-ok", { permissions: [permissionDeclaration("first-ok.items.read")], }), serverSource: `export default { id: "first-ok", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "first-ok.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); await createRuntimeModule(modulesDirectory, "second-bad", { manifest: manifestFor("second-bad"), serverSource: `export default { id: "second-bad", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "second-bad.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_ROUTE_PERMISSION_UNDECLARED"]); expect(error.issues[0]?.moduleId).toBe("second-bad"); }); it("preserves existing rollback behavior when a registered route's module later fails to start", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "routed", { manifest: manifestFor("routed", { permissions: [permissionDeclaration("routed.items.read")], }), serverSource: `export default { id: "routed", version: "0.1.0", register({ http }) { http.route({ method: "GET", path: "/items", permission: "routed.items.read", maxBodyBytes: 1024, handler: ${noopHandlerSource()} }); }, start() { ${recorder.expression}.push("routed:start"); }, stop() { ${recorder.expression}.push("routed:stop"); } };`, }); await createRuntimeModule(modulesDirectory, "failing", { manifest: manifestFor("failing", { dependencies: { modules: { required: { routed: "^0.1.0" } } }, }), serverSource: `export default { id: "failing", version: "0.1.0", start() { throw new Error("boom"); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_START_FAILED"]); expect(recorder.events).toEqual(["routed:start", "routed:stop"]); }); }); describe("already-discovered module runtime", () => { it("starts from an already-discovered inventory without a second directory scan", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "already-discovered", { serverSource: `export default { id: "already-discovered", version: "0.1.0" };`, }); vi.mocked(discoverModules).mockClear(); const discovered = await discoverModules(runtimeOptions(modulesDirectory)); expect(discoverModules).toHaveBeenCalledTimes(1); const runtime = await startDiscoveredModuleRuntime(discovered, fakeModuleRuntimeServices()); expect(discoverModules).toHaveBeenCalledTimes(1); expect(runtime.modules.map((module) => module.id)).toEqual(["already-discovered"]); await runtime.stop(); }); it("preserves the given discovered-module list order exactly, without re-sorting", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "first", { serverSource: `export default { id: "first", version: "0.1.0" };`, }); await createRuntimeModule(modulesDirectory, "second", { serverSource: `export default { id: "second", version: "0.1.0" };`, }); const discovered = await discoverModules(runtimeOptions(modulesDirectory)); expect(discovered.map((module) => module.manifest.id)).toEqual(["first", "second"]); const runtime = await startDiscoveredModuleRuntime( [...discovered].reverse(), fakeModuleRuntimeServices(), ); expect(runtime.modules.map((module) => module.id)).toEqual(["second", "first"]); await runtime.stop(); }); it("startModuleRuntime remains backward-compatible and discovers exactly once", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "compat", { serverSource: `export default { id: "compat", version: "0.1.0" };`, }); vi.mocked(discoverModules).mockClear(); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); expect(discoverModules).toHaveBeenCalledTimes(1); await runtime.stop(); }); }); describe("module runtime services", () => { it("delivers only the manifest-selected database mode and distinct ABI executors", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createReferenceRecorder(); const legacyDatabase = fakeModuleDatabase(); const alphaExecutor = Object.freeze({ abiVersion: 1, query: vi.fn(), transaction: vi.fn(), }) as unknown as ModuleDatabaseExecutor; const betaExecutor = Object.freeze({ abiVersion: 1, query: vi.fn(), transaction: vi.fn(), }) as unknown as ModuleDatabaseExecutor; const services: ModuleRuntimeServicesLookup = new Map([ ["legacy", Object.freeze({ database: legacyDatabase })], ["alpha", Object.freeze({ database: alphaExecutor })], ["beta", Object.freeze({ database: betaExecutor })], ]); await createRuntimeModule(modulesDirectory, "legacy", { serverSource: `export default { id: "legacy", version: "0.1.0", start({ services }) { ${recorder.expression}.push(services.database); } };`, }); for (const id of ["alpha", "beta"]) { await createRuntimeModule(modulesDirectory, id, { manifest: manifestFor(id, { database: { abi: 1 } }), serverSource: `export default { database: { abi: 1 }, id: ${JSON.stringify(id)}, version: "0.1.0", start({ services }) { ${recorder.expression}.push(services.database); } };`, }); } const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory, services)); expect(recorder.values).toEqual([alphaExecutor, betaExecutor, legacyDatabase]); expect("select" in alphaExecutor).toBe(false); expect(alphaExecutor).not.toBe(betaExecutor); await runtime.stop(); }); it("rejects a runtime database declaration that disagrees with its manifest", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "mismatch", { manifest: manifestFor("mismatch", { database: { abi: 1 } }), serverSource: `export default { id: "mismatch", version: "0.1.0" };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toContain("MODULE_RUNTIME_DATABASE_MISMATCH"); }); it("never gives register a services field, and gives start and stop the exact same services object", async () => { const modulesDirectory = await createModulesDirectory(); const services = fakeModuleRuntimeServices(); const registerServices = createReferenceRecorder(); const lifecycleServices = createReferenceRecorder(); await createRuntimeModule(modulesDirectory, "wired", { serverSource: `export default { id: "wired", version: "0.1.0", register(context) { ${registerServices.expression}.push(context.services); }, start(context) { ${lifecycleServices.expression}.push(context.services); }, stop(context) { ${lifecycleServices.expression}.push(context.services); } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory, services)); await runtime.stop(); expect(registerServices.values).toEqual([undefined]); expect(lifecycleServices.values).toHaveLength(2); expect(lifecycleServices.values[0]).toBe(services.get("wired")); expect(lifecycleServices.values[1]).toBe(services.get("wired")); }); it("selects a stable services object per module rather than sharing one across modules", async () => { const modulesDirectory = await createModulesDirectory(); const services = fakeModuleRuntimeServices(); const recorder = createReferenceRecorder(); await createRuntimeModule(modulesDirectory, "alpha", { serverSource: `export default { id: "alpha", version: "0.1.0", start(context) { ${recorder.expression}.push(context.services); } };`, }); await createRuntimeModule(modulesDirectory, "beta", { manifest: manifestFor("beta", { dependencies: { modules: { required: { alpha: "^0.1.0" } } }, }), serverSource: `export default { id: "beta", version: "0.1.0", start(context) { ${recorder.expression}.push(context.services); } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory, services)); expect(recorder.values).toEqual([services.get("alpha"), services.get("beta")]); expect(recorder.values[0]).not.toBe(recorder.values[1]); await runtime.stop(); }); it("gives a rollback stop hook the same services object a start failure left available", async () => { const modulesDirectory = await createModulesDirectory(); const services = fakeModuleRuntimeServices(); const recorder = createReferenceRecorder(); await createRuntimeModule(modulesDirectory, "started", { serverSource: `export default { id: "started", version: "0.1.0", start(context) { ${recorder.expression}.push(context.services); }, stop(context) { ${recorder.expression}.push(context.services); } };`, }); await createRuntimeModule(modulesDirectory, "failing", { manifest: manifestFor("failing", { dependencies: { modules: { required: { started: "^0.1.0" } } }, }), serverSource: `export default { id: "failing", version: "0.1.0", start() { throw new Error("boom"); } };`, }); await expect( startModuleRuntime(runtimeOptions(modulesDirectory, services)), ).rejects.toBeInstanceOf(ModuleRuntimeError); expect(recorder.values).toEqual([services.get("started"), services.get("started")]); }); it("starts and stops a zero-module runtime without ever touching the provided services", async () => { const modulesDirectory = await createModulesDirectory(); const database = fakeModuleDatabase(); const services: ModuleRuntimeServicesLookup = new Map([ ["unused", Object.freeze({ database })], ]); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory, services)); await runtime.stop(); expect(runtime.modules).toEqual([]); expect(vi.mocked(database.select)).not.toHaveBeenCalled(); expect(vi.mocked(database.insert)).not.toHaveBeenCalled(); expect(vi.mocked(database.update)).not.toHaveBeenCalled(); expect(vi.mocked(database.delete)).not.toHaveBeenCalled(); expect(vi.mocked(database.execute)).not.toHaveBeenCalled(); expect(vi.mocked(database.transaction)).not.toHaveBeenCalled(); }); }); /** * Pass 4: a module whose `start` hook fails owns cleanup of whatever it managed to acquire, and * every stop/rollback hook runs under a finite platform deadline. */ describe("module startup rollback and bounded stop hooks", () => { it("releases a partially initialized resource through the failing module's own stop hook", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "partial", { serverSource: `let acquired = false; export default { id: "partial", version: "0.1.0", start() { acquired = true; ${recorder.expression}.push("partial:acquired"); throw new Error("secret-token at /private/path"); }, stop() { if (acquired) { acquired = false; ${recorder.expression}.push("partial:released"); } } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_START_FAILED"]); expect(recorder.events).toEqual(["partial:acquired", "partial:released"]); expect(JSON.stringify(error.issues)).not.toContain("secret-token"); expect(JSON.stringify(error.issues)).not.toContain("/private/path"); }); it("reports the failing module's own rollback failure and still rolls back earlier modules", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "earlier", { serverSource: `export default { id: "earlier", version: "0.1.0", stop() { ${recorder.expression}.push("earlier:stop"); } };`, }); await createRuntimeModule(modulesDirectory, "failing", { manifest: manifestFor("failing", { dependencies: { modules: { required: { earlier: "^0.1.0" } } }, }), serverSource: `export default { id: "failing", version: "0.1.0", start() { throw new Error("start blew up"); }, stop() { ${recorder.expression}.push("failing:stop"); throw new Error("rollback blew up"); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(runtimeIssueCodes(error)).toEqual(["MODULE_START_FAILED", "MODULE_ROLLBACK_FAILED"]); expect(recorder.events).toEqual(["failing:stop", "earlier:stop"]); }); it("gives a ModuleRuntimeSignal thrown during start the same rollback treatment", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); // `capabilities.require` for a capability this module never declared throws a // `ModuleRuntimeSignal` from inside `start`, rather than an ordinary Error. await createRuntimeModule(modulesDirectory, "signal", { serverSource: `export default { id: "signal", version: "0.1.0", start({ capabilities }) { capabilities.require("undeclared.thing"); }, stop() { ${recorder.expression}.push("signal:stop"); } };`, }); const error = await captureRuntimeError(modulesDirectory); expect(error.issues[0]?.phase).toBe("start"); expect(recorder.events).toEqual(["signal:stop"]); }); it("invokes each stop hook at most once across rollback", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "counted", { serverSource: `export default { id: "counted", version: "0.1.0", start() { throw new Error("nope"); }, stop() { ${recorder.expression}.push("counted:stop"); } };`, }); await captureRuntimeError(modulesDirectory); expect(recorder.events.filter((event) => event === "counted:stop")).toHaveLength(1); }); it("bounds a never-settling rollback hook and continues with the earlier module", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "earlier", { serverSource: `export default { id: "earlier", version: "0.1.0", stop() { ${recorder.expression}.push("earlier:stop"); } };`, }); await createRuntimeModule(modulesDirectory, "hanging", { manifest: manifestFor("hanging", { dependencies: { modules: { required: { earlier: "^0.1.0" } } }, }), serverSource: `export default { id: "hanging", version: "0.1.0", start() { throw new Error("nope"); }, stop() { ${recorder.expression}.push("hanging:stop"); return new Promise(() => {}); } };`, }); // A short test-only deadline; production uses MODULE_STOP_HOOK_TIMEOUT_MS unchanged. let captured: ModuleRuntimeError | undefined; try { await startModuleRuntime({ ...runtimeOptions(modulesDirectory), hookTimeoutMs: 25 }); } catch (error) { captured = error as ModuleRuntimeError; } expect(captured).toBeInstanceOf(ModuleRuntimeError); expect(runtimeIssueCodes(captured as ModuleRuntimeError)).toEqual([ "MODULE_START_FAILED", "MODULE_ROLLBACK_TIMEOUT", ]); // The timed-out hook did not block the earlier module's rollback. expect(recorder.events).toEqual(["hanging:stop", "earlier:stop"]); }); it("bounds a never-settling normal stop hook and continues with the earlier module", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createEventRecorder(); await createRuntimeModule(modulesDirectory, "earlier", { serverSource: `export default { id: "earlier", version: "0.1.0", stop() { ${recorder.expression}.push("earlier:stop"); } };`, }); await createRuntimeModule(modulesDirectory, "hanging", { manifest: manifestFor("hanging", { dependencies: { modules: { required: { earlier: "^0.1.0" } } }, }), serverSource: `export default { id: "hanging", version: "0.1.0", stop() { ${recorder.expression}.push("hanging:stop"); return new Promise(() => {}); } };`, }); const runtime = await startModuleRuntime({ ...runtimeOptions(modulesDirectory), hookTimeoutMs: 25, }); let captured: ModuleRuntimeError | undefined; try { await runtime.stop(); } catch (error) { captured = error as ModuleRuntimeError; } expect(runtimeIssueCodes(captured as ModuleRuntimeError)).toEqual(["MODULE_STOP_TIMEOUT"]); expect(recorder.events).toEqual(["hanging:stop", "earlier:stop"]); // Repeated and concurrent callers share the memoized promise and never restart cleanup. const [first, second] = await Promise.allSettled([runtime.stop(), runtime.stop()]); expect(first.status).toBe("rejected"); expect(second.status).toBe("rejected"); expect(recorder.events.filter((event) => event === "hanging:stop")).toHaveLength(1); }); it("never lets a hook that rejects after its deadline become an unhandled rejection", async () => { const modulesDirectory = await createModulesDirectory(); await createRuntimeModule(modulesDirectory, "late", { serverSource: `export default { id: "late", version: "0.1.0", stop() { return new Promise((_resolve, reject) => { setTimeout(() => reject(new Error("late")), 40); }); } };`, }); const unhandled: unknown[] = []; const onUnhandled = (reason: unknown): void => { unhandled.push(reason); }; process.on("unhandledRejection", onUnhandled); try { const runtime = await startModuleRuntime({ ...runtimeOptions(modulesDirectory), hookTimeoutMs: 10, }); await expect(runtime.stop()).rejects.toBeInstanceOf(ModuleRuntimeError); // Give the late rejection time to fire, then confirm nothing escaped. await new Promise((resolve) => setTimeout(resolve, 80)); expect(unhandled).toEqual([]); } finally { process.off("unhandledRejection", onUnhandled); } }); }); describe("module configuration delivery (ADR 0029)", () => { it("hands each module its own frozen configuration in register, start, and stop", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createReferenceRecorder(); await createRuntimeModule(modulesDirectory, "announcements", { manifest: { ...manifestFor("announcements"), configuration: [ { key: "slideSeconds", type: "integer", default: 7 }, { key: "bucket", type: "string", required: true }, ], }, serverSource: `export default { id: "announcements", version: "0.1.0", register(context) { ${recorder.expression}.push(context.config); }, start(context) { ${recorder.expression}.push(context.config); }, stop(context) { ${recorder.expression}.push(context.config); } };`, }); const discovered = await discoverModules(runtimeOptions(modulesDirectory)); const resolution = resolveModuleEligibility( discovered, { modules: [] }, { settings: { records: [{ key: "bucket", moduleId: "announcements", value: "slides" }] } }, ); const runtime = await startDiscoveredModuleRuntime( resolution.eligibleModules, fakeModuleRuntimeServices(), { configurations: resolution.configurations }, ); await runtime.stop(); expect(recorder.values).toHaveLength(3); const [registerConfig, startConfig, stopConfig] = recorder.values as ModuleConfiguration[]; // Configuration is available during `register` too — it is an already-resolved in-memory // snapshot, not a database-backed getter, so it reintroduces no I/O into a hook that must stay // declarative. expect(registerConfig?.getInteger("slideSeconds")).toBe(7); expect(registerConfig?.getString("bucket")).toBe("slides"); // The exact same object across every hook, so two hooks can never disagree about a value. expect(startConfig).toBe(registerConfig); expect(stopConfig).toBe(registerConfig); expect(Object.isFrozen(registerConfig)).toBe(true); }); it("gives each module only its own configuration, never another module's", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createReferenceRecorder(); for (const id of ["alpha", "bravo"]) { await createRuntimeModule(modulesDirectory, id, { manifest: { ...manifestFor(id), configuration: [{ key: "label", type: "string", required: true }], }, serverSource: `export default { id: ${JSON.stringify(id)}, version: "0.1.0", start(context) { ${recorder.expression}.push([context.moduleId, context.config.getString("label"), context.config.keys()]); } };`, }); } const discovered = await discoverModules(runtimeOptions(modulesDirectory)); const resolution = resolveModuleEligibility( discovered, { modules: [] }, { settings: { records: [ { key: "label", moduleId: "alpha", value: "alpha-value" }, { key: "label", moduleId: "bravo", value: "bravo-value" }, ], }, }, ); const runtime = await startDiscoveredModuleRuntime( resolution.eligibleModules, fakeModuleRuntimeServices(), { configurations: resolution.configurations }, ); await runtime.stop(); expect(recorder.values).toEqual([ ["alpha", "alpha-value", ["label"]], ["bravo", "bravo-value", ["label"]], ]); }); it("supplies an empty configuration, never undefined, to a module that declares none", async () => { const modulesDirectory = await createModulesDirectory(); const recorder = createReferenceRecorder(); await createRuntimeModule(modulesDirectory, "plain", { serverSource: `export default { id: "plain", version: "0.1.0", start(context) { ${recorder.expression}.push([typeof context.config, context.config.keys()]); } };`, }); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory)); await runtime.stop(); // Module code never has to branch on whether the platform supplied a configuration. expect(recorder.values).toEqual([["object", []]]); }); it("keeps each module's services object stable while configuration remains per-module", async () => { const modulesDirectory = await createModulesDirectory(); const services = fakeModuleRuntimeServices(); const recorder = createReferenceRecorder(); for (const id of ["alpha", "bravo"]) { await createRuntimeModule(modulesDirectory, id, { manifest: { ...manifestFor(id), configuration: [{ key: "label", type: "string", default: id }], }, serverSource: `export default { id: ${JSON.stringify(id)}, version: "0.1.0", start(context) { ${recorder.expression}.push(context.services); ${recorder.expression}.push(context.config); } };`, }); } const discovered = await discoverModules(runtimeOptions(modulesDirectory, services)); const resolution = resolveModuleEligibility( discovered, { modules: [] }, { settings: { records: [] } }, ); const runtime = await startDiscoveredModuleRuntime(resolution.eligibleModules, services, { configurations: resolution.configurations, }); await runtime.stop(); const [alphaServices, alphaConfig, bravoServices, bravoConfig] = recorder.values; // Configuration remains a sibling of `services`: both are selected per module, but they are // independently composed snapshots with separate contracts and lifetimes. expect(alphaServices).toBe(services.get("alpha")); expect(bravoServices).toBe(services.get("bravo")); expect(alphaServices).not.toBe(bravoServices); expect(alphaConfig).not.toBe(bravoConfig); expect((alphaConfig as ModuleConfiguration).getString("label")).toBe("alpha"); expect((bravoConfig as ModuleConfiguration).getString("label")).toBe("bravo"); }); });