/ scripts / overnight-coverage-monitor.js
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  });