Repository Public
1// V8 hardening (test isolation) — ONE helper for every test file that needs PostgreSQL.2//3// Before: each file used a fixed database name (spw_test, spw_market_test, spw_v6_alerts_api_test…). Two checkouts running4// the suite at the same time (several worktrees, several engineers, CI next to a developer) shared those databases and5// corrupted each other: random failures in market-store, api, agent-networks and the alerts tests.6//7// Now: a database name is unique per CHECKOUT and per RUN and per test file:8//9// spw_t_<start, seconds, base 36>_<4 hex of sha256(repository path)><4 random hex>_<label>10//11// The run id is created once by vitest's global setup (services/api/test/global-db.ts), which also drops every database of the12// run when it ends — whatever happened in the test files (a crash, a timeout, a forgotten teardown). A test file run without13// that global setup makes its own run id; `scripts/clean-test-dbs.sh` removes what such runs (or a killed vitest) leave14// behind: the start time in the name tells how old a database is.15//16// Cost: `create database` is ~40 ms on a local PostgreSQL 17; a suite of ~20 database files pays under two seconds.17import { createHash, randomBytes } from 'node:crypto';18import path from 'node:path';19import { fileURLToPath } from 'node:url';20import postgres from 'postgres';2122export const TEST_DB_PREFIX = 'spw_t_';23export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');24/** `<start seconds, base 36>_<checkout hash><random>`: sortable by age, distinct between checkouts and between runs */25export const newRunId = (): string => `${Math.floor(Date.now() / 1000).toString(36)}_${createHash('sha256').update(REPO_ROOT).digest('hex').slice(0, 4)}${randomBytes(2).toString('hex')}`;26let ownRun: string | null = null;27export const runId = (): string => process.env.SPW_TEST_RUN || (ownRun ??= newRunId());28const ADMIN_URL = (): string => process.env.SPW_TEST_ADMIN_URL ?? 'postgres:///postgres';2930export function testDbName(label: string): string {31 if (!/^[a-z0-9_]{1,30}$/.test(label)) throw new Error(`test database label must be [a-z0-9_]{1,30}: ${label}`);32 return `${TEST_DB_PREFIX}${runId()}_${label}`;33}3435export interface TestDb { name: string; url: string; drop(): Promise<void> }36/** Create an empty database for this test file and return its URL. Call BEFORE importing anything from src/ (the API reads37 * DATABASE_URL when its modules load). `drop()` is optional — the global teardown drops it too — but frees it earlier. */38export async function createTestDb(label: string): Promise<TestDb> {39 const name = testDbName(label), admin = postgres(ADMIN_URL(), { max: 1, onnotice: () => {} });40 try {41 await admin.unsafe(`drop database if exists ${name} with (force)`);42 await admin.unsafe(`create database ${name}`);43 } finally { await admin.end(); }44 return { name, url: `postgres:///${name}`, drop: () => dropTestDbs((n) => n === name).then(() => {}) };45}4647/** Drop the test databases whose name is accepted by `match` (connections are terminated). Returns the names dropped. */48export async function dropTestDbs(match: (name: string) => boolean): Promise<string[]> {49 const admin = postgres(ADMIN_URL(), { max: 1, onnotice: () => {} }), dropped: string[] = [];50 try {51 const rows = await admin<{ datname: string }[]>`select datname from pg_database where datname like ${TEST_DB_PREFIX.replace(/_/g, '\\_') + '%'}`;52 for (const { datname } of rows) {53 if (!/^spw_t_[a-z0-9_]+$/.test(datname) || !match(datname)) continue;54 try { await admin.unsafe(`drop database if exists ${datname} with (force)`); dropped.push(datname); } catch { /* still being created or already gone */ }55 }56 } finally { await admin.end(); }57 return dropped;58}5960/** The environment every API test sets before importing src/: an own database and inert defaults (no broadcast, no workers,61 * a signer URL that answers nothing). `extra` overrides. Returns the database. */62export async function useTestDatabase(label: string, extra: Record<string, string> = {}): Promise<TestDb> {63 const db = await createTestDb(label);64 Object.assign(process.env, { SPW_ORIGIN: 'https://wallet.test', SPW_RP_ID: 'wallet.test', DATABASE_URL: db.url, SPW_SIGNER_SECRET: 'k'.repeat(48),65 SPW_SIGNER_URL: 'http://127.0.0.1:9', SPW_BROADCAST: 'off', SPW_WORKERS: 'off', ...extra });66 return db;67}68