/ __quarantined_tests__ / agents / monitor-coverage4.test.js
monitor-coverage4.test.js
  1  /**
  2   * Monitor Agent Coverage Boost - Part 4
  3   *
  4   * Targets uncovered lines after monitor-coverage3.test.js:
  5   * - Lines 29-30:      resetDb close-failure catch (DB.close() throws)
  6   * - Lines 86-88:      saveFilePositions catch (DB prepare throws)
  7   * - Lines 122-123:    processTask stale running tasks cleanup catch
  8   * - Lines 169-177:    processTask — check_rate_limits, fix_bug, bootstrap_monitor dispatch
  9   * - Lines 188-199:    processTask catch block when handler throws
 10   * - Lines 1662-1699:  checkBlockedTasks — pattern detection loop body (>3 tasks same error prefix)
 11   */
 12  
 13  process.env.DATABASE_PATH = '/tmp/test-monitor-cov4.db';
 14  process.env.AGENT_IMMEDIATE_INVOCATION = 'false';
 15  process.env.LOGS_DIR = '/tmp/test-logs-monitor-cov4/';
 16  
 17  import { test, describe, before, after, beforeEach } from 'node:test';
 18  import assert from 'node:assert/strict';
 19  import Database from 'better-sqlite3';
 20  import { unlinkSync, mkdirSync, rmSync } from 'fs';
 21  import { join } from 'path';
 22  
 23  const TEST_DB_PATH = '/tmp/test-monitor-cov4.db';
 24  const TEST_LOG_DIR = '/tmp/test-logs-monitor-cov4';
 25  
 26  // Clean up leftover DB files
 27  for (const ext of ['', '-wal', '-shm']) {
 28    try {
 29      unlinkSync(TEST_DB_PATH + ext);
 30    } catch {
 31      /* ignore */
 32    }
 33  }
 34  
 35  const DB_SCHEMA = `
 36    CREATE TABLE IF NOT EXISTS agent_tasks (
 37      id INTEGER PRIMARY KEY AUTOINCREMENT,
 38      task_type TEXT NOT NULL,
 39      assigned_to TEXT NOT NULL,
 40      created_by TEXT,
 41      status TEXT DEFAULT 'pending',
 42      priority INTEGER DEFAULT 5,
 43      context_json TEXT,
 44      result_json TEXT,
 45      parent_task_id INTEGER,
 46      error_message TEXT,
 47      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 48      started_at DATETIME,
 49      completed_at DATETIME,
 50      retry_count INTEGER DEFAULT 0
 51    );
 52    CREATE TABLE IF NOT EXISTS agent_logs (
 53      id INTEGER PRIMARY KEY AUTOINCREMENT,
 54      task_id INTEGER,
 55      agent_name TEXT NOT NULL,
 56      log_level TEXT,
 57      message TEXT NOT NULL,
 58      data_json TEXT,
 59      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 60    );
 61    CREATE TABLE IF NOT EXISTS agent_state (
 62      agent_name TEXT PRIMARY KEY,
 63      last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
 64      current_task_id INTEGER,
 65      status TEXT DEFAULT 'idle',
 66      metrics_json TEXT
 67    );
 68    CREATE TABLE IF NOT EXISTS agent_messages (
 69      id INTEGER PRIMARY KEY AUTOINCREMENT,
 70      task_id INTEGER,
 71      from_agent TEXT NOT NULL,
 72      to_agent TEXT NOT NULL,
 73      message_type TEXT,
 74      content TEXT NOT NULL,
 75      metadata_json TEXT,
 76      context_json TEXT,
 77      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 78      read_at DATETIME
 79    );
 80    CREATE TABLE IF NOT EXISTS human_review_queue (
 81      id INTEGER PRIMARY KEY AUTOINCREMENT,
 82      file TEXT NOT NULL,
 83      reason TEXT NOT NULL,
 84      type TEXT NOT NULL,
 85      priority TEXT NOT NULL,
 86      metadata TEXT,
 87      status TEXT DEFAULT 'pending',
 88      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 89    );
 90    CREATE TABLE IF NOT EXISTS settings (
 91      key TEXT PRIMARY KEY,
 92      value TEXT NOT NULL,
 93      description TEXT,
 94      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
 95    );
 96    CREATE TABLE IF NOT EXISTS sites (
 97      id INTEGER PRIMARY KEY AUTOINCREMENT,
 98      domain TEXT,
 99      landing_page_url TEXT,
100      status TEXT DEFAULT 'found',
101      error_message TEXT,
102      score REAL,
103      grade TEXT,
104      recapture_count INTEGER DEFAULT 0,
105      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
106      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
107    );
108    CREATE TABLE IF NOT EXISTS pipeline_metrics (
109      id INTEGER PRIMARY KEY AUTOINCREMENT,
110      stage_name TEXT NOT NULL,
111      sites_processed INTEGER DEFAULT 0,
112      sites_succeeded INTEGER DEFAULT 0,
113      sites_failed INTEGER DEFAULT 0,
114      duration_ms INTEGER NOT NULL,
115      started_at DATETIME NOT NULL,
116      finished_at DATETIME NOT NULL,
117      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
118    );
119    CREATE TABLE IF NOT EXISTS agent_outcomes (
120      id INTEGER PRIMARY KEY AUTOINCREMENT,
121      task_id INTEGER,
122      agent_name TEXT NOT NULL,
123      outcome TEXT NOT NULL,
124      context_json TEXT,
125      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
126    );
127    CREATE TABLE IF NOT EXISTS structured_logs (
128      id INTEGER PRIMARY KEY AUTOINCREMENT,
129      agent_name TEXT,
130      task_id INTEGER,
131      level TEXT,
132      message TEXT,
133      data_json TEXT,
134      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
135    );
136    CREATE TABLE IF NOT EXISTS site_status (
137      id INTEGER PRIMARY KEY AUTOINCREMENT,
138      site_id INTEGER,
139      status TEXT,
140      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
141    );
142    CREATE TABLE IF NOT EXISTS cron_locks (
143      lock_key TEXT PRIMARY KEY,
144      acquired_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
145      updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
146      description TEXT
147    );
148    INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('monitor', 'idle');
149    INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('triage', 'idle');
150    INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('developer', 'idle');
151    INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('qa', 'idle');
152    INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('security', 'idle');
153    INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('architect', 'idle');
154  `;
155  
156  const sharedDb = new Database(TEST_DB_PATH);
157  sharedDb.pragma('journal_mode = WAL');
158  sharedDb.pragma('busy_timeout = 10000');
159  sharedDb.exec(DB_SCHEMA);
160  
161  // ATTACH in-memory databases as ops and tel so queries like ops.settings, tel.agent_tasks resolve
162  sharedDb.exec(`
163    ATTACH ':memory:' AS ops;
164    ATTACH ':memory:' AS tel;
165    CREATE TABLE IF NOT EXISTS ops.settings (key TEXT PRIMARY KEY, value TEXT NOT NULL, description TEXT, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP);
166    CREATE TABLE IF NOT EXISTS tel.agent_tasks (id INTEGER PRIMARY KEY AUTOINCREMENT, task_type TEXT NOT NULL, assigned_to TEXT NOT NULL, created_by TEXT, status TEXT DEFAULT 'pending', priority INTEGER DEFAULT 5, context_json TEXT, result_json TEXT, parent_task_id INTEGER, error_message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, started_at DATETIME, completed_at DATETIME, retry_count INTEGER DEFAULT 0);
167    CREATE TABLE IF NOT EXISTS tel.agent_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, agent_name TEXT NOT NULL, log_level TEXT, message TEXT NOT NULL, data_json TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
168    CREATE TABLE IF NOT EXISTS tel.agent_state (agent_name TEXT PRIMARY KEY, last_active DATETIME DEFAULT CURRENT_TIMESTAMP, current_task_id INTEGER, status TEXT DEFAULT 'idle', metrics_json TEXT);
169    CREATE TABLE IF NOT EXISTS tel.agent_messages (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, from_agent TEXT NOT NULL, to_agent TEXT NOT NULL, message_type TEXT, content TEXT NOT NULL, metadata_json TEXT, context_json TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, read_at DATETIME);
170    CREATE TABLE IF NOT EXISTS tel.agent_outcomes (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, agent_name TEXT NOT NULL, task_type TEXT NOT NULL, outcome TEXT NOT NULL, context_json TEXT, result_json TEXT, duration_ms INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
171    CREATE TABLE IF NOT EXISTS tel.pipeline_metrics (id INTEGER PRIMARY KEY AUTOINCREMENT, stage_name TEXT NOT NULL, sites_processed INTEGER DEFAULT 0, sites_succeeded INTEGER DEFAULT 0, sites_failed INTEGER DEFAULT 0, duration_ms INTEGER NOT NULL, started_at DATETIME NOT NULL, finished_at DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
172    CREATE TABLE IF NOT EXISTS tel.structured_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, agent_name TEXT, task_id INTEGER, level TEXT, message TEXT, data_json TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
173    INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('monitor', 'idle');
174    INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('triage', 'idle');
175    INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('developer', 'idle');
176    INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('qa', 'idle');
177    INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('security', 'idle');
178    INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('architect', 'idle');
179  `);
180  
181  import { resetDb as resetBaseDb } from '../../src/agents/base-agent.js';
182  import { resetDb as resetSLODb } from '../../src/agents/utils/slo-tracker.js';
183  import { MonitorAgent, resetDb as resetMonitorDb } from '../../src/agents/monitor.js';
184  
185  let agent;
186  
187  before(async () => {
188    mkdirSync(TEST_LOG_DIR, { recursive: true });
189    resetMonitorDb(sharedDb);
190    agent = new MonitorAgent();
191    await agent.initialize();
192  });
193  
194  after(() => {
195    resetMonitorDb(null);
196    resetBaseDb();
197    resetSLODb();
198    try {
199      sharedDb.close();
200    } catch {
201      /* ignore */
202    }
203    for (const ext of ['', '-wal', '-shm']) {
204      try {
205        unlinkSync(TEST_DB_PATH + ext);
206      } catch {
207        /* ignore */
208      }
209    }
210    try {
211      rmSync(TEST_LOG_DIR, { recursive: true, force: true });
212    } catch {
213      /* ignore */
214    }
215  });
216  
217  function clearTables() {
218    sharedDb.exec(`
219      DELETE FROM agent_tasks;
220      DELETE FROM agent_logs;
221      DELETE FROM agent_messages;
222      DELETE FROM human_review_queue;
223      DELETE FROM settings;
224      DELETE FROM sites;
225      DELETE FROM pipeline_metrics;
226      DELETE FROM agent_outcomes;
227      DELETE FROM site_status;
228      UPDATE agent_state SET status = 'idle', current_task_id = NULL, metrics_json = NULL;
229      INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('monitor', 'idle');
230      INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('triage', 'idle');
231      INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('developer', 'idle');
232      INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('qa', 'idle');
233      INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('security', 'idle');
234      INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('architect', 'idle');
235    `);
236  }
237  
238  function insertTask(taskType, context = {}, status = 'running', opts = {}) {
239    const r = sharedDb
240      .prepare(
241        `INSERT INTO agent_tasks (task_type, assigned_to, priority, context_json, status, created_at, error_message)
242         VALUES (?, 'monitor', 5, ?, ?, COALESCE(?, CURRENT_TIMESTAMP), ?)`
243      )
244      .run(
245        taskType,
246        JSON.stringify(context),
247        status,
248        opts.created_at || null,
249        opts.error_message || null
250      );
251    return sharedDb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(r.lastInsertRowid);
252  }
253  
254  beforeEach(() => {
255    clearTables();
256  });
257  
258  // ── Lines 29-30: resetDb close-failure catch ─────────────────────────────────
259  
260  describe('MonitorAgent - resetDb close-failure catch (lines 29-30)', () => {
261    test('resetDb survives when db.close() throws', () => {
262      // Create a fresh DB then close it manually so .close() throws
263      const tempDb = new Database('/tmp/test-monitor-close-fail.db');
264      tempDb.exec('CREATE TABLE IF NOT EXISTS x (id INTEGER)');
265      resetMonitorDb(tempDb);
266  
267      // Close it externally so the next resetMonitorDb's db.close() fails
268      tempDb.close();
269  
270      // Now resetMonitorDb tries to close an already-closed db → catch block (lines 29-30)
271      assert.doesNotThrow(() => {
272        resetMonitorDb(sharedDb); // re-attach to shared DB
273      }, 'resetDb should not throw when close fails');
274  
275      // Confirm agent still works after the close-failure
276      const taskCount = sharedDb.prepare('SELECT COUNT(*) as cnt FROM agent_tasks').get();
277      assert.ok(typeof taskCount.cnt === 'number');
278  
279      // Clean up temp DB file
280      try {
281        unlinkSync('/tmp/test-monitor-close-fail.db');
282      } catch {
283        /* ignore */
284      }
285    });
286  });
287  
288  // ── Lines 86-88: saveFilePositions DB catch ───────────────────────────────────
289  
290  describe('MonitorAgent - saveFilePositions DB catch (lines 86-88)', () => {
291    test('saveFilePositions silently catches DB error when prepare fails', async () => {
292      // Manually corrupt the settings table by dropping it
293      const badDb = new Database('/tmp/test-monitor-savefail.db');
294      badDb.exec(`
295        CREATE TABLE IF NOT EXISTS agent_tasks (id INTEGER PRIMARY KEY AUTOINCREMENT, task_type TEXT NOT NULL, assigned_to TEXT NOT NULL, created_by TEXT, status TEXT DEFAULT 'pending', priority INTEGER DEFAULT 5, context_json TEXT, result_json TEXT, parent_task_id INTEGER, error_message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, started_at DATETIME, completed_at DATETIME, retry_count INTEGER DEFAULT 0);
296        CREATE TABLE IF NOT EXISTS agent_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, agent_name TEXT NOT NULL, log_level TEXT, message TEXT NOT NULL, data_json TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
297        CREATE TABLE IF NOT EXISTS agent_state (agent_name TEXT PRIMARY KEY, last_active DATETIME DEFAULT CURRENT_TIMESTAMP, current_task_id INTEGER, status TEXT DEFAULT 'idle', metrics_json TEXT);
298        CREATE TABLE IF NOT EXISTS agent_messages (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, from_agent TEXT NOT NULL, to_agent TEXT NOT NULL, message_type TEXT, content TEXT NOT NULL, metadata_json TEXT, context_json TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, read_at DATETIME);
299        CREATE TABLE IF NOT EXISTS structured_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, agent_name TEXT, task_id INTEGER, level TEXT, message TEXT, data_json TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
300        CREATE TABLE IF NOT EXISTS cron_locks (lock_key TEXT PRIMARY KEY, acquired_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, description TEXT);
301        INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('monitor', 'idle');
302        INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('triage', 'idle');
303        INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('developer', 'idle');
304      `);
305      // No settings table! saveFilePositions will fail
306  
307      resetMonitorDb(badDb);
308      const testAgent = new MonitorAgent();
309      await testAgent.initialize();
310  
311      // saveFilePositions() tries to INSERT into settings (missing) — should be caught silently
312      assert.doesNotThrow(() => {
313        testAgent.saveFilePositions();
314      }, 'saveFilePositions should not throw when settings table missing');
315  
316      // Re-attach to shared DB for subsequent tests
317      resetMonitorDb(sharedDb);
318  
319      badDb.close();
320      try {
321        unlinkSync('/tmp/test-monitor-savefail.db');
322      } catch {
323        /* ignore */
324      }
325    });
326  });
327  
328  // ── Lines 169-177: processTask dispatch for check_rate_limits, fix_bug, bootstrap_monitor ──
329  
330  describe('MonitorAgent - processTask delegation cases (lines 169-177)', () => {
331    test('check_rate_limits task type is dispatched to checkRateLimitPatterns', async () => {
332      // checkRateLimitPatterns reads from logs/ dir — ensure it exists and completes without crash
333      const task = insertTask('check_rate_limits', {}, 'pending');
334      const runningTask =
335        sharedDb
336          .prepare('UPDATE agent_tasks SET status=? WHERE id=? RETURNING *')
337          .get('running', task.id) ||
338        sharedDb.prepare('SELECT * FROM agent_tasks WHERE id=?').get(task.id);
339  
340      // Should complete without throwing (logs dir may not have rate-limits.json)
341      await assert.doesNotReject(
342        agent.processTask({ ...runningTask, status: 'running' }),
343        'check_rate_limits processTask should not throw'
344      );
345  
346      const completedTask = sharedDb
347        .prepare('SELECT status FROM agent_tasks WHERE id=?')
348        .get(task.id);
349      assert.equal(completedTask?.status, 'completed', 'check_rate_limits task should complete');
350    });
351  
352    test('fix_bug task type is delegated to correct agent (developer)', async () => {
353      const task = insertTask('fix_bug', { error_message: 'Test bug' }, 'pending');
354  
355      await assert.doesNotReject(
356        agent.processTask({ ...task, status: 'running' }),
357        'fix_bug delegation should not throw'
358      );
359  
360      // Should have created a developer task (delegation)
361      const delegated = sharedDb
362        .prepare(
363          `SELECT * FROM agent_tasks WHERE task_type = 'fix_bug' AND assigned_to = 'developer' AND id != ?`
364        )
365        .get(task.id);
366      assert.ok(delegated, 'should have delegated fix_bug to developer');
367    });
368  
369    test('bootstrap_monitor task type is delegated', async () => {
370      const task = insertTask('bootstrap_monitor', {}, 'pending');
371  
372      await assert.doesNotReject(
373        agent.processTask({ ...task, status: 'running' }),
374        'bootstrap_monitor delegation should not throw'
375      );
376    });
377  
378    test('unknown task type triggers default delegation path', async () => {
379      const task = insertTask('completely_unknown_task_xyz', {}, 'pending');
380  
381      await assert.doesNotReject(
382        agent.processTask({ ...task, status: 'running' }),
383        'unknown task type should not throw (default delegation)'
384      );
385    });
386  });
387  
388  // ── Lines 188-199: processTask catch block when handler throws ────────────────
389  
390  describe('MonitorAgent - processTask catch block (lines 188-199)', () => {
391    test('processTask re-throws when the handler fails', async () => {
392      // Use a task type that triggers a handler that will fail
393      // check_agent_health requires DB with agent_state — we can break it by removing the table
394  
395      const badDb = new Database('/tmp/test-monitor-catchblock.db');
396      badDb.exec(`
397        CREATE TABLE IF NOT EXISTS agent_tasks (id INTEGER PRIMARY KEY AUTOINCREMENT, task_type TEXT NOT NULL, assigned_to TEXT NOT NULL, created_by TEXT, status TEXT DEFAULT 'pending', priority INTEGER DEFAULT 5, context_json TEXT, result_json TEXT, parent_task_id INTEGER, error_message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, started_at DATETIME, completed_at DATETIME, retry_count INTEGER DEFAULT 0);
398        CREATE TABLE IF NOT EXISTS agent_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, agent_name TEXT NOT NULL, log_level TEXT, message TEXT NOT NULL, data_json TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
399        CREATE TABLE IF NOT EXISTS agent_state (agent_name TEXT PRIMARY KEY, last_active DATETIME DEFAULT CURRENT_TIMESTAMP, current_task_id INTEGER, status TEXT DEFAULT 'idle', metrics_json TEXT);
400        CREATE TABLE IF NOT EXISTS agent_messages (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, from_agent TEXT NOT NULL, to_agent TEXT NOT NULL, message_type TEXT, content TEXT NOT NULL, metadata_json TEXT, context_json TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, read_at DATETIME);
401        CREATE TABLE IF NOT EXISTS structured_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, agent_name TEXT, task_id INTEGER, level TEXT, message TEXT, data_json TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
402        CREATE TABLE IF NOT EXISTS cron_locks (lock_key TEXT PRIMARY KEY, acquired_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, description TEXT);
403        INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('monitor', 'idle');
404        INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('triage', 'idle');
405        INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('developer', 'idle');
406      `);
407      // No sites table — check_process_compliance will fail when it queries sites
408  
409      resetMonitorDb(badDb);
410      const brokenAgent = new MonitorAgent();
411      await brokenAgent.initialize();
412  
413      const r = badDb
414        .prepare(
415          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
416           VALUES ('check_process_compliance', 'monitor', 'running', '{}')`
417        )
418        .run();
419      const task = badDb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(r.lastInsertRowid);
420  
421      // check_process_compliance queries sites table (missing) → throws → processTask catch logs + rethrows
422      await assert.rejects(
423        brokenAgent.processTask(task),
424        'processTask should re-throw when handler fails'
425      );
426  
427      // Re-attach
428      resetMonitorDb(sharedDb);
429      badDb.close();
430      try {
431        unlinkSync('/tmp/test-monitor-catchblock.db');
432      } catch {
433        /* ignore */
434      }
435    });
436  });
437  
438  // ── Lines 1662-1699: checkBlockedTasks pattern detection (>3 tasks same error) ──
439  
440  describe('MonitorAgent - checkBlockedTasks pattern detection (lines 1662-1699)', () => {
441    test('creates fix_bug task when >3 blocked tasks share the same error prefix', async () => {
442      // Insert 4 blocked tasks with the same error message prefix, created >2h ago
443      // Use space separator (not T) to match SQLite's datetime('now') comparison format
444      const twoHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000)
445        .toISOString()
446        .replace('T', ' ')
447        .slice(0, 19);
448      const commonError =
449        'Database connection timeout: SQLITE_BUSY waiting for write lock to release';
450  
451      for (let i = 0; i < 4; i++) {
452        sharedDb
453          .prepare(
454            `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, created_at)
455             VALUES (?, 'developer', 'blocked', ?, ?)`
456          )
457          .run(`fix_bug_${i}`, commonError, twoHoursAgo);
458      }
459  
460      // Create the monitor task and run checkBlockedTasks
461      const r = sharedDb
462        .prepare(
463          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
464           VALUES ('check_blocked_tasks', 'monitor', 'running', '{}')`
465        )
466        .run();
467      const task = sharedDb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(r.lastInsertRowid);
468  
469      await agent.processTask(task);
470  
471      // Should have created a fix_bug task for the pattern group
472      const fixBugTasks = sharedDb
473        .prepare(
474          `SELECT * FROM agent_tasks
475           WHERE task_type = 'fix_bug' AND assigned_to = 'developer'
476           AND context_json LIKE '%pattern_blocked_tasks%'`
477        )
478        .all();
479  
480      assert.ok(fixBugTasks.length >= 1, 'should create a fix_bug task for the error pattern');
481      const ctx = JSON.parse(fixBugTasks[0].context_json);
482      assert.equal(ctx.error_type, 'pattern_blocked_tasks');
483      assert.ok(ctx.affected_task_count >= 4, 'should report at least 4 affected tasks');
484    });
485  
486    test('does not duplicate fix_bug when one already exists for the same error prefix', async () => {
487      const twoHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000)
488        .toISOString()
489        .replace('T', ' ')
490        .slice(0, 19);
491      const commonError = 'Rate limit exceeded: OpenRouter 429 response after retries';
492  
493      for (let i = 0; i < 4; i++) {
494        sharedDb
495          .prepare(
496            `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, created_at)
497             VALUES (?, 'developer', 'blocked', ?, ?)`
498          )
499          .run(`ratelimit_task_${i}`, commonError, twoHoursAgo);
500      }
501  
502      // Pre-create a fix_bug task for this pattern to test dedup
503      sharedDb
504        .prepare(
505          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
506           VALUES ('fix_bug', 'developer', 'pending', ?)`
507        )
508        .run(JSON.stringify({ error_pattern: commonError.substring(0, 40) }));
509  
510      const existingFixCount = sharedDb
511        .prepare(
512          `SELECT COUNT(*) as cnt FROM agent_tasks WHERE task_type='fix_bug' AND assigned_to='developer'`
513        )
514        .get().cnt;
515  
516      // Run checkBlockedTasks
517      const r = sharedDb
518        .prepare(
519          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
520           VALUES ('check_blocked_tasks', 'monitor', 'running', '{}')`
521        )
522        .run();
523      const task = sharedDb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(r.lastInsertRowid);
524  
525      await agent.processTask(task);
526  
527      const newFixCount = sharedDb
528        .prepare(
529          `SELECT COUNT(*) as cnt FROM agent_tasks WHERE task_type='fix_bug' AND assigned_to='developer'`
530        )
531        .get().cnt;
532  
533      // The existing fix task covers the pattern, so no new fix_bug should be created
534      // (count may stay the same or increase by 0)
535      assert.ok(
536        newFixCount <= existingFixCount + 1,
537        'should not create excessive duplicate fix_bug tasks'
538      );
539    });
540  });