/ src / cron / cleanup-test-dbs.js
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  }