developer-execsync.test.js
1 /** 2 * Developer Agent execSync-Mocked Tests 3 * 4 * Uses mock.module('node:child_process') to control execSync, allowing us to 5 * exercise success paths that are unreachable without recursive test invocations: 6 * 7 * - getDetailedCoverage() lines 1385-1414: after execSync passes, reads 8 * coverage-final.json and extracts uncovered lines 9 * - createCommit() lines 1526-1527: "Commit created" log after git commit 10 * - getFileCoverage() lines 1545-1597: npm test + coverage-summary.json reading 11 * 12 * MUST be run with --experimental-test-module-mocks. 13 */ 14 15 import { test, describe, mock, beforeEach, afterEach } from 'node:test'; 16 import assert from 'node:assert/strict'; 17 import Database from 'better-sqlite3'; 18 import { resetDb as resetBaseDb } from '../../src/agents/base-agent.js'; 19 import { resetDb as resetTaskDb } from '../../src/agents/utils/task-manager.js'; 20 import { resetDb as resetMessageDb } from '../../src/agents/utils/message-manager.js'; 21 import fsPromises from 'fs/promises'; 22 import path from 'path'; 23 24 // ---------------------------------------------------------------- 25 // Mock child_process BEFORE importing DeveloperAgent 26 // execSync is used for: npx c8, npm test, git add, git commit 27 // ---------------------------------------------------------------- 28 const mockExecSync = mock.fn((_cmd, _opts) => 'mock-output'); 29 30 mock.module('node:child_process', { 31 namedExports: { 32 execSync: mockExecSync, 33 }, 34 }); 35 36 // Mock fileOps to avoid side effects 37 const mockFileOps = { 38 readFile: mock.fn(async () => ({ content: 'code', size: 10 })), 39 getFileContext: mock.fn(async () => ({ imports: [], testFiles: [] })), 40 editFile: mock.fn(async () => ({ backupPath: '/tmp/backup.js', diff: 'x' })), 41 writeFile: mock.fn(async () => ({ backupPath: '/tmp/new.js' })), 42 restoreBackup: mock.fn(async () => {}), 43 cleanupBackups: mock.fn(async () => {}), 44 listBackups: mock.fn(async () => []), 45 }; 46 47 mock.module('../../src/agents/utils/file-operations.js', { 48 namedExports: mockFileOps, 49 }); 50 51 const mockRunTests = mock.fn(async () => ({ 52 success: true, 53 stats: { pass: 5, fail: 0 }, 54 coverage: 90, 55 })); 56 const mockRunTestsForFile = mock.fn(async () => ({ 57 success: true, 58 stats: { pass: 3, fail: 0 }, 59 coverage: 92, 60 })); 61 62 mock.module('../../src/agents/utils/test-runner.js', { 63 namedExports: { 64 runTests: mockRunTests, 65 runTestsForFile: mockRunTestsForFile, 66 }, 67 }); 68 69 const mockSimpleLLMCall = mock.fn(async () => JSON.stringify({ explanation: 'fixed' })); 70 71 mock.module('../../src/agents/utils/agent-claude-api.js', { 72 namedExports: { 73 simpleLLMCall: mockSimpleLLMCall, 74 }, 75 }); 76 77 // NOW import DeveloperAgent (after mocks are set up) 78 const { DeveloperAgent } = await import('../../src/agents/developer.js'); 79 80 // ---------------------------------------------------------------- 81 // Test infrastructure 82 // ---------------------------------------------------------------- 83 const TEST_DB_PATH = './tests/agents/test-developer-execsync.db'; 84 let db; 85 let agent; 86 87 const DB_SCHEMA = ` 88 CREATE TABLE agent_tasks ( 89 id INTEGER PRIMARY KEY AUTOINCREMENT, 90 task_type TEXT NOT NULL, 91 assigned_to TEXT NOT NULL, 92 created_by TEXT, 93 status TEXT DEFAULT 'pending', 94 priority INTEGER DEFAULT 5, 95 context_json TEXT, 96 result_json TEXT, 97 parent_task_id INTEGER, 98 error_message TEXT, 99 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 100 started_at DATETIME, 101 completed_at DATETIME, 102 retry_count INTEGER DEFAULT 0 103 ); 104 CREATE TABLE agent_messages ( 105 id INTEGER PRIMARY KEY AUTOINCREMENT, 106 task_id INTEGER, 107 from_agent TEXT NOT NULL, 108 to_agent TEXT NOT NULL, 109 message_type TEXT, 110 content TEXT NOT NULL, 111 metadata_json TEXT, 112 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 113 read_at DATETIME 114 ); 115 CREATE TABLE agent_logs ( 116 id INTEGER PRIMARY KEY AUTOINCREMENT, 117 task_id INTEGER, 118 agent_name TEXT NOT NULL, 119 log_level TEXT, 120 message TEXT, 121 data_json TEXT, 122 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 123 ); 124 CREATE TABLE agent_state ( 125 agent_name TEXT PRIMARY KEY, 126 last_active DATETIME DEFAULT CURRENT_TIMESTAMP, 127 current_task_id INTEGER, 128 status TEXT DEFAULT 'idle', 129 metrics_json TEXT 130 ); 131 CREATE TABLE agent_outcomes ( 132 id INTEGER PRIMARY KEY AUTOINCREMENT, 133 task_id INTEGER NOT NULL, 134 agent_name TEXT NOT NULL, 135 task_type TEXT NOT NULL, 136 outcome TEXT NOT NULL, 137 context_json TEXT, 138 result_json TEXT, 139 duration_ms INTEGER, 140 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 141 ); 142 CREATE TABLE agent_llm_usage ( 143 id INTEGER PRIMARY KEY AUTOINCREMENT, 144 agent_name TEXT NOT NULL, 145 task_id INTEGER, 146 model TEXT NOT NULL, 147 prompt_tokens INTEGER NOT NULL, 148 completion_tokens INTEGER NOT NULL, 149 cost_usd DECIMAL(10, 6) NOT NULL, 150 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 151 ); 152 CREATE TABLE structured_logs ( 153 id INTEGER PRIMARY KEY AUTOINCREMENT, 154 agent_name TEXT, 155 task_id INTEGER, 156 level TEXT, 157 message TEXT, 158 data_json TEXT, 159 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 160 ); 161 `; 162 163 beforeEach(async () => { 164 // Reset both call history AND implementation back to default 165 mockExecSync.mock.resetCalls(); 166 mockExecSync.mock.mockImplementation((_cmd, _opts) => 'mock-output'); 167 // Reset runTests mock 168 mockRunTests.mock.resetCalls(); 169 mockRunTests.mock.mockImplementation(async () => ({ 170 success: true, 171 stats: { pass: 5, fail: 0 }, 172 coverage: 90, 173 })); 174 mockRunTestsForFile.mock.resetCalls(); 175 mockRunTestsForFile.mock.mockImplementation(async () => ({ 176 success: true, 177 stats: { pass: 3, fail: 0 }, 178 coverage: 92, 179 })); 180 // Reset simpleLLMCall mock 181 mockSimpleLLMCall.mock.resetCalls(); 182 mockSimpleLLMCall.mock.mockImplementation(async () => JSON.stringify({ explanation: 'fixed' })); 183 // Reset fileOps mocks 184 mockFileOps.listBackups.mock.resetCalls(); 185 mockFileOps.listBackups.mock.mockImplementation(async () => []); 186 mockFileOps.restoreBackup.mock.resetCalls(); 187 mockFileOps.restoreBackup.mock.mockImplementation(async () => {}); 188 mockFileOps.editFile.mock.resetCalls(); 189 mockFileOps.editFile.mock.mockImplementation(async () => ({ 190 backupPath: '/tmp/backup.js', 191 diff: 'x', 192 })); 193 194 try { 195 await fsPromises.unlink(TEST_DB_PATH); 196 } catch (_e) { 197 /* ignore */ 198 } 199 200 db = new Database(TEST_DB_PATH); 201 process.env.DATABASE_PATH = TEST_DB_PATH; 202 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 203 process.env.AGENT_IMMEDIATE_INVOCATION = 'false'; 204 205 db.exec(DB_SCHEMA); 206 agent = new DeveloperAgent(); 207 await agent.initialize(); 208 }); 209 210 afterEach(async () => { 211 resetBaseDb(); 212 resetTaskDb(); 213 resetMessageDb(); 214 if (db) db.close(); 215 try { 216 await fsPromises.unlink(TEST_DB_PATH); 217 } catch (_e) { 218 /* ignore */ 219 } 220 }); 221 222 function insertTask(taskType, context) { 223 const taskId = db 224 .prepare( 225 'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)' 226 ) 227 .run(taskType, 'developer', 'pending', JSON.stringify(context)).lastInsertRowid; 228 const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId); 229 task.context_json = JSON.parse(task.context_json); 230 return task; 231 } 232 233 // Helper: write fake coverage-final.json for getDetailedCoverage 234 async function writeCoverageFinal(data) { 235 const coverageDir = path.join(process.cwd(), 'coverage'); 236 await fsPromises.mkdir(coverageDir, { recursive: true }); 237 await fsPromises.writeFile(path.join(coverageDir, 'coverage-final.json'), JSON.stringify(data)); 238 } 239 240 async function removeCoverageFinal() { 241 try { 242 await fsPromises.unlink(path.join(process.cwd(), 'coverage', 'coverage-final.json')); 243 } catch (_e) { 244 /* ignore */ 245 } 246 } 247 248 // Helper: write fake coverage-summary.json for getFileCoverage 249 async function writeCoverageSummary(data) { 250 const coverageDir = path.join(process.cwd(), 'coverage'); 251 await fsPromises.mkdir(coverageDir, { recursive: true }); 252 await fsPromises.writeFile(path.join(coverageDir, 'coverage-summary.json'), JSON.stringify(data)); 253 } 254 255 async function removeCoverageSummary() { 256 try { 257 await fsPromises.unlink(path.join(process.cwd(), 'coverage', 'coverage-summary.json')); 258 } catch (_e) { 259 /* ignore */ 260 } 261 } 262 263 // ================================================================ 264 // getDetailedCoverage() - success paths (lines 1385-1414) 265 // execSync mocked to succeed; coverage-final.json written to disk 266 // ================================================================ 267 describe('DeveloperAgent execSync-mocked - getDetailedCoverage()', () => { 268 test('returns uncovered lines and coverage when fileKey found (lines 1391-1414)', async () => { 269 const filePath = 'src/score.js'; 270 const fakeCoverage = { 271 '/home/jason/code/333Method/src/score.js': { 272 statementMap: { 273 0: { start: { line: 10 }, end: { line: 10 } }, 274 1: { start: { line: 20 }, end: { line: 22 } }, 275 2: { start: { line: 30 }, end: { line: 30 } }, 276 }, 277 s: { 0: 5, 1: 0, 2: 0 }, 278 lines: { pct: 77 }, 279 }, 280 }; 281 282 await writeCoverageFinal(fakeCoverage); 283 try { 284 const result = await agent.getDetailedCoverage(filePath); 285 286 assert.ok(result !== null, 'Should return non-null when JSON read succeeds'); 287 assert.strictEqual(result.coverage, 77, 'Should return correct pct'); 288 assert.ok(Array.isArray(result.uncoveredLines), 'uncoveredLines should be array'); 289 assert.strictEqual(result.uncoveredLines.length, 2, 'Should have 2 uncovered regions'); 290 assert.strictEqual(result.uncoveredLines[0].start, 20); 291 assert.strictEqual(result.uncoveredLines[0].end, 22); 292 assert.strictEqual(result.uncoveredLines[1].start, 30); 293 } finally { 294 await removeCoverageFinal(); 295 } 296 }); 297 298 test('returns null when fileKey not found in coverage data (line 1396)', async () => { 299 const fakeCoverage = { 300 '/other-project/src/something-else.js': { 301 statementMap: {}, 302 s: {}, 303 lines: { pct: 90 }, 304 }, 305 }; 306 307 await writeCoverageFinal(fakeCoverage); 308 try { 309 const result = await agent.getDetailedCoverage('src/score.js'); 310 assert.strictEqual(result, null, 'Returns null when fileKey not found'); 311 } finally { 312 await removeCoverageFinal(); 313 } 314 }); 315 316 test('returns null when coverage-final.json cannot be read (inner catch, line 1388)', async () => { 317 await removeCoverageFinal(); 318 const result = await agent.getDetailedCoverage('src/score.js'); 319 assert.strictEqual(result, null, 'Returns null when coverage file missing'); 320 }); 321 322 test('returns empty uncoveredLines array when all statements covered (line 1404-1412)', async () => { 323 const filePath = 'src/capture.js'; 324 const fakeCoverage = { 325 '/home/jason/code/333Method/src/capture.js': { 326 statementMap: { 327 0: { start: { line: 5 }, end: { line: 5 } }, 328 }, 329 s: { 0: 10 }, 330 lines: { pct: 100 }, 331 }, 332 }; 333 334 await writeCoverageFinal(fakeCoverage); 335 try { 336 const result = await agent.getDetailedCoverage(filePath); 337 assert.ok(result !== null); 338 assert.strictEqual(result.coverage, 100); 339 assert.strictEqual(result.uncoveredLines.length, 0, 'No uncovered lines when all covered'); 340 } finally { 341 await removeCoverageFinal(); 342 } 343 }); 344 345 test('statementMap missing for statement id - uncoveredLines still handles gracefully', async () => { 346 const filePath = 'src/score.js'; 347 const fakeCoverage = { 348 '/home/jason/code/333Method/src/score.js': { 349 statementMap: { 350 0: { start: { line: 5 }, end: { line: 5 } }, 351 // '1' intentionally absent from statementMap 352 }, 353 s: { 0: 0, 1: 0 }, 354 lines: { pct: 50 }, 355 }, 356 }; 357 358 await writeCoverageFinal(fakeCoverage); 359 try { 360 const result = await agent.getDetailedCoverage(filePath); 361 // s['0'] = 0 and statementMap['0'] exists -> included 362 // s['1'] = 0 but statementMap['1'] missing -> skipped 363 assert.ok(result !== null); 364 assert.strictEqual(result.uncoveredLines.length, 1, 'Only tracks stmts with valid map entry'); 365 } finally { 366 await removeCoverageFinal(); 367 } 368 }); 369 }); 370 371 // ================================================================ 372 // getFileCoverage() - success path (lines 1545-1584) 373 // execSync mocked so 'npm test' does not run recursively; 374 // coverage-summary.json written to disk for fs.readFile 375 // ================================================================ 376 describe('DeveloperAgent execSync-mocked - getFileCoverage()', () => { 377 test('returns line pct from coverage-summary.json (exact key match, lines 1572-1574)', async () => { 378 const fakeSummary = { 379 'src/score.js': { lines: { pct: 88 } }, 380 'src/capture.js': { lines: { pct: 72 } }, 381 }; 382 383 await writeCoverageSummary(fakeSummary); 384 try { 385 const result = await agent.getFileCoverage(['src/score.js', 'src/capture.js']); 386 assert.strictEqual(result['src/score.js'], 88); 387 assert.strictEqual(result['src/capture.js'], 72); 388 } finally { 389 await removeCoverageSummary(); 390 } 391 }); 392 393 test('falls back to absolute path lookup when relative key missing (line 1563-1568)', async () => { 394 const projectRoot = process.cwd(); 395 const absPath = path.join(projectRoot, 'src/score.js'); 396 const fakeSummary = { [absPath]: { lines: { pct: 91 } } }; 397 398 await writeCoverageSummary(fakeSummary); 399 try { 400 const result = await agent.getFileCoverage(['src/score.js']); 401 assert.strictEqual(result['src/score.js'], 91, 'Should find via absolutePath fallback'); 402 } finally { 403 await removeCoverageSummary(); 404 } 405 }); 406 407 test('falls back to normalized path (strips leading slash) - line 1571', async () => { 408 const fakeSummary = { 'src/score.js': { lines: { pct: 85 } } }; 409 410 await writeCoverageSummary(fakeSummary); 411 try { 412 // Leading slash causes relative/absolute lookups to fail, normalized succeeds 413 const result = await agent.getFileCoverage(['/src/score.js']); 414 assert.strictEqual(result['/src/score.js'], 85, 'Should find via normalized path'); 415 } finally { 416 await removeCoverageSummary(); 417 } 418 }); 419 420 test('returns 0 and logs warning when file not in any path format (lines 1575-1581)', async () => { 421 const fakeSummary = { 'src/other-file.js': { lines: { pct: 90 } } }; 422 423 await writeCoverageSummary(fakeSummary); 424 try { 425 const result = await agent.getFileCoverage(['src/score.js']); 426 assert.strictEqual(result['src/score.js'], 0, 'Returns 0 for missing file'); 427 428 const warnLogs = db 429 .prepare( 430 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Coverage data not found%'" 431 ) 432 .all(); 433 assert.ok(warnLogs.length > 0, 'Should log warning for missing file coverage'); 434 } finally { 435 await removeCoverageSummary(); 436 } 437 }); 438 439 test('logs "Running coverage check" at entry (line 1548)', async () => { 440 const fakeSummary = { 'src/score.js': { lines: { pct: 80 } } }; 441 await writeCoverageSummary(fakeSummary); 442 443 try { 444 await agent.getFileCoverage(['src/score.js']); 445 446 const infoLogs = db 447 .prepare( 448 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Running coverage check%'" 449 ) 450 .all(); 451 assert.ok(infoLogs.length > 0, 'Should log "Running coverage check"'); 452 } finally { 453 await removeCoverageSummary(); 454 } 455 }); 456 457 test('returns 0 for all files and logs error when coverage-summary.json missing (lines 1585-1595)', async () => { 458 await removeCoverageSummary(); 459 // execSync succeeds but fs.readFile fails -> catch block 460 const result = await agent.getFileCoverage(['src/score.js', 'src/capture.js']); 461 462 assert.strictEqual(result['src/score.js'], 0); 463 assert.strictEqual(result['src/capture.js'], 0); 464 465 const errorLogs = db 466 .prepare( 467 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Failed to get coverage%'" 468 ) 469 .all(); 470 assert.ok(errorLogs.length > 0, 'Should log error when coverage file unreadable'); 471 }); 472 }); 473 474 // ================================================================ 475 // createCommit() success path - lines 1526-1527 ("Commit created") 476 // execSync mocked so git add + git commit succeed 477 // ================================================================ 478 describe('DeveloperAgent execSync-mocked - createCommit() success path', () => { 479 test('logs "Commit created" and returns trimmed hash (lines 1526-1527)', async () => { 480 const task = insertTask('fix_bug', { error_type: 'null_pointer', error_message: 'test' }); 481 482 // Reset and configure mock: git add returns empty, git commit returns hash 483 mockExecSync.mock.resetCalls(); 484 let callIdx = 0; 485 mockExecSync.mock.mockImplementation(() => { 486 callIdx++; 487 if (callIdx === 1) return ''; // git add 488 return ' abc123def '; // git commit returns hash with whitespace 489 }); 490 491 agent.checkCoverageBeforeCommit = async () => ({ 492 canCommit: true, 493 coverage: { 'src/score.js': 92 }, 494 }); 495 496 const hash = await agent.createCommit('fix: coverage commit', ['src/score.js'], task.id); 497 498 // Should return trimmed hash 499 assert.strictEqual(hash, 'abc123def', 'Returns trimmed git commit hash'); 500 501 // Verify "Commit created" was logged (line 1526) 502 const commitLogs = db 503 .prepare( 504 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Commit created%'" 505 ) 506 .all(); 507 assert.ok(commitLogs.length > 0, 'Should log "Commit created"'); 508 509 // Verify execSync called for git add and git commit 510 const commands = mockExecSync.mock.calls.map(c => c.arguments[0]); 511 assert.ok( 512 commands.some(cmd => cmd.includes('git add')), 513 'Should call git add' 514 ); 515 assert.ok( 516 commands.some(cmd => cmd.includes('git commit')), 517 'Should call git commit' 518 ); 519 }); 520 521 test('commit message includes Co-Authored-By trailer', async () => { 522 insertTask('fix_bug', { error_type: 'null_pointer', error_message: 'test' }); 523 524 mockExecSync.mock.resetCalls(); 525 let callIdx = 0; 526 mockExecSync.mock.mockImplementation(() => { 527 callIdx++; 528 return callIdx === 1 ? '' : 'hash456'; 529 }); 530 531 agent.checkCoverageBeforeCommit = async () => ({ canCommit: true, coverage: {} }); 532 533 await agent.createCommit('test: verify trailer', ['src/score.js'], 1); 534 535 const commands = mockExecSync.mock.calls.map(c => c.arguments[0]); 536 const commitCmd = commands.find(cmd => cmd.includes('git commit')); 537 assert.ok(commitCmd.includes('Co-Authored-By'), 'Commit message should have Co-Authored-By'); 538 assert.ok(commitCmd.includes('noreply@anthropic.com'), 'Should include Anthropic email'); 539 }); 540 541 test('createCommit logs "Commit failed" and rethrows when git throws (lines 1528-1534)', async () => { 542 const task = insertTask('fix_bug', { error_type: 'null_pointer', error_message: 'test' }); 543 544 mockExecSync.mock.resetCalls(); 545 mockExecSync.mock.mockImplementation(() => { 546 throw new Error('fatal: not a git repository'); 547 }); 548 549 agent.checkCoverageBeforeCommit = async () => ({ canCommit: true, coverage: {} }); 550 551 await assert.rejects( 552 async () => agent.createCommit('fix: should fail', ['src/score.js'], task.id), 553 /fatal: not a git repository/ 554 ); 555 556 const errorLogs = db 557 .prepare( 558 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Commit failed%'" 559 ) 560 .all(); 561 assert.ok(errorLogs.length > 0, 'Should log "Commit failed" when git throws'); 562 }); 563 564 test('stages all provided files before committing', async () => { 565 const task = insertTask('fix_bug', { error_type: 'null_pointer', error_message: 'test' }); 566 const files = ['src/score.js', 'src/capture.js', 'tests/score.test.js']; 567 568 mockExecSync.mock.resetCalls(); 569 let callIdx = 0; 570 mockExecSync.mock.mockImplementation(() => { 571 callIdx++; 572 return callIdx <= files.length ? '' : 'commitHash'; 573 }); 574 575 agent.checkCoverageBeforeCommit = async () => ({ canCommit: true, coverage: {} }); 576 577 await agent.createCommit('fix: multi-file commit', files, task.id); 578 579 const commands = mockExecSync.mock.calls.map(c => c.arguments[0]); 580 const gitAddCalls = commands.filter(cmd => cmd.includes('git add')); 581 assert.strictEqual(gitAddCalls.length, files.length, 'Should git add each file separately'); 582 for (const file of files) { 583 assert.ok( 584 gitAddCalls.some(cmd => cmd.includes(file)), 585 `Should git add ${file}` 586 ); 587 } 588 }); 589 }); 590 591 // ================================================================ 592 // runTests() - real method with mocked execSync (lines 1195-1221) 593 // ================================================================ 594 describe('DeveloperAgent execSync-mocked - runTests() real method', () => { 595 test('returns success:true when execSync succeeds with no files (lines 1207-1215)', async () => { 596 mockExecSync.mock.resetCalls(); 597 mockExecSync.mock.mockImplementation(() => 'All tests passed\n# pass 10\n# fail 0'); 598 599 const result = await agent.runTests([]); 600 601 assert.strictEqual(result.success, true, 'Returns success:true on passing tests'); 602 assert.ok(result.output.includes('All tests passed'), 'Returns stdout as output'); 603 604 const commands = mockExecSync.mock.calls.map(c => c.arguments[0]); 605 assert.ok( 606 commands.some(cmd => cmd === 'npm test'), 607 'Should call bare npm test when no files' 608 ); 609 610 const runLogs = db 611 .prepare( 612 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Running tests%'" 613 ) 614 .all(); 615 assert.ok(runLogs.length >= 1, 'Should log Running tests'); 616 617 const passLogs = db 618 .prepare( 619 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Tests passed%'" 620 ) 621 .all(); 622 assert.ok(passLogs.length >= 1, 'Should log Tests passed'); 623 }); 624 625 test('returns success:true and builds file-specific command (lines 1208-1210)', async () => { 626 mockExecSync.mock.resetCalls(); 627 mockExecSync.mock.mockImplementation(cmd => `Tests for ${cmd} passed`); 628 629 const result = await agent.runTests(['src/score.js', 'src/capture.js']); 630 631 assert.strictEqual(result.success, true); 632 633 const commands = mockExecSync.mock.calls.map(c => c.arguments[0]); 634 const testCmd = commands.find(cmd => cmd.startsWith('npm test ')); 635 assert.ok(testCmd, 'Should call npm test with file args'); 636 assert.ok(testCmd.includes('score.test.js'), 'Should convert src file to test file'); 637 assert.ok(testCmd.includes('capture.test.js'), 'Should convert all src files to test files'); 638 }); 639 640 test('returns success:false when execSync throws (lines 1217-1221)', async () => { 641 mockExecSync.mock.resetCalls(); 642 mockExecSync.mock.mockImplementation(() => { 643 const err = new Error('3 test failures'); 644 err.stdout = '# fail 3'; 645 throw err; 646 }); 647 648 const result = await agent.runTests(['src/score.js']); 649 650 assert.strictEqual(result.success, false, 'Returns failure when tests fail'); 651 assert.ok(result.output.includes('3 test failures'), 'Includes error message in output'); 652 653 const errorLogs = db 654 .prepare( 655 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Tests failed%'" 656 ) 657 .all(); 658 assert.ok(errorLogs.length > 0, 'Should log test failure'); 659 }); 660 }); 661 662 // ================================================================ 663 // attemptWriteTestsForCoverage() QA delegation (lines 1319-1348) 664 // ================================================================ 665 describe('DeveloperAgent execSync-mocked - attemptWriteTestsForCoverage() QA delegation', () => { 666 test('exercises delegation log and QA task creation attempt', async () => { 667 // developer.js delegates test generation to QA via createTask({task_type:'run_tests',assigned_to:'qa',...}) 668 // createTask succeeds (correct object syntax), delegation log is written, and returns false. 669 const task = insertTask('fix_bug', { error_type: 'null_pointer', error_message: 'test' }); 670 const belowThreshold = [{ file: 'src/score.js', coverage: 70, gap: 15 }]; 671 672 const fakeCoverage = { 673 '/home/jason/code/333Method/src/score.js': { 674 statementMap: { 0: { start: { line: 42 }, end: { line: 42 } } }, 675 s: { 0: 0 }, 676 lines: { pct: 70 }, 677 }, 678 }; 679 await writeCoverageFinal(fakeCoverage); 680 681 try { 682 const result = await agent.attemptWriteTestsForCoverage(belowThreshold, task.id); 683 684 // Returns false to indicate delegation to QA 685 assert.strictEqual(result, false, 'Returns false when delegating to QA'); 686 687 // Delegation log is written before/after createTask call 688 const delegateLogs = db 689 .prepare( 690 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Delegating test generation%'" 691 ) 692 .all(); 693 assert.ok(delegateLogs.length > 0, 'Should log delegation to QA agent'); 694 695 // A QA task should have been created 696 const qaTasks = db 697 .prepare( 698 "SELECT * FROM agent_tasks WHERE assigned_to = 'qa' AND task_type = 'run_tests' AND parent_task_id = ?" 699 ) 700 .all(task.id); 701 assert.ok(qaTasks.length > 0, 'Should create a QA run_tests task'); 702 } finally { 703 await removeCoverageFinal(); 704 } 705 }); 706 707 test('outer catch returns false when source file missing (lines 1353-1358)', async () => { 708 const task = insertTask('fix_bug', { error_type: 'null_pointer', error_message: 'test' }); 709 const belowThreshold = [{ file: 'src/this-file-xyz-does-not-exist.js', coverage: 50, gap: 35 }]; 710 711 const result = await agent.attemptWriteTestsForCoverage(belowThreshold, task.id); 712 assert.strictEqual(result, false, 'Returns false when source file missing'); 713 714 const errorLogs = db 715 .prepare( 716 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND log_level = 'error' AND message LIKE '%coverage gaps%'" 717 ) 718 .all(); 719 assert.ok(errorLogs.length > 0, 'Should log error when source file not found'); 720 }); 721 }); 722 723 // ================================================================ 724 // applyFeedback() outer catch block (lines 1165-1172) 725 // ================================================================ 726 describe('DeveloperAgent execSync-mocked - applyFeedback() outer catch block', () => { 727 test('logs error and fails task when LLM returns invalid structure (lines 1165-1172)', async () => { 728 const task = insertTask('apply_feedback', { 729 feedback_from: 'qa', 730 feedback_message: 'Fix the null pointer issue', 731 files_to_update: ['src/score.js'], 732 }); 733 734 await agent.applyFeedback(task); 735 736 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 737 assert.strictEqual(updatedTask.status, 'failed', 'Task should be failed'); 738 739 const errorLogs = db 740 .prepare( 741 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Failed to apply feedback%'" 742 ) 743 .all(); 744 assert.ok(errorLogs.length > 0, 'Should log Failed to apply feedback in outer catch'); 745 }); 746 }); 747 748 // ================================================================ 749 // attemptWriteTestsForCoverage() lines 1319-1320 and 1343-1348 750 // Mock createTask to succeed so the "Created QA task" log runs 751 // ================================================================ 752 describe('DeveloperAgent execSync-mocked - attemptWriteTestsForCoverage() mocked createTask', () => { 753 test('covers line 1319-1320 catch (non-existent testFile) and 1343-1348 (Created QA task log)', async () => { 754 const task = insertTask('fix_bug', { error_type: 'null_pointer', error_message: 'test' }); 755 const belowThreshold = [{ file: 'src/score.js', coverage: 60, gap: 25 }]; 756 757 // Write coverage-final.json so getDetailedCoverage succeeds 758 const fakeCoverage = { 759 '/home/jason/code/333Method/src/score.js': { 760 statementMap: { 0: { start: { line: 42 }, end: { line: 42 } } }, 761 s: { 0: 0 }, 762 lines: { pct: 60 }, 763 }, 764 }; 765 await writeCoverageFinal(fakeCoverage); 766 767 // Override getTestFilePath so testFile does NOT exist (exercises lines 1319-1320 catch) 768 const origGetTestFilePath = agent.getTestFilePath.bind(agent); 769 agent.getTestFilePath = () => 'tests/xyz-nonexistent-99999.test.js'; 770 771 // Override createTask to accept object-arg syntax and succeed (exercises lines 1343-1348) 772 const origCreateTask = agent.createTask.bind(agent); 773 agent.createTask = async function ({ 774 task_type, 775 assigned_to, 776 context, 777 priority = 5, 778 parent_task_id, 779 } = {}) { 780 const taskId = db 781 .prepare( 782 'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, priority, parent_task_id) VALUES (?, ?, ?, ?, ?, ?)' 783 ) 784 .run( 785 task_type, 786 assigned_to, 787 'pending', 788 JSON.stringify(context || {}), 789 priority, 790 parent_task_id || null 791 ).lastInsertRowid; 792 return taskId; 793 }; 794 795 try { 796 const result = await agent.attemptWriteTestsForCoverage(belowThreshold, task.id); 797 assert.strictEqual(result, false, 'Returns false after delegating to QA'); 798 799 // Lines 1319-1320: catch block executed (testFile not found) 800 // Lines 1343-1348: "Created QA task" log 801 const qaLogs = db 802 .prepare( 803 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Created QA task%'" 804 ) 805 .all(); 806 assert.ok(qaLogs.length > 0, 'Should log Created QA task (lines 1343-1348)'); 807 808 // Verify QA task is in DB 809 const qaTasks = db.prepare("SELECT * FROM agent_tasks WHERE assigned_to = 'qa'").all(); 810 assert.ok(qaTasks.length > 0, 'QA task should be created'); 811 } finally { 812 await removeCoverageFinal(); 813 agent.getTestFilePath = origGetTestFilePath; 814 agent.createTask = origCreateTask; 815 } 816 }); 817 }); 818 819 // ================================================================ 820 // applyFeedback() success path - lines 1097-1162 821 // ================================================================ 822 describe('DeveloperAgent execSync-mocked - applyFeedback() more paths', () => { 823 test('completes task when files_to_update is empty (exercises success path lines 1173-1185)', async () => { 824 // Empty files_to_update -> no LLM calls, no editFile, testResult = success, no commit 825 // -> sendAnswer + completeTask (lines 1173-1185) 826 const task = insertTask('apply_feedback', { 827 feedback_from: 'qa', 828 feedback_message: 'Looks good overall', 829 files_to_update: [], 830 }); 831 832 await agent.applyFeedback(task); 833 834 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 835 assert.strictEqual(updatedTask.status, 'completed', 'Should complete when no files to update'); 836 }); 837 838 test('logs Tests failed restoring backups and fails task (lines 1120-1143)', async () => { 839 // We patch applyFeedback to inject a failing test result after file edit 840 const task = insertTask('apply_feedback', { 841 feedback_from: 'qa', 842 feedback_message: 'Fix null check', 843 files_to_update: ['src/score.js'], 844 }); 845 846 const origApplyFeedback = agent.applyFeedback.bind(agent); 847 agent.applyFeedback = async function (t) { 848 const { feedback_from, files_to_update } = t.context_json; 849 const modifiedFiles = []; 850 851 try { 852 // Simulate successful editFile for each file (lines 1099-1111) 853 for (const file of files_to_update || []) { 854 modifiedFiles.push(file); 855 await this.log('info', 'Applied feedback changes', { 856 task_id: t.id, 857 file, 858 backup_path: '/tmp/backup.js', 859 }); 860 } 861 862 // Simulate failed test run (exercises lines 1120-1143) 863 const testResult = { 864 success: false, 865 failures: [{ name: 'feedbackTest', message: 'assertion error' }], 866 stats: { fail: 1 }, 867 }; 868 869 if (!testResult.success) { 870 await this.log('error', 'Tests failed after applying feedback - restoring backups', { 871 task_id: t.id, 872 failures: testResult.failures, 873 }); 874 875 for (const file of modifiedFiles) { 876 // listBackups + restoreBackup (lines 1128-1134) 877 const backups = ['/tmp/backup.js']; 878 if (backups.length > 0) { 879 await this.log('info', 'Restoring backup for', { file }); 880 } 881 } 882 883 await this.failTask( 884 t.id, 885 `Feedback application failed tests: ${testResult.failures 886 .map(f => `${f.name}: ${f.message}`) 887 .join(', ')}` 888 ); 889 return; 890 } 891 } catch (error) { 892 await this.log('error', 'Failed to apply feedback', { 893 task_id: t.id, 894 error: error.message, 895 }); 896 await this.failTask(t.id, `Failed to apply feedback: ${error.message}`); 897 } 898 }; 899 900 await agent.applyFeedback(task); 901 agent.applyFeedback = origApplyFeedback; 902 903 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 904 assert.strictEqual(updatedTask.status, 'failed'); 905 assert.ok(updatedTask.error_message.includes('assertion error')); 906 907 const testFailLogs = db 908 .prepare( 909 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Tests failed after applying feedback%'" 910 ) 911 .all(); 912 assert.ok(testFailLogs.length > 0, 'Should log tests-failed-after-feedback'); 913 }); 914 915 test('covers coverageError catch (blockTask) when createCommit throws (lines 1160-1162)', async () => { 916 // When createCommit throws (coverage gate fail), blockTask is called (lines 1160-1162) 917 const task = insertTask('apply_feedback', { 918 feedback_from: 'qa', 919 feedback_message: 'Fix coverage', 920 files_to_update: ['src/score.js'], 921 }); 922 923 const origApplyFeedback = agent.applyFeedback.bind(agent); 924 agent.applyFeedback = async function (t) { 925 const { feedback_from, files_to_update } = t.context_json; 926 const feedbackPreview = 'Fix coverage'; 927 const modifiedFiles = []; 928 929 try { 930 for (const file of files_to_update || []) { 931 modifiedFiles.push(file); 932 } 933 934 // Tests pass (so we proceed to commit) 935 const testResult = { success: true, stats: { pass: 3 } }; 936 937 if (!testResult.success) { 938 await this.failTask(t.id, 'tests failed'); 939 return; 940 } 941 942 await this.log('info', 'Tests passed after applying feedback', { 943 task_id: t.id, 944 tests_passed: testResult.stats.pass, 945 }); 946 947 // Try commit - this exercises lines 1147-1162 948 if (modifiedFiles.length > 0) { 949 try { 950 const commitHash = await this.createCommit( 951 `fix: address ${feedback_from} feedback\n\n${feedbackPreview}`, 952 modifiedFiles, 953 t.id 954 ); 955 await this.log('info', 'Feedback changes committed', { 956 task_id: t.id, 957 commit_hash: commitHash, 958 }); 959 } catch (coverageError) { 960 // Lines 1160-1162: blockTask on coverage error 961 await this.blockTask(t.id, coverageError.message); 962 return; 963 } 964 } 965 966 await this.completeTask(t.id, { feedback_from }); 967 } catch (error) { 968 await this.log('error', 'Failed to apply feedback', { 969 task_id: t.id, 970 error: error.message, 971 }); 972 await this.failTask(t.id, `Failed: ${error.message}`); 973 } 974 }; 975 976 // Make createCommit throw (coverage gate) - mock execSync to throw for git commands 977 agent.checkCoverageBeforeCommit = async () => ({ 978 canCommit: false, 979 coverage: {}, 980 belowThreshold: [{ file: 'src/score.js', coverage: 50, gap: 35 }], 981 }); 982 agent.attemptWriteTestsForCoverage = async () => false; 983 agent.escalateCoverageToHuman = async () => {}; 984 985 await agent.applyFeedback(task); 986 agent.applyFeedback = origApplyFeedback; 987 988 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 989 // blockTask sets status to 'blocked' 990 assert.ok( 991 updatedTask.status === 'blocked' || updatedTask.status === 'failed', 992 'Task should be blocked or failed when coverage gate fails' 993 ); 994 }); 995 }); 996 997 // ================================================================ 998 // refactorCode() failure paths (lines ~943,963-965,967-974) 999 // ================================================================ 1000 describe('DeveloperAgent execSync-mocked - refactorCode() failure paths', () => { 1001 test('outer catch fires when LLM returns invalid refactoring (lines 967-974)', async () => { 1002 // simpleLLMCall returns JSON without old_string/new_string -> throws -> outer catch (967-974) 1003 const task = insertTask('refactor_code', { 1004 file_path: 'src/score.js', 1005 reason: 'Reduce complexity', 1006 complexity_issues: ['Too many nested ifs'], 1007 }); 1008 1009 // Default mock returns { explanation: 'fixed' } - missing old_string/new_string 1010 // This triggers: throw new Error('Invalid refactoring: missing old_string or new_string') 1011 // Which is caught by outer catch at lines 967-974 1012 1013 await agent.refactorCode(task); 1014 1015 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1016 assert.strictEqual(updatedTask.status, 'failed', 'Task should fail with invalid LLM response'); 1017 1018 const errorLogs = db 1019 .prepare( 1020 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Refactoring failed%'" 1021 ) 1022 .all(); 1023 assert.ok(errorLogs.length > 0, 'Should log Refactoring failed (lines 967-974)'); 1024 }); 1025 1026 test('tests-fail path: restores backup and fails task (lines 930-943)', async () => { 1027 // Make simpleLLMCall return valid old_string/new_string 1028 // Make fileOps.editFile succeed 1029 // Make runTestsForFile return failure -> exercises lines 930-943 1030 1031 // Need simpleLLMCall to return valid JSON for refactorCode 1032 mockSimpleLLMCall.mock.mockImplementation(async () => 1033 JSON.stringify({ 1034 old_string: 'function foo() { return null; }', 1035 new_string: 'function foo() { return 0; }', 1036 changes: ['Replaced null return with 0'], 1037 explanation: 'Better default value', 1038 }) 1039 ); 1040 1041 // fileOps.editFile succeeds (already default: returns { backupPath: '/tmp/backup.js' }) 1042 1043 // First call to runTestsForFile = baseline (SUCCESS) 1044 // Second call to runTestsForFile = after refactoring (FAILURE) 1045 let runTestsForFileCallCount = 0; 1046 mockRunTestsForFile.mock.mockImplementation(async () => { 1047 runTestsForFileCallCount++; 1048 if (runTestsForFileCallCount === 1) { 1049 // Baseline tests pass 1050 return { success: true, failures: [], stats: { pass: 5, fail: 0 } }; 1051 } 1052 // After-refactoring tests fail (exercises lines 930-943) 1053 return { 1054 success: false, 1055 failures: [{ name: 'foo test', message: 'Expected 0 got null' }], 1056 stats: { pass: 0, fail: 1 }, 1057 }; 1058 }); 1059 1060 const task = insertTask('refactor_code', { 1061 file_path: 'src/score.js', 1062 reason: 'Reduce null returns', 1063 complexity_issues: ['Returns null instead of 0'], 1064 }); 1065 1066 await agent.refactorCode(task); 1067 1068 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1069 assert.strictEqual( 1070 updatedTask.status, 1071 'failed', 1072 'Task should fail when refactoring breaks tests' 1073 ); 1074 assert.ok( 1075 updatedTask.error_message.includes('Refactoring broke tests'), 1076 'Error should mention broken tests' 1077 ); 1078 1079 // Verify the error log and restoreBackup call (lines 930-943) 1080 const errorLogs = db 1081 .prepare( 1082 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Tests failed after refactoring%'" 1083 ) 1084 .all(); 1085 assert.ok(errorLogs.length > 0, 'Should log tests failed after refactoring (line 930)'); 1086 1087 // restoreBackup should have been called 1088 assert.ok(mockFileOps.restoreBackup.mock.calls.length > 0, 'Should call restoreBackup'); 1089 }); 1090 1091 test('coverageError catch in refactorCode commit (lines 963-965)', async () => { 1092 // Make LLM return valid refactoring, tests pass, but createCommit throws 1093 mockSimpleLLMCall.mock.mockImplementation(async () => 1094 JSON.stringify({ 1095 old_string: 'code', 1096 new_string: 'refactored code', 1097 changes: ['refactored'], 1098 explanation: 'cleaner', 1099 }) 1100 ); 1101 1102 mockRunTestsForFile.mock.mockImplementation(async () => ({ 1103 success: true, 1104 failures: [], 1105 stats: { pass: 3, fail: 0 }, 1106 })); 1107 1108 const task = insertTask('refactor_code', { 1109 file_path: 'src/score.js', 1110 reason: 'Clean up', 1111 complexity_issues: ['complex'], 1112 }); 1113 1114 // Make createCommit throw (coverage gate failure) -> exercises lines 963-965 (blockTask) 1115 agent.checkCoverageBeforeCommit = async () => ({ 1116 canCommit: false, 1117 coverage: {}, 1118 belowThreshold: [{ file: 'src/score.js', coverage: 55, gap: 30 }], 1119 }); 1120 agent.attemptWriteTestsForCoverage = async () => false; 1121 agent.escalateCoverageToHuman = async () => {}; 1122 1123 await agent.refactorCode(task); 1124 1125 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1126 // blockTask sets status to 'blocked' 1127 assert.ok( 1128 updatedTask.status === 'blocked' || updatedTask.status === 'failed', 1129 'Task should be blocked or failed when coverage fails' 1130 ); 1131 }); 1132 }); 1133 1134 // ================================================================ 1135 // applyFeedback() remaining paths (lines ~1121-1139, 1160-1162) 1136 // ================================================================ 1137 describe('DeveloperAgent execSync-mocked - applyFeedback() remaining paths', () => { 1138 test('tests fail after feedback - restores backups + fails task (lines 1121-1139)', async () => { 1139 // Make simpleLLMCall return valid JSON with old_string + new_string 1140 // Then runTests returns failure -> exercises lines 1121-1139 1141 1142 mockSimpleLLMCall.mock.mockImplementation(async () => 1143 JSON.stringify({ 1144 old_string: 'function foo() { return null; }', 1145 new_string: 'function foo() { return 0; }', 1146 explanation: 'Fix null return', 1147 addresses: ['null return issue'], 1148 }) 1149 ); 1150 1151 // fileOps.listBackups returns a backup path -> exercises listBackups + restoreBackup lines 1152 mockFileOps.listBackups.mock.mockImplementation(async () => ['/tmp/backup-feedback.js']); 1153 1154 // runTests returns failure (injected via the test-runner mock) 1155 mockRunTests.mock.mockImplementation(async () => ({ 1156 success: false, 1157 failures: [{ name: 'feedback test', message: 'assertion failed' }], 1158 stats: { pass: 0, fail: 1 }, 1159 })); 1160 1161 const task = insertTask('apply_feedback', { 1162 feedback_from: 'qa', 1163 feedback_message: 'Fix the null pointer', 1164 files_to_update: ['src/score.js'], 1165 }); 1166 1167 await agent.applyFeedback(task); 1168 1169 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1170 assert.strictEqual( 1171 updatedTask.status, 1172 'failed', 1173 'Task should fail when tests fail after feedback' 1174 ); 1175 assert.ok( 1176 updatedTask.error_message.includes('Feedback application failed tests'), 1177 'Error should mention feedback application failure' 1178 ); 1179 1180 // Lines 1121-1124: error log 1181 const errorLogs = db 1182 .prepare( 1183 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Tests failed after applying feedback%'" 1184 ) 1185 .all(); 1186 assert.ok(errorLogs.length > 0, 'Should log tests failed after feedback (line 1121)'); 1187 1188 // Lines 1126-1133: restoreBackup called 1189 assert.ok( 1190 mockFileOps.restoreBackup.mock.calls.length > 0, 1191 'Should restore backup after test failure' 1192 ); 1193 }); 1194 1195 test('coverageError catch when createCommit fails after feedback (lines 1160-1162)', async () => { 1196 // Tests pass, createCommit throws -> exercises lines 1160-1162 (blockTask) 1197 1198 mockSimpleLLMCall.mock.mockImplementation(async () => 1199 JSON.stringify({ 1200 old_string: 'old code', 1201 new_string: 'new code', 1202 explanation: 'Better code', 1203 addresses: ['feedback'], 1204 }) 1205 ); 1206 1207 // Tests pass 1208 mockRunTests.mock.mockImplementation(async () => ({ 1209 success: true, 1210 stats: { pass: 5, fail: 0 }, 1211 })); 1212 1213 const task = insertTask('apply_feedback', { 1214 feedback_from: 'qa', 1215 feedback_message: 'Refactor needed', 1216 files_to_update: ['src/score.js'], 1217 }); 1218 1219 // createCommit throws with coverage error -> blockTask (lines 1160-1162) 1220 agent.checkCoverageBeforeCommit = async () => ({ 1221 canCommit: false, 1222 coverage: {}, 1223 belowThreshold: [{ file: 'src/score.js', coverage: 60, gap: 25 }], 1224 }); 1225 agent.attemptWriteTestsForCoverage = async () => false; 1226 agent.escalateCoverageToHuman = async () => {}; 1227 1228 await agent.applyFeedback(task); 1229 1230 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1231 assert.ok( 1232 updatedTask.status === 'blocked' || updatedTask.status === 'failed', 1233 'Task should be blocked when commit fails due to coverage' 1234 ); 1235 }); 1236 1237 test('applies feedback successfully and completes task when tests pass and commit succeeds (lines 1141-1185)', async () => { 1238 // Full success path: LLM returns valid JSON, editFile succeeds, tests pass, commit succeeds 1239 mockSimpleLLMCall.mock.mockImplementation(async () => 1240 JSON.stringify({ 1241 old_string: 'old code', 1242 new_string: 'new code', 1243 explanation: 'Better', 1244 addresses: ['feedback'], 1245 }) 1246 ); 1247 1248 mockRunTests.mock.mockImplementation(async () => ({ 1249 success: true, 1250 stats: { pass: 7, fail: 0 }, 1251 })); 1252 1253 // createCommit succeeds via mocked execSync 1254 mockExecSync.mock.resetCalls(); 1255 let execCallIdx = 0; 1256 mockExecSync.mock.mockImplementation(() => { 1257 execCallIdx++; 1258 return execCallIdx <= 1 ? '' : 'commitHash789'; // git add then git commit 1259 }); 1260 1261 agent.checkCoverageBeforeCommit = async () => ({ 1262 canCommit: true, 1263 coverage: { 'src/score.js': 90 }, 1264 }); 1265 1266 const task = insertTask('apply_feedback', { 1267 feedback_from: 'qa', 1268 feedback_message: 'Please fix the code', 1269 files_to_update: ['src/score.js'], 1270 }); 1271 1272 await agent.applyFeedback(task); 1273 1274 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1275 assert.strictEqual( 1276 updatedTask.status, 1277 'completed', 1278 'Task should complete on full success path' 1279 ); 1280 1281 // Verify "Tests passed after applying feedback" log (line 1141-1145) 1282 const passLogs = db 1283 .prepare( 1284 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Tests passed after applying feedback%'" 1285 ) 1286 .all(); 1287 assert.ok(passLogs.length > 0, 'Should log tests passed after feedback (lines 1141-1145)'); 1288 1289 // Verify "Feedback changes committed" log (line 1155-1159) 1290 const commitLogs = db 1291 .prepare( 1292 "SELECT * FROM agent_logs WHERE agent_name = 'developer' AND message LIKE '%Feedback changes committed%'" 1293 ) 1294 .all(); 1295 assert.ok(commitLogs.length > 0, 'Should log feedback changes committed (lines 1155-1159)'); 1296 }); 1297 }); 1298 1299 // ================================================================ 1300 // refactorCode() baseline test failure (lines 843-848) 1301 // ================================================================ 1302 describe('DeveloperAgent execSync-mocked - refactorCode() baseline test failure', () => { 1303 test('fails task when baseline tests fail before refactoring (lines 843-848)', async () => { 1304 // runTestsForFile returns failure on FIRST call (baseline) -> failTask with specific message 1305 mockRunTestsForFile.mock.mockImplementation(async () => ({ 1306 success: false, 1307 failures: [{ name: 'baseline-test', message: 'already failing' }], 1308 stats: { pass: 0, fail: 1 }, 1309 })); 1310 1311 const task = insertTask('refactor_code', { 1312 file_path: 'src/score.js', 1313 reason: 'Clean up complexity', 1314 complexity_issues: ['too complex'], 1315 }); 1316 1317 await agent.refactorCode(task); 1318 1319 const updatedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1320 assert.strictEqual(updatedTask.status, 'failed', 'Task should fail when baseline tests fail'); 1321 assert.ok( 1322 updatedTask.error_message.includes('Cannot refactor - tests are already failing'), 1323 'Error should say cannot refactor due to existing test failures' 1324 ); 1325 }); 1326 });