import { type ModuleManifest, parsePermissionId } from "@ekklesia/contracts"; import { describe, expect, it, vi } from "vitest"; import { createPermissionRegistry, PermissionRegistryError } from "../src/authorization/index.js"; import { type DoctorCheck, type DoctorDependencies, type DoctorOptions, deriveDoctorResult, diagnoseEkklesia, doctorExitCode, } from "../src/diagnostics/index.js"; import { type MigrationDatabase, type MigrationDatabaseSession, type MigrationPlanResult, MigrationRunnerError, planModuleMigrations, } from "../src/migrations/index.js"; import type { DiscoveredModule, DiscoveredModuleMigration, ModuleMigrationAggregate, ModuleSchemaAggregate, } from "../src/modules/index.js"; import { ModulePermissionCompositionError } from "../src/modules/permissions.js"; const checksumA = "a".repeat(64); const checksumB = "b".repeat(64); const checksumC = "c".repeat(64); function discoveredModule(id: string, version = "0.1.0"): DiscoveredModule { return { contributions: [], directory: `/unpublished/${id}`, entrypoints: { server: `/unpublished/${id}/server.js` }, manifest: { capabilities: [], compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" }, configuration: [], dependencies: { capabilities: { optional: {}, required: {} }, modules: { optional: {}, required: {} }, }, entrypoints: { server: "./dist/server.js" }, id, manifestVersion: 7, name: `${id} module`, permissions: [], storage: { buckets: [] }, version, } as unknown as ModuleManifest, manifestPath: `/unpublished/${id}/ekklesia.module.json`, packageName: `@test/${id}`, }; } function migration( moduleId: string, tag: string, checksum: string, approvalRequired = false, ): DiscoveredModuleMigration { const index = Number(tag.slice(0, tag.indexOf("_"))); return Object.freeze({ approvalRequired, breakpoints: true, checksum, destructive: Object.freeze( approvalRequired ? { mode: "approval-required" as const, reason: "Operator review is required." } : { mode: "none" as const }, ), file: `${tag}.sql`, identity: `${moduleId}:${tag}`, index, moduleId, sql: "SELECT 1;", tag, timestamp: index + 1, }); } function migrationAggregate( migrations: readonly DiscoveredModuleMigration[] = [], modules: readonly DiscoveredModule[] = [], ): ModuleMigrationAggregate { const moduleIds = [...new Set(migrations.map((entry) => entry.moduleId))]; return Object.freeze({ discoveredModules: Object.freeze([...modules]), migrations: Object.freeze([...migrations]), modules: Object.freeze( moduleIds.map((moduleId) => Object.freeze({ migrations: Object.freeze(migrations.filter((entry) => entry.moduleId === moduleId)), moduleId, }), ), ), }); } function schemaAggregate(modules: readonly DiscoveredModule[] = []): ModuleSchemaAggregate { return Object.freeze({ discoveredModules: Object.freeze([...modules]), drizzleSchema: Object.freeze({}), modules: Object.freeze([]), tables: Object.freeze([]), }); } function emptyDatabase(): MigrationDatabase { return Object.freeze({ openSession: vi.fn().mockRejectedValue(new Error("unused database")), }); } function plan(entries: MigrationPlanResult["entries"] = []): MigrationPlanResult { const appliedCount = entries.filter((entry) => entry.status === "applied").length; const approvalRequiredCount = entries.filter( (entry) => entry.status === "approval-blocked", ).length; return Object.freeze({ appliedCount, approvalRequiredCount, entries: Object.freeze([...entries]), pendingCount: entries.length - appliedCount, }); } function dependencies(overrides: Partial = {}): DoctorDependencies { return { composePermissions: vi.fn((modules: readonly DiscoveredModule[]) => createPermissionRegistry( modules.flatMap((discovered) => [...discovered.manifest.permissions]), ), ), createMigrationDatabase: vi.fn(() => emptyDatabase()), discoverMigrations: vi.fn(async () => migrationAggregate()), discoverModules: vi.fn(async () => Object.freeze([])), discoverSchemas: vi.fn(async () => schemaAggregate()), planMigrations: vi.fn(async () => plan()), probeMail: vi.fn(async () => undefined), probeStorage: vi.fn(async () => undefined), readModuleConfiguration: vi.fn(async () => Object.freeze({ records: Object.freeze([]) })), readModuleLifecycle: vi.fn(async () => Object.freeze({ modules: Object.freeze([]) })), runtimeSatisfies: vi.fn(() => true), ...overrides, }; } function options(databaseUrl?: string): DoctorOptions { return { ...(databaseUrl === undefined ? {} : { databaseUrl }), moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory: "/not-rendered/modules", platformVersion: "0.1.0", }, runtimeRange: ">=24.11.0 <25", runtimeVersion: "24.15.0", }; } function check( report: Awaited>, id: DoctorCheck["id"], ): DoctorCheck { const found = report.checks.find((candidate) => candidate.id === id); if (!found) { throw new Error(`Missing test check ${id}.`); } return found; } function planEntry( identity: string, checksum: string, status: "applied" | "approval-blocked" | "pending", ): MigrationPlanResult["entries"][number] { const [moduleId = "", tag = ""] = identity.split(":"); return Object.freeze({ approvalRequired: status === "approval-blocked", checksum, identity, index: Number(tag.slice(0, tag.indexOf("_"))), moduleId, status, tag, }); } describe("Ekklesia Doctor outbound email reporting", () => { const mailConfiguration = { fromAddress: "noreply@example.org", fromName: "Ekklesia", host: "smtp.example.org", password: "sup3r-secret-relay-password", security: "starttls", username: "relay@example.org", }; it("reports an installation with no SMTP host as complete rather than degraded", async () => { const services = dependencies(); const report = await diagnoseEkklesia(options(), services); const mailCheck = check(report, "outbound-email.configuration"); expect(mailCheck.status).toBe("skipped"); expect(mailCheck.summary).toBe("Outbound email not configured"); // Nothing sends email yet, so an absent provider must not make a deployment read as broken. expect(report.result).not.toBe("not-ready"); expect(services.probeMail).not.toHaveBeenCalled(); }); it("reports a reachable provider without ever naming a credential", async () => { const services = dependencies(); const report = await diagnoseEkklesia({ ...options(), mailConfiguration }, services); const mailCheck = check(report, "outbound-email.configuration"); expect(mailCheck.status).toBe("pass"); const rendered = mailCheck.details.join(" "); expect(rendered).toContain("smtp.example.org:587"); expect(rendered).toContain("starttls"); expect(rendered).toContain("authenticated"); expect(rendered).not.toContain("sup3r-secret-relay-password"); expect(rendered).not.toContain("relay@example.org"); // Honest about what a connection proves: it did not send anything, and deliverability is not // something this check can establish. expect(rendered).toMatch(/did not|without sending/iu); }); it("warns rather than failing when a configured relay does not answer", async () => { const services = dependencies({ probeMail: vi.fn(async () => { throw new Error("connection refused to smtp.example.org with sup3r-secret-relay-password"); }), }); const report = await diagnoseEkklesia({ ...options(), mailConfiguration }, services); const mailCheck = check(report, "outbound-email.configuration"); // A warning, never a failure: the configuration is valid and nothing depends on mail yet. expect(mailCheck.status).toBe("warning"); expect(report.result).not.toBe("not-ready"); // The thrown value carried a credential; none of it reaches the report. expect(mailCheck.details.join(" ")).not.toContain("sup3r-secret-relay-password"); }); it("fails a malformed configuration with a sanitized code and probes nothing", async () => { const services = dependencies(); const report = await diagnoseEkklesia( { ...options(), mailConfiguration: { ...mailConfiguration, port: "not-a-port" } }, services, ); const mailCheck = check(report, "outbound-email.configuration"); expect(mailCheck.status).toBe("failure"); const rendered = mailCheck.details.join(" "); expect(rendered).toContain("MAIL_PORT_INVALID"); expect(rendered).not.toContain("sup3r-secret-relay-password"); // A configuration this broken is never connected to. expect(services.probeMail).not.toHaveBeenCalled(); }); it("refuses plaintext SMTP when the deployment validates as production", async () => { const services = dependencies(); const report = await diagnoseEkklesia( { ...options(), databaseEnvironment: "production", mailConfiguration: { ...mailConfiguration, security: "plaintext" }, }, services, ); expect(check(report, "outbound-email.configuration").details.join(" ")).toContain( "MAIL_SECURITY_INSECURE_IN_PRODUCTION", ); }); }); describe("Ekklesia Doctor diagnostics", () => { it("publishes a healthy deterministic static report without database configuration", async () => { const people = discoveredModule("people"); const calendar = discoveredModule("calendar", "0.2.0"); const inventory = migrationAggregate( [migration("people", "0000_initial", checksumA)], [people, calendar], ); const services = dependencies({ discoverMigrations: vi.fn(async () => inventory), discoverModules: vi.fn(async () => Object.freeze([people, calendar])), discoverSchemas: vi.fn(async () => schemaAggregate([people, calendar])), }); const report = await diagnoseEkklesia(options(), services); expect(report.checks.map((entry) => entry.id)).toEqual([ "platform.runtime", "modules.discovery", "modules.dependencies", "modules.permissions", "modules.lifecycle", "modules.configuration", "schemas.discovery", "migrations.discovery", "database.transport", "postgresql.connection", "postgresql.plan", "identity.configuration", "identity.client-address", "object-storage.integration", "object-storage.bindings", "outbound-email.configuration", ]); expect(check(report, "modules.discovery").details).toEqual(["people 0.1.0", "calendar 0.2.0"]); expect(check(report, "modules.dependencies").details).toEqual(["Order: people → calendar"]); expect(check(report, "modules.permissions")).toMatchObject({ details: ["0 permissions registered"], status: "pass", }); expect(check(report, "migrations.discovery").details).toEqual([ "1 migration · checksums valid", "people:0000_initial · aaaaaaaaaaaa", ]); expect(JSON.stringify(report)).not.toContain(checksumA); expect(report.result).toBe("ready-with-warnings"); expect(doctorExitCode(report)).toBe(0); expect(services.createMigrationDatabase).not.toHaveBeenCalled(); expect(Object.isFrozen(report)).toBe(true); expect(Object.isFrozen(report.checks)).toBe(true); expect(report.checks.every(Object.isFrozen)).toBe(true); expect(report.checks.every((entry) => Object.isFrozen(entry.details))).toBe(true); }); it("derives aggregate results and exit codes with failure precedence", () => { const pass = { details: [], id: "platform.runtime", status: "pass", summary: "pass" } as const; const warning = { ...pass, status: "warning" } as const; const skipped = { ...pass, status: "skipped" } as const; const failure = { ...pass, status: "failure" } as const; expect(deriveDoctorResult([pass])).toBe("ready"); expect(deriveDoctorResult([pass, warning])).toBe("ready-with-warnings"); expect(deriveDoctorResult([pass, skipped])).toBe("ready-with-warnings"); expect(deriveDoctorResult([warning, failure])).toBe("not-ready"); expect(doctorExitCode({ checks: [pass], result: "ready" })).toBe(0); expect(doctorExitCode({ checks: [warning], result: "ready-with-warnings" })).toBe(0); expect(doctorExitCode({ checks: [failure], result: "not-ready" })).toBe(1); }); it("uses the configured production planner seam with finite timeouts", async () => { const createMigrationDatabase = vi.fn(() => emptyDatabase()); const planMigrations = vi.fn(async () => plan()); const report = await diagnoseEkklesia( options("postgresql://not-rendered.invalid/ekklesia"), dependencies({ createMigrationDatabase, planMigrations }), ); // Doctor's own read-only connection carries the same validated transport policy every other // executable uses — it defaults to certificate-verified TLS, exactly like production. expect(createMigrationDatabase).toHaveBeenCalledWith( "postgresql://not-rendered.invalid/ekklesia", { connectionTimeoutMs: 10_000, operationTimeoutMs: 10_000, transport: { mode: "verify-full" }, }, ); expect(planMigrations).toHaveBeenCalledOnce(); expect(check(report, "postgresql.connection")).toMatchObject({ status: "pass", summary: "Connection established", }); expect(check(report, "postgresql.plan")).toMatchObject({ details: ["0 applied · 0 pending · 0 approval-blocked"], status: "pass", }); }); it("uses the read-only planner for a fresh database and closes without mutation", async () => { const pending = migration("people", "0000_initial", checksumA); const aggregate = migrationAggregate([pending], [discoveredModule("people")]); const session: MigrationDatabaseSession = { acquireMigrationLock: vi.fn(), close: vi.fn().mockResolvedValue(undefined), readAppliedMigrations: vi.fn().mockResolvedValue(Object.freeze([])), releaseMigrationLock: vi.fn(), transaction: vi.fn(), }; const database: MigrationDatabase = Object.freeze({ openSession: vi.fn().mockResolvedValue(session), }); const report = await diagnoseEkklesia( options("postgresql://not-rendered.invalid/ekklesia"), dependencies({ createMigrationDatabase: vi.fn(() => database), discoverMigrations: vi.fn(async () => aggregate), planMigrations: planModuleMigrations, }), ); expect(session.readAppliedMigrations).toHaveBeenCalledOnce(); expect(session.close).toHaveBeenCalledOnce(); expect(session.acquireMigrationLock).not.toHaveBeenCalled(); expect(session.releaseMigrationLock).not.toHaveBeenCalled(); expect(session.transaction).not.toHaveBeenCalled(); expect(check(report, "postgresql.plan")).toMatchObject({ status: "warning", summary: "1 migration requires attention", }); }); it("reports applied, pending, and approval-blocked entries in plan order", async () => { const entries = Object.freeze([ planEntry("people:0000_initial", checksumA, "applied"), planEntry("people:0001_add_email", checksumB, "pending"), planEntry("people:0002_remove_old", checksumC, "approval-blocked"), ]); const report = await diagnoseEkklesia( options("postgresql://not-rendered.invalid/ekklesia"), dependencies({ planMigrations: vi.fn(async () => plan(entries)) }), ); expect(check(report, "postgresql.plan")).toEqual({ details: [ "1 applied · 1 pending · 1 approval-blocked", "applied · people:0000_initial · aaaaaaaaaaaa", "pending · people:0001_add_email · bbbbbbbbbbbb", "approval-blocked · people:0002_remove_old · cccccccccccc", ], id: "postgresql.plan", status: "warning", summary: "2 migrations require attention", }); }); it("fails a configured connection without attempting a plan result", async () => { const report = await diagnoseEkklesia( options("postgresql://not-rendered.invalid/ekklesia"), dependencies({ planMigrations: vi .fn() .mockRejectedValue(new MigrationRunnerError("MIGRATION_CONNECTION_FAILED")), }), ); expect(check(report, "postgresql.connection").status).toBe("failure"); expect(check(report, "postgresql.plan").status).toBe("skipped"); expect(report.result).toBe("not-ready"); expect(doctorExitCode(report)).toBe(1); }); it("reports invalid history as a planning failure after connectivity succeeds", async () => { const report = await diagnoseEkklesia( options("postgresql://not-rendered.invalid/ekklesia"), dependencies({ planMigrations: vi .fn() .mockRejectedValue( new MigrationRunnerError("MIGRATION_HISTORY_INVALID", "never-render-this"), ), }), ); expect(check(report, "postgresql.connection").status).toBe("pass"); expect(check(report, "postgresql.plan")).toMatchObject({ details: ["Applied migration history is invalid."], status: "failure", }); expect(JSON.stringify(report)).not.toContain("never-render-this"); }); it("skips every dependent check after atomic module discovery fails", async () => { const hostile = "postgresql://admin:password@host.invalid/db SELECT secret FROM x /home/operator/.env"; const discoverSchemas = vi.fn(); const discoverMigrations = vi.fn(); const createMigrationDatabase = vi.fn(); const report = await diagnoseEkklesia( options("postgresql://not-rendered.invalid/ekklesia"), dependencies({ createMigrationDatabase, discoverMigrations, discoverModules: vi.fn().mockRejectedValue(new Error(hostile)), discoverSchemas, }), ); expect(check(report, "modules.discovery").status).toBe("failure"); for (const id of [ "modules.dependencies", "modules.permissions", "schemas.discovery", "migrations.discovery", "postgresql.connection", "postgresql.plan", ] as const) { expect(check(report, id).status).toBe("skipped"); } expect(check(report, "platform.runtime").status).toBe("pass"); expect(check(report, "object-storage.integration").status).toBe("skipped"); expect(discoverSchemas).not.toHaveBeenCalled(); expect(discoverMigrations).not.toHaveBeenCalled(); expect(createMigrationDatabase).not.toHaveBeenCalled(); expect(JSON.stringify(report)).not.toContain(hostile); expect(JSON.stringify(report)).not.toContain("password"); }); it("passes permission composition with a safe declaration count when modules are installed", async () => { const calendar = discoveredModule("calendar"); const report = await diagnoseEkklesia( options(), dependencies({ composePermissions: vi.fn(() => createPermissionRegistry([ { baseline: "restricted", description: "Read calendar events.", id: parsePermissionId("calendar.events.read"), }, ]), ), discoverModules: vi.fn(async () => Object.freeze([calendar])), }), ); expect(check(report, "modules.permissions")).toMatchObject({ details: ["1 permission registered"], status: "pass", }); }); it("reuses the already-discovered module list rather than rediscovering modules for permission composition", async () => { const calendar = discoveredModule("calendar"); const composePermissions = vi.fn(() => createPermissionRegistry([])); const discoverModules = vi.fn(async () => Object.freeze([calendar])); await diagnoseEkklesia(options(), dependencies({ composePermissions, discoverModules })); expect(discoverModules).toHaveBeenCalledOnce(); expect(composePermissions).toHaveBeenCalledOnce(); expect(composePermissions).toHaveBeenCalledWith([calendar]); }); it("fails permission composition when a module claims the reserved core.* namespace, distinctly from a generic duplicate", async () => { const report = await diagnoseEkklesia( options(), dependencies({ composePermissions: vi.fn(() => { throw new ModulePermissionCompositionError("core", "core.rogue.permission"); }), }), ); expect(check(report, "modules.permissions")).toMatchObject({ details: [ 'Module "core" declared permission "core.rogue.permission" in the reserved core.* namespace.', ], status: "failure", }); expect(report.result).toBe("not-ready"); }); it("fails permission composition on a generic duplicate declaration, distinctly from a reserved-namespace violation", async () => { const report = await diagnoseEkklesia( options(), dependencies({ composePermissions: vi.fn(() => { throw new PermissionRegistryError("calendar.events.read"); }), }), ); expect(check(report, "modules.permissions")).toMatchObject({ details: ['Permission "calendar.events.read" is declared more than once.'], status: "failure", }); expect(report.result).toBe("not-ready"); }); it("sanitizes an unexpected permission-composition exception to a fixed generic detail", async () => { const hostile = "postgresql://admin:password@host.invalid/db /home/operator/.env"; const report = await diagnoseEkklesia( options(), dependencies({ composePermissions: vi.fn(() => { throw new Error(hostile); }), }), ); expect(check(report, "modules.permissions")).toMatchObject({ details: ["The installed permission declarations could not be composed safely."], status: "failure", }); expect(JSON.stringify(report)).not.toContain(hostile); }); it("continues independent schema, migration, and PostgreSQL checks after permission composition fails", async () => { const planMigrations = vi.fn(async () => plan()); const report = await diagnoseEkklesia( options("postgresql://not-rendered.invalid/ekklesia"), dependencies({ composePermissions: vi.fn(() => { throw new PermissionRegistryError("calendar.events.read"); }), planMigrations, }), ); expect(check(report, "modules.permissions").status).toBe("failure"); expect(check(report, "schemas.discovery").status).toBe("pass"); expect(check(report, "migrations.discovery").status).toBe("pass"); expect(check(report, "postgresql.connection").status).toBe("pass"); expect(planMigrations).toHaveBeenCalledOnce(); }); it("continues independent migration and PostgreSQL checks after schema failure", async () => { const planMigrations = vi.fn(async () => plan()); const report = await diagnoseEkklesia( options("postgresql://not-rendered.invalid/ekklesia"), dependencies({ discoverSchemas: vi.fn().mockRejectedValue(new Error("hostile schema detail")), planMigrations, }), ); expect(check(report, "schemas.discovery").status).toBe("failure"); expect(check(report, "migrations.discovery").status).toBe("pass"); expect(check(report, "postgresql.connection").status).toBe("pass"); expect(planMigrations).toHaveBeenCalledOnce(); expect(JSON.stringify(report)).not.toContain("hostile schema detail"); }); it("sanitizes an unexpected configured-database exception", async () => { const hostile = "postgresql://root:hunter2@private.invalid:5432/db -- DROP TABLE x; /srv/secret/.env"; const report = await diagnoseEkklesia( options("postgresql://also-secret.invalid/ekklesia"), dependencies({ planMigrations: vi.fn().mockRejectedValue(new Error(hostile)) }), ); const serialized = JSON.stringify(report); expect(check(report, "postgresql.connection").status).toBe("failure"); expect(serialized).not.toContain("hunter2"); expect(serialized).not.toContain("private.invalid"); expect(serialized).not.toContain("DROP TABLE"); expect(serialized).not.toContain("/srv/secret"); expect(serialized).not.toContain("also-secret.invalid"); }); it("skips the storage check, without probing, when no provider is configured", async () => { const services = dependencies(); const report = await diagnoseEkklesia(options(), services); expect(check(report, "object-storage.integration")).toEqual({ details: [ "No S3_ENDPOINT is set. This is a complete deployment unless a module requires storage.", ], id: "object-storage.integration", status: "skipped", summary: "Object storage not configured", }); expect(services.probeStorage).not.toHaveBeenCalled(); }); it("continues static discovery when runtime validation fails", async () => { const services = dependencies({ runtimeSatisfies: vi.fn(() => false) }); const report = await diagnoseEkklesia(options(), services); expect(check(report, "platform.runtime").status).toBe("failure"); expect(check(report, "modules.discovery").status).toBe("pass"); expect(services.discoverSchemas).toHaveBeenCalledOnce(); expect(services.discoverMigrations).toHaveBeenCalledOnce(); }); it("skips identity configuration (not a failure) when none of it is set", async () => { const report = await diagnoseEkklesia(options(), dependencies()); const identity = check(report, "identity.configuration"); expect(identity.status).toBe("skipped"); expect(identity.details[0]).toContain("Core identity schema defines 4 tables"); expect(report.result).toBe("ready-with-warnings"); expect(doctorExitCode(report)).toBe(0); }); it("passes identity configuration when a valid secret, base URL, and trusted origins are provided", async () => { const report = await diagnoseEkklesia( { ...options(), identityConfiguration: { baseUrl: "https://identity.example.test", environment: "production", secret: "correct-horse-battery-staple-0123456789", trustedOrigins: ["https://identity.example.test"], }, }, dependencies(), ); const identity = check(report, "identity.configuration"); expect(identity.status).toBe("pass"); expect(report.result).toBe("ready-with-warnings"); // PostgreSQL is still skipped in this fixture }); it("fails identity configuration for an invalid secret without leaking it", async () => { const hostileSecret = "s3cr3t-do-not-log"; const report = await diagnoseEkklesia( { ...options(), identityConfiguration: { baseUrl: "https://identity.example.test", environment: "production", secret: hostileSecret, trustedOrigins: ["https://identity.example.test"], }, }, dependencies(), ); const identity = check(report, "identity.configuration"); expect(identity.status).toBe("failure"); expect(identity.details.some((detail) => detail.includes("SECRET_TOO_SHORT"))).toBe(true); expect(report.result).toBe("not-ready"); expect(doctorExitCode(report)).toBe(1); expect(JSON.stringify(report)).not.toContain(hostileSecret); }); it("passes identity configuration and reports public signup disabled by default", async () => { const report = await diagnoseEkklesia( { ...options(), identityConfiguration: { baseUrl: "https://identity.example.test", environment: "production", secret: "correct-horse-battery-staple-0123456789", trustedOrigins: ["https://identity.example.test"], }, }, dependencies(), ); const identity = check(report, "identity.configuration"); expect(identity.status).toBe("pass"); expect(identity.details.some((detail) => detail.includes("disabled (safe default)"))).toBe( true, ); }); it("reports public signup as explicitly enabled when configured in development", async () => { const report = await diagnoseEkklesia( { ...options(), identityConfiguration: { baseUrl: "http://localhost:3000", environment: "development", publicSignupEnabled: "true", secret: "correct-horse-battery-staple-0123456789", trustedOrigins: ["http://localhost:3000"], }, }, dependencies(), ); const identity = check(report, "identity.configuration"); expect(identity.status).toBe("pass"); expect(identity.details.some((detail) => detail.includes("explicitly ENABLED"))).toBe(true); }); it("blocks readiness — not merely warns — when production configuration enables public signup", async () => { const report = await diagnoseEkklesia( { ...options(), identityConfiguration: { baseUrl: "https://identity.example.test", environment: "production", publicSignupEnabled: "true", secret: "correct-horse-battery-staple-0123456789", trustedOrigins: ["https://identity.example.test"], }, }, dependencies(), ); const identity = check(report, "identity.configuration"); expect(identity.status).toBe("failure"); expect( identity.details.some((detail) => detail.includes("PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION")), ).toBe(true); expect(report.result).toBe("not-ready"); expect(doctorExitCode(report)).toBe(1); }); it("fails identity configuration for a misspelled public-signup value, never silently disabling or enabling it", async () => { const report = await diagnoseEkklesia( { ...options(), identityConfiguration: { baseUrl: "https://identity.example.test", environment: "production", publicSignupEnabled: "yes", secret: "correct-horse-battery-staple-0123456789", trustedOrigins: ["https://identity.example.test"], }, }, dependencies(), ); const identity = check(report, "identity.configuration"); expect(identity.status).toBe("failure"); expect( identity.details.some((detail) => detail.includes("PUBLIC_SIGNUP_ENABLED_INVALID")), ).toBe(true); expect(report.result).toBe("not-ready"); }); it("passes database transport by default, reporting verified TLS without revealing any connection detail", async () => { const report = await diagnoseEkklesia( { ...options("postgresql://admin:hunter2@db.internal.example:5432/ekklesia") }, dependencies(), ); const transport = check(report, "database.transport"); expect(transport.status).toBe("pass"); expect(transport.summary).toBe("Database TLS verified"); const rendered = JSON.stringify(transport); expect(rendered).not.toContain("hunter2"); expect(rendered).not.toContain("db.internal.example"); expect(rendered).not.toContain("postgresql://"); }); it("blocks readiness when a production deployment would reach PostgreSQL without verified TLS", async () => { for (const [label, extra] of [ ["explicit production plaintext", { databaseTransport: { mode: "disable" } }], [ "connection-string downgrade", { databaseUrl: "postgresql://db.example.test/ekklesia?sslmode=disable" }, ], ["malformed mode", { databaseTransport: { mode: "require" } }], ] as const) { const report = await diagnoseEkklesia( { ...options("postgresql://db.example.test/ekklesia"), ...extra }, dependencies(), ); const transport = check(report, "database.transport"); expect(transport.status, `expected ${label} to block readiness`).toBe("failure"); expect(transport.summary).toBe("Database transport unsafe"); expect(report.result).toBe("not-ready"); expect(doctorExitCode(report)).toBe(1); } }); it("passes development plaintext only when development is explicitly selected", async () => { const report = await diagnoseEkklesia( { ...options("postgresql://127.0.0.1:5432/ekklesia"), databaseEnvironment: "development", }, dependencies(), ); const transport = check(report, "database.transport"); expect(transport.status).toBe("pass"); expect(transport.summary).toContain("disabled"); }); it("passes client-address configuration with direct mode when nothing is configured", async () => { const report = await diagnoseEkklesia(options(), dependencies()); const clientAddress = check(report, "identity.client-address"); expect(clientAddress.status).toBe("pass"); expect(clientAddress.summary).toContain("Direct"); expect(report.result).toBe("ready-with-warnings"); }); it("passes client-address configuration with an explicitly configured trusted-proxy mode", async () => { const report = await diagnoseEkklesia( { ...options(), clientAddress: { header: "x-forwarded-for", mode: "trusted-proxy", trustedProxies: ["10.0.0.1", "10.0.0.2"], }, }, dependencies(), ); const clientAddress = check(report, "identity.client-address"); expect(clientAddress.status).toBe("pass"); expect(clientAddress.summary).toContain("Trusted-proxy"); expect(clientAddress.details.some((detail) => detail.includes("x-forwarded-for"))).toBe(true); }); it("fails client-address configuration for an invalid trusted-proxy entry without leaking it", async () => { const hostileEntry = "not-an-ip-secret-value"; const report = await diagnoseEkklesia( { ...options(), clientAddress: { header: "x-forwarded-for", mode: "trusted-proxy", trustedProxies: [hostileEntry], }, }, dependencies(), ); const clientAddress = check(report, "identity.client-address"); expect(clientAddress.status).toBe("failure"); expect( clientAddress.details.some((detail) => detail.includes("CLIENT_ADDRESS_TRUSTED_PROXY_INVALID"), ), ).toBe(true); expect(JSON.stringify(report)).not.toContain(hostileEntry); expect(report.result).toBe("not-ready"); }); it("fails client-address configuration for an incomplete trusted-proxy mode (missing header)", async () => { const report = await diagnoseEkklesia( { ...options(), clientAddress: { mode: "trusted-proxy", trustedProxies: ["10.0.0.1"] }, }, dependencies(), ); const clientAddress = check(report, "identity.client-address"); expect(clientAddress.status).toBe("failure"); expect(report.result).toBe("not-ready"); }); it("never constructs a migration database or Better Auth merely to check identity configuration", async () => { const services = dependencies(); await diagnoseEkklesia( { ...options(), identityConfiguration: { baseUrl: "https://identity.example.test", secret: "correct-horse-battery-staple-0123456789", trustedOrigins: ["https://identity.example.test"], }, }, services, ); expect(services.createMigrationDatabase).not.toHaveBeenCalled(); }); }); const databaseUrl = "postgresql://not-rendered.invalid/ekklesia"; describe("Ekklesia Doctor — module lifecycle", () => { /** A discovered module double with a real required-dependency edge, so blocking is exercised * through the same resolver production uses rather than a hand-written eligibility record. */ function dependentModule(id: string, requires: string): DiscoveredModule { const base = discoveredModule(id); return { ...base, manifest: { ...base.manifest, dependencies: { capabilities: { optional: {}, required: {} }, modules: { optional: {}, required: { [requires]: "^0.1.0" } }, }, } as unknown as ModuleManifest, }; } it("skips the check, rather than guessing a default, when DATABASE_URL is not configured", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("calendar")])), }); const report = await diagnoseEkklesia(options(), services); expect(check(report, "modules.lifecycle").status).toBe("skipped"); expect(check(report, "modules.lifecycle").details).toEqual(["DATABASE_URL is not configured."]); // Configuration lives in PostgreSQL; reporting "everything is enabled" without reading it would // be a guess an operator could act on. expect(services.readModuleLifecycle).not.toHaveBeenCalled(); }); it("skips the check when discovery itself failed", async () => { const services = dependencies({ discoverModules: vi.fn(async () => { throw new Error("broken inventory"); }), }); const report = await diagnoseEkklesia(options(databaseUrl), services); expect(check(report, "modules.lifecycle").status).toBe("skipped"); expect(check(report, "modules.lifecycle").details).toEqual([ "Complete module discovery is required.", ]); }); it("passes and lists each module when everything enabled can run", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("storage"), discoveredModule("calendar", "0.2.0")]), ), }); const report = await diagnoseEkklesia(options(databaseUrl), services); expect(check(report, "modules.lifecycle").status).toBe("pass"); expect(check(report, "modules.lifecycle").details).toEqual([ "2 eligible · 0 disabled · 0 blocked", "storage 0.1.0 · enabled", "calendar 0.2.0 · enabled", ]); }); it("still passes when a module is deliberately disabled — a decision is not a problem", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("calendar")])), readModuleLifecycle: vi.fn(async () => Object.freeze({ modules: Object.freeze([{ enabled: false, moduleId: "calendar" }]) }), ), }); const report = await diagnoseEkklesia(options(databaseUrl), services); expect(check(report, "modules.lifecycle").status).toBe("pass"); expect(check(report, "modules.lifecycle").details).toEqual([ "0 eligible · 1 disabled · 0 blocked", "calendar 0.1.0 · disabled", ]); }); it("distinguishes the platform default from a decision somebody made", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("calendar")])), readModuleLifecycle: vi.fn(async () => Object.freeze({ modules: Object.freeze([]) })), }); const report = await diagnoseEkklesia(options(databaseUrl), services); // Nothing stored: the module is enabled by the platform default, and the detail says so. expect(check(report, "modules.lifecycle").details).toContain("calendar 0.1.0 · enabled"); }); it("warns when an enabled module cannot run, naming the dependency an operator must act on", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("storage"), dependentModule("announcements", "storage")]), ), readModuleLifecycle: vi.fn(async () => Object.freeze({ modules: Object.freeze([{ enabled: false, moduleId: "storage" }]) }), ), }); const report = await diagnoseEkklesia(options(databaseUrl), services); const lifecycle = check(report, "modules.lifecycle"); // An enabled module that cannot run is the anomaly worth surfacing — and it is exactly the // question an operator would otherwise take to the logs. expect(lifecycle.status).toBe("warning"); expect(lifecycle.summary).toBe("1 enabled module cannot run"); expect(lifecycle.details).toEqual([ "0 eligible · 1 disabled · 1 blocked", "storage 0.1.0 · disabled", "announcements 0.1.0 · blocked · required_dependency_disabled · storage", ]); expect(report.result).toBe("ready-with-warnings"); }); it("fails safely, without a driver detail, when the configuration cannot be read", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("calendar")])), readModuleLifecycle: vi.fn(async () => { throw new Error("connect ECONNREFUSED 10.40.0.2:5432 password=hunter2"); }), }); const report = await diagnoseEkklesia(options(databaseUrl), services); const lifecycle = check(report, "modules.lifecycle"); expect(lifecycle.status).toBe("failure"); expect(lifecycle.details).toEqual([ "The stored module enablement configuration could not be read safely.", ]); expect(JSON.stringify(lifecycle)).not.toContain("hunter2"); expect(JSON.stringify(lifecycle)).not.toContain("10.40.0.2"); }); it("reads with finite timeouts and the same validated transport policy every executable uses", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("calendar")])), }); await diagnoseEkklesia(options(databaseUrl), services); expect(services.readModuleLifecycle).toHaveBeenCalledWith( databaseUrl, expect.objectContaining({ connectionTimeoutMs: 10_000, operationTimeoutMs: 10_000, transport: expect.objectContaining({ mode: "verify-full" }), }), ); }); it("still reports lifecycle when an unrelated check fails, and never blocks on one", async () => { const services = dependencies({ composePermissions: vi.fn(() => { throw new Error("composition exploded"); }), discoverModules: vi.fn(async () => Object.freeze([discoveredModule("calendar")])), }); const report = await diagnoseEkklesia(options(databaseUrl), services); expect(check(report, "modules.permissions").status).toBe("failure"); expect(check(report, "modules.lifecycle").status).toBe("pass"); }); }); describe("Ekklesia Doctor — module configuration", () => { function configuringModule( id: string, configuration: readonly Record[], ): DiscoveredModule { const base = discoveredModule(id); return { ...base, manifest: { ...base.manifest, configuration } as unknown as ModuleManifest, }; } it("skips the check, rather than guessing, when DATABASE_URL is not configured", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("announcements")])), }); const report = await diagnoseEkklesia(options(), services); expect(check(report, "modules.configuration").status).toBe("skipped"); expect(services.readModuleConfiguration).not.toHaveBeenCalled(); }); it("passes and says so plainly when a module declares no configuration", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("calendar")])), }); const report = await diagnoseEkklesia(options(databaseUrl), services); expect(check(report, "modules.configuration").status).toBe("pass"); expect(check(report, "modules.configuration").details).toEqual([ "0 modules declaring configuration · 0 blocked", "calendar 0.1.0 · no configuration declared", ]); }); it("passes with a key count when configuration is satisfied, and prints no value", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([ configuringModule("announcements", [ { key: "slideSeconds", type: "integer", default: 7, required: false }, { key: "bucket", type: "string", required: true }, ]), ]), ), readModuleConfiguration: vi.fn(async () => Object.freeze({ records: Object.freeze([ { key: "bucket", moduleId: "announcements", value: "very-specific-bucket-name" }, ]), }), ), }); const report = await diagnoseEkklesia(options(databaseUrl), services); const configuration = check(report, "modules.configuration"); expect(configuration.status).toBe("pass"); expect(configuration.details).toEqual([ "1 module declaring configuration · 0 blocked", "announcements 0.1.0 · 2 keys declared · valid", ]); // Counts and keys, never values: `pnpm doctor` output gets pasted into chat. expect(JSON.stringify(configuration)).not.toContain("very-specific-bucket-name"); }); it("warns and names the offending key when required configuration is missing", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([ configuringModule("announcements", [{ key: "bucket", type: "string", required: true }]), ]), ), }); const report = await diagnoseEkklesia(options(databaseUrl), services); const configuration = check(report, "modules.configuration"); expect(configuration.status).toBe("warning"); expect(configuration.summary).toBe("1 module cannot run with its current configuration"); expect(configuration.details).toEqual([ "1 module declaring configuration · 1 blocked", "announcements 0.1.0 · 1 key declared · configuration_missing · bucket", ]); }); it("names a missing secret's key and never its value", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([ configuringModule("announcements", [ { key: "storageCredential", type: "secret", required: true }, ]), ]), ), }); const report = await diagnoseEkklesia(options(databaseUrl), services); expect(check(report, "modules.configuration").details).toContain( "announcements 0.1.0 · 1 key declared · secret_missing · storageCredential", ); }); it("reports a supplied secret as satisfied, without printing it", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([ configuringModule("announcements", [ { key: "storageCredential", type: "secret", required: true }, ]), ]), ), }); const report = await diagnoseEkklesia( { ...options(databaseUrl), moduleSecrets: { get: () => "super-secret-value" }, }, services, ); const configuration = check(report, "modules.configuration"); expect(configuration.status).toBe("pass"); expect(JSON.stringify(configuration)).not.toContain("super-secret-value"); }); it("does not evaluate a disabled module's configuration, and says so", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([ configuringModule("announcements", [{ key: "bucket", type: "string", required: true }]), ]), ), readModuleLifecycle: vi.fn(async () => Object.freeze({ modules: Object.freeze([{ enabled: false, moduleId: "announcements" }]) }), ), }); const report = await diagnoseEkklesia(options(databaseUrl), services); const configuration = check(report, "modules.configuration"); // Claiming it is valid would assert something nothing checked; claiming it is broken would be // noise about a module nobody asked to run. expect(configuration.status).toBe("pass"); expect(configuration.details).toContain( "announcements 0.1.0 · 1 key declared · not evaluated (disabled)", ); }); it("fails safely, without a driver detail, when the configuration cannot be read", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("announcements")])), readModuleConfiguration: vi.fn(async () => { throw new Error("connect ECONNREFUSED 10.40.0.2:5432 password=hunter2"); }), }); const report = await diagnoseEkklesia(options(databaseUrl), services); const configuration = check(report, "modules.configuration"); expect(configuration.status).toBe("failure"); expect(JSON.stringify(configuration)).not.toContain("hunter2"); expect(JSON.stringify(configuration)).not.toContain("10.40.0.2"); }); it("agrees with the lifecycle check, because both come from one resolution", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([ configuringModule("announcements", [{ key: "bucket", type: "string", required: true }]), ]), ), }); const report = await diagnoseEkklesia(options(databaseUrl), services); // Doctor can never report a module as runnable that startup would block, or the reverse. expect(check(report, "modules.lifecycle").status).toBe("warning"); expect(check(report, "modules.configuration").status).toBe("warning"); expect(check(report, "modules.lifecycle").details).toContain( "announcements 0.1.0 · blocked · configuration_missing · bucket", ); }); }); describe("Ekklesia Doctor — object storage", () => { const storageConfig = { accessKey: "AKIAEXAMPLEACCESSID", bucketPrefix: "ekklesia", deliveryEndpoint: "https://storage.example.test", endpoint: "https://storage.example.test", forcePathStyle: true, region: "us-east-1", secretKey: "sk-example-secret-material", } as const; function storingModule(id: string, buckets: readonly string[]): DiscoveredModule { const base = discoveredModule(id); return { ...base, manifest: { ...base.manifest, storage: { buckets: buckets.map((bucketId) => ({ id: bucketId })) }, } as unknown as ModuleManifest, }; } it("does not probe, and stays healthy, when no provider is configured and nothing needs one", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("calendar")])), }); const report = await diagnoseEkklesia(options(databaseUrl), services); expect(services.probeStorage).not.toHaveBeenCalled(); expect(check(report, "object-storage.integration").status).toBe("skipped"); // The load-bearing assertion: an installation whose modules need no object storage is not // unhealthy for lacking a provider. expect(check(report, "object-storage.bindings").status).toBe("pass"); expect(check(report, "object-storage.bindings").details).toEqual([ "0 modules declare a logical storage bucket.", ]); }); it("fails the bindings check when a module requires storage and none is configured", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([storingModule("announcements", ["announcement-assets"])]), ), }); const report = await diagnoseEkklesia(options(databaseUrl), services); const bindings = check(report, "object-storage.bindings"); expect(bindings.status).toBe("failure"); expect(bindings.details[0]).toContain("Configure an S3-compatible Ekklesia storage provider"); expect(bindings.details).toContain( "announcements · announcement-assets · unbound (no storage provider configured)", ); // And the lifecycle check agrees, because both come from one resolution. expect(check(report, "modules.lifecycle").details).toContain( "announcements 0.1.0 · blocked · storage_unavailable · announcement-assets", ); }); it("resolves and lists every binding when a provider is configured", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([storingModule("announcements", ["announcement-assets"])]), ), }); const report = await diagnoseEkklesia( { ...options(databaseUrl), storageConfiguration: storageConfig }, services, ); const bindings = check(report, "object-storage.bindings"); expect(bindings.status).toBe("pass"); // The physical bucket is named here deliberately: Doctor is an operator tool, and someone // debugging a permissions problem needs to know which bucket to look at. It is still never // reachable from module-facing code. expect(bindings.details[0]).toMatch( /^announcements · announcement-assets → ekklesia-announcements--announcement-assets-[0-9a-f]{12}$/u, ); }); it("probes a configured provider and reports it reachable, without printing a credential", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("calendar")])), }); const report = await diagnoseEkklesia( { ...options(databaseUrl), storageConfiguration: storageConfig }, services, ); const integration = check(report, "object-storage.integration"); expect(services.probeStorage).toHaveBeenCalledWith(storageConfig); expect(integration.status).toBe("pass"); expect(integration.details[0]).toContain("https://storage.example.test"); const serialized = JSON.stringify(integration); expect(serialized).not.toContain("AKIAEXAMPLEACCESSID"); expect(serialized).not.toContain("sk-example-secret-material"); }); it("warns rather than fails when a configured provider is unreachable", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([discoveredModule("calendar")])), probeStorage: vi.fn(async () => { throw new Error("connect ECONNREFUSED 10.40.0.2:8333 accessKey=AKIAEXAMPLEACCESSID"); }), }); const report = await diagnoseEkklesia( { ...options(databaseUrl), storageConfiguration: storageConfig }, services, ); const integration = check(report, "object-storage.integration"); // A warning, never a failure. A momentary network blip must not make the whole platform read as // NOT READY, and it must never look like a module problem (ADR 0030). expect(integration.status).toBe("warning"); expect(report.result).not.toBe("not-ready"); expect(integration.details).toContain( "The configured storage service did not respond. Modules already running are unaffected.", ); const serialized = JSON.stringify(integration); expect(serialized).not.toContain("AKIAEXAMPLEACCESSID"); expect(serialized).not.toContain("10.40.0.2"); }); it("still resolves bindings when the provider is unreachable, because binding is not health", async () => { const services = dependencies({ discoverModules: vi.fn(async () => Object.freeze([storingModule("announcements", ["announcement-assets"])]), ), probeStorage: vi.fn(async () => { throw new Error("unreachable"); }), }); const report = await diagnoseEkklesia( { ...options(databaseUrl), storageConfiguration: storageConfig }, services, ); expect(check(report, "object-storage.integration").status).toBe("warning"); // The module is still eligible: a configured-but-briefly-unreachable provider is an operational // condition, not a lifecycle state. expect(check(report, "object-storage.bindings").status).toBe("pass"); expect(check(report, "modules.lifecycle").details).toContain("announcements 0.1.0 · enabled"); }); it("skips bindings when discovery itself failed", async () => { const services = dependencies({ discoverModules: vi.fn(async () => { throw new Error("broken inventory"); }), }); const report = await diagnoseEkklesia(options(databaseUrl), services); expect(check(report, "object-storage.bindings").status).toBe("skipped"); }); });