runner-coverage2.test.js
1 /** 2 * Runner Coverage 2 - Additional coverage for src/agents/runner.js 3 * Tests exported functions that work without mock.module(): 4 * - getAgentStats (lines 137-184): only needs DB with agent_tasks 5 * - checkCircuitBreakers (lines 193-305): needs agent_state + agent_tasks 6 * - runAgentCycle disabled path (lines 61-64): AGENT_SYSTEM_ENABLED=false 7 * - resetDb (lines 27-37) 8 * 9 * NOTE: Does NOT use mock.module() — safe to include in full coverage suite. 10 */ 11 12 import { describe, test, beforeEach, afterEach } from 'node:test'; 13 import assert from 'node:assert/strict'; 14 import Database from 'better-sqlite3'; 15 import path from 'path'; 16 import { fileURLToPath } from 'url'; 17 18 const __filename = fileURLToPath(import.meta.url); 19 const __dirname = path.dirname(__filename); 20 21 // ── Set DATABASE_PATH BEFORE importing runner.js ──────────────────────────── 22 const TEST_DB_PATH = `/tmp/runner-coverage2-test-${Date.now()}.db`; 23 process.env.DATABASE_PATH = TEST_DB_PATH; 24 process.env.AGENT_SYSTEM_ENABLED = 'false'; // prevent real agent execution 25 process.env.AGENT_AUTO_COMMIT = 'false'; 26 process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'false'; 27 process.env.AGENT_PARALLEL_EXECUTION = 'false'; 28 29 // ── Create test DB schema ──────────────────────────────────────────────────── 30 const TEST_SCHEMA = ` 31 CREATE TABLE IF NOT EXISTS agent_tasks ( 32 id INTEGER PRIMARY KEY AUTOINCREMENT, 33 task_type TEXT NOT NULL, 34 assigned_to TEXT NOT NULL, 35 created_by TEXT, 36 status TEXT DEFAULT 'pending', 37 priority INTEGER DEFAULT 5, 38 context_json TEXT, 39 result_json TEXT, 40 error_message TEXT, 41 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 42 started_at DATETIME, 43 completed_at DATETIME, 44 retry_count INTEGER DEFAULT 0 45 ); 46 47 CREATE TABLE IF NOT EXISTS agent_state ( 48 agent_name TEXT PRIMARY KEY, 49 last_active DATETIME DEFAULT CURRENT_TIMESTAMP, 50 current_task_id INTEGER, 51 status TEXT DEFAULT 'idle', 52 metrics_json TEXT 53 ); 54 55 CREATE TABLE IF NOT EXISTS agent_logs ( 56 id INTEGER PRIMARY KEY AUTOINCREMENT, 57 task_id INTEGER, 58 agent_name TEXT NOT NULL, 59 log_level TEXT, 60 message TEXT, 61 data_json TEXT, 62 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 63 ); 64 `; 65 66 const setupDb = new Database(TEST_DB_PATH); 67 setupDb.exec(TEST_SCHEMA); 68 setupDb.close(); 69 70 // ── Import runner AFTER DB is set up and env vars are set ─────────────────── 71 const { runAgentCycle, getAgentStats, checkCircuitBreakers, resetDb } = 72 await import('../../src/agents/runner.js'); 73 74 describe('runner.js - runAgentCycle disabled path', () => { 75 beforeEach(() => { 76 process.env.AGENT_SYSTEM_ENABLED = 'false'; 77 }); 78 79 afterEach(() => { 80 process.env.AGENT_SYSTEM_ENABLED = 'false'; 81 }); 82 83 test('returns disabled result when AGENT_SYSTEM_ENABLED is false', async () => { 84 const result = await runAgentCycle({ tasksPerAgent: 5 }); 85 assert.strictEqual(result.enabled, false); 86 assert.strictEqual(result.processed, 0); 87 }); 88 }); 89 90 describe('runner.js - getAgentStats', () => { 91 test('returns stats structure with empty DB', () => { 92 const stats = getAgentStats(); 93 assert.ok(stats.agents !== undefined); 94 assert.ok(Array.isArray(stats.agents)); 95 assert.ok(stats.overall !== undefined); 96 assert.ok(typeof stats.overall.total === 'number'); 97 assert.ok(typeof stats.overall.success_rate === 'number'); 98 assert.ok(typeof stats.overall.failure_rate === 'number'); 99 }); 100 101 test('returns stats with tasks in DB', () => { 102 // Insert test tasks 103 const db = new Database(TEST_DB_PATH); 104 db.prepare( 105 `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at) 106 VALUES ('fix_bug', 'Developer', 'completed', datetime('now', '-30 minutes'))` 107 ).run(); 108 db.prepare( 109 `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at, started_at, completed_at) 110 VALUES ('run_tests', 'QA', 'completed', datetime('now', '-30 minutes'), datetime('now', '-25 minutes'), datetime('now', '-20 minutes'))` 111 ).run(); 112 db.prepare( 113 `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at) 114 VALUES ('fix_bug', 'Developer', 'failed', datetime('now', '-30 minutes'))` 115 ).run(); 116 db.close(); 117 118 const stats = getAgentStats(); 119 assert.ok(Array.isArray(stats.agents)); 120 // Should have Developer and/or QA in stats 121 const hasAgents = stats.agents.length > 0; 122 if (hasAgents) { 123 const agent = stats.agents[0]; 124 assert.ok('success_rate' in agent); 125 assert.ok('failure_rate' in agent); 126 } 127 }); 128 }); 129 130 describe('runner.js - checkCircuitBreakers', () => { 131 test('returns empty array with no agent state', () => { 132 const blocked = checkCircuitBreakers(); 133 assert.ok(Array.isArray(blocked)); 134 // No agents in state, so none blocked 135 }); 136 137 test('returns blocked agents when failure rate exceeds threshold', () => { 138 const db = new Database(TEST_DB_PATH); 139 140 // Insert agent with high failure rate (11 total, 8 failed = 72.7%) 141 db.prepare( 142 `INSERT OR REPLACE INTO agent_state (agent_name, status) VALUES ('TestAgent', 'active')` 143 ).run(); 144 for (let i = 0; i < 11; i++) { 145 const status = i < 8 ? 'failed' : 'completed'; 146 db.prepare( 147 `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at) 148 VALUES ('fix_bug', 'TestAgent', ?, datetime('now', '-30 minutes'))` 149 ).run(status); 150 } 151 db.close(); 152 153 // AGENT_CIRCUIT_BREAKER_THRESHOLD is 0.3 (30%), TestAgent has 72.7% failure rate 154 process.env.AGENT_CIRCUIT_BREAKER_THRESHOLD = '0.3'; 155 const blocked = checkCircuitBreakers(); 156 assert.ok(Array.isArray(blocked)); 157 // TestAgent should be blocked (failure_rate 0.727 > threshold 0.3, total >= 10) 158 const testAgentBlocked = blocked.find(b => b.agent === 'TestAgent'); 159 if (testAgentBlocked) { 160 assert.ok(testAgentBlocked.failure_rate > 0.3); 161 } 162 }); 163 164 test('auto-recovers blocked agent after cooldown', () => { 165 const db = new Database(TEST_DB_PATH); 166 167 // Insert agent state that is blocked with expired cooldown 168 const triggeredAt = new Date(Date.now() - 40 * 60 * 1000).toISOString(); // 40 min ago 169 db.prepare( 170 `INSERT OR REPLACE INTO agent_state (agent_name, status, metrics_json) 171 VALUES ('RecoveryAgent', 'blocked', ?)` 172 ).run(JSON.stringify({ circuit_breaker_triggered_at: triggeredAt })); 173 174 // Insert low failure rate for this agent (1 total, 0 failed = 0%) 175 db.prepare( 176 `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at) 177 VALUES ('fix_bug', 'RecoveryAgent', 'completed', datetime('now', '-30 minutes'))` 178 ).run(); 179 db.close(); 180 181 // Cooldown is 30 min, triggered 40 min ago → should auto-recover 182 process.env.AGENT_CIRCUIT_BREAKER_COOLDOWN = '30'; 183 const blocked = checkCircuitBreakers(); 184 assert.ok(Array.isArray(blocked)); 185 // RecoveryAgent should NOT be blocked (auto-recovered) 186 const recoveryAgentBlocked = blocked.find(b => b.agent === 'RecoveryAgent'); 187 assert.ok(!recoveryAgentBlocked, 'RecoveryAgent should have been auto-recovered'); 188 }); 189 }); 190 191 describe('runner.js - resetDb', () => { 192 test('resetDb closes and resets the db instance', () => { 193 // Just verify it doesn't throw 194 assert.doesNotThrow(() => { 195 resetDb(); 196 }); 197 }); 198 199 test('resetDb with external db sets it as the active connection', () => { 200 const externalDb = new Database(TEST_DB_PATH); 201 assert.doesNotThrow(() => { 202 resetDb(externalDb); 203 }); 204 // Close it after 205 externalDb.close(); 206 resetDb(); 207 }); 208 });