/** * Docker Compose baseline test — locks in the [E00-S02-T01/T02/T03/T04] * `docker compose up -d` DB + app baseline, the PostgreSQL health gate, the * app health endpoint and the DB volume persistence for the workspace. * * Acceptance criteria covered (each test fails without the committed state): * - "docker compose up -d starts the database" → the committed * `compose.yaml` declares a `db` service backed by a PostgreSQL image * with the credentials the app expects and a published default port, so * `docker compose up -d` creates and starts the database container. When * a Docker daemon + Compose plugin are available (CI/dev machines), the * suite additionally runs the real stack (`docker compose up -d` → * `docker compose ps` → `docker compose down`) and asserts the `db` * container is up (and healthy). * - "docker compose up -d starts the application" → the committed * `compose.yaml` declares an `app` service built from the committed * `apps/server/Dockerfile` (multi-stage: Node 24.19.0 bookworm-slim + * frozen pnpm install → `tsc` build of `@personal-blog/server` → * `node apps/server/dist/index.js`), with a published default port. Since * T03 the real-stack probe asserts the `app` container stays **running** * (the server now serves the health endpoint instead of exiting) and the * health endpoint answers HTTP 200 with a healthy body inside the * container. * - "PostgreSQL health check gates application start" → the committed * `compose.yaml` declares a `healthcheck` on the `db` service that probes * readiness with `pg_isready` against the same credentials the database * was created with (`$$`-escaped so the container env applies), with an * interval/retries so a still-booting database is re-probed rather than * failed instantly. * - "app waits for the database before starting" → the `app` service * depends on `db` with `condition: service_healthy`, so Compose only * starts the application once PostgreSQL reports healthy (the real-stack * probe asserts the `db` container is healthy when Docker reports it). * - "database volume persists across restart" and "database volume * persists across recreate" → the committed `compose.yaml` declares a * named `db-data` volume and mounts it at PostgreSQL's data directory * (`/var/lib/postgresql/data`), so the database files survive * `docker compose restart` (same containers) and `docker compose down` → * `docker compose up -d` (containers recreated — the container * filesystem is discarded, so only a named volume carries the data). * When Docker is available, the real-stack probe writes a fixture row, * then asserts it survives both operations; data is reset with * `docker compose down -v` (the issue's rollback note). * * Run: `node --test tests/compose-config.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 } 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 Compose file and app image definition under test. */ const COMPOSE_PATH = 'compose.yaml'; const DOCKERFILE_PATH = 'apps/server/Dockerfile'; const DOCKERIGNORE_PATH = '.dockerignore'; // --------------------------------------------------------------------------- // Minimal block-YAML parser (no dependencies, lockfile untouched) // --------------------------------------------------------------------------- // // Covers exactly the subset the committed compose.yaml uses: nested block // mappings, block sequences of scalars, plain / single-quoted / double-quoted // scalars and `#` comments. Anything else (flow collections, anchors/aliases, // `|`/`>` block scalars, merge keys) is rejected loudly so the parser can // never silently misread the structure it locks in. /** Drops a `#` comment that is not inside a quoted scalar. */ function stripComment(line) { let quote = null; for (let i = 0; i < line.length; i += 1) { const ch = line[i]; if (quote) { if (ch === quote) quote = null; } else if (ch === "'" || ch === '"') { quote = ch; } else if (ch === '#' && (i === 0 || /\s/.test(line[i - 1]))) { return line.slice(0, i).trimEnd(); } } return line.trimEnd(); } /** Unquotes a plain / single-quoted / double-quoted scalar. */ function scalarValue(raw) { if (raw.length >= 2 && raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1); if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) { return raw.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); } return raw; } /** * Parses the supported block-YAML subset into plain JS objects/arrays. * Throws on any construct the committed file does not use. */ function parseYaml(text) { const lines = []; for (const raw of text.split(/\r?\n/)) { const expanded = raw.replace(/\t/g, ' '); if (!expanded.trim() || expanded.trim().startsWith('#')) continue; const indent = expanded.length - expanded.trimStart().length; const content = stripComment(expanded.trimStart()).trim(); if (!content) continue; lines.push({ indent, content }); } let pos = 0; const parseBlock = (indent) => { const node = {}; while (pos < lines.length && lines[pos].indent >= indent) { if (lines[pos].indent > indent) { throw new Error(`unexpected indentation at "${lines[pos].content}"`); } const { content } = lines[pos]; const match = /^([^:#][^:]*):(?:\s+(.*))?$/.exec(content); if (!match) { throw new Error(`expected "key: value", got "${content}"`); } const key = scalarValue(match[1].trim()); const rest = match[2] === undefined ? undefined : match[2].trim(); pos += 1; if (rest === undefined || rest === '') { if (pos < lines.length && lines[pos].indent > indent) { if (lines[pos].content.startsWith('-')) { node[key] = parseSequence(lines[pos].indent); } else { node[key] = parseBlock(lines[pos].indent); } } else { node[key] = null; } } else { node[key] = scalarValue(rest); } } return node; }; const parseSequence = (indent) => { const items = []; while (pos < lines.length && lines[pos].indent >= indent) { if (lines[pos].indent > indent) { throw new Error(`unexpected indentation in sequence at "${lines[pos].content}"`); } const { content } = lines[pos]; if (!content.startsWith('-')) break; const rest = content.slice(1).trim(); if (!rest) throw new Error(`empty sequence item at "-"`); items.push(scalarValue(rest)); pos += 1; } return items; }; if (lines.length === 0) return {}; return parseBlock(0); } // --------------------------------------------------------------------------- // Compose structure assertions (shared by the tests and the mutation probes) // --------------------------------------------------------------------------- /** * Asserts the committed compose.yaml declares a `db` service that * `docker compose up -d` can start: a PostgreSQL image, the credentials the * app expects, and a published default port. */ function assertDbService(compose) { assert.ok(compose.services, 'compose.yaml must declare a top-level "services" map'); const db = compose.services.db; assert.ok(db, 'compose.yaml must declare a "db" service (docker compose up -d starts the database)'); assert.equal( db.image, 'postgres:18.6-bookworm', 'the "db" service must use the committed PostgreSQL 18.6 image (postgres:18.6-bookworm — E00-S03-T01 ' + 'pins the exact minor so the database container exposes the expected PostgreSQL version)', ); const env = db.environment ?? {}; assert.equal(env.POSTGRES_DB, '${POSTGRES_DB:-eppp}', 'db must set POSTGRES_DB (with a default)'); assert.equal(env.POSTGRES_USER, '${POSTGRES_USER:-eppp}', 'db must set POSTGRES_USER (with a default)'); assert.ok( typeof env.POSTGRES_PASSWORD === 'string' && env.POSTGRES_PASSWORD.length > 0, 'db must set POSTGRES_PASSWORD (with a default)', ); assert.ok( Array.isArray(db.ports) && db.ports.some((p) => p.endsWith(':5432')), 'db must publish the PostgreSQL port (a mapping ending in ":5432")', ); } /** * Asserts the committed compose.yaml declares an `app` service that * `docker compose up -d` can start: built from the committed Dockerfile, * pointed at the db service, and started only after the database is healthy * (depends_on with condition: service_healthy). */ function assertAppService(compose) { const app = compose.services?.app; assert.ok(app, 'compose.yaml must declare an "app" service (docker compose up -d starts the application)'); assert.equal( app.build?.context, '.', 'the "app" service must build from the repository root context (build.context: ".")', ); assert.equal( app.build?.dockerfile, DOCKERFILE_PATH, `the "app" service must build the committed ${DOCKERFILE_PATH} image`, ); const env = app.environment ?? {}; assert.ok( typeof env.DATABASE_URL === 'string' && /@db:5432\//.test(env.DATABASE_URL), 'app must set DATABASE_URL pointing at the db service host (postgres://…@db:5432/…)', ); assert.ok( Array.isArray(app.ports) && app.ports.some((p) => p.endsWith(':3000')), 'app must publish the application port (a mapping ending in ":3000")', ); assert.equal( app.depends_on?.db?.condition, 'service_healthy', 'app must depend on db with condition: service_healthy so it waits for the database before starting', ); } /** * Asserts the committed compose.yaml gates the app on PostgreSQL health: the * `db` service declares a `pg_isready` healthcheck against the credentials it * was created with, with an interval and retries so a booting database is * re-probed instead of failed instantly. */ function assertDbHealthcheck(compose) { const db = compose.services?.db; assert.ok(db, 'compose.yaml must declare a "db" service'); const hc = db.healthcheck; assert.ok(hc, 'the "db" service must declare a healthcheck (PostgreSQL health check gates application start)'); assert.match( hc.test ?? '', /pg_isready/, 'the db healthcheck must probe readiness with pg_isready', ); assert.ok( (hc.test ?? '').includes('$${POSTGRES_USER}') && (hc.test ?? '').includes('$${POSTGRES_DB}'), 'the db healthcheck must probe the same credentials the database was created with ' + '($${POSTGRES_USER}/$${POSTGRES_DB}, $$-escaped so the container env applies)', ); assert.ok(hc.interval, 'the db healthcheck must declare an interval so readiness is re-probed'); assert.ok( Number(hc.retries) >= 1, 'the db healthcheck must declare retries so a booting database is not failed instantly', ); } /** * Asserts the committed compose.yaml makes the database volume persist: the * `db` service mounts the named `db-data` volume at PostgreSQL's data * directory (`/var/lib/postgresql/data`) and the top-level `volumes` map * declares that volume. That is what lets the database survive `docker * compose restart` (containers restarted in place) and `docker compose down` * → `docker compose up -d` (containers recreated — the container filesystem * is discarded, so only a named volume carries the data across). Removing * either half breaks the criterion (see the mutation probes below). */ function assertDbVolume(compose) { const db = compose.services?.db; assert.ok(db, 'compose.yaml must declare a "db" service'); assert.ok( Array.isArray(db.volumes) && db.volumes.includes('db-data:/var/lib/postgresql/data'), "the \"db\" service must mount the named db-data volume at PostgreSQL's data directory " + '(a "db-data:/var/lib/postgresql/data" entry) so the database survives restart and recreate', ); const volumes = compose.volumes ?? {}; assert.ok( Object.prototype.hasOwnProperty.call(volumes, 'db-data'), 'compose.yaml must declare the named "db-data" volume (top-level "volumes: db-data:") ' + 'so the mount target exists and is preserved across recreate', ); } // --------------------------------------------------------------------------- // Docker probe helpers (integration tests skip 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 reports healthy (or the * deadline passes). Used by the T04 persistence probe after `up`, `restart` * and `down` + `up` so fixture queries never race a still-booting database. */ function waitForDbHealthy(deadlineMs = 60_000) { const deadline = Date.now() + deadlineMs; let last = ''; while (Date.now() < deadline) { const ps = run('docker', ['compose', '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()}")`); } const DOCKER_COMPOSE = dockerComposeAvailable(); const DOCKER_DAEMON = dockerDaemonAvailable(); // --------------------------------------------------------------------------- // Tests — the committed state that makes `docker compose up -d` work // --------------------------------------------------------------------------- test('compose.yaml exists, parses, and declares exactly the db and app services', () => { assert.ok(existsSync(path.join(REPO_ROOT, COMPOSE_PATH)), `committed ${COMPOSE_PATH} must exist`); const compose = parseYaml(read(COMPOSE_PATH)); assert.deepEqual( Object.keys(compose.services ?? {}).sort(), ['app', 'db'], 'compose.yaml must declare exactly the "db" and "app" services at T01/T02/T03', ); }); test('the database service is defined so "docker compose up -d" starts the database', () => { assertDbService(parseYaml(read(COMPOSE_PATH))); }); test('the application service is defined so "docker compose up -d" starts the application', () => { assertAppService(parseYaml(read(COMPOSE_PATH))); }); test('PostgreSQL health check gates application start (db declares a pg_isready healthcheck)', () => { assertDbHealthcheck(parseYaml(read(COMPOSE_PATH))); }); test('the app waits for the database before starting (depends_on db with condition service_healthy)', () => { const app = parseYaml(read(COMPOSE_PATH)).services?.app; assert.ok(app, 'compose.yaml must declare an "app" service'); assert.equal( app.depends_on?.db?.condition, 'service_healthy', 'app must depend on db with condition: service_healthy so it starts only after PostgreSQL is healthy', ); }); test('the database volume persists across restart and recreate (db mounts the named db-data volume)', () => { assertDbVolume(parseYaml(read(COMPOSE_PATH))); }); test('the application image is defined by a committed multi-stage Dockerfile', () => { assert.ok(existsSync(path.join(REPO_ROOT, DOCKERFILE_PATH)), `committed ${DOCKERFILE_PATH} must exist`); const dockerfile = read(DOCKERFILE_PATH); assert.match( dockerfile, /FROM node:24\.19\.0-bookworm-slim AS build/, 'the Dockerfile must build on the committed Node 24.19.0 bookworm-slim base (FROM node:24.19.0-bookworm-slim AS build)', ); assert.match( dockerfile, /FROM node:24\.19\.0-bookworm-slim AS runtime/, 'the Dockerfile must ship a Node 24.19.0 bookworm-slim runtime stage (FROM node:24.19.0-bookworm-slim AS runtime)', ); assert.match( dockerfile, /pnpm install --frozen-lockfile/, 'the Dockerfile must install with the frozen lockfile (reproducible builds)', ); assert.match( dockerfile, /pnpm --filter @personal-blog\/server build/, 'the Dockerfile must compile the server package (pnpm --filter @personal-blog/server build)', ); assert.match( dockerfile, /node\b[^\n]*apps\/server\/dist\/index\.js/, 'the Dockerfile runtime stage must run the compiled server entrypoint (node apps/server/dist/index.js)', ); }); test('the build context excludes local artifacts and environment files', () => { assert.ok( existsSync(path.join(REPO_ROOT, DOCKERIGNORE_PATH)), `committed ${DOCKERIGNORE_PATH} must exist so local artifacts stay out of the build context`, ); const patterns = read(DOCKERIGNORE_PATH) .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith('#')); // T08 hardened these to their `**/`-prefixed forms so the exclusions also // apply at any nested depth (Docker's matcher anchors slash-less patterns to // the context root). for (const required of ['**/node_modules', 'dist', '**/.env', '.git']) { assert.ok( patterns.includes(required), `.dockerignore must exclude "${required}" (got: ${patterns.join(', ')})`, ); } }); // --------------------------------------------------------------------------- // Real-stack probes — run the actual acceptance command when Docker is present // --------------------------------------------------------------------------- test('the committed compose.yaml validates against the Compose spec (docker compose config)', { skip: !DOCKER_COMPOSE }, () => { const result = run('docker', ['compose', 'config', '--quiet'], { cwd: REPO_ROOT }); assert.equal( result.status, 0, `"docker compose config" must exit 0 for the committed ${COMPOSE_PATH}:\n` + `${(result.stdout || '')}\n${(result.stderr || '')}`.trim(), ); }); test('docker compose up -d starts the database and application containers', { skip: !DOCKER_COMPOSE || !DOCKER_DAEMON }, (t) => { const up = run('docker', ['compose', 'up', '-d'], { cwd: REPO_ROOT }); assert.equal( up.status, 0, `"docker compose up -d" must exit 0:\n${(up.stdout || '')}\n${(up.stderr || '')}`.trim(), ); try { const ps = run('docker', ['compose', 'ps', '-a', '--format', 'json'], { cwd: REPO_ROOT }); assert.equal(ps.status, 0, `"docker compose ps" must exit 0:\n${(ps.stderr || ps.stdout || '').trim()}`); const containers = parsePsJson(ps.stdout); const db = containers.find((c) => field(c, 'Service', 'service') === 'db'); assert.ok(db, 'docker compose up -d must create the "db" container'); const dbState = String(field(db, 'State', 'state') ?? ''); assert.match( dbState, /running|up/i, `the "db" container must be running after "docker compose up -d" (state: "${dbState}")`, ); const dbHealth = String(field(db, 'Health', 'health') ?? ''); if (dbHealth) { assert.match( dbHealth, /healthy/i, `the "db" container must be healthy before the app starts (health: "${dbHealth}")`, ); } const app = containers.find((c) => field(c, 'Service', 'service') === 'app'); assert.ok(app, 'docker compose up -d must create the "app" container'); const appState = String(field(app, 'State', 'state') ?? ''); assert.match( appState, /running|up/i, `the "app" container must stay running after "docker compose up -d" (state: "${appState}") — since T03 the server serves the health endpoint and must not exit`, ); // T03 + E00-S03-T06: the app serves the health endpoint — HTTP smoke test // against the endpoint inside the app container (no host-port dependency), // polling until it answers or times out. Since T06 the endpoint is the // readiness probe: it answers 503 {"status":"not ready"} while the startup // migration run is in flight and 200 {"status":"ok"} only after it // completes, so the probe treats a 503 (and a connection failure) as // "still starting — retry" and fail-fasts only on a definitive non-2xx // answer. let healthOutput = ''; let healthOk = false; for (let attempt = 0; attempt < 30 && !healthOk; attempt += 1) { const probe = run('docker', ['compose', 'exec', '-T', 'app', 'node', '-e', ` fetch('http://127.0.0.1:3000/health') .then(async (res) => { console.log(res.status, await res.text()); process.exit(res.ok ? 0 : res.status === 503 ? 2 : 1); }) .catch(() => process.exit(2)); `], { cwd: REPO_ROOT, timeout: 15_000 }); const output = String(probe.stdout ?? '') + String(probe.stderr ?? ''); if (probe.status === 0) { healthOk = true; healthOutput = output; } else if (probe.status === 1) { healthOutput = output; // answered but not 2xx — fail fast break; } else { run(process.execPath, ['-e', 'setTimeout(() => {}, 1000)']); // app still starting — retry } } assert.ok( healthOk, `GET /health must answer 2xx inside the app container once the stack is up (last probe: "${healthOutput.trim()}")`, ); assert.match(healthOutput, /200/, `GET /health must return HTTP 200 (got: "${healthOutput.trim()}")`); assert.match( healthOutput, /"status":"ok"/, `GET /health must report a healthy application (got: "${healthOutput.trim()}")`, ); } finally { run('docker', ['compose', 'down'], { cwd: REPO_ROOT }); } }); test('a database fixture survives docker compose restart and recreate (named db-data volume)', { skip: !DOCKER_COMPOSE || !DOCKER_DAEMON }, () => { // T04 acceptance criteria: the database volume persists across restart and // across recreate. The probe writes a fixture row through the running db // service, then asserts it is still there after `docker compose restart` // (containers restarted in place) and after `docker compose down` + // `docker compose up -d` (containers **recreated** — the container // filesystem is discarded, so only the named `db-data` volume can carry // the data across). `docker compose down -v` (the issue's rollback note) // cleans up in `finally` so runs stay isolated. Credentials use the // committed defaults, the same ones the app's DATABASE_URL hardcodes. const execPsql = (args) => run('docker', ['compose', 'exec', '-T', 'db', 'psql', '-U', 'eppp', '-d', 'eppp', ...args], { cwd: REPO_ROOT, timeout: 60_000, }); const createFixture = () => execPsql([ '-v', 'ON_ERROR_STOP=1', '-c', "CREATE TABLE t04_persist_probe (note text); INSERT INTO t04_persist_probe VALUES ('t04-fixture');", ]); const countFixture = () => execPsql(['-tA', '-c', 'SELECT count(*) FROM t04_persist_probe;']); const up = run('docker', ['compose', 'up', '-d'], { cwd: REPO_ROOT }); assert.equal( up.status, 0, `"docker compose up -d" must exit 0:\n${(up.stdout || '')}\n${(up.stderr || '')}`.trim(), ); try { waitForDbHealthy(); const created = createFixture(); assert.equal( created.status, 0, `fixture creation via psql must succeed:\n${(created.stdout || '')}\n${(created.stderr || '')}`.trim(), ); // Acceptance 1 — "database volume persists across restart": `docker // compose restart` stops and starts the same containers; the fixture // must still be queryable afterwards. const restart = run('docker', ['compose', 'restart'], { cwd: REPO_ROOT, timeout: 120_000 }); assert.equal( restart.status, 0, `"docker compose restart" must exit 0:\n${(restart.stdout || '')}\n${(restart.stderr || '')}`.trim(), ); waitForDbHealthy(); const afterRestart = countFixture(); assert.equal( afterRestart.status, 0, `fixture query after restart must succeed:\n${(afterRestart.stdout || '')}\n${(afterRestart.stderr || '')}`.trim(), ); assert.match( afterRestart.stdout.trim(), /^1$/m, `the database fixture must survive "docker compose restart" (volume persists across restart; got: "${afterRestart.stdout.trim()}")`, ); // Acceptance 2 — "database volume persists across recreate": `docker // compose down` (without -v, so named volumes survive) removes the // containers, then `docker compose up -d` recreates them from scratch — // the only thing that can carry the fixture across is the named volume. const down = run('docker', ['compose', 'down'], { cwd: REPO_ROOT, timeout: 120_000 }); assert.equal( down.status, 0, `"docker compose down" must exit 0:\n${(down.stdout || '')}\n${(down.stderr || '')}`.trim(), ); const upAgain = run('docker', ['compose', 'up', '-d'], { cwd: REPO_ROOT, timeout: 120_000 }); assert.equal( upAgain.status, 0, `"docker compose up -d" after down must exit 0:\n${(upAgain.stdout || '')}\n${(upAgain.stderr || '')}`.trim(), ); waitForDbHealthy(); const afterRecreate = countFixture(); assert.equal( afterRecreate.status, 0, `fixture query after recreate must succeed:\n${(afterRecreate.stdout || '')}\n${(afterRecreate.stderr || '')}`.trim(), ); assert.match( afterRecreate.stdout.trim(), /^1$/m, `the database fixture must survive "docker compose down" + "docker compose up -d" (volume persists across recreate; got: "${afterRecreate.stdout.trim()}")`, ); } finally { // Rollback note from the issue: `docker compose down -v` resets the data. run('docker', ['compose', 'down', '-v'], { cwd: REPO_ROOT, timeout: 120_000 }); } }); test('the database container exposes the expected PostgreSQL version (18.6)', { skip: !DOCKER_COMPOSE || !DOCKER_DAEMON }, () => { // E00-S03-T01 acceptance criteria: a PostgreSQL 18.6 container is used as // the database AND the container exposes the expected PostgreSQL version. // The committed image tag is pinned to postgres:18.6-bookworm (asserted // statically above); this probe starts the stack and asks the running // server itself (`SHOW server_version`) — the issue's test plan ("start the // container and confirm PostgreSQL 18.6") — so the version the container // actually exposes is verified end to end, not just the declared tag. const up = run('docker', ['compose', 'up', '-d'], { cwd: REPO_ROOT }); assert.equal( up.status, 0, `"docker compose up -d" must exit 0:\n${(up.stdout || '')}\n${(up.stderr || '')}`.trim(), ); try { waitForDbHealthy(); const version = run( 'docker', ['compose', 'exec', '-T', 'db', 'psql', '-U', 'eppp', '-d', 'eppp', '-tA', '-c', 'SHOW server_version;'], { cwd: REPO_ROOT, timeout: 60_000 }, ); assert.equal( version.status, 0, `"SHOW server_version" must succeed against the running db container:\n` + `${(version.stdout || '')}\n${(version.stderr || '')}`.trim(), ); assert.match( version.stdout.trim(), /^18\.6\b/, `the running db container must expose PostgreSQL 18.6 (SHOW server_version got: "${version.stdout.trim()}")`, ); } finally { run('docker', ['compose', 'down'], { cwd: REPO_ROOT }); } }); // --------------------------------------------------------------------------- // Non-vacuous probes — the assertions above really do fail on violations // --------------------------------------------------------------------------- test('the YAML parser reads the committed structure (non-vacuous parser probe)', () => { const parsed = parseYaml(` services: db: image: postgres:18.6-bookworm environment: POSTGRES_DB: eppp ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U \$\${POSTGRES_USER} -d \$\${POSTGRES_DB}"] interval: 5s retries: 5 app: build: context: . dockerfile: apps/server/Dockerfile depends_on: db: condition: service_healthy `); assert.equal(parsed.services.db.image, 'postgres:18.6-bookworm'); assert.equal(parsed.services.db.environment.POSTGRES_DB, 'eppp'); assert.deepEqual(parsed.services.db.ports, ['5432:5432']); assert.match(parsed.services.db.healthcheck.test, /pg_isready/); assert.equal(parsed.services.db.healthcheck.retries, '5'); assert.equal(parsed.services.app.build.dockerfile, 'apps/server/Dockerfile'); assert.equal(parsed.services.app.depends_on.db.condition, 'service_healthy'); }); test('removing the db service makes the database criterion fail (mutation probe)', () => { const text = read(COMPOSE_PATH); const withoutDb = text.replace(/^ db:\n(?: .*\n?)*/m, ''); assert.notEqual(withoutDb, text, 'the mutation must actually remove the db service block'); const compose = parseYaml(withoutDb); assert.throws(() => assertDbService(compose), /"db" service/); }); test('reverting the db image to a floating major tag fails the PostgreSQL 18.6 criterion (mutation probe)', () => { // E00-S03-T01 pins the exact 18.6 minor (postgres:18.6-bookworm). A // floating major tag (postgres:18-bookworm / postgres:18) would not // guarantee the container exposes PostgreSQL 18.6, so reverting to one must // fail the pinned-image assertion. const text = read(COMPOSE_PATH); const mutated = text.replace('postgres:18.6-bookworm', 'postgres:18-bookworm'); assert.notEqual(mutated, text, 'the mutation must actually revert the pinned image tag'); const compose = parseYaml(mutated); assert.throws(() => assertDbService(compose), /18\.6/); }); test('removing the app service makes the application criterion fail (mutation probe)', () => { const text = read(COMPOSE_PATH); const withoutApp = text.replace(/^ app:\n(?: .*\n?)*/m, ''); assert.notEqual(withoutApp, text, 'the mutation must actually remove the app service block'); const compose = parseYaml(withoutApp); assert.throws(() => assertAppService(compose), /"app" service/); }); test('removing the db healthcheck makes the health-gate criterion fail (mutation probe)', () => { const text = read(COMPOSE_PATH); const withoutHealthcheck = text.replace(/^ healthcheck:\n(?: .*\n?)*/m, ''); assert.notEqual(withoutHealthcheck, text, 'the mutation must actually remove the db healthcheck block'); const compose = parseYaml(withoutHealthcheck); assert.throws(() => assertDbHealthcheck(compose), /healthcheck/); }); test('reverting depends_on to a plain list breaks the healthy-gate criterion (mutation probe)', () => { const text = read(COMPOSE_PATH); const withoutGate = text.replace( /^ depends_on:\n db:\n condition: service_healthy\n/m, ' depends_on:\n - db\n', ); assert.notEqual(withoutGate, text, 'the mutation must actually replace the healthy-conditioned depends_on'); const compose = parseYaml(withoutGate); assert.throws(() => { const app = compose.services?.app; assert.equal( app?.depends_on?.db?.condition, 'service_healthy', 'app must depend on db with condition: service_healthy', ); }, /service_healthy/); }); test('a Dockerfile without the runtime entrypoint fails the image criterion (mutation probe)', () => { const dockerfile = read(DOCKERFILE_PATH); const withoutCmd = dockerfile.replace(/CMD \[[^\]]*\]/g, ''); assert.notEqual(withoutCmd, dockerfile, 'the mutation must actually remove the CMD'); const asserts = () => { assert.match(withoutCmd, /node\s+apps\/server\/dist\/index\.js/, 'must run the compiled entrypoint'); }; assert.throws(asserts, /compiled entrypoint/); }); test('removing the db volume mount makes the persistence criterion fail (mutation probe)', () => { const text = read(COMPOSE_PATH); const withoutMount = text.replace( ' volumes:\n - db-data:/var/lib/postgresql/data\n', '', ); assert.notEqual(withoutMount, text, 'the mutation must actually remove the db volume mount'); const compose = parseYaml(withoutMount); assert.throws(() => assertDbVolume(compose), /db-data/); }); test('removing the named db-data volume declaration makes the persistence criterion fail (mutation probe)', () => { const text = read(COMPOSE_PATH); const withoutVolume = text.replace('volumes:\n db-data:\n', ''); assert.notEqual(withoutVolume, text, 'the mutation must actually remove the top-level volumes declaration'); const compose = parseYaml(withoutVolume); assert.throws(() => assertDbVolume(compose), /db-data/); }); test('mounting the volume at the wrong path fails the persistence criterion (mutation probe)', () => { const text = read(COMPOSE_PATH); const wrongPath = text.replace( 'db-data:/var/lib/postgresql/data', 'db-data:/var/lib/postgresql', ); assert.notEqual(wrongPath, text, 'the mutation must actually change the mount target'); const compose = parseYaml(wrongPath); assert.throws(() => assertDbVolume(compose), /data directory/); });