/** * US-6 — deployment via Makefile + Docker. * * These checks are infrastructure-level: they shell out to `make` and the * container runtime to assert the build/run targets behave correctly and * the SQLite volume survives a restart. * * The container is expected to already be `up` when the suite starts (the * harness runs `make build && make start && make create-admin` before * Playwright). So `AC-6.1`/`AC-6.2` are verified via image presence + HTTP * smoke; `AC-6.3` exercises stop/restart/logs; `AC-6.4` writes a row, restarts, * and re-reads it; `AC-6.5` confirms the test targets exist as Makefile rules. */ import { test, expect } from '@playwright/test'; import { execSync } from 'node:child_process'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { readFileSync } from 'node:fs'; import { adminToken, createEngagement, deleteAllEngagements, listEngagements, waitForHealth, } from '../fixtures/api'; const RUNTIME = process.env.MIMIC_CONTAINER_CMD ?? 'docker'; const CONTAINER = process.env.MIMIC_CONTAINER ?? 'mimic'; const IMAGE = process.env.MIMIC_IMAGE ?? 'mimic:latest'; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, '../..'); function run(cmd: string, opts: { ignoreFail?: boolean } = {}): { status: number; out: string } { try { const out = execSync(cmd, { cwd: REPO_ROOT, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', }); return { status: 0, out }; } catch (e) { const err = e as { status?: number; stdout?: Buffer | string; stderr?: Buffer | string }; const out = (typeof err.stdout === 'string' ? err.stdout : err.stdout?.toString() ?? '') + (typeof err.stderr === 'string' ? err.stderr : err.stderr?.toString() ?? ''); if (opts.ignoreFail) return { status: err.status ?? -1, out }; throw new Error(`command failed (${err.status}): ${cmd}\n${out}`); } } test.describe('US-6 — deployment via Docker + Makefile', () => { test('AC-6.1 — image mimic:latest exists (built by make build)', () => { const r = run(`${RUNTIME} images --format "{{.Repository}}:{{.Tag}}"`); expect(r.out).toMatch(new RegExp(IMAGE.replace(/[.:]/g, '\\$&'))); }); test('AC-6.2 — container responds on http://localhost:5000 (front + /api/health)', async () => { await waitForHealth(5_000); // Frontend is served at "/". Use 127.0.0.1 explicitly so envs where // `localhost` resolves to ::1 (where the container port isn't bound) // don't break this contract test. const base = (process.env.MIMIC_BASE_URL ?? 'http://127.0.0.1:5000').replace(/\/$/, ''); const html = run(`curl -fsS ${base}/`).out; expect(html).toMatch(//i); expect(html.toLowerCase()).toMatch(/mimic/); }); test('AC-6.3 — make stop / make restart / make logs are well-formed targets', () => { // Don't actually stop the container mid-suite — that would tear down // the other tests. Instead, verify the Makefile rules exist and are // syntactically valid by asking make for their recipes via `--just-print`. // `make restart` expands to `make stop && make start`, so we look for // those sub-commands instead of a literal "restart" token. const dry = run('make --dry-run --no-print-directory stop restart logs'); expect(dry.out).toMatch(new RegExp(`${RUNTIME} stop ${CONTAINER}`)); expect(dry.out).toMatch(/make stop && make start/); expect(dry.out).toMatch(new RegExp(`${RUNTIME} logs -f ${CONTAINER}`)); const makefile = readFileSync(resolve(REPO_ROOT, 'Makefile'), 'utf8'); expect(makefile).toMatch(/^stop:/m); expect(makefile).toMatch(/^restart:/m); expect(makefile).toMatch(/^logs:/m); }); test('AC-6.4 — SQLite persists across container restart (named volume mimic-data)', async () => { const token = await adminToken(); // Seed a unique engagement. const marker = `AC-6.4-persistence-${Date.now()}`; await createEngagement(token, { name: marker, start_date: '2026-12-01' }); // Restart the container (NOT make restart, since that would also rm — // we do `runtime restart` which keeps the same container + volume). run(`${RUNTIME} restart ${CONTAINER}`); await waitForHealth(20_000); const token2 = await adminToken(); const items = await listEngagements(token2); const found = items.find((i) => i.name === marker); expect(found, `engagement seeded before restart should survive`).toBeTruthy(); // Cleanup. await deleteAllEngagements(token2); }); test('AC-6.5 — make test-backend / test-frontend / test-e2e are defined', () => { const makefile = readFileSync(resolve(REPO_ROOT, 'Makefile'), 'utf8'); expect(makefile).toMatch(/^test-backend:/m); expect(makefile).toMatch(/^test-frontend:/m); expect(makefile).toMatch(/^test-e2e:/m); // Dry-run them to make sure the recipes are syntactically valid. const dry = run('make --dry-run --no-print-directory test-backend test-frontend test-e2e'); expect(dry.out).toMatch(/pytest/); expect(dry.out).toMatch(/npm run test/); expect(dry.out).toMatch(/playwright test/); }); });