import { Database } from 'bun:sqlite'; import { afterEach, describe, expect, test } from 'bun:test'; import { rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { resetTestDbPath } from '../test-utils/db-path'; import { closeDb, createDbClient, getDb } from './client'; // #798: a command that opened the db while a deploy still held it died on // SQLITE_BUSY immediately, because the connection carried no busy timeout. describe('createDbClient concurrency', () => { const paths: string[] = []; const freshPath = () => { const path = join(tmpdir(), `celilo-client-test-${Bun.nanoseconds()}.db`); paths.push(path); return path; }; afterEach(() => { for (const path of paths.splice(0)) { for (const suffix of ['', '-wal', '-shm']) { rmSync(`${path}${suffix}`, { force: true }); } } }); test('sets a non-zero busy timeout so a contended open waits instead of throwing', () => { const { $client } = createDbClient({ path: freshPath() }); const timeout = $client.query<{ timeout: number }, []>('PRAGMA busy_timeout').get()?.timeout; // Red before #798: bun:sqlite defaults to 0, i.e. fail on first contention. expect(timeout).toBeGreaterThan(0); }); test('opens in WAL mode', () => { const { $client } = createDbClient({ path: freshPath() }); const mode = $client.query<{ journal_mode: string }, []>('PRAGMA journal_mode').get(); expect(mode?.journal_mode.toLowerCase()).toBe('wal'); }); test('a second open succeeds while another connection holds a write transaction', () => { const path = freshPath(); createDbClient({ path }); // Hold the write lock the way a deploy in progress does. const writer = new Database(path); writer.run(`PRAGMA busy_timeout = ${0}`); writer.run('BEGIN IMMEDIATE'); try { // Reading is what `module show-config` does; it must not throw. const { $client } = createDbClient({ path, readonly: true }); const mode = $client.query<{ journal_mode: string }, []>('PRAGMA journal_mode').get(); expect(mode?.journal_mode.toLowerCase()).toBe('wal'); } finally { writer.run('ROLLBACK'); writer.close(); } }); }); /** * The tripwire from celilo#1343: under bun test, a default-path open must be * refused instead of landing on the operator's real database. The scratch-DB * preload (apps/celilo/test-preload.ts) arms CELILO_DB_PATH only when bun * runs from apps/celilo/, so every one of these tests strips the redirects * itself to reproduce the out-of-scope state. */ describe('createDbClient operator-database tripwire (celilo#1343)', () => { afterEach(() => { closeDb(); resetTestDbPath(); delete process.env.CELILO_DATA_DIR; delete process.env.ENVIRONMENT; }); test('default-path open under bun test throws, naming the fix', () => { delete process.env.CELILO_DB_PATH; delete process.env.CELILO_DATA_DIR; delete process.env.ENVIRONMENT; expect(() => getDb()).toThrow(/CELILO_DB_PATH/); }); test('CELILO_DATA_DIR redirect keeps the default-path open allowed', () => { delete process.env.CELILO_DB_PATH; delete process.env.ENVIRONMENT; process.env.CELILO_DATA_DIR = join(tmpdir(), 'celilo-tripwire-redirect'); const db = createDbClient(); db.$client.close(); }); test('explicit config path bypasses the tripwire', () => { delete process.env.CELILO_DB_PATH; delete process.env.CELILO_DATA_DIR; delete process.env.ENVIRONMENT; const db = createDbClient({ path: join(tmpdir(), 'celilo-tripwire-explicit', 'celilo.db'), }); db.$client.close(); }); });