monitor-slo.test.js
1 /** 2 * Monitor Agent SLO Integration Tests 3 * 4 * Tests Monitor agent's integration with SLO tracker 5 */ 6 7 import { test } from 'node:test'; 8 import assert from 'node:assert'; 9 import Database from 'better-sqlite3'; 10 import fs from 'fs'; 11 12 // MUST set before monitor.js imports (it opens DB at module level) 13 const TEST_DB_PATH = './db/test-monitor-slo.db'; 14 process.env.DATABASE_PATH = TEST_DB_PATH; 15 process.env.AGENT_IMMEDIATE_INVOCATION = 'false'; 16 17 import { MonitorAgent, resetDb as resetMonitorDb } from '../../src/agents/monitor.js'; 18 import { resetDb as resetSLODb } from '../../src/agents/utils/slo-tracker.js'; 19 import { resetDb as resetBaseDb } from '../../src/agents/base-agent.js'; 20 import { resetDbConnection as resetTaskManagerDb } from '../../src/agents/utils/task-manager.js'; 21 22 function setupTestDb() { 23 if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); 24 25 const db = new Database(TEST_DB_PATH); 26 27 // Create full schema needed for agent tests 28 db.exec(` 29 CREATE TABLE IF NOT EXISTS sites ( 30 id INTEGER PRIMARY KEY AUTOINCREMENT, 31 domain TEXT, 32 landing_page_url TEXT, 33 status TEXT DEFAULT 'found', 34 error_message TEXT, 35 score REAL, 36 grade TEXT, 37 recapture_count INTEGER DEFAULT 0, 38 updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, 39 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 40 ); 41 42 CREATE TABLE IF NOT EXISTS site_status ( 43 id INTEGER PRIMARY KEY AUTOINCREMENT, 44 site_id INTEGER, 45 status TEXT, 46 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 47 ); 48 49 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); 50 51 CREATE TABLE IF NOT EXISTS pipeline_metrics ( 52 id INTEGER PRIMARY KEY AUTOINCREMENT, 53 stage_name TEXT NOT NULL, 54 sites_processed INTEGER DEFAULT 0, 55 sites_succeeded INTEGER DEFAULT 0, 56 sites_failed INTEGER DEFAULT 0, 57 duration_ms INTEGER NOT NULL, 58 started_at DATETIME NOT NULL, 59 finished_at DATETIME NOT NULL, 60 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 61 ); 62 63 CREATE TABLE IF NOT EXISTS agent_tasks ( 64 id INTEGER PRIMARY KEY AUTOINCREMENT, 65 task_type TEXT NOT NULL, 66 assigned_to TEXT NOT NULL, 67 created_by TEXT, 68 status TEXT DEFAULT 'pending', 69 priority INTEGER DEFAULT 5, 70 context_json TEXT, 71 result_json TEXT, 72 parent_task_id INTEGER, 73 error_message TEXT, 74 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 75 started_at DATETIME, 76 completed_at DATETIME, 77 retry_count INTEGER DEFAULT 0 78 ); 79 80 CREATE TABLE IF NOT EXISTS agent_logs ( 81 id INTEGER PRIMARY KEY AUTOINCREMENT, 82 task_id INTEGER, 83 agent_name TEXT NOT NULL, 84 log_level TEXT, 85 message TEXT NOT NULL, 86 data_json TEXT, 87 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 88 ); 89 90 CREATE TABLE IF NOT EXISTS agent_state ( 91 agent_name TEXT PRIMARY KEY, 92 status TEXT DEFAULT 'idle', 93 current_task_id INTEGER, 94 metrics_json TEXT, 95 last_active DATETIME DEFAULT CURRENT_TIMESTAMP 96 ); 97 98 CREATE TABLE IF NOT EXISTS agent_messages ( 99 id INTEGER PRIMARY KEY AUTOINCREMENT, 100 task_id INTEGER, 101 from_agent TEXT NOT NULL, 102 to_agent TEXT NOT NULL, 103 message_type TEXT, 104 content TEXT NOT NULL, 105 metadata_json TEXT, 106 context_json TEXT, 107 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 108 read_at DATETIME 109 ); 110 111 CREATE TABLE IF NOT EXISTS settings ( 112 key TEXT PRIMARY KEY, 113 value TEXT NOT NULL, 114 description TEXT, 115 updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 116 ); 117 118 CREATE TABLE IF NOT EXISTS human_review_queue ( 119 id INTEGER PRIMARY KEY AUTOINCREMENT, 120 file TEXT NOT NULL, 121 reason TEXT NOT NULL, 122 type TEXT NOT NULL, 123 priority TEXT NOT NULL, 124 metadata TEXT, 125 status TEXT DEFAULT 'pending', 126 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 127 ); 128 129 INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('monitor', 'idle'); 130 INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('triage', 'idle'); 131 INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('developer', 'idle'); 132 INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('qa', 'idle'); 133 INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('security', 'idle'); 134 INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES ('architect', 'idle'); 135 `); 136 137 // ATTACH in-memory databases as ops and tel so queries like ops.settings, tel.agent_tasks resolve 138 db.exec(` 139 ATTACH ':memory:' AS ops; 140 ATTACH ':memory:' AS tel; 141 CREATE TABLE IF NOT EXISTS ops.settings (key TEXT PRIMARY KEY, value TEXT NOT NULL, description TEXT, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP); 142 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); 143 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); 144 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); 145 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); 146 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); 147 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); 148 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); 149 INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('monitor', 'idle'); 150 INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('triage', 'idle'); 151 INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('developer', 'idle'); 152 INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('qa', 'idle'); 153 INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('security', 'idle'); 154 INSERT OR IGNORE INTO tel.agent_state (agent_name, status) VALUES ('architect', 'idle'); 155 `); 156 157 return db; 158 } 159 160 test('Monitor Agent - processTask handles check_slo_compliance', async () => { 161 // Reset all module DB connections, then create fresh test DB 162 resetBaseDb(); 163 resetSLODb(); 164 resetTaskManagerDb(); 165 const db = setupTestDb(); 166 // Inject shared DB into monitor.js so it uses same connection 167 resetMonitorDb(db); 168 169 // Create monitor task 170 const taskResult = db 171 .prepare( 172 `INSERT INTO agent_tasks (task_type, assigned_to, priority, context_json) 173 VALUES (?, ?, ?, ?)` 174 ) 175 .run('check_slo_compliance', 'monitor', 7, '{}'); 176 177 const taskId = taskResult.lastInsertRowid; 178 179 const monitor = new MonitorAgent(); 180 await monitor.initialize(); 181 182 // Get the task 183 const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId); 184 185 // Process it 186 await monitor.processTask(task); 187 188 // Verify task was completed 189 const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId); 190 191 assert.strictEqual(completedTask.status, 'completed'); 192 193 const result = JSON.parse(completedTask.result_json); 194 assert.ok(typeof result.total_slos === 'number'); 195 assert.ok(typeof result.violations === 'number'); 196 assert.ok(typeof result.compliance_rate === 'number'); 197 198 // Detach monitor db, then close and cleanup 199 resetMonitorDb(null); 200 resetBaseDb(); 201 resetSLODb(); 202 resetTaskManagerDb(); 203 db.close(); 204 try { 205 fs.unlinkSync(TEST_DB_PATH); 206 } catch { 207 /* ignore */ 208 } 209 }); 210 211 test('Monitor Agent - SLO compliance creates Architect tasks on violations', async () => { 212 // Reset all module DB connections, then create fresh test DB 213 resetBaseDb(); 214 resetSLODb(); 215 resetTaskManagerDb(); 216 const db = setupTestDb(); 217 // Inject shared DB into monitor.js so it uses same connection 218 resetMonitorDb(db); 219 220 // Create violations by adding slow sites 221 const now = new Date(); 222 223 for (let i = 1; i <= 20; i++) { 224 const start = new Date(now.getTime() - 150 * 60 * 1000); // 150 min ago (violates 60 min SLO) 225 226 db.prepare('INSERT INTO sites (id, domain, status) VALUES (?, ?, ?)').run( 227 i, 228 `site${i}.com`, 229 'assets_captured' 230 ); 231 232 db.prepare('INSERT INTO site_status (site_id, status, created_at) VALUES (?, ?, ?)').run( 233 i, 234 'found', 235 start.toISOString() 236 ); 237 238 db.prepare('INSERT INTO site_status (site_id, status, created_at) VALUES (?, ?, ?)').run( 239 i, 240 'assets_captured', 241 now.toISOString() 242 ); 243 } 244 245 // Create monitor task 246 const taskResult = db 247 .prepare( 248 `INSERT INTO agent_tasks (task_type, assigned_to, priority, context_json) 249 VALUES (?, ?, ?, ?)` 250 ) 251 .run('check_slo_compliance', 'monitor', 7, '{}'); 252 253 const taskId = taskResult.lastInsertRowid; 254 255 const monitor = new MonitorAgent(); 256 await monitor.initialize(); 257 258 const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId); 259 260 await monitor.processTask(task); 261 262 // Check that Architect tasks were created 263 const architectTasks = db 264 .prepare( 265 `SELECT * FROM agent_tasks 266 WHERE assigned_to = 'architect' 267 AND task_type = 'design_optimization'` 268 ) 269 .all(); 270 271 assert.ok(architectTasks.length > 0, 'Should create at least one Architect task'); 272 273 // Verify task context 274 const architectTask = architectTasks[0]; 275 const context = JSON.parse(architectTask.context_json); 276 277 assert.strictEqual(context.optimization_type, 'slo_violation'); 278 assert.ok(context.stage_name); 279 assert.ok(context.description); 280 assert.ok(typeof context.current_p95 === 'number'); 281 assert.ok(typeof context.target_duration === 'number'); 282 assert.ok(context.severity); 283 284 // Detach monitor db, then close and cleanup 285 resetMonitorDb(null); 286 resetBaseDb(); 287 resetSLODb(); 288 resetTaskManagerDb(); 289 db.close(); 290 try { 291 fs.unlinkSync(TEST_DB_PATH); 292 } catch { 293 /* ignore */ 294 } 295 }); 296 297 test('Monitor Agent - recurring tasks include check_slo_compliance', async () => { 298 // Reset all module DB connections, then create fresh test DB 299 resetBaseDb(); 300 resetSLODb(); 301 resetTaskManagerDb(); 302 const db = setupTestDb(); 303 // Inject shared DB into monitor.js so it uses same connection 304 resetMonitorDb(db); 305 306 const monitor = new MonitorAgent(); 307 await monitor.initialize(); 308 309 // Call ensureRecurringTasks 310 await monitor.ensureRecurringTasks(); 311 312 // Check that check_slo_compliance task was created 313 const sloTask = db 314 .prepare( 315 `SELECT * FROM agent_tasks 316 WHERE assigned_to = 'monitor' 317 AND task_type = 'check_slo_compliance' 318 AND status = 'pending'` 319 ) 320 .get(); 321 322 assert.ok(sloTask, 'Should create check_slo_compliance recurring task'); 323 assert.strictEqual(sloTask.priority, 7); 324 325 // Detach monitor db, then close and cleanup 326 resetMonitorDb(null); 327 resetBaseDb(); 328 resetSLODb(); 329 resetTaskManagerDb(); 330 db.close(); 331 try { 332 fs.unlinkSync(TEST_DB_PATH); 333 } catch { 334 /* ignore */ 335 } 336 });