0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
CI / Shell Format Check (push) Waiting to run
CI / Check Ruby (3.4) (push) Waiting to run
CI / CI Config (push) Waiting to run
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Blocked by required conditions
CI / Build on Node ${{ matrix.node }} (push) Blocked by required conditions
CI / Style Check (push) Waiting to run
CI / Generate Assets (push) Waiting to run
CI / Check Python (3.14) (push) Waiting to run
CI / Check Python (3.9) (push) Waiting to run
CI / Build Docs (push) Waiting to run
CI / Code Scan Action (push) Waiting to run
CI / Site tests (push) Waiting to run
CI / webui tests (push) Waiting to run
CI / Run Integration Tests (push) Waiting to run
CI / Run Smoke Tests (push) Waiting to run
CI / Go Tests (push) Waiting to run
CI / Share Test (push) Waiting to run
CI / Redteam (Production API) (push) Waiting to run
CI / Redteam (Staging API) (push) Waiting to run
CI / GitHub Actions Lint (push) Waiting to run
CI / Check Ruby (3.0) (push) Waiting to run
release-please / release-please (push) Waiting to run
release-please / build (push) Blocked by required conditions
release-please / publish-npm (push) Blocked by required conditions
release-please / publish-npm-backfill (push) Waiting to run
release-please / docker (push) Blocked by required conditions
release-please / publish-code-scan-action (push) Blocked by required conditions
release-please / attest-code-scan-action (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
195 lines
6.4 KiB
TypeScript
195 lines
6.4 KiB
TypeScript
import { pathToFileURL } from 'node:url';
|
|
import fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
import { createClient } from '@libsql/client/node';
|
|
import { sql } from 'drizzle-orm';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { mockProcessEnv } from './util/utils';
|
|
|
|
const ORIGINAL_ENV = { ...process.env };
|
|
|
|
describe('database WAL mode', () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
mockProcessEnv({ ...ORIGINAL_ENV }, { clear: true });
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptfoo-dbtest-'));
|
|
mockProcessEnv({ PROMPTFOO_CONFIG_DIR: tempDir });
|
|
mockProcessEnv({ IS_TESTING: undefined });
|
|
mockProcessEnv({ PROMPTFOO_DISABLE_WAL_MODE: undefined });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
mockProcessEnv(ORIGINAL_ENV, { clear: true });
|
|
// Close the database connection if it exists
|
|
try {
|
|
const database = await import('../src/database');
|
|
await database.closeDb();
|
|
} catch (err) {
|
|
console.error('Error closing database:', err);
|
|
}
|
|
|
|
// Add a small delay to ensure connections are fully closed on Windows
|
|
if (process.platform === 'win32') {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
|
|
try {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
} catch (err) {
|
|
console.warn(`Could not remove temp directory ${tempDir}:`, err);
|
|
// On Windows, sometimes we need multiple attempts
|
|
if (process.platform === 'win32') {
|
|
try {
|
|
// Try a second time after a short delay
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
} catch {
|
|
console.error(`Failed to remove temp directory after retry: ${tempDir}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('enables WAL journal mode by default', async () => {
|
|
// First import and initialize the database to trigger WAL mode configuration
|
|
const database = await import('../src/database');
|
|
const db = await database.getDb();
|
|
const dbPath = database.getDbPath();
|
|
|
|
await db.run('CREATE TABLE wal_probe (id INTEGER PRIMARY KEY, value TEXT)');
|
|
await db.run("INSERT INTO wal_probe (value) VALUES ('uses wal')");
|
|
|
|
expect(fs.existsSync(`${dbPath}-wal`)).toBe(true);
|
|
|
|
// Close it to ensure we don't get resource conflicts
|
|
await database.closeDb();
|
|
|
|
// Then independently verify the journal mode using a direct connection
|
|
const directDb = createClient({ url: pathToFileURL(dbPath).href });
|
|
|
|
try {
|
|
const result = await directDb.execute('PRAGMA journal_mode;');
|
|
const journalMode = String(result.rows[0]?.journal_mode ?? '');
|
|
expect(journalMode.toLowerCase()).toBe('wal');
|
|
} finally {
|
|
// Make sure to close this connection too
|
|
directDb.close();
|
|
}
|
|
});
|
|
|
|
it('opens database paths with URL-reserved characters', async () => {
|
|
const configDir = path.join(tempDir, 'nested#config');
|
|
fs.mkdirSync(configDir);
|
|
mockProcessEnv({ PROMPTFOO_CONFIG_DIR: configDir });
|
|
|
|
const database = await import('../src/database');
|
|
await database.getDb();
|
|
|
|
expect(fs.existsSync(database.getDbPath())).toBe(true);
|
|
});
|
|
|
|
it('skips WAL mode when PROMPTFOO_DISABLE_WAL_MODE is set', async () => {
|
|
mockProcessEnv({ PROMPTFOO_DISABLE_WAL_MODE: 'true' });
|
|
|
|
const database = await import('../src/database');
|
|
await database.getDb();
|
|
await database.closeDb();
|
|
|
|
const dbPath = database.getDbPath();
|
|
const directDb = createClient({ url: pathToFileURL(dbPath).href });
|
|
|
|
try {
|
|
const result = await directDb.execute('PRAGMA journal_mode;');
|
|
const journalMode = String(result.rows[0]?.journal_mode ?? '');
|
|
// Should be in default mode (delete) when WAL is disabled
|
|
expect(journalMode.toLowerCase()).toBe('delete');
|
|
} finally {
|
|
directDb.close();
|
|
}
|
|
});
|
|
|
|
it('does not enable WAL mode while testing', async () => {
|
|
mockProcessEnv({ IS_TESTING: 'true' });
|
|
|
|
const database = await import('../src/database');
|
|
const db = await database.getDb();
|
|
|
|
// Unit tests use a shared in-memory DB and skip WAL setup.
|
|
expect(db).toBeDefined();
|
|
});
|
|
|
|
describe('closeDbIfOpen', () => {
|
|
it('should close database when it is open', async () => {
|
|
const database = await import('../src/database');
|
|
|
|
// Open the database
|
|
await database.getDb();
|
|
expect(database.isDbOpen()).toBe(true);
|
|
|
|
// Close it using closeDbIfOpen
|
|
await database.closeDbIfOpen();
|
|
expect(database.isDbOpen()).toBe(false);
|
|
});
|
|
|
|
it('should do nothing when database is not open', async () => {
|
|
const database = await import('../src/database');
|
|
|
|
// Ensure database is not open
|
|
expect(database.isDbOpen()).toBe(false);
|
|
|
|
// closeDbIfOpen should not throw
|
|
await expect(database.closeDbIfOpen()).resolves.toBeUndefined();
|
|
expect(database.isDbOpen()).toBe(false);
|
|
});
|
|
|
|
it('should be safe to call multiple times', async () => {
|
|
const database = await import('../src/database');
|
|
|
|
// Open the database
|
|
await database.getDb();
|
|
expect(database.isDbOpen()).toBe(true);
|
|
|
|
// Close multiple times - should not throw
|
|
await database.closeDbIfOpen();
|
|
expect(database.isDbOpen()).toBe(false);
|
|
|
|
await database.closeDbIfOpen();
|
|
expect(database.isDbOpen()).toBe(false);
|
|
|
|
await database.closeDbIfOpen();
|
|
expect(database.isDbOpen()).toBe(false);
|
|
});
|
|
});
|
|
|
|
it('verifies WAL checkpoint settings', async () => {
|
|
const database = await import('../src/database');
|
|
const db = await database.getDb();
|
|
|
|
const autocheckpoint = (await db.get(sql`PRAGMA wal_autocheckpoint;`)) as unknown as {
|
|
wal_autocheckpoint: number;
|
|
};
|
|
expect(autocheckpoint.wal_autocheckpoint).toBe(1000);
|
|
|
|
const synchronous = (await db.get(sql`PRAGMA synchronous;`)) as unknown as {
|
|
synchronous: number;
|
|
};
|
|
// NORMAL = 1 in SQLite
|
|
expect(synchronous.synchronous).toBe(1);
|
|
});
|
|
|
|
it('applies a busy timeout so contended writes wait instead of failing immediately', async () => {
|
|
const database = await import('../src/database');
|
|
const db = await database.getDb();
|
|
|
|
const busyTimeout = (await db.get(sql`PRAGMA busy_timeout;`)) as unknown as {
|
|
timeout: number;
|
|
};
|
|
// Keep brief lock contention from failing immediately with SQLITE_BUSY.
|
|
expect(busyTimeout.timeout).toBe(5000);
|
|
});
|
|
});
|