/** * Migration failure diagnostic test — locks in the [E00-S03-T05] structured * diagnostic produced when a migration fails. * * Acceptance criteria covered (each test fails without the committed state): * - "migration failure produces a structured diagnostic" → the committed * `packages/database-postgres/src/runner.ts` defines a `MigrationRunner` * that applies pending migrations through the migration ledger (E00-S03-T03) * and, when a migration fails, throws a `MigrationFailedError` whose * `.diagnostic` is a structured object (`migration`, `phase`, `cause`, * `applied`, `pending`); the error is serializable (`toJSON()` returns a * plain object, including a structured cause — for pg errors the `code`). * The deterministic stub-pool behavioral probe drives the committed * module with an intentionally failing migration fixture and confirms the * structured diagnostic, and the docker-gated real-stack probe does the * same against a real database (the issue's test plan: "run an * intentionally failing migration fixture and confirm the diagnostic"). * - "the diagnostic identifies the failing migration" → the diagnostic's * `migration` field is the failing migration's version (also named in the * error `message` and in `toJSON()`), and `phase` distinguishes 'apply' * (the migration's `up` threw) from 'record' (the ledger insert threw * after a successful `up`); `applied`/`pending` carry the ledger state at * failure time (disjoint, in run order — the failing migration is still * pending). Locked in statically (mutation probes prove non-vacuity) and * behaviorally (stub-pool probe: apply-failure and record-failure * fixtures both produce a diagnostic naming the failing migration; * real-stack probe: a fixture whose `up` runs invalid SQL fails with a * diagnostic naming that migration and carrying the pg error code). * - the runner is part of the driver boundary: `src/index.ts` re-exports * `MigrationRunner` + `MigrationFailedError` (and the migration types), so * no other package needs the `pg` driver to run migrations. * * Run: `node --test tests/database-postgres-diagnostic.test.mjs` * (node:test — built into Node >= 18; no dependencies, lockfile untouched.) */ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, existsSync, writeFileSync, rmSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8'); /** The committed runner module and the driver boundary that re-exports it. */ const RUNNER_SRC = 'packages/database-postgres/src/runner.ts'; const INDEX_SRC = 'packages/database-postgres/src/index.ts'; /** The root test glob (root `scripts.test`, E00-S01-T12) that runs every suite. */ const ROOT_TEST_GLOB = 'tests/**/*.test.mjs'; /** The CI job that gates the failure-diagnostic criterion on every PR. */ const CI_JOB = 'database-postgres-diagnostic'; /** Versions used by the real-stack probe's intentionally failing migration fixture. */ const OK_MIGRATION = '2026-08-30_001_fixture_ok'; const FAIL_MIGRATION = '2026-08-30_002_fixture_fail'; /** Table the real-stack probe's ok migration creates (and its missing sibling for the failing one). */ const FIXTURE_TABLE = 'diagnostic_fixture_ok'; // --------------------------------------------------------------------------- // Static assertions on the committed runner source // --------------------------------------------------------------------------- /** * Asserts the runner module exists with the documented shape: a * `MigrationRunner` class, a `MigrationFailedError` class, and the * `Migration`/`MigrationDiagnostic`/`MigrationRunResult` types. Fails fast on * a missing module; the mutation probes below prove the assertions are * non-vacuous. */ function assertRunnerModule(src) { assert.match(src, /export class MigrationRunner/, 'the runner module must export the MigrationRunner class'); assert.match(src, /export class MigrationFailedError/, 'the runner module must export the MigrationFailedError class'); assert.match(src, /export interface Migration \{/, 'the runner module must export the Migration step interface'); assert.match(src, /up\(pool: Pool\)/, 'a migration step must apply through an up(pool) function'); assert.match(src, /export interface MigrationDiagnostic/, 'the runner module must export the MigrationDiagnostic interface'); assert.match(src, /export interface MigrationRunResult/, 'the runner module must export the MigrationRunResult interface'); } /** * Asserts the diagnostic is structured and identifies the failing migration: * the `MigrationDiagnostic` interface carries `migration` (the failing * migration's version), `phase` ('apply' | 'record'), `cause`, `applied` and * `pending`. Fails fast on a missing field; the mutation probes prove the * assertions are non-vacuous. */ function assertDiagnosticShape(src) { const diagSrc = src.slice( src.indexOf('export interface MigrationDiagnostic'), src.indexOf('export interface MigrationRunResult'), ); assert.match( diagSrc, /migration:\s*string/, 'the diagnostic must identify the failing migration (migration: string)', ); assert.match( diagSrc, /phase:\s*MigrationFailurePhase/, 'the diagnostic must record where the run failed (phase: MigrationFailurePhase — apply | record)', ); assert.match(diagSrc, /cause:\s*unknown/, 'the diagnostic must carry the underlying cause (cause: unknown)'); assert.match( diagSrc, /applied:\s*string\[\]/, 'the diagnostic must list the migrations applied before the failure (applied: string[])', ); assert.match( diagSrc, /pending:\s*string\[\]/, 'the diagnostic must list the migrations still pending at failure time (pending: string[])', ); } /** * Asserts `MigrationFailedError` carries the structured diagnostic and is * serializable: a `readonly diagnostic: MigrationDiagnostic` field, a * `toJSON()` method, and a message naming the failing migration. */ function assertFailedError(src) { const errSrc = src.slice( src.indexOf('export class MigrationFailedError'), src.indexOf('export class MigrationRunner'), ); assert.match( errSrc, /readonly diagnostic:\s*MigrationDiagnostic/, 'MigrationFailedError must carry the structured diagnostic (readonly diagnostic: MigrationDiagnostic)', ); assert.match(errSrc, /toJSON\(\)/, 'the error must be serializable (toJSON())'); assert.match( errSrc, /migration "\$\{diagnostic\.migration\}" failed during/, 'the error message must name the failing migration', ); } /** * Asserts the runner wraps a failing migration into the structured diagnostic * instead of rethrowing the raw error: both failure paths (apply and record) * build the diagnostic with the failing migration's version, and the * diagnostic error is constructed (new MigrationFailedError). */ function assertWrapsFailure(src) { const runnerBody = src.slice(src.indexOf('export class MigrationRunner')); assert.match( runnerBody, /new MigrationFailedError\(\{/, 'the runner must construct the structured diagnostic error (new MigrationFailedError({ ... }))', ); assert.match( runnerBody, /failure\(migration\.version, 'apply', error, recorded\)/, "the apply-failure path must build the diagnostic with the failing migration's version", ); assert.match( runnerBody, /failure\(migration\.version, 'record', error, recorded\)/, "the record-failure path must build the diagnostic with the failing migration's version", ); } /** Asserts the runner applies pending migrations through the migration ledger (exactly once). */ function assertLedgerIntegration(src) { const runnerBody = src.slice(src.indexOf('export class MigrationRunner')); assert.match( runnerBody, /await this\.ledger\.ensure\(\);/, 'the runner must ensure the ledger exists before applying (self-sufficient on an empty database)', ); assert.match( runnerBody, /await this\.ledger\.applied\(\)/, 'the runner must read the applied migrations from the ledger before applying (never double-applies)', ); assert.match( runnerBody, /await this\.ledger\.record\(migration\.version\);/, 'the runner must record each applied migration in the ledger', ); } /** Asserts the driver boundary re-exports the runner + diagnostic from the package entrypoint. */ function assertBoundaryReexport(src) { assert.match( src, /export \{ MigrationRunner, MigrationFailedError \} from '\.\/runner\.js'/, 'the driver boundary must re-export the runner (src/index.ts → ./runner.js)', ); assert.match( src, /export type \{ Migration, MigrationDiagnostic, MigrationRunResult, MigrationFailurePhase \} from '\.\/runner\.js'/, 'the driver boundary must re-export the runner types (src/index.ts → ./runner.js)', ); } // --------------------------------------------------------------------------- // Docker probe helpers (integration test skips cleanly without Docker) // --------------------------------------------------------------------------- function run(cmd, args, opts = {}) { return spawnSync(cmd, args, { encoding: 'utf8', timeout: 600_000, ...opts, }); } /** True when the `docker` CLI with the Compose plugin is on PATH. */ function dockerComposeAvailable() { try { return run('docker', ['compose', 'version'], { timeout: 15_000 }).status === 0; } catch { return false; } } /** True when a reachable Docker daemon exists. */ function dockerDaemonAvailable() { try { return run('docker', ['info'], { timeout: 15_000 }).status === 0; } catch { return false; } } /** Parses `docker compose ps --format json` (JSON array or one object per line). */ function parsePsJson(stdout) { const text = String(stdout).trim(); if (!text) return []; try { const parsed = JSON.parse(text); return Array.isArray(parsed) ? parsed : [parsed]; } catch { return text .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => JSON.parse(line)); } } /** Tolerant field lookup across compose ps JSON shapes. */ function field(container, ...names) { for (const name of names) { if (container[name] !== undefined) return container[name]; } return undefined; } /** * Polls `docker compose ps` until the db container of the given project * reports healthy (or the deadline passes), so the probe never races a * still-booting database. */ function waitForDbHealthy(project, deadlineMs = 60_000) { const deadline = Date.now() + deadlineMs; let last = ''; while (Date.now() < deadline) { const ps = run('docker', ['compose', '-p', project, 'ps', '--format', 'json'], { cwd: REPO_ROOT, timeout: 15_000, }); if (ps.status === 0) { last = ps.stdout; const db = parsePsJson(ps.stdout).find((c) => field(c, 'Service', 'service') === 'db'); if (db && /healthy/i.test(String(field(db, 'Health', 'health') ?? ''))) return; } run(process.execPath, ['-e', 'setTimeout(() => {}, 1000)']); // db still booting — retry } throw new Error(`the db container did not become healthy within ${deadlineMs}ms (last ps: "${last.trim()}")`); } // --------------------------------------------------------------------------- // Deterministic behavioral probe (no database, no Docker) — the issue's test // plan: "run an intentionally failing migration fixture and confirm the // diagnostic", driven against a stub pool so it runs anywhere Node can strip // types (Node >= 23.6, i.e. the CI Node 24). // --------------------------------------------------------------------------- /** * The host-side probe body: drives the committed `MigrationRunner` with a * stub pg pool (the same SQL shapes the committed ledger module uses, plus an * insert that can be made to fail for versions starting with 'fail') through * three scenarios — a successful run + idempotent rerun, an apply-failure * fixture (the intentionally failing migration), and a record-failure fixture * — and reports the structured diagnostics. Written to a temp file inside * `packages/database-postgres/` so `pg` resolves through the package's own * dependency links, then removed. */ const PROBE_SOURCE = ` import { MigrationRunner, MigrationFailedError } from './src/runner.ts'; import { MigrationLedger } from './src/ledger.ts'; // A stub pg pool: tracks the migration ledger in memory (the same SQL shapes // the committed ledger module issues) and fails a ledger insert for versions // starting with 'fail' — so the committed runner is driven deterministically // with no database and no Docker. const makeStubPool = () => { const store = new Map(); const queries = []; return { store, queries, async query(text, values) { queries.push({ text, values }); if (/^INSERT INTO schema_migrations/.test(text)) { const version = values[0]; if (version.startsWith('fail')) { const err = new Error('the ledger insert failed (read-only transaction)'); err.code = '25006'; throw err; } store.set(version, new Date().toISOString()); return { rows: [], rowCount: 1 }; } if (/^SELECT version FROM schema_migrations/.test(text)) { const versions = [...store.keys()].sort(); return { rows: versions.map((version) => ({ version })), rowCount: versions.length }; } return { rows: [], rowCount: 0 }; }, }; }; const ok = (version) => ({ version, up: async (pool) => { await pool.query('SELECT 1'); } }); const results = {}; // Scenario 1 — success: all migrations apply in order; a rerun skips them // (the story's "second migration run is idempotent"). { const pool = makeStubPool(); const ledger = new MigrationLedger(pool); const runner = new MigrationRunner(pool, [ok('ok-1'), ok('ok-2')], ledger); const first = await runner.run(); const second = await runner.run(); results.success = { first, second, ledger: [...pool.store.keys()] }; } // Scenario 2 — apply failure: the intentionally failing migration fixture. { const pool = makeStubPool(); const ledger = new MigrationLedger(pool); const cause = new Error('relation "diagnostic_missing_table" does not exist'); cause.code = '42P01'; const failing = { version: 'fail-2', up: async () => { throw cause; } }; const runner = new MigrationRunner(pool, [ok('ok-1'), failing, ok('ok-3')], ledger); let outcome = { rejected: false }; try { await runner.run(); } catch (error) { outcome = { rejected: true, isMigrationFailedError: error instanceof MigrationFailedError, name: error.name, message: error.message, migration: error.diagnostic.migration, phase: error.diagnostic.phase, applied: error.diagnostic.applied, pending: error.diagnostic.pending, causeIsOriginal: error.diagnostic.cause === cause, causeName: error.diagnostic.cause.name, causeCode: error.diagnostic.cause.code, toJson: JSON.parse(JSON.stringify(error)), }; } results.applyFailure = { ...outcome, ledger: [...pool.store.keys()] }; } // Scenario 3 — record failure: the migration's up succeeds but the ledger // insert fails — the diagnostic must still identify the failing migration, // with phase 'record'. { const pool = makeStubPool(); const ledger = new MigrationLedger(pool); const runner = new MigrationRunner(pool, [ok('ok-1'), ok('fail-record'), ok('ok-3')], ledger); let outcome = { rejected: false }; try { await runner.run(); } catch (error) { outcome = { rejected: true, name: error.name, migration: error.diagnostic.migration, phase: error.diagnostic.phase, applied: error.diagnostic.applied, pending: error.diagnostic.pending, causeCode: error.diagnostic.cause.code, toJson: JSON.parse(JSON.stringify(error)), }; } results.recordFailure = { ...outcome, ledger: [...pool.store.keys()] }; } console.log('DIAGNOSTIC_PROBE_RESULT ' + JSON.stringify(results)); `; // --------------------------------------------------------------------------- // Real-stack probe — the issue's test plan against a real database // --------------------------------------------------------------------------- /** * The host-side probe body: runs the committed `MigrationRunner` against a * real database with an intentionally failing migration fixture — the ok * migration creates a table, the failing migration runs valid SQL against a * table that does not exist (pg error 42P01) — and reports the structured * diagnostic plus the ledger state and whether the ok migration took effect. * Written to a temp file inside `packages/database-postgres/` so `pg` * resolves through the package's own dependency links, then removed. */ const REAL_PROBE_SOURCE = ` import { Pool } from 'pg'; import { MigrationRunner, MigrationFailedError } from './src/runner.ts'; import { MigrationLedger } from './src/ledger.ts'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); const result = {}; try { const ledger = new MigrationLedger(pool); const runner = new MigrationRunner(pool, [ { version: '${OK_MIGRATION}', up: async (p) => { await p.query('CREATE TABLE ${FIXTURE_TABLE} (id integer)'); }, }, { version: '${FAIL_MIGRATION}', // The intentionally failing migration fixture: valid SQL against a // table that does not exist -> pg error 42P01 (undefined_table). up: async (p) => { await p.query('SELECT * FROM ${FIXTURE_TABLE}_missing'); }, }, ], ledger); try { await runner.run(); } catch (error) { result.rejected = true; result.isMigrationFailedError = error instanceof MigrationFailedError; result.name = error.name; result.message = error.message; result.migration = error.diagnostic.migration; result.phase = error.diagnostic.phase; result.applied = error.diagnostic.applied; result.pending = error.diagnostic.pending; result.causeName = error.diagnostic.cause && error.diagnostic.cause.name; result.causeCode = error.diagnostic.cause && error.diagnostic.cause.code; result.toJson = JSON.parse(JSON.stringify(error)); } result.ledgerApplied = await ledger.applied(); const tableClass = await pool.query('SELECT to_regclass($1) AS cls', ['public.' + '${FIXTURE_TABLE}']); result.fixtureTable = tableClass.rows[0] ? tableClass.rows[0].cls : null; console.log('DIAGNOSTIC_PROBE_RESULT ' + JSON.stringify(result)); } finally { await pool.end(); } `; // --------------------------------------------------------------------------- // Criterion tests // --------------------------------------------------------------------------- test('the runner module exists in the driver-owner package and defines the runner and its diagnostic types', () => { assert.ok(existsSync(path.join(REPO_ROOT, RUNNER_SRC)), `committed ${RUNNER_SRC} must exist`); assertRunnerModule(read(RUNNER_SRC)); }); test('the failure diagnostic is structured and identifies the failing migration (migration/phase/cause/applied/pending)', () => { assertDiagnosticShape(read(RUNNER_SRC)); }); test('MigrationFailedError carries the structured diagnostic and is serializable, naming the failing migration', () => { assertFailedError(read(RUNNER_SRC)); }); test('the runner wraps a failing migration into the structured diagnostic instead of rethrowing the raw error', () => { assertWrapsFailure(read(RUNNER_SRC)); }); test('the runner applies pending migrations through the migration ledger exactly once', () => { assertLedgerIntegration(read(RUNNER_SRC)); }); test('the driver boundary re-exports the runner and the diagnostic from the package entrypoint', () => { const src = read(INDEX_SRC); assert.match( src, /from '\.\/runner\.js'/, 'the driver boundary must import the runner module (from \'./runner.js\')', ); assertBoundaryReexport(src); }); test('the failure-diagnostic criterion is enforced in CI', () => { // Picked up by the root test command (root `scripts.test` glob). const scripts = JSON.parse(read('package.json')).scripts ?? {}; assert.equal( scripts.test, `node --test "${ROOT_TEST_GLOB}"`, `root scripts.test must run the "${ROOT_TEST_GLOB}" glob so this suite runs with the rest`, ); // And a dedicated CI job gates it on every PR. const workflow = read('.gitea/workflows/ci.yml'); assert.ok( workflow.includes(`node --test tests/database-postgres-diagnostic.test.mjs`), `CI must run the failure-diagnostic suite (job "${CI_JOB}") on every PR`, ); }); // --------------------------------------------------------------------------- // Behavioral probe — the issue's test plan: "run an intentionally failing // migration fixture and confirm the diagnostic" (deterministic, no Docker) // --------------------------------------------------------------------------- /** True when this Node can execute the committed `.ts` runner module (>= 23.6, type stripping). */ const TS_STRIPPING = (() => { const [major, minor] = process.versions.node.split('.').map(Number); return major > 23 || (major === 23 && minor >= 6); })(); test('an intentionally failing migration produces a structured diagnostic identifying it (behavioral probe)', { skip: !TS_STRIPPING }, () => { // The issue's test plan, driven deterministically: the committed runner is // exercised with a stub pool through three scenarios — a successful run and // idempotent rerun, an apply-failure fixture (the intentionally failing // migration), and a record-failure fixture — and the structured diagnostic // is asserted to identify the failing migration in every case. const probeFile = path.join(REPO_ROOT, 'packages/database-postgres', `.diagnostic-probe-${process.pid}.mjs`); try { writeFileSync(probeFile, PROBE_SOURCE); const probe = run(process.execPath, [probeFile], { cwd: REPO_ROOT, timeout: 30_000 }); assert.equal( probe.status, 0, `the diagnostic probe must exit 0:\n${(probe.stdout || '')}\n${(probe.stderr || '')}`.trim(), ); const resultLine = (probe.stdout || '') .split('\n') .map((l) => l.trim()) .find((l) => l.startsWith('DIAGNOSTIC_PROBE_RESULT')); assert.ok(resultLine, `the diagnostic probe must report a result line (stdout: "${(probe.stdout || '').trim()}")`); const result = JSON.parse(resultLine.slice('DIAGNOSTIC_PROBE_RESULT'.length).trim()); // Scenario 1 — success: all migrations apply in order, and a rerun skips // them (the story's "second migration run is idempotent"). assert.deepEqual( result.success.first, { applied: ['ok-1', 'ok-2'], skipped: [] }, 'a successful run must apply every migration in order', ); assert.deepEqual( result.success.second, { applied: [], skipped: ['ok-1', 'ok-2'] }, 'a rerun must skip everything already recorded in the ledger (never double-applies)', ); assert.deepEqual(result.success.ledger, ['ok-1', 'ok-2'], 'the ledger must record the applied migrations'); // Scenario 2 — apply failure: the intentionally failing migration fixture // (fail-2's up throws a pg-like error with code 42P01). assert.equal(result.applyFailure.rejected, true, 'run() must reject when a migration fails'); assert.equal(result.applyFailure.isMigrationFailedError, true, 'the failure must be a MigrationFailedError'); assert.equal(result.applyFailure.name, 'MigrationFailedError', 'the error name must be MigrationFailedError'); assert.match(result.applyFailure.message, /fail-2/, 'the error message must name the failing migration'); assert.equal(result.applyFailure.migration, 'fail-2', 'the diagnostic must identify the failing migration'); assert.equal(result.applyFailure.phase, 'apply', 'the diagnostic must report the apply phase'); assert.deepEqual(result.applyFailure.applied, ['ok-1'], 'the diagnostic must list the migrations applied before the failure'); assert.deepEqual(result.applyFailure.pending, ['fail-2', 'ok-3'], 'the diagnostic must list the still-pending migrations, including the failing one'); assert.equal(result.applyFailure.causeIsOriginal, true, 'the diagnostic must preserve the original underlying cause'); assert.equal(result.applyFailure.causeName, 'Error', 'the diagnostic cause must carry the error name'); assert.equal(result.applyFailure.causeCode, '42P01', 'the diagnostic cause must carry the pg error code'); assert.equal(result.applyFailure.toJson.diagnostic.migration, 'fail-2', 'toJSON() must identify the failing migration'); assert.equal(result.applyFailure.toJson.diagnostic.phase, 'apply', 'toJSON() must carry the failure phase'); assert.equal(result.applyFailure.toJson.diagnostic.cause.code, '42P01', 'toJSON() must carry a structured cause'); assert.deepEqual(result.applyFailure.ledger, ['ok-1'], 'only the migrations before the failure may be recorded'); // Scenario 3 — record failure: the migration's up succeeds but the ledger // insert fails — the diagnostic must still identify the failing migration, // with phase 'record'. assert.equal(result.recordFailure.rejected, true, 'run() must reject when recording a migration fails'); assert.equal(result.recordFailure.name, 'MigrationFailedError', 'a record failure must also be a MigrationFailedError'); assert.equal(result.recordFailure.migration, 'fail-record', 'a record failure must identify the failing migration'); assert.equal(result.recordFailure.phase, 'record', 'the diagnostic must report the record phase'); assert.deepEqual(result.recordFailure.applied, ['ok-1'], 'the diagnostic must list the migrations applied before the failure'); assert.deepEqual(result.recordFailure.pending, ['fail-record', 'ok-3'], 'the failing migration must still be pending (never recorded)'); assert.equal(result.recordFailure.causeCode, '25006', 'the diagnostic must carry the ledger-insert error code'); assert.equal(result.recordFailure.toJson.diagnostic.migration, 'fail-record', 'toJSON() must identify the failing migration on a record failure'); assert.deepEqual(result.recordFailure.ledger, ['ok-1'], 'the failed record must not be in the ledger'); } finally { rmSync(probeFile, { force: true }); } }); // --------------------------------------------------------------------------- // Real-stack probe — the issue's test plan against a real database // --------------------------------------------------------------------------- const DOCKER_COMPOSE = dockerComposeAvailable(); const DOCKER_DAEMON = dockerDaemonAvailable(); // An isolated compose project + non-default host port so this probe never // collides with the compose-config suite's default-project containers, the // ledger probe's project/port, the lock probe's project/port, or the default // 5432 binding when several suites run on the same host. const COMPOSE_PROJECT = 'eppp-diagnostic-probe'; const POSTGRES_HOST_PORT = '55434'; const DATABASE_URL = `postgres://eppp:eppp@127.0.0.1:${POSTGRES_HOST_PORT}/eppp`; test('an intentionally failing migration fixture produces a structured diagnostic identifying it (real stack)', { skip: !DOCKER_COMPOSE || !DOCKER_DAEMON || !TS_STRIPPING }, () => { // The issue's test plan: "run an intentionally failing migration fixture // and confirm the diagnostic". The probe starts the committed compose `db` // service (its own project + host port), runs the committed runner against // the real database with a fixture whose second migration executes valid // SQL against a missing table, and asserts the run rejects with a // MigrationFailedError whose diagnostic identifies the failing migration, // reports phase 'apply', carries the pg error code (42P01), lists the // applied/pending state, and is serializable — while the ok migration's // effect (its table) and ledger row persist and the failing migration is // not recorded. Rollback: drop the fixture table and ledger (issue rollback // note) and `docker compose -p eppp-diagnostic-probe down`. const composeEnv = { ...process.env, POSTGRES_PORT: POSTGRES_HOST_PORT }; const compose = (args, opts = {}) => run('docker', ['compose', '-p', COMPOSE_PROJECT, ...args], { cwd: REPO_ROOT, env: composeEnv, ...opts }); const execPsql = (args, opts = {}) => compose(['exec', '-T', 'db', 'psql', '-U', 'eppp', '-d', 'eppp', ...args], opts); const probeFile = path.join(REPO_ROOT, 'packages/database-postgres', `.diagnostic-probe-real-${process.pid}.mjs`); try { const up = compose(['up', '-d', 'db'], { timeout: 180_000 }); assert.equal( up.status, 0, `"docker compose up -d db" must exit 0:\n${(up.stdout || '')}\n${(up.stderr || '')}`.trim(), ); waitForDbHealthy(COMPOSE_PROJECT); // Clean slate (also recovers from a previously interrupted run): drop the // fixture table and the ledger so the database is empty of migration state. const clean = execPsql([ '-v', 'ON_ERROR_STOP=1', '-c', `DROP TABLE IF EXISTS ${FIXTURE_TABLE}; DROP TABLE IF EXISTS schema_migrations;`, ]); assert.equal( clean.status, 0, `dropping leftover fixture/ledger tables must succeed:\n${(clean.stdout || '')}\n${(clean.stderr || '')}`.trim(), ); // Run the committed runner module against the real database. writeFileSync(probeFile, REAL_PROBE_SOURCE); const probe = run(process.execPath, [probeFile], { cwd: REPO_ROOT, env: { ...process.env, DATABASE_URL }, timeout: 60_000, }); assert.equal( probe.status, 0, `the diagnostic real-stack probe must exit 0:\n${(probe.stdout || '')}\n${(probe.stderr || '')}`.trim(), ); const resultLine = (probe.stdout || '') .split('\n') .map((l) => l.trim()) .find((l) => l.startsWith('DIAGNOSTIC_PROBE_RESULT')); assert.ok(resultLine, `the diagnostic real-stack probe must report a result line (stdout: "${(probe.stdout || '').trim()}")`); const result = JSON.parse(resultLine.slice('DIAGNOSTIC_PROBE_RESULT'.length).trim()); // "migration failure produces a structured diagnostic": the run rejects // with a MigrationFailedError carrying a structured diagnostic. assert.equal(result.rejected, true, 'run() must reject when the failing migration fixture fails'); assert.equal(result.isMigrationFailedError, true, 'the failure must be a MigrationFailedError'); assert.equal(result.name, 'MigrationFailedError', 'the error name must be MigrationFailedError'); assert.match(result.message, /002_fixture_fail/, 'the error message must name the failing migration'); // "the diagnostic identifies the failing migration": the failing fixture // migration is named in the diagnostic and serialized form, phase is // 'apply', and the pg cause (code 42P01) is preserved. assert.equal(result.migration, FAIL_MIGRATION, 'the diagnostic must identify the failing migration'); assert.equal(result.phase, 'apply', 'the diagnostic must report the apply phase'); assert.deepEqual(result.applied, [OK_MIGRATION], 'the diagnostic must list the migration applied before the failure'); assert.deepEqual(result.pending, [FAIL_MIGRATION], 'the diagnostic must list the still-pending migrations'); assert.equal(result.causeName, 'error', 'the diagnostic cause must be the pg error'); assert.equal(result.causeCode, '42P01', 'the diagnostic must carry the pg error code (undefined_table)'); assert.equal(result.toJson.diagnostic.migration, FAIL_MIGRATION, 'toJSON() must identify the failing migration'); assert.equal(result.toJson.diagnostic.phase, 'apply', 'toJSON() must carry the failure phase'); assert.equal(result.toJson.diagnostic.cause.code, '42P01', 'toJSON() must carry a structured cause'); // The ok migration's effect persists: it is recorded in the ledger and its // table exists; the failing migration is not recorded. assert.deepEqual(result.ledgerApplied, [OK_MIGRATION], 'only the ok migration may be recorded in the ledger'); assert.ok(result.fixtureTable, 'the ok migration must have taken effect (fixture table exists)'); // Cross-check from inside the database container. const ledgerVersions = execPsql(['-tA', '-c', 'SELECT version FROM schema_migrations ORDER BY version;']); assert.equal( ledgerVersions.status, 0, `ledger rows query via psql must succeed:\n${(ledgerVersions.stdout || '')}\n${(ledgerVersions.stderr || '')}`.trim(), ); assert.match( ledgerVersions.stdout, new RegExp(OK_MIGRATION.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), 'the ok migration must be recorded in the ledger', ); assert.doesNotMatch( ledgerVersions.stdout, new RegExp(FAIL_MIGRATION.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), 'the failing migration must not be recorded in the ledger', ); } finally { // Rollback note from the issue: revert the diagnostic/error handling // changes — here, drop the fixture table and reset migration state, then // tear down the isolated project. try { execPsql(['-c', `DROP TABLE IF EXISTS ${FIXTURE_TABLE}; DROP TABLE IF EXISTS schema_migrations;`]); } catch { // container may already be gone — the compose down below still cleans up } compose(['down', '-v'], { timeout: 120_000 }); rmSync(probeFile, { force: true }); } }); // --------------------------------------------------------------------------- // Non-vacuous probes — the assertions above really do fail on violations // --------------------------------------------------------------------------- test('removing the migration field makes the identifies-the-failing-migration criterion fail (mutation probe)', () => { const src = read(RUNNER_SRC); const withoutMigration = src.replace(/ migration: string;\n/, ''); assert.notEqual(withoutMigration, src, 'the mutation must actually remove the migration field'); assert.throws(() => assertDiagnosticShape(withoutMigration), /migration/); }); test('removing the phase field makes the structured-diagnostic criterion fail (mutation probe)', () => { const src = read(RUNNER_SRC); const withoutPhase = src.replace(/ phase: MigrationFailurePhase;\n/, ''); assert.notEqual(withoutPhase, src, 'the mutation must actually remove the phase field'); assert.throws(() => assertDiagnosticShape(withoutPhase), /MigrationFailurePhase/); }); test('removing the pending field makes the structured-diagnostic criterion fail (mutation probe)', () => { const src = read(RUNNER_SRC); const withoutPending = src.replace(/ pending: string\[\];\n/, ''); assert.notEqual(withoutPending, src, 'the mutation must actually remove the pending field'); assert.throws(() => assertDiagnosticShape(withoutPending), /pending/); }); test('replacing the wrapped failure with a bare rethrow makes the structured-diagnostic criterion fail (mutation probe)', () => { const src = read(RUNNER_SRC); const bareRethrow = src .replace(/throw this\.failure\(migration\.version, 'apply', error, recorded\);\n/, 'throw error;\n') .replace(/throw this\.failure\(migration\.version, 'record', error, recorded\);\n/, 'throw error;\n'); assert.notEqual(bareRethrow, src, 'the mutation must actually replace the wrapped failures'); assert.throws(() => assertWrapsFailure(bareRethrow), /apply-failure path/); }); test('constructing a plain Error instead of MigrationFailedError makes the structured-diagnostic criterion fail (mutation probe)', () => { const src = read(RUNNER_SRC); const plainError = src.replace(/new MigrationFailedError\(\{/, 'new Error({'); assert.notEqual(plainError, src, 'the mutation must actually replace the MigrationFailedError construction'); assert.throws(() => assertWrapsFailure(plainError), /new MigrationFailedError/); }); test('removing the diagnostic field from MigrationFailedError makes the structured-diagnostic criterion fail (mutation probe)', () => { const src = read(RUNNER_SRC); const withoutField = src.replace(/ readonly diagnostic: MigrationDiagnostic;\n/, ''); assert.notEqual(withoutField, src, 'the mutation must actually remove the diagnostic field'); assert.throws(() => assertFailedError(withoutField), /readonly diagnostic/); }); test('removing toJSON() makes the serializable-diagnostic criterion fail (mutation probe)', () => { const src = read(RUNNER_SRC); const withoutToJson = src.replaceAll('toJSON()', 'toJson()'); assert.notEqual(withoutToJson, src, 'the mutation must actually rename toJSON()'); assert.throws(() => assertFailedError(withoutToJson), /toJSON\(\)/); }); test('dropping the runner re-export from the boundary fails the boundary criterion (mutation probe)', () => { const src = read(INDEX_SRC); const withoutReexport = src.replace(/export \{ MigrationRunner, MigrationFailedError \} from '\.\/runner\.js';\n/, ''); assert.notEqual(withoutReexport, src, 'the mutation must actually remove the runner re-export'); assert.throws(() => assertBoundaryReexport(withoutReexport), /re-export/); }); test('a placeholder runner module fails the runner-module criterion (mutation probe)', () => { assert.throws( () => assertRunnerModule('export class MigrationRunner {}\n'), /MigrationFailedError/, ); });