feat: add Ekklesia doctor command
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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";
|
||||
|
||||
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 {
|
||||
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");
|
||||
},
|
||||
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.
|
||||
|
||||
Data definitions
|
||||
✓ Schemas validated
|
||||
0 modules · 0 tables
|
||||
✓ Migrations validated
|
||||
0 migrations · checksums valid
|
||||
|
||||
PostgreSQL
|
||||
○ Connection check skipped
|
||||
DATABASE_URL is not configured.
|
||||
○ Migration plan skipped
|
||||
A validated migration inventory and configured PostgreSQL connection are required.
|
||||
|
||||
Object storage
|
||||
○ Check unavailable
|
||||
Core storage integration has not been implemented yet.
|
||||
|
||||
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:[email protected]/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:[email protected]/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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user