assembleProviderConfig now reads hero_strip_items; admin rate-cards, site-text, and tour-stops tests were still on stale migration bundles. www/payment-methods tests must query ?provider=quinn to match repo defaults. Run each test file in its own bun process so the per-process throwaway DB does not leak committed fixtures across files in CI.
116 lines
4.9 KiB
TypeScript
116 lines
4.9 KiB
TypeScript
#!/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<string, string> = { ...(process.env as Record<string, string>) };
|
|
// 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';
|
|
|
|
// One bun process per file so test-db.ts provisions a fresh throwaway database
|
|
// per file. A single `bun test f1 f2 …` shares one module-load DB across the
|
|
// whole argv list, which lets beforeAll fixtures and committed writes leak
|
|
// between files and flakes list/count assertions mid-suite.
|
|
let exitCode = 0;
|
|
for (const file of filesToRun) {
|
|
log(`[run-tests] ▶ ${file}`);
|
|
const proc = Bun.spawn(['bun', 'test', '--parallel=1', `--timeout=${TIMEOUT}`, file], {
|
|
env: childEnv,
|
|
stdout: 'inherit',
|
|
stderr: 'inherit',
|
|
stdin: 'inherit',
|
|
});
|
|
const code = await proc.exited;
|
|
if (code !== 0) exitCode = code;
|
|
}
|
|
process.exit(exitCode);
|