developer-deep.test.js
1 /** 2 * Developer Agent Deep Coverage Tests 3 * 4 * Uses method-level mocking to test implementation paths inside 5 * fixBug, implementFeature, refactorCode, applyFeedback, createCommit, 6 * escalateCoverageToHuman, and getFileCoverage. 7 */ 8 9 import { test, describe, beforeEach, afterEach } from 'node:test'; 10 import assert from 'node:assert/strict'; 11 import Database from 'better-sqlite3'; 12 import { DeveloperAgent } from '../../src/agents/developer.js'; 13 import { resetDb as resetBaseDb } from '../../src/agents/base-agent.js'; 14 import { resetDb as resetTaskDb } from '../../src/agents/utils/task-manager.js'; 15 import { resetDb as resetMessageDb } from '../../src/agents/utils/message-manager.js'; 16 import fsPromises from 'fs/promises'; 17 18 const TEST_DB_PATH = './tests/agents/test-developer-deep.db'; 19 let db; 20 let agent; 21 22 beforeEach(async () => { 23 try { 24 await fsPromises.unlink(TEST_DB_PATH); 25 } catch (_e) { 26 /* ignore */ 27 } 28 db = new Database(TEST_DB_PATH); 29 process.env.DATABASE_PATH = TEST_DB_PATH; 30 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 31 process.env.AGENT_IMMEDIATE_INVOCATION = 'false'; 32 db.exec(` 33 CREATE TABLE agent_tasks ( 34 id INTEGER PRIMARY KEY AUTOINCREMENT, 35 task_type TEXT NOT NULL, 36 assigned_to TEXT NOT NULL, 37 created_by TEXT, 38 status TEXT DEFAULT 'pending', 39 priority INTEGER DEFAULT 5, 40 context_json TEXT, 41 result_json TEXT, 42 parent_task_id INTEGER, 43 error_message TEXT, 44 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 45 started_at DATETIME, 46 completed_at DATETIME, 47 retry_count INTEGER DEFAULT 0 48 ); 49 CREATE TABLE agent_messages ( 50 id INTEGER PRIMARY KEY AUTOINCREMENT, 51 task_id INTEGER, 52 from_agent TEXT NOT NULL, 53 to_agent TEXT NOT NULL, 54 message_type TEXT, 55 content TEXT NOT NULL, 56 metadata_json TEXT, 57 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 58 read_at DATETIME 59 ); 60 CREATE TABLE agent_logs ( 61 id INTEGER PRIMARY KEY AUTOINCREMENT, 62 task_id INTEGER, 63 agent_name TEXT NOT NULL, 64 log_level TEXT, 65 message TEXT, 66 data_json TEXT, 67 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 68 ); 69 CREATE TABLE agent_state ( 70 agent_name TEXT PRIMARY KEY, 71 last_active DATETIME DEFAULT CURRENT_TIMESTAMP, 72 current_task_id INTEGER, 73 status TEXT DEFAULT 'idle', 74 metrics_json TEXT 75 ); 76 CREATE TABLE agent_outcomes ( 77 id INTEGER PRIMARY KEY AUTOINCREMENT, 78 task_id INTEGER NOT NULL, 79 agent_name TEXT NOT NULL, 80 task_type TEXT NOT NULL, 81 outcome TEXT NOT NULL, 82 context_json TEXT, 83 result_json TEXT, 84 duration_ms INTEGER, 85 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 86 ); 87 CREATE TABLE agent_llm_usage ( 88 id INTEGER PRIMARY KEY AUTOINCREMENT, 89 agent_name TEXT NOT NULL, 90 task_id INTEGER, 91 model TEXT NOT NULL, 92 prompt_tokens INTEGER NOT NULL, 93 completion_tokens INTEGER NOT NULL, 94 cost_usd DECIMAL(10, 6) NOT NULL, 95 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 96 ); 97 CREATE TABLE structured_logs ( 98 id INTEGER PRIMARY KEY AUTOINCREMENT, 99 agent_name TEXT, 100 task_id INTEGER, 101 level TEXT, 102 message TEXT, 103 data_json TEXT, 104 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 105 ); 106 `); 107 agent = new DeveloperAgent(); 108 await agent.initialize(); 109 }); 110 111 afterEach(async () => { 112 resetBaseDb(); 113 resetTaskDb(); 114 resetMessageDb(); 115 if (db) db.close(); 116 try { 117 await fsPromises.unlink(TEST_DB_PATH); 118 } catch (_e) { 119 /* ignore */ 120 } 121 }); 122 123 describe('DeveloperAgent Deep - escalateCoverageToHuman real implementation', () => { 124 test('sends question to architect with file details', async () => { 125 const taskId = db 126 .prepare( 127 'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)' 128 ) 129 .run( 130 'fix_bug', 131 'developer', 132 'pending', 133 JSON.stringify({ error_message: 'test' }) 134 ).lastInsertRowid; 135 136 const belowThreshold = [ 137 { file: 'src/score.js', coverage: 60, gap: 25 }, 138 { file: 'src/enrich.js', coverage: 72, gap: 13 }, 139 ]; 140 141 await agent.escalateCoverageToHuman(belowThreshold, taskId); 142 143 const messages = db 144 .prepare( 145 "SELECT * FROM agent_messages WHERE from_agent = 'developer' AND to_agent = 'architect' AND message_type = 'question'" 146 ) 147 .all(); 148 149 assert.ok(messages.length >= 1); 150 const { content } = messages[0]; 151 assert.ok(content.includes('src/score.js'), 'should mention score.js'); 152 assert.ok(content.includes('85%'), 'should mention 85% threshold'); 153 }); 154 155 test('escalateCoverageToHuman includes all below-threshold files in message', async () => { 156 const taskId = db 157 .prepare( 158 'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)' 159 ) 160 .run( 161 'fix_bug', 162 'developer', 163 'pending', 164 JSON.stringify({ error_message: 'test' }) 165 ).lastInsertRowid; 166 167 const belowThreshold = [{ file: 'src/score.js', coverage: 60, gap: 25 }]; 168 169 await agent.escalateCoverageToHuman(belowThreshold, taskId); 170 171 const messages = db 172 .prepare( 173 "SELECT * FROM agent_messages WHERE from_agent = 'developer' AND to_agent = 'architect'" 174 ) 175 .all(); 176 assert.ok(messages.length >= 1); 177 // Message should mention options a, b, c 178 assert.ok( 179 messages[0].content.includes('(a)') || messages[0].content.includes('Refactor'), 180 'message should include guidance options' 181 ); 182 }); 183 }); 184 185 describe('DeveloperAgent Deep - createCommit escalation path', () => { 186 test('createCommit calls attemptWriteTestsForCoverage when coverage fails', async () => { 187 const originalCheckCoverage = agent.checkCoverageBeforeCommit.bind(agent); 188 const originalAttemptWrite = agent.attemptWriteTestsForCoverage.bind(agent); 189 const originalEscalate = agent.escalateCoverageToHuman.bind(agent); 190 191 let attemptWriteCalled = false; 192 let escalateCalled = false; 193 194 agent.checkCoverageBeforeCommit = async function (files, taskId) { 195 return { 196 canCommit: false, 197 belowThreshold: [{ file: 'src/score.js', coverage: 60, gap: 25 }], 198 coverage: {}, 199 }; 200 }; 201 202 agent.attemptWriteTestsForCoverage = async function (bt, tid) { 203 attemptWriteCalled = true; 204 return false; 205 }; 206 207 agent.escalateCoverageToHuman = async function (bt, tid) { 208 escalateCalled = true; 209 await this.log('warn', 'Escalating coverage', { task_id: tid }); 210 }; 211 212 const taskId = db 213 .prepare( 214 'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)' 215 ) 216 .run( 217 'fix_bug', 218 'developer', 219 'pending', 220 JSON.stringify({ error_message: 'test' }) 221 ).lastInsertRowid; 222 223 await assert.rejects( 224 () => agent.createCommit('fix: something', ['src/score.js'], taskId), 225 /Coverage gate failed/ 226 ); 227 228 assert.ok(attemptWriteCalled, 'attemptWriteTestsForCoverage should have been called'); 229 assert.ok(escalateCalled, 'escalateCoverageToHuman should have been called'); 230 231 agent.checkCoverageBeforeCommit = originalCheckCoverage; 232 agent.attemptWriteTestsForCoverage = originalAttemptWrite; 233 agent.escalateCoverageToHuman = originalEscalate; 234 }); 235 236 test('createCommit re-checks coverage after attemptWriteTestsForCoverage returns true', async () => { 237 const originalCheckCoverage = agent.checkCoverageBeforeCommit.bind(agent); 238 const originalAttemptWrite = agent.attemptWriteTestsForCoverage.bind(agent); 239 240 let checkCallCount = 0; 241 242 agent.checkCoverageBeforeCommit = async function (files, taskId) { 243 checkCallCount++; 244 return { 245 canCommit: false, 246 belowThreshold: [{ file: 'src/score.js', coverage: 60, gap: 25 }], 247 coverage: {}, 248 }; 249 }; 250 251 agent.attemptWriteTestsForCoverage = async function (bt, tid) { 252 return true; // Thinks it wrote tests 253 }; 254 255 const taskId = db 256 .prepare( 257 'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)' 258 ) 259 .run( 260 'fix_bug', 261 'developer', 262 'pending', 263 JSON.stringify({ error_message: 'test' }) 264 ).lastInsertRowid; 265 266 await assert.rejects( 267 () => agent.createCommit('fix: something', ['src/score.js'], taskId), 268 /Coverage still below 85%/ 269 ); 270 271 assert.strictEqual(checkCallCount, 2, 'coverage should be checked twice'); 272 273 agent.checkCoverageBeforeCommit = originalCheckCoverage; 274 agent.attemptWriteTestsForCoverage = originalAttemptWrite; 275 }); 276 }); 277 278 describe('DeveloperAgent Deep - getFileCoverage real error path', () => { 279 test('getFileCoverage returns 0 coverage when npm test fails (real method)', async () => { 280 // This exercises the actual error-handling path in getFileCoverage (lines 1585-1596) 281 // npm test will fail in this environment without proper setup 282 const result = await agent.getFileCoverage(['src/fake-module-xyz123.js']); 283 284 assert.strictEqual(typeof result, 'object'); 285 assert.ok('src/fake-module-xyz123.js' in result, 'should have entry for the file'); 286 // In a test environment without proper npm test setup, returns 0 287 assert.strictEqual(result['src/fake-module-xyz123.js'], 0); 288 }); 289 290 test('getFileCoverage handles empty files array', async () => { 291 const result = await agent.getFileCoverage([]); 292 assert.deepStrictEqual(result, {}); 293 }); 294 }); 295 296 describe('DeveloperAgent Deep - checkCoverageBeforeCommit real implementation paths', () => { 297 test('calls getFileCoverage with source files and returns canCommit false when below 85', async () => { 298 const originalGetFileCoverage = agent.getFileCoverage.bind(agent); 299 let calledWithFiles = null; 300 301 agent.getFileCoverage = async function (files) { 302 calledWithFiles = files; 303 const results = {}; 304 for (const f of files) results[f] = 60; 305 return results; 306 }; 307 308 const taskId = db 309 .prepare( 310 'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)' 311 ) 312 .run( 313 'fix_bug', 314 'developer', 315 'pending', 316 JSON.stringify({ error_message: 'test' }) 317 ).lastInsertRowid; 318 319 const result = await agent.checkCoverageBeforeCommit(['src/score.js'], taskId); 320 321 assert.strictEqual(result.canCommit, false); 322 assert.deepStrictEqual(calledWithFiles, ['src/score.js']); 323 assert.strictEqual(result.belowThreshold.length, 1); 324 assert.ok(result.reason.includes('85%')); 325 326 agent.getFileCoverage = originalGetFileCoverage; 327 }); 328 329 test('calls getFileCoverage and returns canCommit true when all files pass', async () => { 330 const originalGetFileCoverage = agent.getFileCoverage.bind(agent); 331 332 agent.getFileCoverage = async function (files) { 333 const results = {}; 334 for (const f of files) results[f] = 90; 335 return results; 336 }; 337 338 const taskId = db 339 .prepare( 340 'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)' 341 ) 342 .run( 343 'fix_bug', 344 'developer', 345 'pending', 346 JSON.stringify({ error_message: 'test' }) 347 ).lastInsertRowid; 348 349 const result = await agent.checkCoverageBeforeCommit(['src/score.js', 'src/enrich.js'], taskId); 350 351 assert.strictEqual(result.canCommit, true); 352 assert.ok('src/score.js' in result.coverage); 353 354 agent.getFileCoverage = originalGetFileCoverage; 355 }); 356 }); 357 358 describe('DeveloperAgent Deep - runTests real implementation', () => { 359 test('agent.runTests with no files builds npm test command', async () => { 360 // Test the agent instance runTests method (not the imported utility) 361 // This method uses execSync which will fail in test environment 362 // Testing that it returns success: false gracefully when tests fail 363 const result = await agent.runTests([]); 364 assert.strictEqual(typeof result, 'object'); 365 assert.ok('success' in result); 366 assert.ok('output' in result); 367 }); 368 369 test('agent.runTests with specific files builds file-specific command', async () => { 370 const result = await agent.runTests(['src/fake-file.js']); 371 assert.strictEqual(typeof result, 'object'); 372 assert.ok('success' in result); 373 }); 374 });