import fs from "node:fs"; import net from "node:net"; import os from "node:os"; import path from "node:path"; import { applyPendingMigrations, ensurePostgresDatabase } from "./client.js"; import { prepareEmbeddedPostgresNativeRuntime } from "./embedded-postgres-native.js"; type EmbeddedPostgresInstance = { initialise(): Promise; start(): Promise; stop(): Promise; }; type EmbeddedPostgresCtor = new (opts: { databaseDir: string; user: string; password: string; port: number; persistent: boolean; initdbFlags?: string[]; onLog?: (message: unknown) => void; onError?: (message: unknown) => void; }) => EmbeddedPostgresInstance; export type EmbeddedPostgresTestSupport = { supported: boolean; reason?: string; }; export type EmbeddedPostgresTestDatabase = { connectionString: string; cleanup(): Promise; }; let embeddedPostgresSupportPromise: Promise | null = null; const DEFAULT_PAPERCLIP_EMBEDDED_POSTGRES_PORT = 54329; function getReservedTestPorts(): Set { const configuredPorts = [ DEFAULT_PAPERCLIP_EMBEDDED_POSTGRES_PORT, Number.parseInt(process.env.PAPERCLIP_EMBEDDED_POSTGRES_PORT ?? "", 10), ...String(process.env.PAPERCLIP_TEST_POSTGRES_RESERVED_PORTS ?? "") .split(",") .map((value) => Number.parseInt(value.trim(), 10)), ]; return new Set(configuredPorts.filter((port) => Number.isInteger(port) && port > 0 && port <= 65535)); } async function getEmbeddedPostgresCtor(): Promise { const mod = await import("embedded-postgres"); await prepareEmbeddedPostgresNativeRuntime(); return mod.default as EmbeddedPostgresCtor; } async function getAvailablePort(): Promise { const reservedPorts = getReservedTestPorts(); for (let attempt = 0; attempt < 20; attempt += 1) { const port = await new Promise((resolve, reject) => { const server = net.createServer(); server.unref(); server.on("error", reject); server.listen(0, "127.0.0.1", () => { const address = server.address(); if (!address || typeof address === "string") { server.close(() => reject(new Error("Failed to allocate test port"))); return; } const { port } = address; server.close((error) => { if (error) reject(error); else resolve(port); }); }); }); if (!reservedPorts.has(port)) return port; } throw new Error( `Failed to allocate embedded Postgres test port outside reserved Paperclip ports: ${[ ...reservedPorts, ].join(", ")}`, ); } async function createEmbeddedPostgresTestInstance(tempDirPrefix: string) { const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix)); const port = await getAvailablePort(); const EmbeddedPostgres = await getEmbeddedPostgresCtor(); const instance = new EmbeddedPostgres({ databaseDir: dataDir, user: "paperclip", password: "paperclip", port, persistent: true, initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"], onLog: () => {}, onError: () => {}, }); return { dataDir, port, instance }; } function cleanupEmbeddedPostgresTestDirs(dataDir: string) { fs.rmSync(dataDir, { recursive: true, force: true }); } // Upper bound (ms) on how long we wait for the embedded Postgres cluster to // stop gracefully before abandoning the wait and returning from the hook. const EMBEDDED_POSTGRES_STOP_TIMEOUT_MS = 5000; // `embedded-postgres@18.1.0-beta.16` exposes only `stop(): Promise` — no // shutdown-mode argument. Internally it SIGINTs the postgres process (already // PostgreSQL "fast shutdown") and resolves *only* on the child's `exit` event, // with no time bound of its own. Under the loaded serial server shard a slow // shutdown checkpoint can push that past vitest's hookTimeout and hang the // afterAll hook. So we bound the graceful stop: if it overruns, we stop waiting // and return so the hook completes. The SIGINT has already been delivered, so // the abandoned process still exits on its own (and again when the runner exits). // Errors are swallowed, matching prior behavior. // // `cleanupFn` (data-dir reclaim) is chained on the raw `stop()` promise, not on // the timeout race, so the disposable data dir is removed *only after* `stop()` // actually settles — i.e. once the child Postgres process has exited. Removing // it on the timeout path would pull the data files out from under a still-running // cluster and provoke checkpoint / WAL I/O errors. In the fast path `cleanupFn` // has run by the time this resolves; in the timeout path it runs asynchronously // once the abandoned process finally exits. async function stopEmbeddedPostgresBounded( instance: EmbeddedPostgresInstance | null, cleanupFn?: () => void, ): Promise { if (!instance) { cleanupFn?.(); return; } let timer: ReturnType | undefined; const stopped = instance .stop() .catch(() => { // Swallow shutdown errors — the data dir is reclaimed regardless. }) .finally(() => { try { cleanupFn?.(); } catch { // Best-effort reclaim; ignore removal errors. } }); try { await Promise.race([ stopped, new Promise((resolve) => { timer = setTimeout(resolve, EMBEDDED_POSTGRES_STOP_TIMEOUT_MS); timer.unref?.(); }), ]); } finally { if (timer) clearTimeout(timer); } } function formatEmbeddedPostgresError(error: unknown): string { if (error instanceof Error && error.message.length > 0) return error.message; if (typeof error === "string" && error.length > 0) return error; return "embedded Postgres startup failed"; } async function probeEmbeddedPostgresSupport(): Promise { let dataDir: string | null = null; let instance: EmbeddedPostgresInstance | null = null; try { const created = await createEmbeddedPostgresTestInstance( "paperclip-embedded-postgres-probe-", ); dataDir = created.dataDir; instance = created.instance; await instance.initialise(); await instance.start(); return { supported: true }; } catch (error) { return { supported: false, reason: formatEmbeddedPostgresError(error), }; } finally { await stopEmbeddedPostgresBounded(instance, () => { if (dataDir) cleanupEmbeddedPostgresTestDirs(dataDir); }); } } export async function getEmbeddedPostgresTestSupport(): Promise { if (!embeddedPostgresSupportPromise) { embeddedPostgresSupportPromise = probeEmbeddedPostgresSupport(); } return await embeddedPostgresSupportPromise; } export async function startEmbeddedPostgresTestDatabase( tempDirPrefix: string, ): Promise { let dataDir: string | null = null; let instance: EmbeddedPostgresInstance | null = null; try { const created = await createEmbeddedPostgresTestInstance(tempDirPrefix); dataDir = created.dataDir; instance = created.instance; const { port } = created; await instance.initialise(); await instance.start(); const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`; await ensurePostgresDatabase(adminConnectionString, "paperclip"); const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`; await applyPendingMigrations(connectionString); return { connectionString, cleanup: async () => { await stopEmbeddedPostgresBounded(instance, () => { if (dataDir) cleanupEmbeddedPostgresTestDirs(dataDir); }); }, }; } catch (error) { await stopEmbeddedPostgresBounded(instance, () => { if (dataDir) cleanupEmbeddedPostgresTestDirs(dataDir); }); throw new Error( `Failed to start embedded PostgreSQL test database: ${formatEmbeddedPostgresError(error)}`, ); } }