import { afterEach, describe, expect, it } from "vitest"; import { type DoctorDependencies, diagnoseEkklesia, formatDoctorReport, } from "../src/diagnostics/index.js"; import type { MigrationDatabase } from "../src/migrations/index.js"; import type { ModuleMigrationAggregate, ModuleSchemaAggregate } from "../src/modules/index.js"; import { composePlatformPermissionRegistry } from "../src/modules/permissions.js"; const originalNoColor = process.env.NO_COLOR; afterEach(() => { if (originalNoColor === undefined) { delete process.env.NO_COLOR; } else { process.env.NO_COLOR = originalNoColor; } }); function zeroSchemaAggregate(): ModuleSchemaAggregate { return Object.freeze({ discoveredModules: Object.freeze([]), drizzleSchema: Object.freeze({}), modules: Object.freeze([]), tables: Object.freeze([]), }); } function zeroMigrationAggregate(): ModuleMigrationAggregate { return Object.freeze({ discoveredModules: Object.freeze([]), migrations: Object.freeze([]), modules: Object.freeze([]), }); } function noConfigurationDependencies(): DoctorDependencies { return { composePermissions: composePlatformPermissionRegistry, createMigrationDatabase: () => { throw new Error("database must not be constructed"); }, discoverMigrations: async () => zeroMigrationAggregate(), discoverModules: async () => Object.freeze([]), discoverSchemas: async () => zeroSchemaAggregate(), planMigrations: async () => { throw new Error("database must not be planned"); }, probeStorage: async () => { throw new Error("storage must not be probed"); }, readModuleConfiguration: async () => { throw new Error("module configuration must not be read"); }, readModuleLifecycle: async () => { throw new Error("module lifecycle configuration must not be read"); }, runtimeSatisfies: () => true, }; } describe("Ekklesia Doctor terminal report", () => { it("renders the real zero-module and no-database shape exactly", async () => { const report = await diagnoseEkklesia( { moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory: "/not-rendered/modules", platformVersion: "0.1.0", }, runtimeRange: ">=24.11.0 <25", runtimeVersion: "24.15.0", }, noConfigurationDependencies(), ); expect(formatDoctorReport(report).join("\n")).toBe(`Ekklesia Doctor Platform ✓ Runtime version supported Active Node.js satisfies the declared supported range. Modules ✓ Module discovery complete 0 modules installed ✓ Dependency graph valid Dependency order is empty. ✓ Permission registry composed 5 permissions registered ○ Module lifecycle skipped DATABASE_URL is not configured. ○ Module configuration skipped DATABASE_URL is not configured. Data definitions ✓ Schemas validated 0 modules · 0 tables ✓ Migrations validated 0 migrations · checksums valid PostgreSQL ✓ Database TLS verified TLS required with certificate and hostname verification. ○ Connection check skipped DATABASE_URL is not configured. ○ Migration plan skipped A validated migration inventory and configured PostgreSQL connection are required. Identity ○ Identity configuration skipped Core identity schema defines 4 tables (ekklesia_core_account, ekklesia_core_session, ekklesia_core_user, ekklesia_core_verification). Identity runtime configuration (signing key, base URL, trusted origins) is not set. ✓ Direct client-address mode Trusts only the actual immediate socket peer for rate limiting; no forwarded header is trusted. Object storage ○ Object storage not configured No S3_ENDPOINT is set. This is a complete deployment unless a module requires storage. ✓ No module requires object storage 0 modules declare a logical storage bucket. Result: READY WITH WARNINGS`); }); it("does not depend on NO_COLOR or terminal interactivity", async () => { const report = await diagnoseEkklesia( { moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory: "/not-rendered/modules", platformVersion: "0.1.0", }, runtimeRange: ">=24.11.0 <25", runtimeVersion: "24.15.0", }, noConfigurationDependencies(), ); const baseline = formatDoctorReport(report); const originalIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY"); try { process.env.NO_COLOR = "1"; Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true }); const interactiveWithoutColor = formatDoctorReport(report); process.env.NO_COLOR = ""; Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: false }); const nonInteractive = formatDoctorReport(report); expect(interactiveWithoutColor).toEqual(baseline); expect(nonInteractive).toEqual(baseline); expect(interactiveWithoutColor.some((line) => line.includes("\u001B"))).toBe(false); expect(Object.isFrozen(interactiveWithoutColor)).toBe(true); } finally { if (originalIsTtyDescriptor) { Object.defineProperty(process.stdout, "isTTY", originalIsTtyDescriptor); } else { delete (process.stdout as { isTTY?: boolean }).isTTY; } } }); it("never renders secrets from a hostile database exception", async () => { const hostile = "postgresql://admin:secret@host.invalid/db SELECT password FROM users /home/me/.env"; const database = Object.freeze({}) as MigrationDatabase; const dependencies: DoctorDependencies = { ...noConfigurationDependencies(), createMigrationDatabase: () => database, planMigrations: async () => { throw new Error(hostile); }, }; const report = await diagnoseEkklesia( { databaseUrl: "postgresql://another:secret@host.invalid/db", moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory: "/not-rendered/modules", platformVersion: "0.1.0", }, runtimeRange: ">=24.11.0 <25", runtimeVersion: "24.15.0", }, dependencies, ); const output = formatDoctorReport(report).join("\n"); for (const forbidden of [ "admin", "secret", "host.invalid", "SELECT", "password", "/home/me", "postgresql://", ]) { expect(output).not.toContain(forbidden); } }); });