Skip to main content
Glama
database-failures.test.js9.22 kB
/** * Negative tests for database failure handling * Tests graceful handling of SQLite errors, timeouts, and connection issues */ import { describe, it, expect, beforeEach, vi } from 'vitest' describe('Database Failure Handling', () => { describe('SQLite error handling', () => { // Simulate SQLite query function that can fail const createMockSqlite = (options = {}) => { const { shouldFail = false, errorType = 'BUSY', errorMessage = 'database is locked', failAfter = 0, results = [] } = options let callCount = 0 return (db, query, queryOptions = {}) => { callCount++ if (shouldFail && callCount > failAfter) { const error = new Error(errorMessage) error.code = errorType throw error } return results } } it('should handle database locked error (SQLITE_BUSY)', () => { const mockSqlite = createMockSqlite({ shouldFail: true, errorType: 'SQLITE_BUSY', errorMessage: 'database is locked' }) const executeQuery = () => { try { return { success: true, data: mockSqlite('test.db', 'SELECT * FROM messages') } } catch (e) { if (e.code === 'SQLITE_BUSY') { return { success: false, error: 'database_locked', retryable: true } } throw e } } const result = executeQuery() expect(result.success).toBe(false) expect(result.error).toBe('database_locked') expect(result.retryable).toBe(true) }) it('should handle permission denied error', () => { const mockSqlite = createMockSqlite({ shouldFail: true, errorType: 'SQLITE_CANTOPEN', errorMessage: 'unable to open database file' }) const executeQuery = () => { try { return { success: true, data: mockSqlite('test.db', 'SELECT * FROM messages') } } catch (e) { if (e.code === 'SQLITE_CANTOPEN') { return { success: false, error: 'permission_denied', retryable: false } } throw e } } const result = executeQuery() expect(result.success).toBe(false) expect(result.error).toBe('permission_denied') expect(result.retryable).toBe(false) }) it('should handle corrupted database error', () => { const mockSqlite = createMockSqlite({ shouldFail: true, errorType: 'SQLITE_CORRUPT', errorMessage: 'database disk image is malformed' }) const executeQuery = () => { try { return { success: true, data: mockSqlite('test.db', 'SELECT * FROM messages') } } catch (e) { if (e.code === 'SQLITE_CORRUPT') { return { success: false, error: 'database_corrupted', retryable: false } } throw e } } const result = executeQuery() expect(result.success).toBe(false) expect(result.error).toBe('database_corrupted') }) it('should handle empty result sets gracefully', () => { const mockSqlite = createMockSqlite({ results: [] }) const getMessages = () => { const results = mockSqlite('Messages.db', 'SELECT * FROM message') return results || [] } const messages = getMessages() expect(messages).toEqual([]) expect(messages).toHaveLength(0) }) }) describe('query timeout handling', () => { it('should respect timeout option', async () => { const createTimeoutQuery = (timeoutMs) => { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error('Query timeout')) }, timeoutMs) // Simulate slow query setTimeout(() => { clearTimeout(timer) resolve([{ id: 1 }]) }, timeoutMs + 100) // Always slower than timeout }) } const executeWithTimeout = async () => { try { await createTimeoutQuery(50) return { success: true } } catch (e) { if (e.message === 'Query timeout') { return { success: false, error: 'timeout' } } throw e } } const result = await executeWithTimeout() expect(result.success).toBe(false) expect(result.error).toBe('timeout') }) it('should complete before timeout when query is fast', async () => { const createFastQuery = (timeoutMs) => { return new Promise((resolve) => { setTimeout(() => { resolve([{ id: 1 }]) }, 10) // Much faster than timeout }) } const result = await createFastQuery(1000) expect(result).toEqual([{ id: 1 }]) }) }) describe('mid-indexing failure recovery', () => { it('should track progress for recovery after failure', async () => { const BATCH_SIZE = 32 let processedCount = 0 let lastSuccessfulBatch = -1 const items = Array.from({ length: 100 }, (_, i) => ({ id: i })) const processWithFailure = async (failAtBatch) => { processedCount = 0 lastSuccessfulBatch = -1 for (let i = 0; i < items.length; i += BATCH_SIZE) { const batchIndex = Math.floor(i / BATCH_SIZE) if (batchIndex === failAtBatch) { throw new Error('Simulated failure') } // Process batch processedCount += Math.min(BATCH_SIZE, items.length - i) lastSuccessfulBatch = batchIndex } return { processed: processedCount } } // Fail at batch 2 (after processing 64 items) try { await processWithFailure(2) } catch (e) { expect(e.message).toBe('Simulated failure') } expect(processedCount).toBe(64) // 2 batches of 32 expect(lastSuccessfulBatch).toBe(1) // Batches 0 and 1 completed }) it('should allow resuming from last successful batch', async () => { const BATCH_SIZE = 32 const items = Array.from({ length: 100 }, (_, i) => ({ id: i })) const processFromBatch = (startBatch) => { const startIndex = startBatch * BATCH_SIZE let processed = 0 for (let i = startIndex; i < items.length; i += BATCH_SIZE) { processed += Math.min(BATCH_SIZE, items.length - i) } return processed } // Resume from batch 2 const remaining = processFromBatch(2) expect(remaining).toBe(36) // 100 - 64 = 36 items remaining }) }) describe('connection recovery', () => { it('should retry connection on transient failure', async () => { let attempts = 0 const maxRetries = 3 const connectWithRetry = async () => { for (let i = 0; i < maxRetries; i++) { attempts++ try { if (attempts < 3) { throw new Error('Connection failed') } return { connected: true } } catch (e) { if (i === maxRetries - 1) throw e await new Promise(r => setTimeout(r, 10)) } } } const result = await connectWithRetry() expect(result.connected).toBe(true) expect(attempts).toBe(3) }) it('should fail after max retries', async () => { const maxRetries = 3 const connectWithRetry = async () => { for (let i = 0; i < maxRetries; i++) { try { throw new Error('Connection failed') } catch (e) { if (i === maxRetries - 1) throw e await new Promise(r => setTimeout(r, 10)) } } } await expect(connectWithRetry()).rejects.toThrow('Connection failed') }) }) describe('null and undefined data handling', () => { it('should handle null database path', () => { const validateDbPath = (path) => { if (!path) return { valid: false, error: 'path_required' } return { valid: true } } expect(validateDbPath(null)).toEqual({ valid: false, error: 'path_required' }) expect(validateDbPath(undefined)).toEqual({ valid: false, error: 'path_required' }) expect(validateDbPath('')).toEqual({ valid: false, error: 'path_required' }) }) it('should handle null query results', () => { const processResults = (results) => { if (results === null || results === undefined) { return [] } return results } expect(processResults(null)).toEqual([]) expect(processResults(undefined)).toEqual([]) expect(processResults([{ id: 1 }])).toEqual([{ id: 1 }]) }) it('should handle messages with null fields', () => { const processMessage = (msg) => { return { id: String(msg.id ?? 'unknown'), text: msg.text ?? '', sender: msg.sender ?? 'Unknown', date: msg.date ?? null } } const result = processMessage({ id: 1, text: null, sender: undefined }) expect(result.id).toBe('1') expect(result.text).toBe('') expect(result.sender).toBe('Unknown') expect(result.date).toBeNull() }) }) })

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sfls1397/Apple-Tools-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server