// Orchestrates everything a real browser session needs, mirroring production's Caddy-routed // single-origin setup (see proxy.ts) since that routing contract was never actually exercised by // any of this project's prior manual verification — see README's e2e section. // // Uses globalSetup/globalTeardown (manual orchestration) rather than Playwright's built-in // `webServer` option: the proxy has no independent readiness signal until api+frontend are // already listening, which `webServer`'s single command-per-entry model doesn't fit well. // // No `caddy` binary is available in this dev environment, hence the hand-rolled proxy.ts instead // of running real Caddy. import { execSync, spawn, type ChildProcess } from "node:child_process"; import { writeFileSync } from "node:fs"; import postgres from "postgres"; import { ADMIN_TOKEN, API_DIR, API_PORT, DATABASE_URL, FRONTEND_DIR, FRONTEND_PORT, MINIO_ACCESS_KEY, MINIO_CONTAINER, MINIO_PORT, MINIO_SECRET_KEY, PG_CONTAINER, PG_PORT, PROXY_ORIGIN, PROXY_PORT, REDIS_CONTAINER, REDIS_PORT, REDIS_URL, ROOT, STATE_FILE, } from "./config"; function tryRemoveContainer(name: string) { try { execSync(`docker rm -f ${name}`, { stdio: "ignore" }); } catch { // fine — container didn't exist } } async function waitFor(check: () => Promise, label: string, timeoutMs = 45000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { if (await check().catch(() => false)) return; await new Promise((r) => setTimeout(r, 300)); } throw new Error(`timed out waiting for ${label} to become ready`); } async function httpOk(url: string) { const res = await fetch(url); return res.ok; } const children: ChildProcess[] = []; // Windows/Bun sometimes double-binds a port across process restarts (see MCMapper's project // memory) — tracking every spawned PID explicitly and killing them all in global-teardown.ts // avoids that class of zombie-listener bug for these throwaway dev-server instances. function spawnTracked(label: string, cmd: string, args: string[], cwd: string, env: Record) { const child = spawn(cmd, args, { cwd, env: { ...process.env, ...env }, stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32", // Posix: makes the child its own process-group leader so global-teardown.ts can kill // `-pid` (the whole group, including any grandchildren) instead of just this one process. detached: process.platform !== "win32", }); child.stdout?.on("data", (d) => process.stdout.write(`[${label}] ${d}`)); child.stderr?.on("data", (d) => process.stderr.write(`[${label}] ${d}`)); children.push(child); return child; } function encodeSolidSection(blockIdMeta: number): string { const bytes = new Uint8Array(4096 * 2); const view = new DataView(bytes.buffer); for (let i = 0; i < 4096; i++) view.setUint16(i * 2, blockIdMeta, true); let binary = ""; for (const b of bytes) binary += String.fromCharCode(b); return btoa(binary); } export default async function globalSetup() { tryRemoveContainer(PG_CONTAINER); tryRemoveContainer(REDIS_CONTAINER); tryRemoveContainer(MINIO_CONTAINER); execSync( `docker run -d --name ${PG_CONTAINER} -e POSTGRES_USER=mcmapper -e POSTGRES_PASSWORD=mcmapper -e POSTGRES_DB=mcmapper -p ${PG_PORT}:5432 postgres:17-alpine`, { stdio: "inherit" }, ); execSync(`docker run -d --name ${REDIS_CONTAINER} -p ${REDIS_PORT}:6379 redis:7-alpine`, { stdio: "inherit" }); execSync( `docker run -d --name ${MINIO_CONTAINER} -e MINIO_ROOT_USER=${MINIO_ACCESS_KEY} -e MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY} -p ${MINIO_PORT}:9000 minio/minio:latest server /data`, { stdio: "inherit" }, ); const sql = postgres(DATABASE_URL); await waitFor(async () => { await sql`select 1`; return true; }, "postgres"); execSync("bun run migrate", { cwd: API_DIR, stdio: "inherit", env: { ...process.env, DATABASE_URL } }); // Seed a server + a linked account/session (so markers.spec.ts can exercise the server-synced // marker path, not just the anonymous/localStorage one) + a 5x5-chunk solid terrain footprint // centered on the world origin, where the map's default view (`setView([0,0],0)`) is centered // — generous enough that a drag anywhere near the visible center reliably selects real, // meshable terrain without the test needing to compute exact Leaflet pixel<->world math. const serverToken = `e2e-${crypto.randomUUID()}`; const [server] = await sql<{ id: string }[]>` insert into servers (name, token) values ('e2e-server', ${serverToken}) returning id `; const [account] = await sql<{ id: string }[]>` insert into accounts (mc_uuid, username, auth_mode) values (${"e2e-" + crypto.randomUUID()}, 'e2e-player', 'online') returning id `; const sessionToken = `e2e-session-${crypto.randomUUID()}`; await sql`insert into sessions (token, account_id) values (${sessionToken}, ${account!.id})`; const blocks = encodeSolidSection((2 << 4) | 0); // grass for (let x = -2; x <= 2; x++) { for (let z = -2; z <= 2; z++) { await sql` insert into chunk_sections (server_id, dimension, x, z, section_y, blocks) values (${server!.id}, 0, ${x}, ${z}, 3, ${blocks}) `; } } await sql.end(); spawnTracked("api", "bun", ["run", "src/index.ts"], API_DIR, { PORT: String(API_PORT), DATABASE_URL, REDIS_URL, MINIO_ENDPOINT: "localhost", MINIO_PORT: String(MINIO_PORT), MINIO_USE_SSL: "false", MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MCMAPPER_ADMIN_TOKEN: ADMIN_TOKEN, }); await waitFor(() => httpOk(`http://localhost:${API_PORT}/health`), "api"); spawnTracked("frontend", "bun", ["run", "src/index.ts"], FRONTEND_DIR, { PORT: String(FRONTEND_PORT), API_URL: `http://localhost:${API_PORT}`, }); await waitFor(() => httpOk(`http://localhost:${FRONTEND_PORT}/health`), "frontend"); spawnTracked("proxy", "bun", ["run", "proxy.ts"], ROOT + "/e2e", { E2E_PROXY_PORT: String(PROXY_PORT), E2E_API_ORIGIN: `http://localhost:${API_PORT}`, E2E_FRONTEND_ORIGIN: `http://localhost:${FRONTEND_PORT}`, }); await waitFor(() => httpOk(`${PROXY_ORIGIN}/__proxy_health`), "proxy"); writeFileSync( STATE_FILE, JSON.stringify({ pids: children.map((c) => c.pid).filter((pid): pid is number => pid !== undefined), containers: [PG_CONTAINER, REDIS_CONTAINER, MINIO_CONTAINER], baseURL: PROXY_ORIGIN, serverId: server!.id, serverToken, sessionToken, accountId: account!.id, adminToken: ADMIN_TOKEN, }), ); console.log("[global-setup] ready:", PROXY_ORIGIN); }