Files
PersonalBlog/tests/database-postgres-diagnostic.test.mjs
implementer 52190d082c
CI / Frozen lockfile install (pull_request) Successful in 44s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 28s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 26s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 44s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 51s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
CI / Migration failure diagnostic (E00-S03-T05) (pull_request) Failing after 51s
test: lock in the migration failure diagnostic (E00-S03-T05)
Static assertions + mutation probes on the committed runner source, a
deterministic stub-pool behavioral probe (intentionally failing migration
fixture -> structured diagnostic naming the failing migration, apply and
record phases), a docker-gated real-stack probe against a real database
(the issue's test plan), and CI enforcement via the additive
database-postgres-diagnostic job.
2026-08-30 01:17:23 +00:00

798 lines
37 KiB
JavaScript

/**
* 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/,
);
});