Files
Ekklesia/core/tests/doctor.test.ts
T
ekklesiaandClaude Opus 5.5 2a64383af5 test(deploy): Doctor must check the client-address configuration the API runs
Behind the reference proxy the API runs in trusted-proxy mode, but
compose.proxy.yaml gives those variables to `api` only. The documented
Doctor command runs as `migrate`, so it validated an empty
configuration and reported "Direct client-address mode" for an
installation that trusts its proxy. That is what the first production
installation saw.

Fails today:
- deploy.test.ts: the proxy overlay gives `migrate` no client-address
  environment;
- doctor.test.ts: the check does not say whose configuration it read;
- smoke-deploy.sh: Doctor through the proxy overlay does not report
  trusted-proxy mode (Docker).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 18:38:29 -05:00

1660 lines
62 KiB
TypeScript

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> = {}): 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<ReturnType<typeof diagnoseEkklesia>>,
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: "[email protected]",
fromName: "Ekklesia",
host: "smtp.example.org",
password: "sup3r-secret-relay-password",
security: "starttls",
username: "[email protected]",
};
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("[email protected]");
// 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:[email protected]/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:[email protected]/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:[email protected]: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
});
describe("Dashboard and API topology (ADR 0045)", () => {
const secret = "correct-horse-battery-staple-0123456789";
async function identityCheck(identity: {
readonly baseUrl: string;
readonly dashboardOrigin?: string;
readonly environment?: "development" | "production";
readonly trustedOrigins: readonly string[];
}) {
const report = await diagnoseEkklesia(
{
...options(),
identityConfiguration: { environment: "production", secret, ...identity },
},
dependencies(),
);
return { check: check(report, "identity.configuration"), report };
}
it("reports the same-origin topology when no Dashboard origin is set", async () => {
const { check: identity } = await identityCheck({
baseUrl: "https://ekklesia.example.test",
trustedOrigins: ["https://ekklesia.example.test"],
});
expect(identity.status).toBe("pass");
expect(identity.details).toContain(
"Topology: same-origin (the Dashboard is served from the API's origin).",
);
});
it("reports the split topology for sibling hostnames without printing either", async () => {
const { check: identity, report } = await identityCheck({
baseUrl: "https://api.example.test",
dashboardOrigin: "https://dash.example.test",
trustedOrigins: ["https://dash.example.test"],
});
expect(identity.status).toBe("pass");
expect(identity.details).toContain(
"Topology: split (the Dashboard and the API are on separate origins).",
);
expect(JSON.stringify(report)).not.toContain("dash.example.test");
expect(JSON.stringify(report)).not.toContain("api.example.test");
});
it.each([
["different registrable domains", "https://dash.example.test", "https://api.example.org"],
["two IP addresses", "https://10.0.0.2", "https://10.0.0.1"],
["a parent that is only a top-level domain", "https://dash.test", "https://api.test"],
])("warns, without failing, when split hostnames may not share a site: %s", async (_case, dashboardOrigin, baseUrl) => {
const { check: identity, report } = await identityCheck({
baseUrl,
dashboardOrigin,
trustedOrigins: [dashboardOrigin],
});
expect(identity.status).toBe("warning");
expect(identity.details.join("\n")).toContain("not siblings under one parent domain");
expect(doctorExitCode(report)).toBe(0);
});
it("treats another port on the same host as the same site (split development)", async () => {
const { check: identity } = await identityCheck({
baseUrl: "http://localhost:3000",
dashboardOrigin: "http://localhost:5173",
environment: "development",
trustedOrigins: ["http://localhost:5173"],
});
expect(identity.status).toBe("pass");
});
it("fails, as the API's startup would, for a Dashboard origin the API does not trust", async () => {
const { check: identity, report } = await identityCheck({
baseUrl: "https://api.example.test",
dashboardOrigin: "https://dash.example.test",
trustedOrigins: ["https://other.example.test"],
});
expect(identity.status).toBe("failure");
expect(identity.details.some((detail) => detail.includes("DASHBOARD_ORIGIN_UNTRUSTED"))).toBe(
true,
);
expect(doctorExitCode(report)).toBe(1);
});
});
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:[email protected]: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("says whose client-address configuration it checked: this process's, not a running API's", async () => {
for (const clientAddress of [
undefined,
{ header: "x-forwarded-for", mode: "trusted-proxy" as const, trustedProxies: ["10.0.0.1"] },
{ header: "x-forwarded-for", mode: "trusted-proxy" as const, trustedProxies: ["bad"] },
]) {
const report = await diagnoseEkklesia(
{ ...options(), ...(clientAddress ? { clientAddress } : {}) },
dependencies(),
);
const details = check(report, "identity.client-address").details;
expect(details.some((detail) => detail.includes("environment Doctor runs with"))).toBe(true);
}
});
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<string, unknown>[],
): 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");
});
});
/**
* A browser contribution whose target capability nobody provides is otherwise silent: the manifest
* is valid, the artifact exists, and the screen renders nothing with no error anywhere (ADR 0041).
* Doctor names it.
*/
describe("Doctor reports declared browser contributions", () => {
function contributing(
id: string,
contribution: { capability: string; capabilityRange: string },
): DiscoveredModule {
const base = discoveredModule(id);
return {
...base,
contributions: [
{
artifactPath: `/unpublished/${id}/contributions/slide.js`,
capability: contribution.capability,
capabilityRange: contribution.capabilityRange,
id: `${id}.slide`,
version: "1.0.0",
},
],
};
}
function providing(id: string, capability: { id: string; version: string }): DiscoveredModule {
const base = discoveredModule(id);
return {
...base,
manifest: { ...base.manifest, capabilities: [capability] } as DiscoveredModule["manifest"],
};
}
async function detailsFor(modules: readonly DiscoveredModule[]): Promise<readonly string[]> {
const report = await diagnoseEkklesia(
options(),
dependencies({
discoverModules: vi.fn(async () => Object.freeze([...modules])),
discoverSchemas: vi.fn(async () => schemaAggregate(modules)),
}),
);
return check(report, "modules.discovery").details;
}
it("says nothing extra for a module that contributes nothing", async () => {
expect(await detailsFor([discoveredModule("people")])).toEqual(["people 0.1.0"]);
});
it("reports a contribution whose provider satisfies its declared range", async () => {
const details = await detailsFor([
providing("signage", { id: "signage.slide-renderer", version: "1.2.0" }),
contributing("announcements", {
capability: "signage.slide-renderer",
capabilityRange: "^1.0.0",
}),
]);
expect(details).toEqual([
"signage 0.1.0",
"announcements 0.1.0 · 1 browser contribution (1 with a compatible provider)",
]);
});
it("reports a contribution nobody provides, and one outside its range, as unmatched", async () => {
expect(
await detailsFor([
contributing("announcements", {
capability: "signage.slide-renderer",
capabilityRange: "^1.0.0",
}),
]),
).toEqual(["announcements 0.1.0 · 1 browser contribution (0 with a compatible provider)"]);
expect(
await detailsFor([
providing("signage", { id: "signage.slide-renderer", version: "3.0.0" }),
contributing("announcements", {
capability: "signage.slide-renderer",
capabilityRange: "^1.0.0",
}),
]),
).toEqual([
"signage 0.1.0",
"announcements 0.1.0 · 1 browser contribution (0 with a compatible provider)",
]);
});
});