import type { ModuleEnablementRecord } from "@ekklesia/contracts"; import type { DiscoveredModule, ModuleLifecycleConfiguration, ModuleLifecycleSnapshot, ModuleLifecycleStore, SetModuleEnabledInput, } from "@ekklesia/core/modules"; import { ModuleLifecycleStoreError, resolveModuleEligibility } from "@ekklesia/core/modules"; /** * An in-memory `ModuleLifecycleStore` for tests that exercise **routing and resolution**, not * persistence. * * It reproduces only what those tests depend on — records survive a write and read back in * module-id order, and an invalid identifier is rejected — and deliberately reproduces nothing * else. The real table's constraints, the upsert's conflict target, and the fact that a record for * an undeployed module is retained are proven against real PostgreSQL in * `core/tests/module-lifecycle-store.integration.ts`, so this double can never quietly stand in for * a persistence guarantee nobody checked. */ export function createInMemoryModuleLifecycleStore( seed: readonly ModuleEnablementRecord[] = [], ): ModuleLifecycleStore { const records = new Map( seed.map((record) => [record.moduleId, record.enabled] as const), ); return Object.freeze({ async readConfiguration(): Promise { return Object.freeze({ modules: Object.freeze( [...records.entries()] .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) .map(([moduleId, enabled]) => Object.freeze({ enabled, moduleId })), ), }); }, async setEnabled(input: SetModuleEnabledInput): Promise { if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(input.moduleId)) { throw new ModuleLifecycleStoreError("MODULE_LIFECYCLE_MODULE_ID_INVALID"); } records.set(input.moduleId, input.enabled); return Object.freeze({ enabled: input.enabled, moduleId: input.moduleId }); }, }); } /** * Builds the boot snapshot the same way `startApi` does — resolve eligibility from the discovered * inventory and the stored configuration, then record what would have started — rather than * hand-writing one, so a test can never assert against a snapshot the real composition would never * produce. */ export function moduleLifecycleSnapshotFor( discoveredModules: readonly DiscoveredModule[], configuration: ModuleLifecycleConfiguration = { modules: [] }, ): ModuleLifecycleSnapshot { const resolution = resolveModuleEligibility(discoveredModules, configuration); return Object.freeze({ discoveredModules, eligibility: resolution.entries, startedModuleIds: Object.freeze(resolution.eligibleModules.map((module) => module.manifest.id)), }); } /** The lifecycle option for a `createApiApp` caller that exercises no modules at all — the explicit * "no modules" statement the required option asks for, never an implicit default. */ export function emptyModuleLifecycle(): { readonly snapshot: ModuleLifecycleSnapshot; readonly store: ModuleLifecycleStore; } { return { snapshot: moduleLifecycleSnapshotFor([]), store: createInMemoryModuleLifecycleStore(), }; }