cleanup-test-dbs.js
1 /** 2 * Cleanup leftover test database files 3 * 4 * Test DBs created by unit tests are supposed to be deleted in after() hooks, 5 * but when tests crash or are killed, the hooks don't run and files accumulate. 6 * 7 * This job deletes test DB files older than 1 hour from: 8 * - tests/ (legacy location: test-monitor-*, test-qa-*, etc.) 9 * - tests/agents/ (test-runner-coverage.db etc.) 10 * - /tmp/ (current location: test-*.db) 11 * 12 * Registered in cron_jobs via migration 064 (runs daily). 13 * Can also run directly: node src/cron/cleanup-test-dbs.js 14 */ 15 16 import { readdirSync, statSync, unlinkSync, existsSync } from 'fs'; 17 import { join, dirname } from 'path'; 18 import { fileURLToPath } from 'url'; 19 import Logger from '../utils/logger.js'; 20 21 const __filename = fileURLToPath(import.meta.url); 22 const __dirname = dirname(__filename); 23 const projectRoot = join(__dirname, '..', '..'); 24 25 const logger = new Logger('cleanup-test-dbs'); 26 27 // Patterns matching test DB filenames 28 const TEST_DB_PATTERNS = [ 29 /^test-monitor-/, 30 /^test-qa-/, 31 /^test-runner-/, 32 /^test-architect-/, 33 /^test-developer-/, 34 /^test-triage-/, 35 /^test-security-/, 36 /^test-sites-/, 37 /^test-[a-z].*-\d{13}/, // timestamp-suffixed test DBs 38 ]; 39 40 const MAX_AGE_MS = 60 * 60 * 1000; // 1 hour — safe to delete anything older 41 42 function isTestDb(filename) { 43 if (!filename.endsWith('.db')) return false; 44 return TEST_DB_PATTERNS.some(p => p.test(filename)); 45 } 46 47 function scanDir(dir) { 48 if (!existsSync(dir)) return { deleted: 0, freed: 0 }; 49 50 let deleted = 0; 51 let freed = 0; 52 const now = Date.now(); 53 54 let entries; 55 try { 56 entries = readdirSync(dir); 57 } catch { 58 return { deleted: 0, freed: 0 }; 59 } 60 61 for (const name of entries) { 62 if (!isTestDb(name)) continue; 63 64 const fullPath = join(dir, name); 65 let stat; 66 try { 67 stat = statSync(fullPath); 68 } catch { 69 continue; 70 } 71 72 const ageMs = now - stat.mtimeMs; 73 if (ageMs < MAX_AGE_MS) continue; // might still be in use 74 75 // Delete main file + WAL/SHM sidecar files 76 for (const ext of ['', '-wal', '-shm']) { 77 const p = fullPath + ext; 78 try { 79 const s = statSync(p); 80 unlinkSync(p); 81 freed += s.size; 82 } catch { 83 // already gone or permission error — skip 84 } 85 } 86 87 deleted++; 88 logger.debug(`Deleted ${fullPath} (age: ${Math.round(ageMs / 60000)}min)`); 89 } 90 91 return { deleted, freed }; 92 } 93 94 export function runCleanupTestDbs() { 95 const scanDirs = [ 96 join(projectRoot, 'tests'), 97 join(projectRoot, 'tests', 'agents'), 98 '/tmp', 99 '/dev/shm', 100 ]; 101 102 let totalDeleted = 0; 103 let totalFreedBytes = 0; 104 105 for (const dir of scanDirs) { 106 const { deleted, freed } = scanDir(dir); 107 totalDeleted += deleted; 108 totalFreedBytes += freed; 109 } 110 111 const result = { 112 deleted: totalDeleted, 113 freed_kb: Math.round(totalFreedBytes / 1024), 114 }; 115 116 if (totalDeleted > 0) { 117 logger.info('Test DB cleanup complete', result); 118 } else { 119 logger.info('No stale test DBs found'); 120 } 121 122 return result; 123 } 124 125 // Standalone execution 126 const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; 127 if (isMain) { 128 try { 129 const result = runCleanupTestDbs(); 130 console.log(`Deleted ${result.deleted} test DB(s), freed ${result.freed_kb} KB`); 131 process.exit(0); 132 } catch (err) { 133 console.error('Cleanup failed:', err); 134 process.exit(1); 135 } 136 }