import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import { applyModuleMigrations, type AppliedMigrationRecord, type MigrationApproval, type MigrationApprovalProvider, type MigrationDatabase, type MigrationDatabaseSession, type MigrationHistoryInsert, MigrationRunnerError, type MigrationRunnerErrorCode, type MigrationTransaction, planModuleMigrations, } from "../src/migrations/runner.js"; import type { DiscoveredModuleMigration, ModuleMigrationAggregate, } from "../src/modules/migration-discovery.js"; const appliedAt = "2026-07-21T12:00:00.000Z"; const hostileDatabaseError = new Error( 'postgresql://admin:super-secret@private-db.internal/ekklesia failed at /home/ekklesia/private.sql while running DROP TABLE "people_records"', ); interface MigrationFixtureOptions { readonly approvalRequired?: boolean; readonly sql?: string; readonly tag?: string; } interface ModuleFixture { readonly id: string; readonly migrations: readonly DiscoveredModuleMigration[]; } function migration( moduleId: string, index: number, options: MigrationFixtureOptions = {}, ): DiscoveredModuleMigration { const tag = options.tag ?? `${index.toString().padStart(4, "0")}_change`; const sql = options.sql ?? `SELECT ${index + 1};\n`; const approvalRequired = options.approvalRequired ?? false; return Object.freeze({ approvalRequired, breakpoints: true, checksum: createHash("sha256").update(sql).digest("hex"), destructive: Object.freeze( approvalRequired ? { mode: "approval-required" as const, reason: "An administrator must approve this destructive fixture migration.", } : { mode: "none" as const }, ), file: `${tag}.sql`, identity: `${moduleId}:${tag}`, index, moduleId, sql, tag, timestamp: 1_700_000_000_000 + index, }); } function aggregate(...modules: readonly ModuleFixture[]): ModuleMigrationAggregate { const frozenModules = Object.freeze( modules.map((module) => Object.freeze({ migrations: Object.freeze([...module.migrations]), moduleId: module.id, }), ), ); return Object.freeze({ discoveredModules: Object.freeze([]), migrations: Object.freeze(frozenModules.flatMap((module) => module.migrations)), modules: frozenModules, }); } function historyRecord( source: DiscoveredModuleMigration, appliedOrder: number, overrides: Partial = {}, ): AppliedMigrationRecord { return Object.freeze({ appliedAt, appliedOrder, checksum: source.checksum, identity: source.identity, index: source.index, moduleId: source.moduleId, tag: source.tag, ...overrides, }); } type FailurePoint = | "close" | "commit" | "ensure-history" | "execute" | "history-insert" | "lock" | "open" | "read-history" | "release-lock"; interface FakeDatabaseOptions { readonly failures?: readonly FailurePoint[]; readonly history?: readonly AppliedMigrationRecord[]; readonly historyAfterLock?: readonly AppliedMigrationRecord[]; readonly historyTableExists?: boolean; readonly lockResult?: boolean; readonly statementFailureIndex?: number; } class FakeMigrationDatabase implements MigrationDatabase { readonly events: string[] = []; readonly executedStatements: string[] = []; readonly failures: ReadonlySet; readonly historyAfterLock: readonly AppliedMigrationRecord[] | undefined; readonly insertedMigrations: MigrationHistoryInsert[] = []; readonly lockResult: boolean; readonly statementFailureIndex: number | undefined; committedHistory: AppliedMigrationRecord[]; historyTableExists: boolean; constructor(options: FakeDatabaseOptions = {}) { this.committedHistory = [...(options.history ?? [])]; this.failures = new Set(options.failures ?? []); this.historyAfterLock = options.historyAfterLock; this.historyTableExists = options.historyTableExists ?? true; this.lockResult = options.lockResult ?? true; this.statementFailureIndex = options.statementFailureIndex; } async openSession(): Promise { this.events.push("session:open"); if (this.failures.has("open")) { throw hostileDatabaseError; } return new FakeMigrationDatabaseSession(this); } } class FakeMigrationDatabaseSession implements MigrationDatabaseSession { readonly #database: FakeMigrationDatabase; #statementCount = 0; constructor(database: FakeMigrationDatabase) { this.#database = database; } async acquireMigrationLock(timeoutMs: number): Promise { this.#database.events.push(`lock:acquire:${timeoutMs}`); if (this.#database.failures.has("lock")) { throw hostileDatabaseError; } if (this.#database.lockResult && this.#database.historyAfterLock) { this.#database.committedHistory = [...this.#database.historyAfterLock]; } return this.#database.lockResult; } async close(): Promise { this.#database.events.push("session:close"); if (this.#database.failures.has("close")) { throw hostileDatabaseError; } } async readAppliedMigrations(): Promise { this.#database.events.push("history:read"); if (this.#database.failures.has("read-history")) { throw hostileDatabaseError; } return Object.freeze([...this.#database.committedHistory]); } async releaseMigrationLock(): Promise { this.#database.events.push("lock:release"); if (this.#database.failures.has("release-lock")) { throw hostileDatabaseError; } } async transaction(operation: (transaction: MigrationTransaction) => Promise): Promise { this.#database.events.push("transaction:begin"); const stagedHistory = [...this.#database.committedHistory]; const stagedInserts: MigrationHistoryInsert[] = []; let stagedTableExists = this.#database.historyTableExists; const transaction = Object.freeze({ ensureHistoryTable: async () => { this.#database.events.push("history:ensure"); if (this.#database.failures.has("ensure-history")) { throw hostileDatabaseError; } stagedTableExists = true; }, executeMigrationSql: async (sql) => { this.#database.events.push("sql:execute"); this.#database.executedStatements.push(sql); const statementIndex = this.#statementCount; this.#statementCount += 1; if ( this.#database.failures.has("execute") || this.#database.statementFailureIndex === statementIndex ) { throw hostileDatabaseError; } }, insertAppliedMigration: async (insert) => { this.#database.events.push(`history:insert:${insert.identity}`); if (this.#database.failures.has("history-insert")) { throw hostileDatabaseError; } const frozenInsert = Object.freeze({ ...insert }); stagedInserts.push(frozenInsert); stagedHistory.push( Object.freeze({ ...frozenInsert, appliedAt, appliedOrder: stagedHistory.length + 1, }), ); }, }); try { const result = await operation(transaction); if (this.#database.failures.has("commit")) { throw hostileDatabaseError; } this.#database.committedHistory = stagedHistory; this.#database.historyTableExists = stagedTableExists; this.#database.insertedMigrations.push(...stagedInserts); this.#database.events.push("transaction:commit"); return result; } catch (error) { this.#database.events.push("transaction:rollback"); throw error; } } } class CapturingApprovalProvider implements MigrationApprovalProvider { readonly requests: (readonly MigrationApproval[])[] = []; readonly #response: readonly MigrationApproval[]; readonly #throwError: boolean; constructor(response: readonly MigrationApproval[], throwError = false) { this.#response = response; this.#throwError = throwError; } async getApprovals( required: readonly MigrationApproval[], ): Promise { this.requests.push(required); if (this.#throwError) { throw hostileDatabaseError; } return this.#response; } } function approvalFor(source: DiscoveredModuleMigration): MigrationApproval { return Object.freeze({ checksum: source.checksum, identity: source.identity, moduleId: source.moduleId, }); } async function captureRunnerError( operation: Promise, code: MigrationRunnerErrorCode, ): Promise { try { await operation; } catch (error) { expect(error).toBeInstanceOf(MigrationRunnerError); const runnerError = error as MigrationRunnerError; expect(runnerError.code).toBe(code); expect(Object.isFrozen(runnerError)).toBe(true); return runnerError; } throw new Error(`Expected ${code}.`); } function expectNoPersistentMutation(database: FakeMigrationDatabase): void { expect(database.events).not.toContain("transaction:begin"); expect(database.events).not.toContain("history:ensure"); expect(database.events).not.toContain("sql:execute"); expect(database.insertedMigrations).toEqual([]); } describe("module migration planning", () => { it("treats a fresh database with a missing history table as empty without writing", async () => { const initial = migration("people", 0); const database = new FakeMigrationDatabase({ historyTableExists: false }); const result = await planModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, }); expect(result).toEqual({ appliedCount: 0, approvalRequiredCount: 0, entries: [ { approvalRequired: false, checksum: initial.checksum, identity: initial.identity, index: 0, moduleId: "people", status: "pending", tag: initial.tag, }, ], pendingCount: 1, }); expect(database.events).toEqual(["session:open", "history:read", "session:close"]); expect(database.historyTableExists).toBe(false); expectNoPersistentMutation(database); }); it("returns an empty frozen plan when no installed module owns migrations", async () => { const database = new FakeMigrationDatabase({ historyTableExists: false }); const inventory = aggregate({ id: "announcements", migrations: [] }); const result = await planModuleMigrations({ aggregate: inventory, database, }); const applyResult = await applyModuleMigrations({ aggregate: inventory, database }); expect(result).toEqual({ appliedCount: 0, approvalRequiredCount: 0, entries: [], pendingCount: 0, }); expect(Object.isFrozen(result)).toBe(true); expect(Object.isFrozen(result.entries)).toBe(true); expect(applyResult).toEqual({ applied: [], appliedCount: 0, noOp: true }); expect(database.historyTableExists).toBe(false); expect(database.events).not.toContain("transaction:begin"); }); it("reports applied, pending, and approval-blocked entries in aggregate order", async () => { const peopleInitial = migration("people", 0, { tag: "0000_initial" }); const peopleArchive = migration("people", 1, { approvalRequired: true, tag: "0001_archive", }); const calendarInitial = migration("calendar", 0, { tag: "0000_initial" }); const inventory = aggregate( { id: "people", migrations: [peopleInitial, peopleArchive] }, { id: "calendar", migrations: [calendarInitial] }, ); const result = await planModuleMigrations({ aggregate: inventory, database: new FakeMigrationDatabase({ history: [historyRecord(peopleInitial, 1)] }), }); expect(result.entries.map((entry) => [entry.identity, entry.status])).toEqual([ [peopleInitial.identity, "applied"], [peopleArchive.identity, "approval-blocked"], [calendarInitial.identity, "pending"], ]); expect(result).toMatchObject({ appliedCount: 1, approvalRequiredCount: 1, pendingCount: 2, }); expect(Object.isFrozen(result)).toBe(true); expect(Object.isFrozen(result.entries)).toBe(true); expect(result.entries.every(Object.isFrozen)).toBe(true); }); it("accepts historical interleaving while enforcing each module's journal prefix", async () => { const peopleInitial = migration("people", 0); const peopleLater = migration("people", 1); const calendarInitial = migration("calendar", 0); const inventory = aggregate( { id: "people", migrations: [peopleInitial, peopleLater] }, { id: "calendar", migrations: [calendarInitial] }, ); const database = new FakeMigrationDatabase({ history: [ historyRecord(peopleInitial, 1), historyRecord(calendarInitial, 2), historyRecord(peopleLater, 3), ], }); const result = await planModuleMigrations({ aggregate: inventory, database }); expect(result.entries.every((entry) => entry.status === "applied")).toBe(true); expect(result.pendingCount).toBe(0); }); it.each([ [ "a journal gap", (_first: DiscoveredModuleMigration, second: DiscoveredModuleMigration) => [ historyRecord(second, 1), ], ], [ "out-of-order module history", (first: DiscoveredModuleMigration, second: DiscoveredModuleMigration) => [ historyRecord(second, 1), historyRecord(first, 2), ], ], ])("rejects per-module history with %s", async (_label, makeHistory) => { const first = migration("people", 0); const second = migration("people", 1); const database = new FakeMigrationDatabase({ history: makeHistory(first, second) }); await captureRunnerError( planModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [first, second] }), database, }), "MIGRATION_HISTORY_INVALID", ); expect(database.events.at(-1)).toBe("session:close"); }); it("allows gaps in database-assigned applied order", async () => { const peopleInitial = migration("people", 0); const peopleLater = migration("people", 1); const calendarInitial = migration("calendar", 0); const inventory = aggregate( { id: "people", migrations: [peopleInitial, peopleLater] }, { id: "calendar", migrations: [calendarInitial] }, ); const result = await planModuleMigrations({ aggregate: inventory, database: new FakeMigrationDatabase({ history: [ historyRecord(peopleInitial, 7), historyRecord(calendarInitial, 40), historyRecord(peopleLater, 99), ], }), }); expect(result.appliedCount).toBe(3); expect(result.pendingCount).toBe(0); }); it.each([ [ "duplicate identity", (source: DiscoveredModuleMigration) => [historyRecord(source, 1), historyRecord(source, 2)], ], [ "inconsistent known metadata", (source: DiscoveredModuleMigration) => [historyRecord(source, 1, { moduleId: "calendar" })], ], ])("rejects %s records without repairing history", async (_label, makeHistory) => { const initial = migration("people", 0); const history = makeHistory(initial); const database = new FakeMigrationDatabase({ history }); await captureRunnerError( planModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, }), "MIGRATION_HISTORY_INVALID", ); expect(database.committedHistory).toEqual(history); }); it("distinguishes exact checksum mismatches from unknown applied identities", async () => { const initial = migration("people", 0); const inventory = aggregate({ id: "people", migrations: [initial] }); const mismatch = new FakeMigrationDatabase({ history: [historyRecord(initial, 1, { checksum: "0".repeat(64) })], }); const unknown = new FakeMigrationDatabase({ history: [ historyRecord(initial, 1, { identity: "hostile:postgresql-admin-secret-private-path", }), ], }); const mismatchError = await captureRunnerError( planModuleMigrations({ aggregate: inventory, database: mismatch }), "MIGRATION_CHECKSUM_MISMATCH", ); const unknownError = await captureRunnerError( planModuleMigrations({ aggregate: inventory, database: unknown }), "MIGRATION_UNKNOWN_APPLIED", ); expect(mismatchError.migrationIdentity).toBe(initial.identity); expect(unknownError.message).not.toContain("hostile"); expect(unknownError.message).not.toContain("private-path"); }); }); describe("module migration apply", () => { it("acquires the lock before reading history and applies missing-history work atomically", async () => { const initial = migration("people", 0); const database = new FakeMigrationDatabase({ historyTableExists: false }); const result = await applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, }); expect(database.events).toEqual([ "session:open", "lock:acquire:10000", "history:read", "transaction:begin", "history:ensure", "sql:execute", `history:insert:${initial.identity}`, "transaction:commit", "lock:release", "session:close", ]); expect(database.historyTableExists).toBe(true); expect(database.committedHistory.map((record) => record.identity)).toEqual([initial.identity]); expect(result).toEqual({ applied: [ { checksum: initial.checksum, identity: initial.identity, index: initial.index, moduleId: initial.moduleId, tag: initial.tag, }, ], appliedCount: 1, noOp: false, }); }); it("re-reads history under the lock before deciding which migrations remain", async () => { const initial = migration("people", 0); const later = migration("people", 1); const database = new FakeMigrationDatabase({ history: [], historyAfterLock: [historyRecord(initial, 1)], }); const result = await applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial, later] }), database, }); expect(result.applied.map((entry) => entry.identity)).toEqual([later.identity]); expect(database.executedStatements).toEqual([later.sql]); expect(database.events.indexOf("lock:acquire:10000")).toBeLessThan( database.events.indexOf("history:read"), ); }); it("returns a genuine frozen no-op when history is already current", async () => { const initial = migration("people", 0); const database = new FakeMigrationDatabase({ history: [historyRecord(initial, 1)] }); const result = await applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, }); expect(result).toEqual({ applied: [], appliedCount: 0, noOp: true }); expect(Object.isFrozen(result)).toBe(true); expect(Object.isFrozen(result.applied)).toBe(true); expectNoPersistentMutation(database); expect(database.events).toEqual([ "session:open", "lock:acquire:10000", "history:read", "lock:release", "session:close", ]); }); it("returns a locked no-op without creating history for an inventory with no migrations", async () => { const database = new FakeMigrationDatabase({ historyTableExists: false }); const result = await applyModuleMigrations({ aggregate: aggregate({ id: "announcements", migrations: [] }), database, }); expect(result).toEqual({ applied: [], appliedCount: 0, noOp: true }); expect(database.historyTableExists).toBe(false); expect(database.events).toEqual([ "session:open", "lock:acquire:10000", "history:read", "lock:release", "session:close", ]); }); it("applies a later migration after a valid existing prefix", async () => { const initial = migration("people", 0); const later = migration("people", 1); const database = new FakeMigrationDatabase({ history: [historyRecord(initial, 1)] }); const result = await applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial, later] }), database, }); expect(result.applied.map((entry) => entry.identity)).toEqual([later.identity]); expect(database.committedHistory.map((record) => record.identity)).toEqual([ initial.identity, later.identity, ]); }); it("executes dependency-first aggregate order and native journal order in one batch", async () => { const peopleInitial = migration("people", 0); const peopleLater = migration("people", 1); const calendarInitial = migration("calendar", 0); const database = new FakeMigrationDatabase(); const result = await applyModuleMigrations({ aggregate: aggregate( { id: "people", migrations: [peopleInitial, peopleLater] }, { id: "calendar", migrations: [calendarInitial] }, ), database, }); expect(result.applied.map((entry) => entry.identity)).toEqual([ peopleInitial.identity, peopleLater.identity, calendarInitial.identity, ]); expect(database.events.filter((event) => event === "transaction:begin")).toHaveLength(1); expect(database.events.filter((event) => event === "transaction:commit")).toHaveLength(1); expect(database.insertedMigrations.map((entry) => entry.identity)).toEqual( result.applied.map((entry) => entry.identity), ); }); it("sends each migration's complete SQL to PostgreSQL in exactly one call, byte for byte", async () => { const sql = 'CREATE TABLE "people_records" ("id" integer);\n--> statement-breakpoint\nALTER TABLE "people_records" ADD COLUMN "name" text; SELECT 2;'; const initial = migration("people", 0, { sql }); const database = new FakeMigrationDatabase(); await applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, }); // One call, unmodified: the runner never splits, trims, normalizes, or rewrites migration SQL. // PostgreSQL parses the batch, and Drizzle's marker lines are ordinary `--` comments to it. expect(database.executedStatements).toEqual([sql]); expect(database.events.indexOf(`history:insert:${initial.identity}`)).toBeGreaterThan( database.events.lastIndexOf("sql:execute"), ); }); it.each([ [ "a string literal containing the marker bytes", `INSERT INTO "notes" ("body") VALUES ('keep this: --> statement-breakpoint intact');`, ], [ "a dollar-quoted function body containing the marker and semicolons", `CREATE FUNCTION touch() RETURNS void AS $$\nBEGIN\n -- --> statement-breakpoint\n PERFORM 1;\n PERFORM 2;\nEND;\n$$ LANGUAGE plpgsql;`, ], ["a line comment containing the marker", `-- note: --> statement-breakpoint\nSELECT 1;`], ["a block comment containing the marker", `/* --> statement-breakpoint */ SELECT 1;`], [ "a quoted identifier containing the marker", `CREATE TABLE "odd--> statement-breakpoint name" ("id" integer);`, ], ])("never fragments %s", async (_label, sql) => { const initial = migration("people", 0, { sql }); const database = new FakeMigrationDatabase(); await applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, }); expect(database.executedStatements).toEqual([sql]); }); it("sends several generated migrations as one call each, in order", async () => { const first = migration("people", 0, { sql: `SELECT 1;\n--> statement-breakpoint\nSELECT 2;` }); const second = migration("people", 1, { sql: `SELECT 3;` }); const database = new FakeMigrationDatabase(); await applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [first, second] }), database, }); expect(database.executedStatements).toEqual([first.sql, second.sql]); }); it("rolls back the whole pending batch on a later SQL failure and preserves prior history", async () => { const prior = migration("people", 0); const pendingFirst = migration("people", 1); const pendingSecond = migration("people", 2); const database = new FakeMigrationDatabase({ history: [historyRecord(prior, 1)], statementFailureIndex: 1, }); const error = await captureRunnerError( applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [prior, pendingFirst, pendingSecond] }), database, }), "MIGRATION_EXECUTION_FAILED", ); expect(error.migrationIdentity).toBe(pendingSecond.identity); expect(database.committedHistory).toEqual([historyRecord(prior, 1)]); expect(database.insertedMigrations).toEqual([]); expect(database.events).toContain(`history:insert:${pendingFirst.identity}`); expect(database.events).toContain("transaction:rollback"); expect(database.events).not.toContain("transaction:commit"); expect(database.events.slice(-2)).toEqual(["lock:release", "session:close"]); }); it("rolls back SQL when the matching history insertion fails", async () => { const initial = migration("people", 0); const database = new FakeMigrationDatabase({ failures: ["history-insert"] }); await captureRunnerError( applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, }), "MIGRATION_EXECUTION_FAILED", ); expect(database.executedStatements).toEqual([initial.sql]); expect(database.committedHistory).toEqual([]); expect(database.events).toContain("transaction:rollback"); }); it("publishes no success when commit fails and still releases the session", async () => { const initial = migration("people", 0); const database = new FakeMigrationDatabase({ failures: ["commit"] }); await captureRunnerError( applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, }), "MIGRATION_EXECUTION_FAILED", ); expect(database.committedHistory).toEqual([]); expect(database.insertedMigrations).toEqual([]); expect(database.events).toContain("transaction:rollback"); expect(database.events.slice(-2)).toEqual(["lock:release", "session:close"]); }); it("times out without reading history or trying to release an unheld lock", async () => { const initial = migration("people", 0); const database = new FakeMigrationDatabase({ lockResult: false }); await captureRunnerError( applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, lockTimeoutMs: 250, }), "MIGRATION_LOCK_TIMEOUT", ); expect(database.events).toEqual(["session:open", "lock:acquire:250", "session:close"]); expectNoPersistentMutation(database); }); it("does not ask for approval for ordinary migrations", async () => { const initial = migration("people", 0); const approvals = new CapturingApprovalProvider([]); const result = await applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), approvals, database: new FakeMigrationDatabase(), }); expect(result.appliedCount).toBe(1); expect(approvals.requests).toEqual([]); }); it("accepts an exact destructive approval bound to module, identity, and checksum", async () => { const destructive = migration("people", 0, { approvalRequired: true }); const approvals = new CapturingApprovalProvider([approvalFor(destructive)]); const database = new FakeMigrationDatabase(); const result = await applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [destructive] }), approvals, database, }); expect(result.applied.map((entry) => entry.identity)).toEqual([destructive.identity]); expect(approvals.requests).toHaveLength(1); expect(approvals.requests[0]).toEqual([approvalFor(destructive)]); expect(Object.isFrozen(approvals.requests[0])).toBe(true); expect(approvals.requests[0]?.every(Object.isFrozen)).toBe(true); }); it.each([ ["missing", []], ["wildcard-like", [{ moduleId: "*", identity: "*", checksum: "*" }]], ["wrong identity", [{ moduleId: "people", identity: "people:*", checksum: "CHECKSUM" }]], ["wrong module", [{ moduleId: "calendar", identity: "IDENTITY", checksum: "CHECKSUM" }]], ["wrong checksum", [{ moduleId: "people", identity: "IDENTITY", checksum: "0".repeat(64) }]], ])("blocks a destructive migration for a %s approval before mutation", async (_label, raw) => { const destructive = migration("people", 0, { approvalRequired: true }); const supplied = raw.map((entry) => Object.freeze({ checksum: entry.checksum === "CHECKSUM" ? destructive.checksum : entry.checksum, identity: entry.identity === "IDENTITY" ? destructive.identity : entry.identity, moduleId: entry.moduleId, }), ); const database = new FakeMigrationDatabase(); const error = await captureRunnerError( applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [destructive] }), approvals: new CapturingApprovalProvider(supplied), database, }), "MIGRATION_APPROVAL_REQUIRED", ); expect(error.migrationIdentity).toBe(destructive.identity); expectNoPersistentMutation(database); expect(database.events.slice(-2)).toEqual(["lock:release", "session:close"]); }); it("preflights every destructive approval before starting any persistent mutation", async () => { const first = migration("people", 0, { approvalRequired: true }); const second = migration("people", 1, { approvalRequired: true }); const approvals = new CapturingApprovalProvider([approvalFor(first)]); const database = new FakeMigrationDatabase(); await captureRunnerError( applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [first, second] }), approvals, database, }), "MIGRATION_APPROVAL_REQUIRED", ); expect(approvals.requests[0]).toEqual([approvalFor(first), approvalFor(second)]); expectNoPersistentMutation(database); }); it("returns frozen deterministic applied results", async () => { const first = migration("people", 0); const second = migration("people", 1); const inventory = aggregate({ id: "people", migrations: [first, second] }); const [left, right] = await Promise.all([ applyModuleMigrations({ aggregate: inventory, database: new FakeMigrationDatabase() }), applyModuleMigrations({ aggregate: inventory, database: new FakeMigrationDatabase() }), ]); expect(left).toEqual(right); expect(Object.isFrozen(left)).toBe(true); expect(Object.isFrozen(left.applied)).toBe(true); expect(left.applied.every(Object.isFrozen)).toBe(true); }); }); describe("module migration failures and side effects", () => { it("sanitizes hostile connection, history, approval, and execution failures", async () => { const ordinary = migration("people", 0); const destructive = migration("people", 0, { approvalRequired: true }); const inventory = aggregate({ id: "people", migrations: [ordinary] }); const cases: readonly [Promise, MigrationRunnerErrorCode][] = [ [ planModuleMigrations({ aggregate: inventory, database: new FakeMigrationDatabase({ failures: ["open"] }), }), "MIGRATION_CONNECTION_FAILED", ], [ planModuleMigrations({ aggregate: inventory, database: new FakeMigrationDatabase({ failures: ["read-history"] }), }), "MIGRATION_HISTORY_READ_FAILED", ], [ applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [destructive] }), approvals: new CapturingApprovalProvider([], true), database: new FakeMigrationDatabase(), }), "MIGRATION_APPROVAL_LOOKUP_FAILED", ], [ applyModuleMigrations({ aggregate: inventory, database: new FakeMigrationDatabase({ failures: ["execute"] }), }), "MIGRATION_EXECUTION_FAILED", ], ]; for (const [operation, code] of cases) { const error = await captureRunnerError(operation, code); const publicText = `${error.message}\n${error.stack ?? ""}`; expect(publicText).not.toContain("super-secret"); expect(publicText).not.toContain("private-db.internal"); expect(publicText).not.toContain("/home/ekklesia"); expect(publicText).not.toContain("DROP TABLE"); expect("cause" in error).toBe(false); } }); it("always closes sessions and preserves a primary error when cleanup also fails", async () => { const initial = migration("people", 0); const database = new FakeMigrationDatabase({ failures: ["read-history", "release-lock", "close"], }); const error = await captureRunnerError( applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, }), "MIGRATION_HISTORY_READ_FAILED", ); expect(error.code).toBe("MIGRATION_HISTORY_READ_FAILED"); expect(database.events.slice(-2)).toEqual(["lock:release", "session:close"]); }); it("reports cleanup failure when successful work cannot release its resources", async () => { const initial = migration("people", 0); const database = new FakeMigrationDatabase({ failures: ["release-lock"] }); await captureRunnerError( applyModuleMigrations({ aggregate: aggregate({ id: "people", migrations: [initial] }), database, }), "MIGRATION_CLEANUP_FAILED", ); expect(database.events.at(-1)).toBe("session:close"); }); it("does not connect or execute SQL merely by importing the runner", async () => { const database = new FakeMigrationDatabase(); await import("../src/migrations/runner.js"); expect(database.events).toEqual([]); expect(database.executedStatements).toEqual([]); expect(database.committedHistory).toEqual([]); }); });