import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import type { ModuleDatabase, ModuleRuntimeServices } from "@ekklesia/contracts"; import { afterEach, describe, expect, it, vi } from "vitest"; import { discoverModules } from "../src/modules/discovery.js"; import { ModuleDiscoveryError, ModuleRuntimeError, type ModuleRuntimeIssueCode, startDiscoveredModuleRuntime, startModuleRuntime, } from "../src/modules/index.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 }>; dependencies?: Record; permissions?: Array<{ id: string; baseline: string; description: string }>; } = {}, ): Record { return { manifestVersion: 3, id, name: `${id} module`, version: "0.1.0", compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0", }, capabilities: options.capabilities ?? [], permissions: options.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)})]`, }; } /** 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(): ModuleRuntimeServices { return Object.freeze({ database: fakeModuleDatabase() }); } function runtimeOptions( modulesDirectory: string, services: ModuleRuntimeServices = 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"]); 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); }); }); 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("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([ "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("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); expect(lifecycleServices.values[1]).toBe(services); }); it("shares the exact same services object identity across every module's start hook", 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, services]); 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, services]); }); it("starts and stops a zero-module runtime without ever touching the provided services", async () => { const modulesDirectory = await createModulesDirectory(); const services = fakeModuleRuntimeServices(); const runtime = await startModuleRuntime(runtimeOptions(modulesDirectory, services)); await runtime.stop(); expect(runtime.modules).toEqual([]); expect(vi.mocked(services.database.select)).not.toHaveBeenCalled(); expect(vi.mocked(services.database.insert)).not.toHaveBeenCalled(); expect(vi.mocked(services.database.update)).not.toHaveBeenCalled(); expect(vi.mocked(services.database.delete)).not.toHaveBeenCalled(); expect(vi.mocked(services.database.execute)).not.toHaveBeenCalled(); expect(vi.mocked(services.database.transaction)).not.toHaveBeenCalled(); }); });