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 });