lilith-platform.live/codebase/@features/api/scripts/run-tests.ts
Natalie 6a155be0c9 perf(ci): template DB clone, parallel test workers, split CI/deploy queues
- Build one migrated template per run-tests invocation; per-file DBs clone
  via CREATE DATABASE TEMPLATE (~seconds) instead of replaying 148 migrations
- Run up to 4 test workers on CI (QUINN_TEST_WORKERS); sweep orphans once
- QUINN_CI_FAST smoke subset (12 files) on push/PR; full suite on dispatch
- ci-${{ ref }} concurrency separate from deploy-${{ ref }}; cancel stale runs
- Cache Playwright browsers on quinn.www deploy workflow
2026-06-24 03:59:48 -04:00

186 lines
No EOL
6.7 KiB
TypeScript

#!/usr/bin/env bun
/**
* Smart api test runner.
*
* Optimizations (CI / QUINN_REQUIRE_DB_TESTS):
* - One TEMPLATE database with all test migrations applied once per run
* - Per-file DBs cloned via CREATE DATABASE … TEMPLATE (~seconds, not ~20s migrate)
* - Parallel workers (QUINN_TEST_WORKERS, default 4 on CI)
* - QUINN_CI_FAST=1 runs a smoke subset on push/PR; workflow_dispatch runs full
*
* Off-LAN: probes DB latency and skips DB-dependent files when slow/unreachable.
*/
import { readFileSync } from 'node:fs';
import { Glob } from 'bun';
import postgres from 'postgres';
import {
buildTestTemplate,
dropTestDatabase,
sweepOrphanTestDbs,
DEFAULT_ADMIN_DB_URL,
} from './test-template';
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 = process.env['QUINN_TEST_ADMIN_DB_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';
const ciFast = process.env['QUINN_CI_FAST'] === '1';
const workers = Math.max(
1,
Number(process.env['QUINN_TEST_WORKERS'] ?? (isCi ? '4' : '1')),
);
/** Push/PR smoke — full suite on workflow_dispatch (QUINN_CI_FULL=1 or no QUINN_CI_FAST). */
const CI_FAST_FILES = new Set([
'src/__tests__/pg-migration.test.ts',
'src/__tests__/www-provider-config-shop.test.ts',
'src/__tests__/admin-content-drops.test.ts',
'src/__tests__/admin-gallery-items.test.ts',
'src/__tests__/vip-api.test.ts',
'src/__tests__/my-clients.test.ts',
'src/__tests__/server.test.ts',
'src/__tests__/public-contact.test.ts',
'src/__tests__/engine-drafts.test.ts',
'src/__tests__/short-links.test.ts',
'src/__tests__/www-blog.test.ts',
'src/__tests__/admin-lore-sections.test.ts',
]);
/** 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`;
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;
}
}
function chunkFiles(files: string[], n: number): string[][] {
const chunks: string[][] = Array.from({ length: n }, () => []);
for (let i = 0; i < files.length; i += 1) {
chunks[i % n]!.push(files[i]!);
}
return chunks.filter((c) => c.length > 0);
}
async function runOneFile(file: string, childEnv: Record<string, string>): Promise<number> {
log(`[run-tests] ▶ ${file}`);
const proc = Bun.spawn(['bun', 'test', '--parallel=1', `--timeout=${TIMEOUT}`, file], {
env: childEnv,
stdout: 'inherit',
stderr: 'inherit',
stdin: 'inherit',
});
return proc.exited;
}
async function runWorker(files: string[], childEnv: Record<string, string>): Promise<number> {
let exitCode = 0;
for (const file of files) {
const code = await runOneFile(file, childEnv);
if (code !== 0) exitCode = code;
}
return exitCode;
}
const { skipDb, reason } = await decide();
const allFiles = [...new Glob('src/**/*.test.ts').scanSync('.')].sort();
let filesToRun = skipDb ? allFiles.filter((f) => !isDbDependent(f)) : allFiles;
if (ciFast && !skipDb) {
const fast = filesToRun.filter((f) => CI_FAST_FILES.has(f));
log(`[run-tests] QUINN_CI_FAST=1 — running ${fast.length}/${filesToRun.length} smoke file(s)`);
filesToRun = fast;
}
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 LAN (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<string, string> = { ...(process.env as Record<string, string>) };
if (skipDb) childEnv['QUINN_SKIP_DB_TESTS'] = '1';
else childEnv['QUINN_REQUIRE_DB_TESTS'] = '1';
let templateName: string | undefined;
if (!skipDb) {
const adminUrl = DEFAULT_ADMIN_DB_URL;
const swept = await sweepOrphanTestDbs(adminUrl);
if (swept > 0) log(`[run-tests] swept ${swept} orphan test database(s)`);
log('[run-tests] building migration template (once per run)…');
const t0 = performance.now();
templateName = await buildTestTemplate(adminUrl);
log(`[run-tests] template ${templateName} ready in ${Math.round(performance.now() - t0)}ms`);
childEnv['QUINN_TEST_DB_TEMPLATE'] = templateName;
childEnv['QUINN_TEST_SKIP_SWEEP'] = '1';
childEnv['QUINN_TEST_ADMIN_DB_URL'] = adminUrl;
}
log(`[run-tests] ${filesToRun.length} file(s), ${workers} worker(s)`);
const chunks = chunkFiles(filesToRun, workers);
let exitCode = 0;
try {
const results = await Promise.all(chunks.map((chunk) => runWorker(chunk, childEnv)));
for (const code of results) {
if (code !== 0) exitCode = code;
}
} finally {
if (templateName) {
log(`[run-tests] dropping template ${templateName}`);
await dropTestDatabase(templateName, childEnv['QUINN_TEST_ADMIN_DB_URL'] ?? ADMIN_URL).catch((err) => {
log(`[run-tests] template drop failed: ${String(err)}`);
});
}
}
process.exit(exitCode);