import { execSync } from 'node:child_process' import { writeFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = dirname(fileURLToPath(import.meta.url)) const API_HOST = process.env.E2E_API_URL || 'http://localhost:8100' const API_BASE = `${API_HOST}/api/v1` const COMPOSE_FILE = resolve(__dirname, '../../docker-compose.test.yml') const COMPOSE = `docker compose -p nuzlocke-test -f ${COMPOSE_FILE}` const FIXTURES_PATH = resolve(__dirname, '.fixtures.json') function run(cmd: string): string { console.log(`[setup] ${cmd}`) return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'inherit'] }) } async function waitForApi(url: string, maxAttempts = 30): Promise { for (let i = 0; i < maxAttempts; i++) { try { const res = await fetch(url) if (res.ok) return } catch { // not ready yet } await new Promise((r) => setTimeout(r, 2000)) } throw new Error(`API at ${url} not ready after ${maxAttempts} attempts`) } async function api( path: string, options?: RequestInit, ): Promise { const res = await fetch(`${API_BASE}${path}`, { headers: { 'Content-Type': 'application/json' }, ...options, }) if (!res.ok) { const body = await res.text() throw new Error(`API ${options?.method ?? 'GET'} ${path} → ${res.status}: ${body}`) } return res.json() as Promise } export default async function globalSetup() { // 1. Start test DB + API run(`${COMPOSE} up -d --build`) // 2. Wait for API to be healthy console.log('[setup] Waiting for API to be ready...') await waitForApi(`${API_HOST}/`) // 3. Run migrations run(`${COMPOSE} exec -T test-api alembic -c /app/alembic.ini upgrade head`) // 4. Seed reference data (run from /app/src where the app package lives) run(`${COMPOSE} exec -T -w /app/src test-api python -m app.seeds`) // 5. Create test fixtures via API const games = await api>('/games') const game = games[0] if (!game) throw new Error('No games found after seeding') const routes = await api>( `/games/${game.id}/routes?flat=true`, ) // Pick leaf routes (no children — a route is a leaf if no other route has it as parent) const parentIds = new Set(routes.map((r) => r.parentRouteId).filter(Boolean)) const leafRoutes = routes.filter((r) => !parentIds.has(r.id)) if (leafRoutes.length < 3) throw new Error(`Need ≥3 leaf routes, found ${leafRoutes.length}`) const pokemonRes = await api<{ items: Array<{ id: number; name: string }> }>( '/pokemon?limit=10', ) const pokemon = pokemonRes.items if (pokemon.length < 3) throw new Error(`Need ≥3 pokemon, found ${pokemon.length}`) // Create a test run const testRun = await api<{ id: number }>('/runs', { method: 'POST', body: JSON.stringify({ gameId: game.id, name: 'E2E Test Run', rules: { duplicatesClause: true, shinyClause: true }, }), }) // Create encounters: caught, fainted, missed const statuses = ['caught', 'fainted', 'missed'] as const for (let i = 0; i < 3; i++) { await api(`/runs/${testRun.id}/encounters`, { method: 'POST', body: JSON.stringify({ routeId: leafRoutes[i]!.id, pokemonId: pokemon[i]!.id, nickname: `Test ${statuses[i]}`, status: statuses[i], catchLevel: statuses[i] === 'missed' ? null : 5 + i * 10, }), }) } // Create a genlocke with 2 game legs const secondGame = games[1] ?? game const genlocke = await api<{ id: number }>('/genlockes', { method: 'POST', body: JSON.stringify({ name: 'E2E Test Genlocke', gameIds: [game.id, secondGame.id], genlockeRules: {}, nuzlockeRules: { duplicatesClause: true }, }), }) // 6. Write fixtures file const fixtures = { gameId: game.id, runId: testRun.id, genlockeId: genlocke.id, } writeFileSync(FIXTURES_PATH, JSON.stringify(fixtures, null, 2)) console.log('[setup] Fixtures written:', fixtures) }