import { BeforeAll, AfterAll, setDefaultTimeout } from '@cucumber/cucumber' import { spawn, spawnSync, type ChildProcess } from 'child_process' import { mkdirSync, rmSync } from 'fs' import { resolve, dirname } from 'path' import { fileURLToPath } from 'url' import { config } from './config.js' setDefaultTimeout(config.timeout) let serverProcess: ChildProcess | undefined // Provision the isolated e2e db with the real tooling: `pikku db reset` applies // every migration in db/sqlite/ and the db/sqlite-seed.sql seed. PIKKU_SQLITE_DB // points createConfig at this file (see packages/functions/src/config.ts). function provisionDb(projectDir: string, dbFile: string): void { const res = spawnSync('npx', ['pikku', 'db', 'reset'], { cwd: projectDir, env: { ...process.env, PIKKU_SQLITE_DB: dbFile }, encoding: 'utf8', }) if (res.status !== 0) { throw new Error( `pikku db reset failed (${res.status}):\n${res.stdout ?? ''}\n${res.stderr ?? ''}` ) } } async function waitForServer(url: string, timeoutMs = 30_000) { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { try { const res = await fetch(url) if (res.ok || res.status < 500) { console.log(`[backend] ready on ${url}`) return } } catch { // not ready yet } await new Promise((r) => setTimeout(r, 500)) } throw new Error(`Backend did not start within ${timeoutMs / 1000}s on ${url}`) } BeforeAll(async function () { const __dirname = dirname(fileURLToPath(import.meta.url)) const projectDir = resolve(__dirname, '../../..') const port = new URL(config.apiUrl).port // Fresh SQLite file per run, provisioned by `pikku db reset`. for (const suffix of ['', '-wal', '-shm']) { rmSync(`${config.dbFile}${suffix}`, { force: true }) } mkdirSync(dirname(config.dbFile), { recursive: true }) provisionDb(projectDir, config.dbFile) const serverEnv: Record = { ...(process.env as Record), PORT: port, E2E_DB_FILE: config.dbFile, BETTER_AUTH_SECRET: 'e2e-test-secret', AUTH_SECRET: 'e2e-test-secret', } // Start the isolated SQLite backend. serverProcess = spawn('npx', ['tsx', 'backend/bin/start-e2e.ts'], { cwd: projectDir, env: serverEnv, stdio: 'pipe', detached: true, }) serverProcess.stderr?.on('data', (d: Buffer) => process.stderr.write(`[backend] ${d}`) ) serverProcess.stdout?.on('data', (d: Buffer) => process.stdout.write(`[backend] ${d}`) ) await waitForServer(`${config.apiUrl}/health-check`) // The admin (admin@perauset.org / "test") comes from db/sqlite-seed.sql via // `pikku db reset`. Per-scenario users are created at runtime by the // `a user ... exists` steps (ensureUser). }) AfterAll(async function () { if (serverProcess?.pid) { try { process.kill(-serverProcess.pid, 'SIGTERM') } catch { // process may already be dead } serverProcess = undefined } })