#!/usr/bin/env bun /** * Smart api test runner. * * Almost every api test is an integration test against the Postgres on black, * which is only low-latency from the apricot/LAN path. Run from elsewhere (e.g. * plum, over the mesh) the per-test DB round-trips blow the timeout and the * suite grinds through 60s-timeout failures. This runner probes the test DB and, * when it is unreachable or too slow, skips the DB-dependent test files (and * tells the harness — via QUINN_SKIP_DB_TESTS — to no-op its DB setup) so the * DB-free subset still runs and the suite stays meaningful off-LAN. * * Decision order: * 1. CI or QUINN_REQUIRE_DB_TESTS=1 → always run the FULL suite (fail loud; * CI must never silently skip a broken DB). * 2. QUINN_SKIP_DB_TESTS=1 → always skip DB tests. * 3. otherwise (auto) → skip DB tests if the DB is unreachable or * its round-trip exceeds QUINN_DB_LATENCY_SKIP_MS (default 250ms). * * What gets skipped is always logged — never a silent truncation. */ import { readFileSync } from 'node:fs'; import { Glob } from 'bun'; import postgres from 'postgres'; const TIMEOUT = process.env['QUINN_TEST_TIMEOUT'] ?? '60000'; const LATENCY_THRESHOLD_MS = Number(process.env['QUINN_DB_LATENCY_SKIP_MS'] ?? '250'); const DB_URL = process.env['QUINN_TEST_DB_URL'] ?? 'postgresql://quinn:devpassword@black.lan:25435/quinn_test'; const ADMIN_URL = DB_URL.replace(/\/[^/]+$/, '/postgres'); const isCi = process.env['CI'] != null || process.env['GITHUB_ACTIONS'] != null || process.env['GITEA_ACTIONS'] != null; const forceRequire = isCi || process.env['QUINN_REQUIRE_DB_TESTS'] === '1'; const forceSkip = process.env['QUINN_SKIP_DB_TESTS'] === '1'; /** Files referencing the DB harness are integration tests needing the live DB. */ const HARNESS_RE = /test-db|openTestDb|initTestDb|createTestApp|injectDb|activeTestTx|runMigrations/; function log(line: string): void { process.stdout.write(`${line}\n`); } async function probeDb(): Promise<{ reachable: boolean; rttMs: number | null }> { const sql = postgres(ADMIN_URL, { max: 1, connect_timeout: 4, idle_timeout: 1, onnotice: () => { /* quiet */ } }); try { await sql`SELECT 1`; // connect + warm up const t0 = performance.now(); for (let i = 0; i < 3; i++) await sql`SELECT 1`; return { reachable: true, rttMs: Math.round((performance.now() - t0) / 3) }; } catch { return { reachable: false, rttMs: null }; } finally { await sql.end({ timeout: 2 }).catch(() => { /* ignore */ }); } } async function decide(): Promise<{ skipDb: boolean; reason: string }> { if (forceRequire) return { skipDb: false, reason: isCi ? 'CI — full suite required' : 'QUINN_REQUIRE_DB_TESTS=1' }; if (forceSkip) return { skipDb: true, reason: 'QUINN_SKIP_DB_TESTS=1' }; const { reachable, rttMs } = await probeDb(); if (!reachable) return { skipDb: true, reason: 'test DB unreachable' }; if (rttMs != null && rttMs > LATENCY_THRESHOLD_MS) { return { skipDb: true, reason: `DB round-trip ${rttMs}ms > ${LATENCY_THRESHOLD_MS}ms (not on the fast LAN path)` }; } return { skipDb: false, reason: `DB round-trip ${rttMs ?? 0}ms` }; } function isDbDependent(file: string): boolean { try { return HARNESS_RE.test(readFileSync(file, 'utf8')); } catch { return false; } } const { skipDb, reason } = await decide(); const allFiles = [...new Glob('src/**/*.test.ts').scanSync('.')].sort(); const filesToRun = skipDb ? allFiles.filter((f) => !isDbDependent(f)) : allFiles; const skippedCount = allFiles.length - filesToRun.length; log(`[run-tests] DB tests ${skipDb ? 'SKIPPED' : 'enabled'} — ${reason}`); if (skipDb) { log(`[run-tests] skipping ${skippedCount} DB-dependent file(s); running ${filesToRun.length} DB-free file(s).`); log('[run-tests] run on apricot (fast LAN to black) or set QUINN_REQUIRE_DB_TESTS=1 to include them.'); } if (filesToRun.length === 0) { log('[run-tests] no files to run — nothing to do.'); process.exit(0); } const childEnv: Record = { ...(process.env as Record) }; // Tell the harness which mode it is in so its module-load DB setup matches the // file selection above (skip → no-op DB setup; require → force it on). if (skipDb) childEnv['QUINN_SKIP_DB_TESTS'] = '1'; else childEnv['QUINN_REQUIRE_DB_TESTS'] = '1'; const proc = Bun.spawn(['bun', 'test', '--parallel=1', `--timeout=${TIMEOUT}`, ...filesToRun], { env: childEnv, stdout: 'inherit', stderr: 'inherit', stdin: 'inherit', }); process.exit(await proc.exited);