overnight-coverage-monitor.js
1 #!/usr/bin/env node 2 /** 3 * Overnight Coverage Monitor 4 * 5 * Monitors test coverage progress toward 85% target. 6 * Runs the agent system and adds more write_test tasks as needed. 7 * 8 * Usage: node scripts/overnight-coverage-monitor.js 9 */ 10 11 import { createDatabaseConnection } from '../src/utils/db.js'; 12 import { execSync } from 'child_process'; 13 import fs from 'fs/promises'; 14 import path from 'path'; 15 import { fileURLToPath } from 'url'; 16 17 const __dirname = path.dirname(fileURLToPath(import.meta.url)); 18 const PROJECT_ROOT = path.join(__dirname, '..'); 19 const LOG_FILE = '/tmp/overnight-coverage-monitor.log'; 20 const TARGET_COVERAGE = 85; 21 const CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes 22 23 async function appendLog(msg) { 24 const ts = new Date().toISOString(); 25 const line = `[${ts}] ${msg}\n`; 26 console.log(line.trim()); 27 await fs.appendFile(LOG_FILE, line).catch(() => {}); 28 } 29 30 function getDb() { 31 return createDatabaseConnection(path.join(PROJECT_ROOT, 'db/sites.db')); 32 } 33 34 async function runTests() { 35 await appendLog('Running full test suite...'); 36 const testLogFile = '/tmp/overnight-test-output.log'; 37 try { 38 // Redirect output to file to avoid ENOBUFS (4000+ tests produce >1MB output) 39 execSync(`npm test > ${testLogFile} 2>&1`, { 40 cwd: PROJECT_ROOT, 41 shell: true, 42 timeout: 600000, // 10 min timeout 43 env: { 44 ...process.env, 45 PATH: `/usr/bin:${process.env.PATH}`, 46 }, 47 }); 48 await appendLog('Tests completed successfully'); 49 return true; 50 } catch (err) { 51 // Tests may fail but coverage is still generated 52 await appendLog( 53 `Tests completed with errors (coverage still captured): ${err.message.slice(0, 100)}` 54 ); 55 return false; 56 } 57 } 58 59 async function getCoverage() { 60 try { 61 const summaryPath = path.join(PROJECT_ROOT, 'coverage/coverage-summary.json'); 62 const data = JSON.parse(await fs.readFile(summaryPath, 'utf8')); 63 return data; 64 } catch (err) { 65 await appendLog(`Could not read coverage: ${err.message}`); 66 return null; 67 } 68 } 69 70 function getFilesBelow(coverageData, threshold) { 71 return Object.entries(coverageData) 72 .filter(([key]) => key !== 'total') 73 .map(([file, cov]) => ({ 74 file: file.replace(`${PROJECT_ROOT}/`, '').replace(PROJECT_ROOT, ''), 75 pct: cov.statements.pct, 76 total: cov.statements.total, 77 })) 78 .filter(f => f.pct < threshold && f.total > 5 && f.file.startsWith('src/')); 79 } 80 81 function getPendingTaskCount() { 82 const db = getDb(); 83 const result = db 84 .prepare( 85 "SELECT COUNT(*) as cnt FROM tel.agent_tasks WHERE task_type='write_test' AND status IN ('pending','running')" 86 ) 87 .get(); 88 db.close(); 89 return result.cnt; 90 } 91 92 function createWriteTestTask(file, currentCoverage, priority, instructions) { 93 const db = getDb(); 94 const context = JSON.stringify({ 95 source: 'overnight-monitor', 96 target_coverage: TARGET_COVERAGE, 97 current_coverage: currentCoverage, 98 files_to_test: [file], 99 file, 100 test_instructions: instructions, 101 campaign: 'overnight-85pct-target', 102 created_at: new Date().toISOString(), 103 }); 104 105 db.prepare( 106 "INSERT INTO tel.agent_tasks (task_type, assigned_to, priority, status, context_json, created_at) VALUES ('write_test', 'qa', ?, 'pending', ?, datetime('now'))" 107 ).run(priority, context); 108 db.close(); 109 } 110 111 async function runAgentCycle() { 112 await appendLog('Running agent cycle...'); 113 try { 114 const node22 = '/usr/bin/node'; 115 execSync(`${node22} src/agents/runner.js --tasks=5`, { 116 cwd: PROJECT_ROOT, 117 encoding: 'utf8', 118 timeout: 300000, // 5 min timeout 119 stdio: 'pipe', 120 }); 121 await appendLog('Agent cycle completed'); 122 } catch (err) { 123 await appendLog(`Agent cycle error: ${err.message.slice(0, 150)}`); 124 } 125 } 126 127 function getAgentTaskStats() { 128 const db = getDb(); 129 const stats = db 130 .prepare( 131 "SELECT status, COUNT(*) as cnt FROM tel.agent_tasks WHERE task_type='write_test' AND created_at > datetime('now', '-24 hours') GROUP BY status" 132 ) 133 .all(); 134 db.close(); 135 return stats; 136 } 137 138 async function monitorLoop() { 139 let iteration = 0; 140 141 await appendLog('=== OVERNIGHT COVERAGE MONITOR STARTED ==='); 142 await appendLog(`Target: ${TARGET_COVERAGE}% statement coverage`); 143 await appendLog(`Check interval: ${CHECK_INTERVAL_MS / 60000} minutes`); 144 145 while (true) { 146 iteration++; 147 await appendLog(`\n--- Iteration ${iteration} ---`); 148 149 // Run tests to get fresh coverage 150 await runTests(); 151 152 // Get coverage data 153 const coverageData = await getCoverage(); 154 if (!coverageData) { 155 await appendLog('No coverage data available, waiting...'); 156 await new Promise(r => setTimeout(r, CHECK_INTERVAL_MS)); 157 continue; 158 } 159 160 const { total } = coverageData; 161 162 // Guard: if c8 failed (EACCES or other error), total statements will be 0 163 if (total.statements.total === 0) { 164 await appendLog( 165 'WARNING: Coverage data has 0 statements - c8 may have failed (permission error?). Skipping this iteration.' 166 ); 167 await appendLog('Check /tmp/overnight-test-output.log for errors.'); 168 await new Promise(r => setTimeout(r, CHECK_INTERVAL_MS)); 169 continue; 170 } 171 172 const stmtsPct = typeof total.statements.pct === 'number' ? total.statements.pct : 0; 173 const funcsPct = typeof total.functions.pct === 'number' ? total.functions.pct : 0; 174 const branchPct = typeof total.branches.pct === 'number' ? total.branches.pct : 0; 175 176 await appendLog(`Coverage: ${stmtsPct}% stmts | ${funcsPct}% funcs | ${branchPct}% branches`); 177 178 if (stmtsPct >= TARGET_COVERAGE) { 179 await appendLog(`\nš TARGET REACHED! Coverage is ${stmtsPct}% >= ${TARGET_COVERAGE}%`); 180 await appendLog('Overnight mission complete!'); 181 break; 182 } 183 184 const gap = (TARGET_COVERAGE - stmtsPct).toFixed(1); 185 await appendLog(`Gap to target: ${gap}% remaining`); 186 187 // Find files still below threshold 188 const lowFiles = getFilesBelow(coverageData, TARGET_COVERAGE); 189 await appendLog(`Files below ${TARGET_COVERAGE}%: ${lowFiles.length}`); 190 191 // Check pending tasks 192 const pendingCount = getPendingTaskCount(); 193 await appendLog(`Pending/running write_test tasks: ${pendingCount}`); 194 195 // Agent task stats 196 const taskStats = getAgentTaskStats(); 197 await appendLog(`Task stats (24h): ${JSON.stringify(taskStats)}`); 198 199 // If fewer than 5 pending tasks, create more for low-coverage files 200 if (pendingCount < 5 && lowFiles.length > 0) { 201 await appendLog(`Creating new write_test tasks for ${Math.min(lowFiles.length, 8)} files...`); 202 203 // Sort by impact (most uncovered statements first) 204 const sorted = lowFiles.sort((a, b) => { 205 const aUncovered = a.total * (1 - a.pct / 100); 206 const bUncovered = b.total * (1 - b.pct / 100); 207 return bUncovered - aUncovered; 208 }); 209 210 const toCreate = sorted.slice(0, 8); 211 for (const f of toCreate) { 212 const priority = Math.min(10, Math.max(1, Math.round(10 - f.pct / 10))); 213 const instructions = `Coverage improvement pass ${iteration}. Current: ${f.pct}%, target: ${TARGET_COVERAGE}%. File has ${f.total} statements with ${Math.round(f.total * (1 - f.pct / 100))} uncovered. Focus on the most impactful uncovered code paths. Use mock.module() for external dependencies.`; 214 215 createWriteTestTask(f.file, f.pct, priority, instructions); 216 await appendLog(` Created task for ${f.file} (${f.pct}%)`); 217 } 218 } 219 220 // Run agent cycle to process pending tasks 221 await runAgentCycle(); 222 223 await appendLog(`Waiting ${CHECK_INTERVAL_MS / 60000} minutes before next check...`); 224 await new Promise(r => setTimeout(r, CHECK_INTERVAL_MS)); 225 } 226 } 227 228 // Start monitoring 229 monitorLoop().catch(async err => { 230 await appendLog(`Monitor crashed: ${err.message}\n${err.stack}`); 231 process.exit(1); 232 });