developer-coverage3.test.js
1 /** 2 * Developer Agent Coverage3 Tests 3 * 4 * Targets uncovered paths NOT covered by developer.test.js or developer-coverage2.test.js: 5 * - createCommit() - happy path (git add + commit via execSync), commit failure (throw) 6 * - createCommit() - testsWritten=true branch (recheck passes/fails) 7 * - getFileCoverage() - execSync success + coverage data lookup (multiple path formats), 8 * file not found in coverage data (warn + 0), execSync failure catch 9 * - getDetailedCoverage() - execSync success + coverage parse, fileKey not found, 10 * readFileCoverage failure (returns null), uncovered line extraction 11 * - attemptWriteTestsForCoverage() - full loop (read source, getDetailedCoverage, read test, 12 * missing test file, createTask), no coverageData (continue), 13 * outer catch branch 14 * - escalateCoverageToHuman() - askQuestion called with correct content 15 * - runTests() - success path (with files + empty files), failure path (execSync throws) 16 * - fileExists() - file exists (returns true), file missing (returns false) 17 * - implementFeature() - new file creation path (writeFile), parent task result_json parsing, 18 * requirements as plain string, missing file_content throws 19 * - processTask() - 'unknown' task type → delegateToCorrectAgent 20 * - refactorCode() - complexity_issues as non-array string 21 * - applyFeedback() - empty files_to_update → skips commit block 22 * 23 * Run with: 24 * NODE_ENV=test LOGS_DIR=/tmp/test-logs DATABASE_PATH=/tmp/test-dev-cov3.db \ 25 * node \ 26 * --experimental-test-module-mocks --test tests/agents/developer-coverage3.test.js 27 */ 28 29 import { test, describe, mock, beforeEach, afterEach } from 'node:test'; 30 import assert from 'node:assert/strict'; 31 import Database from 'better-sqlite3'; 32 import { resetDb as resetBaseDb } from '../../src/agents/base-agent.js'; 33 import { resetDb as resetTaskDb } from '../../src/agents/utils/task-manager.js'; 34 import { resetDb as resetMessageDb } from '../../src/agents/utils/message-manager.js'; 35 import fsPromises from 'fs/promises'; 36 37 // ---------------------------------------------------------------- 38 // Mock module-level dependencies BEFORE importing DeveloperAgent 39 // ---------------------------------------------------------------- 40 41 const mockReadFile = mock.fn(async () => ({ 42 content: 'function foo() { return null; }', 43 size: 32, 44 })); 45 const mockGetFileContext = mock.fn(async () => ({ 46 imports: ['import fs from "node:fs"'], 47 testFiles: ['tests/score.test.js'], 48 })); 49 const mockEditFile = mock.fn(async () => ({ backupPath: '/tmp/backup-cov3.js', diff: 'changed' })); 50 const mockWriteFile = mock.fn(async () => ({ backupPath: '/tmp/new-cov3.js' })); 51 const mockRestoreBackup = mock.fn(async () => {}); 52 const mockCleanupBackups = mock.fn(async () => {}); 53 const mockListBackups = mock.fn(async () => ['/tmp/backup-cov3.js']); 54 55 mock.module('../../src/agents/utils/file-operations.js', { 56 namedExports: { 57 readFile: mockReadFile, 58 getFileContext: mockGetFileContext, 59 editFile: mockEditFile, 60 writeFile: mockWriteFile, 61 restoreBackup: mockRestoreBackup, 62 cleanupBackups: mockCleanupBackups, 63 listBackups: mockListBackups, 64 }, 65 }); 66 67 const mockRunTests = mock.fn(async () => ({ 68 success: true, 69 stats: { pass: 5, fail: 0 }, 70 failures: [], 71 coverage: 90, 72 })); 73 const mockRunTestsForFile = mock.fn(async () => ({ 74 success: true, 75 stats: { pass: 3, fail: 0 }, 76 failures: [], 77 coverage: 92, 78 })); 79 80 mock.module('../../src/agents/utils/test-runner.js', { 81 namedExports: { 82 runTests: mockRunTests, 83 runTestsForFile: mockRunTestsForFile, 84 }, 85 }); 86 87 // Default: valid JSON fix 88 const mockSimpleLLMCall = mock.fn(async () => 89 JSON.stringify({ 90 old_string: 'function foo() { return null; }', 91 new_string: 'function foo() { return null ?? 0; }', 92 explanation: 'Added nullish coalescing', 93 test_cases: ['test null return'], 94 changes: ['Used nullish coalescing'], 95 addresses: ['null return'], 96 file_content: '// new file\nfunction foo() { return 0; }\nexport { foo };', 97 }) 98 ); 99 100 mock.module('../../src/agents/utils/agent-claude-api.js', { 101 namedExports: { 102 simpleLLMCall: mockSimpleLLMCall, 103 }, 104 }); 105 106 // Import DeveloperAgent AFTER mocks are set up 107 const { DeveloperAgent, _deps } = await import('../../src/agents/developer.js'); 108 109 // ---------------------------------------------------------------- 110 // DB schema 111 // ---------------------------------------------------------------- 112 const DB_SCHEMA = ` 113 CREATE TABLE agent_tasks ( 114 id INTEGER PRIMARY KEY AUTOINCREMENT, 115 task_type TEXT NOT NULL, 116 assigned_to TEXT NOT NULL, 117 created_by TEXT, 118 status TEXT DEFAULT 'pending', 119 priority INTEGER DEFAULT 5, 120 context_json TEXT, 121 result_json TEXT, 122 parent_task_id INTEGER, 123 error_message TEXT, 124 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 125 started_at DATETIME, 126 completed_at DATETIME, 127 retry_count INTEGER DEFAULT 0 128 ); 129 CREATE TABLE agent_messages ( 130 id INTEGER PRIMARY KEY AUTOINCREMENT, 131 task_id INTEGER, 132 from_agent TEXT NOT NULL, 133 to_agent TEXT NOT NULL, 134 message_type TEXT, 135 content TEXT NOT NULL, 136 metadata_json TEXT, 137 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 138 read_at DATETIME 139 ); 140 CREATE TABLE agent_logs ( 141 id INTEGER PRIMARY KEY AUTOINCREMENT, 142 task_id INTEGER, 143 agent_name TEXT NOT NULL, 144 log_level TEXT, 145 message TEXT, 146 data_json TEXT, 147 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 148 ); 149 CREATE TABLE agent_state ( 150 agent_name TEXT PRIMARY KEY, 151 last_active DATETIME DEFAULT CURRENT_TIMESTAMP, 152 current_task_id INTEGER, 153 status TEXT DEFAULT 'idle', 154 metrics_json TEXT 155 ); 156 CREATE TABLE agent_outcomes ( 157 id INTEGER PRIMARY KEY AUTOINCREMENT, 158 task_id INTEGER NOT NULL, 159 agent_name TEXT NOT NULL, 160 task_type TEXT NOT NULL, 161 outcome TEXT NOT NULL, 162 context_json TEXT, 163 result_json TEXT, 164 duration_ms INTEGER, 165 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 166 ); 167 CREATE TABLE agent_llm_usage ( 168 id INTEGER PRIMARY KEY AUTOINCREMENT, 169 agent_name TEXT NOT NULL, 170 task_id INTEGER, 171 model TEXT NOT NULL, 172 prompt_tokens INTEGER NOT NULL, 173 completion_tokens INTEGER NOT NULL, 174 cost_usd DECIMAL(10, 6) NOT NULL, 175 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 176 ); 177 CREATE TABLE structured_logs ( 178 id INTEGER PRIMARY KEY AUTOINCREMENT, 179 agent_name TEXT, 180 task_id INTEGER, 181 level TEXT, 182 message TEXT, 183 data_json TEXT, 184 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 185 ); 186 `; 187 188 const TEST_DB_PATH = `/tmp/test-developer-cov3-${Date.now()}.db`; 189 let db; 190 let agent; 191 192 function resetMocks() { 193 mockReadFile.mock.resetCalls(); 194 mockGetFileContext.mock.resetCalls(); 195 mockEditFile.mock.resetCalls(); 196 mockWriteFile.mock.resetCalls(); 197 mockRestoreBackup.mock.resetCalls(); 198 mockCleanupBackups.mock.resetCalls(); 199 mockListBackups.mock.resetCalls(); 200 mockRunTests.mock.resetCalls(); 201 mockRunTestsForFile.mock.resetCalls(); 202 mockSimpleLLMCall.mock.resetCalls(); 203 } 204 205 // Save original _deps functions so we can restore them 206 let savedDeps = {}; 207 208 beforeEach(async () => { 209 resetMocks(); 210 211 // Reset all _deps overrides 212 Object.assign(_deps, savedDeps); 213 214 try { 215 await fsPromises.unlink(TEST_DB_PATH); 216 } catch (_e) { 217 /* ignore */ 218 } 219 220 db = new Database(TEST_DB_PATH); 221 process.env.DATABASE_PATH = TEST_DB_PATH; 222 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 223 process.env.AGENT_IMMEDIATE_INVOCATION = 'false'; 224 225 db.exec(DB_SCHEMA); 226 227 agent = new DeveloperAgent(); 228 await agent.initialize(); 229 }); 230 231 afterEach(async () => { 232 resetBaseDb(); 233 resetTaskDb(); 234 resetMessageDb(); 235 if (db) db.close(); 236 try { 237 await fsPromises.unlink(TEST_DB_PATH); 238 } catch (_e) { 239 /* ignore */ 240 } 241 }); 242 243 // Capture initial _deps values 244 savedDeps = { ...Object.fromEntries(Object.entries(_deps).map(([k, v]) => [k, v])) }; 245 246 function insertTask(taskType, context, extra = {}) { 247 const taskId = db 248 .prepare( 249 `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, parent_task_id) 250 VALUES (?, ?, ?, ?, ?)` 251 ) 252 .run( 253 taskType, 254 'developer', 255 'pending', 256 JSON.stringify(context), 257 extra.parent_task_id || null 258 ).lastInsertRowid; 259 260 const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId); 261 task.context_json = JSON.parse(task.context_json); 262 return task; 263 } 264 265 // ================================================================ 266 // runTests() method - the instance method (not _deps.runTests) 267 // ================================================================ 268 describe('DeveloperAgent - runTests() instance method', () => { 269 test('runTests() with no files builds "npm test" command and returns success', async () => { 270 // Override _deps.execSync to return a success output 271 _deps.execSync = mock.fn(() => 'All tests passed\n'); 272 273 const result = await agent.runTests([]); 274 275 assert.strictEqual(result.success, true); 276 assert.ok(typeof result.output === 'string'); 277 278 // Should have called execSync with "npm test" 279 const { calls } = _deps.execSync.mock; 280 assert.ok(calls.length >= 1, 'execSync should be called'); 281 assert.ok(calls[calls.length - 1].arguments[0].includes('npm test'), 'Should call npm test'); 282 }); 283 284 test('runTests() with specific files includes them in command', async () => { 285 _deps.execSync = mock.fn(() => 'Tests passed\n'); 286 287 const result = await agent.runTests(['src/score.js', 'src/capture.js']); 288 289 assert.strictEqual(result.success, true); 290 291 const { calls } = _deps.execSync.mock; 292 const cmd = calls[calls.length - 1].arguments[0]; 293 // Command should include test file paths 294 assert.ok(cmd.includes('npm test'), 'Should use npm test'); 295 assert.ok( 296 cmd.includes('score.test.js') || cmd.includes('capture.test.js'), 297 'Should include test file paths' 298 ); 299 }); 300 301 test('runTests() catches execSync errors and returns failure', async () => { 302 _deps.execSync = mock.fn(() => { 303 throw new Error('npm test failed: 3 tests failed'); 304 }); 305 306 const result = await agent.runTests(['src/score.js']); 307 308 assert.strictEqual(result.success, false); 309 assert.ok(result.output.includes('npm test failed') || result.output.includes('failed')); 310 }); 311 }); 312 313 // ================================================================ 314 // fileExists() - tests file access 315 // ================================================================ 316 describe('DeveloperAgent - fileExists()', () => { 317 test('returns true for existing file', async () => { 318 // /tmp always exists on Linux 319 const result = await agent.fileExists('/tmp'); 320 assert.strictEqual(result, true); 321 }); 322 323 test('returns false for non-existent file', async () => { 324 const result = await agent.fileExists('/tmp/this-file-definitely-does-not-exist-xyz123.js'); 325 assert.strictEqual(result, false); 326 }); 327 }); 328 329 // ================================================================ 330 // getFileCoverage() - various path resolution scenarios 331 // ================================================================ 332 describe('DeveloperAgent - getFileCoverage()', () => { 333 test('returns coverage from exact file key in coverage data', async () => { 334 const coverageSummary = { 335 'src/score.js': { lines: { pct: 88.5 } }, 336 'src/capture.js': { lines: { pct: 91.2 } }, 337 }; 338 339 _deps.execSync = mock.fn(() => ''); 340 _deps.readFileCoverage = mock.fn(async () => JSON.stringify(coverageSummary)); 341 342 const result = await agent.getFileCoverage(['src/score.js', 'src/capture.js']); 343 344 assert.strictEqual(result['src/score.js'], 88.5); 345 assert.strictEqual(result['src/capture.js'], 91.2); 346 }); 347 348 test('falls back to absolute path when relative path not in coverage data', async () => { 349 const projectRoot = process.cwd(); 350 const absPath = `${projectRoot}/src/score.js`; 351 352 const coverageSummary = { 353 [absPath]: { lines: { pct: 76.0 } }, 354 }; 355 356 _deps.execSync = mock.fn(() => ''); 357 _deps.readFileCoverage = mock.fn(async () => JSON.stringify(coverageSummary)); 358 359 const result = await agent.getFileCoverage(['src/score.js']); 360 361 assert.strictEqual(result['src/score.js'], 76.0); 362 }); 363 364 test('falls back to /src/score.js (with leading slash) key', async () => { 365 const coverageSummary = { 366 '/src/score.js': { lines: { pct: 82.3 } }, 367 }; 368 369 _deps.execSync = mock.fn(() => ''); 370 _deps.readFileCoverage = mock.fn(async () => JSON.stringify(coverageSummary)); 371 372 const result = await agent.getFileCoverage(['src/score.js']); 373 374 assert.strictEqual(result['src/score.js'], 82.3); 375 }); 376 377 test('logs warning and returns 0 when file not found in coverage data under any key', async () => { 378 const coverageSummary = { 379 'src/other-file.js': { lines: { pct: 95.0 } }, 380 }; 381 382 _deps.execSync = mock.fn(() => ''); 383 _deps.readFileCoverage = mock.fn(async () => JSON.stringify(coverageSummary)); 384 385 const result = await agent.getFileCoverage(['src/score.js']); 386 387 assert.strictEqual(result['src/score.js'], 0); 388 389 // Should have logged a warning 390 const warnLogs = db 391 .prepare( 392 "SELECT * FROM agent_logs WHERE log_level = 'warn' AND message LIKE '%Coverage data not found%'" 393 ) 394 .all(); 395 assert.ok(warnLogs.length > 0, 'Should warn when file not in coverage data'); 396 }); 397 398 test('returns 0 for all files when execSync throws', async () => { 399 _deps.execSync = mock.fn(() => { 400 throw new Error('npm test failed with exit code 1'); 401 }); 402 403 const result = await agent.getFileCoverage(['src/score.js', 'src/capture.js']); 404 405 assert.strictEqual(result['src/score.js'], 0); 406 assert.strictEqual(result['src/capture.js'], 0); 407 408 // Should have logged an error 409 const errorLogs = db 410 .prepare( 411 "SELECT * FROM agent_logs WHERE log_level = 'error' AND message LIKE '%Failed to get coverage%'" 412 ) 413 .all(); 414 assert.ok(errorLogs.length > 0, 'Should log error when execSync throws'); 415 }); 416 417 test('returns 0 for all files when readFileCoverage throws', async () => { 418 _deps.execSync = mock.fn(() => ''); 419 _deps.readFileCoverage = mock.fn(async () => { 420 throw new Error('ENOENT: coverage file not found'); 421 }); 422 423 const result = await agent.getFileCoverage(['src/score.js']); 424 425 assert.strictEqual(result['src/score.js'], 0); 426 }); 427 428 test('handles normalized path (removes leading slashes)', async () => { 429 const coverageSummary = { 430 'src/score.js': { lines: { pct: 90.0 } }, 431 }; 432 433 _deps.execSync = mock.fn(() => ''); 434 _deps.readFileCoverage = mock.fn(async () => JSON.stringify(coverageSummary)); 435 436 // Pass a path with leading slash - should normalize and find it 437 const result = await agent.getFileCoverage(['//src/score.js']); 438 439 // The file has leading slashes - normalization strips them to 'src/score.js' 440 // The key stored in results is the original '//src/score.js' 441 // Normalized lookup should find it 442 const val = result['//src/score.js']; 443 assert.strictEqual(val, 90.0, 'Should resolve via normalized path'); 444 }); 445 }); 446 447 // ================================================================ 448 // getDetailedCoverage() - coverage file parsing 449 // ================================================================ 450 describe('DeveloperAgent - getDetailedCoverage()', () => { 451 test('returns null when readFileCoverage throws (coverage file missing)', async () => { 452 _deps.execSync = mock.fn(() => ''); 453 _deps.readFileCoverage = mock.fn(async p => { 454 if (p.includes('coverage-final.json')) { 455 throw new Error('ENOENT: no such file'); 456 } 457 return ''; 458 }); 459 460 const result = await agent.getDetailedCoverage('src/score.js'); 461 assert.strictEqual(result, null); 462 }); 463 464 test('returns null when coverage-final.json does not contain the file key', async () => { 465 const coverageFinal = { 466 '/some/other/file.js': { 467 s: { 0: 1, 1: 0 }, 468 statementMap: { 469 0: { start: { line: 1 }, end: { line: 1 } }, 470 1: { start: { line: 5 }, end: { line: 5 } }, 471 }, 472 lines: { pct: 50 }, 473 }, 474 }; 475 476 _deps.execSync = mock.fn(() => ''); 477 _deps.readFileCoverage = mock.fn(async () => JSON.stringify(coverageFinal)); 478 479 const result = await agent.getDetailedCoverage('src/score.js'); 480 assert.strictEqual(result, null); 481 }); 482 483 test('returns uncovered lines when file is found in coverage-final.json', async () => { 484 const coverageFinal = { 485 '/some/path/src/score.js': { 486 s: { 0: 5, 1: 0, 2: 3, 3: 0 }, 487 statementMap: { 488 0: { start: { line: 10 }, end: { line: 10 } }, 489 1: { start: { line: 20 }, end: { line: 22 } }, 490 2: { start: { line: 30 }, end: { line: 30 } }, 491 3: { start: { line: 45 }, end: { line: 47 } }, 492 }, 493 lines: { pct: 50 }, 494 }, 495 }; 496 497 _deps.execSync = mock.fn(() => ''); 498 _deps.readFileCoverage = mock.fn(async () => JSON.stringify(coverageFinal)); 499 500 const result = await agent.getDetailedCoverage('src/score.js'); 501 502 assert.ok(result !== null, 'Should return coverage data'); 503 assert.ok(Array.isArray(result.uncoveredLines), 'Should have uncoveredLines array'); 504 assert.strictEqual(result.uncoveredLines.length, 2, 'Should find 2 uncovered statements'); 505 assert.strictEqual(result.coverage, 50); 506 507 // Check uncovered lines contain the right start lines 508 const starts = result.uncoveredLines.map(l => l.start); 509 assert.ok(starts.includes(20), 'Line 20 should be uncovered'); 510 assert.ok(starts.includes(45), 'Line 45 should be uncovered'); 511 }); 512 513 test('returns null when execSync throws', async () => { 514 _deps.execSync = mock.fn(() => { 515 throw new Error('Command failed'); 516 }); 517 518 const result = await agent.getDetailedCoverage('src/score.js'); 519 assert.strictEqual(result, null); 520 }); 521 }); 522 523 // ================================================================ 524 // attemptWriteTestsForCoverage() - full coverage paths 525 // ================================================================ 526 describe('DeveloperAgent - attemptWriteTestsForCoverage()', () => { 527 test('returns false (delegates to QA) when coverageData has uncovered lines', async () => { 528 const task = insertTask('fix_bug', { error_message: 'test' }); 529 530 // Mock readFileCoverage to return source file content 531 _deps.readFileCoverage = mock.fn(async p => { 532 if (p.endsWith('.js') && !p.includes('coverage')) { 533 return 'function foo() { return null; }'; 534 } 535 // For the coverage-final.json 536 return JSON.stringify({ 537 '/abs/src/score.js': { 538 s: { 0: 0, 1: 5 }, 539 statementMap: { 540 0: { start: { line: 5 }, end: { line: 5 } }, 541 1: { start: { line: 10 }, end: { line: 10 } }, 542 }, 543 lines: { pct: 50 }, 544 }, 545 }); 546 }); 547 548 // Override getDetailedCoverage to return uncovered lines 549 const origGetDetail = agent.getDetailedCoverage.bind(agent); 550 agent.getDetailedCoverage = async () => ({ 551 uncoveredLines: [ 552 { start: 5, end: 5 }, 553 { start: 15, end: 17 }, 554 ], 555 coverage: 50, 556 }); 557 558 const result = await agent.attemptWriteTestsForCoverage( 559 [{ file: 'src/score.js', coverage: 50, gap: 35 }], 560 task.id 561 ); 562 563 // Should return false (delegated to QA, didn't write tests itself) 564 assert.strictEqual(result, false); 565 566 // Should have created a QA task 567 const qaTasks = db 568 .prepare("SELECT * FROM agent_tasks WHERE assigned_to = 'qa' AND task_type = 'run_tests'") 569 .all(); 570 assert.ok(qaTasks.length > 0, 'Should create QA run_tests task'); 571 572 const ctx = JSON.parse(qaTasks[0].context_json); 573 assert.strictEqual(ctx.source_file, 'src/score.js'); 574 assert.strictEqual(ctx.target_coverage, 85); 575 assert.ok(Array.isArray(ctx.uncovered_lines), 'Should pass uncovered lines to QA'); 576 577 agent.getDetailedCoverage = origGetDetail; 578 }); 579 580 test('reads existing test file when it exists', async () => { 581 const task = insertTask('fix_bug', { error_message: 'test' }); 582 583 // readFileCoverage returns content for both source and test files 584 _deps.readFileCoverage = mock.fn(async p => { 585 if (p.includes('coverage')) { 586 return JSON.stringify({ 587 '/abs/src/capture.js': { 588 s: { 0: 0 }, 589 statementMap: { 0: { start: { line: 10 }, end: { line: 10 } } }, 590 lines: { pct: 60 }, 591 }, 592 }); 593 } 594 return '// existing test file content\ntest("foo", () => {});'; 595 }); 596 597 const origGetDetail = agent.getDetailedCoverage.bind(agent); 598 agent.getDetailedCoverage = async () => ({ 599 uncoveredLines: [{ start: 10, end: 10 }], 600 coverage: 60, 601 }); 602 603 const result = await agent.attemptWriteTestsForCoverage( 604 [{ file: 'src/capture.js', coverage: 60, gap: 25 }], 605 task.id 606 ); 607 608 assert.strictEqual(result, false); 609 610 // readFileCoverage should have been called for the test file too 611 const readCalls = _deps.readFileCoverage.mock.calls; 612 assert.ok(readCalls.length >= 2, 'Should read source file and test file'); 613 614 agent.getDetailedCoverage = origGetDetail; 615 }); 616 617 test('continues to next file when getDetailedCoverage returns null', async () => { 618 const task = insertTask('fix_bug', { error_message: 'test' }); 619 620 _deps.readFileCoverage = mock.fn(async () => 'source content'); 621 622 const origGetDetail = agent.getDetailedCoverage.bind(agent); 623 // Return null for first file - should continue, not throw 624 agent.getDetailedCoverage = async () => null; 625 626 const result = await agent.attemptWriteTestsForCoverage( 627 [ 628 { file: 'src/score.js', coverage: 40, gap: 45 }, 629 { file: 'src/capture.js', coverage: 50, gap: 35 }, 630 ], 631 task.id 632 ); 633 634 // Returns false (no tests actually written, delegates to QA when coverage data exists) 635 // With null coverage data for all files, loop completes without creating tasks 636 assert.strictEqual(result, false); 637 638 agent.getDetailedCoverage = origGetDetail; 639 }); 640 641 test('returns false and logs error when readFileCoverage throws in outer catch', async () => { 642 const task = insertTask('fix_bug', { error_message: 'test' }); 643 644 // Make readFileCoverage throw immediately 645 _deps.readFileCoverage = mock.fn(async () => { 646 throw new Error('Permission denied reading source file'); 647 }); 648 649 const result = await agent.attemptWriteTestsForCoverage( 650 [{ file: 'src/score.js', coverage: 40, gap: 45 }], 651 task.id 652 ); 653 654 assert.strictEqual(result, false); 655 656 // Should log an error 657 const errorLogs = db 658 .prepare( 659 "SELECT * FROM agent_logs WHERE log_level = 'error' AND message LIKE '%Failed to analyze coverage%'" 660 ) 661 .all(); 662 assert.ok(errorLogs.length > 0, 'Should log error when coverage analysis fails'); 663 }); 664 }); 665 666 // ================================================================ 667 // escalateCoverageToHuman() - asks architect for guidance 668 // ================================================================ 669 describe('DeveloperAgent - escalateCoverageToHuman()', () => { 670 test('sends question to architect with files below threshold listed', async () => { 671 const task = insertTask('fix_bug', { error_message: 'test' }); 672 673 await agent.escalateCoverageToHuman( 674 [ 675 { file: 'src/score.js', coverage: 65, gap: 20 }, 676 { file: 'src/capture.js', coverage: 72, gap: 13 }, 677 ], 678 task.id 679 ); 680 681 // Should have sent a message to architect 682 const msgs = db.prepare("SELECT * FROM agent_messages WHERE to_agent = 'architect'").all(); 683 assert.ok(msgs.length > 0, 'Should ask architect'); 684 685 const { content } = msgs[0]; 686 assert.ok(content.includes('85%') || content.includes('85'), 'Should mention threshold'); 687 assert.ok(content.includes('src/score.js'), 'Should list affected file'); 688 assert.ok(content.includes('65%') || content.includes('65'), 'Should show current coverage'); 689 690 // Should have logged a warning 691 const warnLogs = db 692 .prepare( 693 "SELECT * FROM agent_logs WHERE log_level = 'warn' AND message LIKE '%Escalating coverage%'" 694 ) 695 .all(); 696 assert.ok(warnLogs.length > 0, 'Should log warning about escalation'); 697 }); 698 699 test('escalateCoverageToHuman works with single file', async () => { 700 const task = insertTask('fix_bug', { error_message: 'test' }); 701 702 await agent.escalateCoverageToHuman( 703 [{ file: 'src/enrich.js', coverage: 45, gap: 40 }], 704 task.id 705 ); 706 707 const msgs = db.prepare("SELECT * FROM agent_messages WHERE to_agent = 'architect'").all(); 708 assert.ok(msgs.length > 0, 'Should send message to architect'); 709 assert.ok(msgs[0].content.includes('src/enrich.js')); 710 }); 711 }); 712 713 // ================================================================ 714 // createCommit() - happy path and failure paths 715 // ================================================================ 716 describe('DeveloperAgent - createCommit()', () => { 717 test('successful commit: stages files and returns commit hash', async () => { 718 const task = insertTask('fix_bug', { error_message: 'test' }); 719 720 // Make coverage check pass 721 const origCheck = agent.checkCoverageBeforeCommit.bind(agent); 722 agent.checkCoverageBeforeCommit = async () => ({ canCommit: true, coverage: {} }); 723 724 // Mock execSync: git add returns '', git commit returns hash 725 _deps.execSync = mock.fn(cmd => { 726 if (cmd.includes('git add')) return ''; 727 if (cmd.includes('git commit')) return 'abc123def456\n'; 728 return ''; 729 }); 730 731 const hash = await agent.createCommit( 732 'fix(scoring): null pointer fix', 733 ['src/score.js'], 734 task.id 735 ); 736 737 assert.ok(typeof hash === 'string', 'Should return a commit hash string'); 738 assert.ok(hash.includes('abc123') || hash.length > 0, 'Hash should be non-empty'); 739 740 // Verify git add was called for each file 741 const execCalls = _deps.execSync.mock.calls; 742 const addCalls = execCalls.filter(c => c.arguments[0].includes('git add')); 743 assert.ok(addCalls.length >= 1, 'Should call git add for files'); 744 assert.ok(addCalls[0].arguments[0].includes('src/score.js'), 'Should add the correct file'); 745 746 // Verify git commit was called 747 const commitCalls = execCalls.filter(c => c.arguments[0].includes('git commit')); 748 assert.ok(commitCalls.length >= 1, 'Should call git commit'); 749 750 agent.checkCoverageBeforeCommit = origCheck; 751 }); 752 753 test('commit failure: throws error when git commit fails', async () => { 754 const task = insertTask('fix_bug', { error_message: 'test' }); 755 756 agent.checkCoverageBeforeCommit = async () => ({ canCommit: true, coverage: {} }); 757 758 _deps.execSync = mock.fn(cmd => { 759 if (cmd.includes('git add')) return ''; 760 if (cmd.includes('git commit')) throw new Error('git: nothing to commit'); 761 return ''; 762 }); 763 764 await assert.rejects( 765 async () => agent.createCommit('fix: test', ['src/score.js'], task.id), 766 /nothing to commit/, 767 'Should re-throw git commit errors' 768 ); 769 770 // Should log the commit failure 771 const errorLogs = db 772 .prepare( 773 "SELECT * FROM agent_logs WHERE log_level = 'error' AND message LIKE '%Commit failed%'" 774 ) 775 .all(); 776 assert.ok(errorLogs.length > 0, 'Should log commit failure'); 777 }); 778 779 test('coverage gate: testsWritten=true branch re-checks and passes → commits', async () => { 780 const task = insertTask('fix_bug', { error_message: 'test' }); 781 782 // First call: canCommit=false; second call (recheck): canCommit=true 783 let checkCallCount = 0; 784 agent.checkCoverageBeforeCommit = async () => { 785 checkCallCount++; 786 if (checkCallCount === 1) { 787 return { 788 canCommit: false, 789 coverage: { 'src/score.js': 70 }, 790 belowThreshold: [{ file: 'src/score.js', coverage: 70, gap: 15 }], 791 reason: 'Coverage gate: 1 file(s) below 85%', 792 }; 793 } 794 return { canCommit: true, coverage: { 'src/score.js': 90 } }; 795 }; 796 797 // attemptWriteTestsForCoverage returns true (tests written successfully) 798 agent.attemptWriteTestsForCoverage = async () => true; 799 800 // git operations succeed 801 _deps.execSync = mock.fn(cmd => { 802 if (cmd.includes('git add')) return ''; 803 if (cmd.includes('git commit')) return 'commit-hash-789\n'; 804 return ''; 805 }); 806 807 const hash = await agent.createCommit('fix: test', ['src/score.js'], task.id); 808 assert.ok(hash.length > 0, 'Should return commit hash after recheck passes'); 809 assert.strictEqual(checkCallCount, 2, 'Should check coverage twice'); 810 }); 811 812 test('coverage gate: testsWritten=true branch re-checks and still fails → throws', async () => { 813 const task = insertTask('fix_bug', { error_message: 'test' }); 814 815 let checkCallCount = 0; 816 agent.checkCoverageBeforeCommit = async () => { 817 checkCallCount++; 818 // Both checks fail 819 return { 820 canCommit: false, 821 coverage: { 'src/score.js': 70 }, 822 belowThreshold: [{ file: 'src/score.js', coverage: 70, gap: 15 }], 823 reason: 'Coverage gate: 1 file(s) below 85%', 824 }; 825 }; 826 827 // attemptWriteTestsForCoverage returns true 828 agent.attemptWriteTestsForCoverage = async () => true; 829 agent.escalateCoverageToHuman = async () => {}; 830 831 await assert.rejects( 832 async () => agent.createCommit('fix: test', ['src/score.js'], task.id), 833 /Coverage still below 85%/, 834 'Should throw when coverage still fails after test generation' 835 ); 836 }); 837 838 test('coverage gate: testsWritten=false → escalates and throws', async () => { 839 const task = insertTask('fix_bug', { error_message: 'test' }); 840 841 agent.checkCoverageBeforeCommit = async () => ({ 842 canCommit: false, 843 coverage: { 'src/score.js': 60 }, 844 belowThreshold: [{ file: 'src/score.js', coverage: 60, gap: 25 }], 845 reason: 'Coverage gate: 1 file(s) below 85%', 846 }); 847 848 agent.attemptWriteTestsForCoverage = async () => false; 849 850 let escalateCalled = false; 851 agent.escalateCoverageToHuman = async () => { 852 escalateCalled = true; 853 }; 854 855 await assert.rejects( 856 async () => agent.createCommit('fix: test', ['src/score.js'], task.id), 857 /Coverage gate failed/, 858 'Should throw coverage gate error' 859 ); 860 861 assert.ok(escalateCalled, 'Should call escalateCoverageToHuman when tests cannot be written'); 862 }); 863 864 test('createCommit with no source files skips coverage gate', async () => { 865 const task = insertTask('fix_bug', { error_message: 'test' }); 866 867 // No src/ files → checkCoverageBeforeCommit returns canCommit=true immediately 868 _deps.execSync = mock.fn(cmd => { 869 if (cmd.includes('git add')) return ''; 870 if (cmd.includes('git commit')) return 'doc-commit-hash\n'; 871 return ''; 872 }); 873 874 const hash = await agent.createCommit( 875 'docs: update README', 876 ['README.md', 'docs/overview.md'], 877 task.id 878 ); 879 880 assert.ok(hash.length > 0, 'Should commit documentation files'); 881 }); 882 }); 883 884 // ================================================================ 885 // processTask() - unknown task type → delegateToCorrectAgent 886 // ================================================================ 887 describe('DeveloperAgent - processTask() unknown task type', () => { 888 test('routes unknown task types to delegateToCorrectAgent', async () => { 889 let delegateCalled = false; 890 let delegatedTask = null; 891 892 agent.delegateToCorrectAgent = async t => { 893 delegateCalled = true; 894 delegatedTask = t; 895 }; 896 897 const task = insertTask('write_tests', { test_file: 'tests/score.test.js' }); 898 await agent.processTask(task); 899 900 assert.ok(delegateCalled, 'Should call delegateToCorrectAgent for unknown task type'); 901 assert.strictEqual(delegatedTask.id, task.id); 902 }); 903 904 test('logs warning for unknown task type before delegating', async () => { 905 agent.delegateToCorrectAgent = async () => {}; 906 907 const task = insertTask('completely_unknown_type', { some: 'data' }); 908 await agent.processTask(task); 909 910 const warnLogs = db 911 .prepare( 912 "SELECT * FROM agent_logs WHERE log_level = 'warn' AND message LIKE '%Unknown task type%'" 913 ) 914 .all(); 915 assert.ok(warnLogs.length > 0, 'Should log warning for unknown task type'); 916 }); 917 }); 918 919 // ================================================================ 920 // implementFeature() - new file creation path (writeFile) 921 // ================================================================ 922 describe('DeveloperAgent - implementFeature() new file creation', () => { 923 test('creates new file when readFile throws (file does not exist)', async () => { 924 // Make validateWorkflowDependencies pass 925 agent.validateWorkflowDependencies = async () => ({ valid: true }); 926 927 // readFile throws → fileExists=false → will use writeFile 928 mockReadFile.mock.mockImplementationOnce(async () => { 929 throw new Error('ENOENT: no such file or directory'); 930 }); 931 932 // LLM returns new file content (no old_string/new_string, just file_content) 933 mockSimpleLLMCall.mock.mockImplementationOnce(async () => 934 JSON.stringify({ 935 file_content: '// new module\nexport function newFeature() { return true; }', 936 explanation: 'Created new feature module', 937 test_cases: ['test new feature'], 938 }) 939 ); 940 941 agent.createCommit = async () => 'new-file-hash'; 942 943 const task = insertTask('implement_feature', { 944 feature_description: 'Create new caching module', 945 requirements: ['Cache database results'], 946 files_to_modify: ['src/cache-module.js'], 947 }); 948 949 await agent.implementFeature(task); 950 951 // writeFile should have been called (new file creation) 952 assert.ok(mockWriteFile.mock.calls.length >= 1, 'Should call writeFile for new files'); 953 }); 954 955 test('throws when new file LLM response is missing file_content', async () => { 956 agent.validateWorkflowDependencies = async () => ({ valid: true }); 957 958 // readFile throws → new file path 959 mockReadFile.mock.mockImplementationOnce(async () => { 960 throw new Error('ENOENT'); 961 }); 962 963 // LLM returns response without file_content 964 mockSimpleLLMCall.mock.mockImplementationOnce(async () => 965 JSON.stringify({ 966 explanation: 'Incomplete response', 967 // No file_content 968 }) 969 ); 970 971 const task = insertTask('implement_feature', { 972 feature_description: 'Create new module', 973 requirements: ['Some requirement'], 974 files_to_modify: ['src/new-feature.js'], 975 }); 976 977 await agent.implementFeature(task); 978 979 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 980 assert.strictEqual(updated.status, 'failed'); 981 assert.ok( 982 updated.error_message.includes('file_content') || 983 updated.error_message.includes('Failed to implement'), 984 'Should fail when file_content is missing' 985 ); 986 }); 987 988 test('uses parent task result_json design proposal when parent task exists', async () => { 989 agent.validateWorkflowDependencies = async () => ({ valid: true }); 990 991 // Create a parent task with a result_json design proposal 992 const parentId = db 993 .prepare( 994 `INSERT INTO agent_tasks (task_type, assigned_to, status, result_json) 995 VALUES (?, ?, ?, ?)` 996 ) 997 .run( 998 'implementation_plan', 999 'developer', 1000 'completed', 1001 JSON.stringify({ 1002 design_proposal: { 1003 title: 'Caching Design', 1004 files_affected: ['src/cache.js'], 1005 }, 1006 }) 1007 ).lastInsertRowid; 1008 1009 agent.createCommit = async () => 'feat-with-parent-hash'; 1010 1011 const task = insertTask( 1012 'implement_feature', 1013 { 1014 feature_description: 'Add caching', 1015 requirements: ['Cache responses'], 1016 files_to_modify: ['src/cache.js'], 1017 }, 1018 { parent_task_id: parentId } 1019 ); 1020 1021 await agent.implementFeature(task); 1022 1023 // LLM should have been called with the design proposal context 1024 const llmCalls = mockSimpleLLMCall.mock.calls; 1025 assert.ok(llmCalls.length >= 1, 'Should call LLM'); 1026 const promptArg = llmCalls[0].arguments[2]; 1027 assert.ok( 1028 typeof promptArg === 'object' && typeof promptArg.prompt === 'string', 1029 'Should pass prompt object' 1030 ); 1031 // The prompt should include the design proposal 1032 assert.ok( 1033 promptArg.prompt.includes('Caching Design') || 1034 promptArg.prompt.includes('design_proposal') || 1035 promptArg.prompt.length > 100, 1036 'Prompt should be non-trivial' 1037 ); 1038 }); 1039 1040 test('requirements as plain string (not array) included in prompt', async () => { 1041 agent.validateWorkflowDependencies = async () => ({ valid: true }); 1042 1043 let capturedPrompt = ''; 1044 mockSimpleLLMCall.mock.mockImplementationOnce(async (agentName, taskId, opts) => { 1045 capturedPrompt = opts.prompt; 1046 return JSON.stringify({ 1047 old_string: 'function foo() { return null; }', 1048 new_string: 'function foo() { return 0; }', 1049 explanation: 'Fixed', 1050 test_cases: [], 1051 }); 1052 }); 1053 1054 agent.createCommit = async () => 'string-req-hash'; 1055 1056 const task = insertTask('implement_feature', { 1057 feature_description: 'Add validation', 1058 requirements: 'Validate all inputs before processing', // String, not array 1059 files_to_modify: ['src/score.js'], 1060 }); 1061 1062 await agent.implementFeature(task); 1063 1064 assert.ok( 1065 capturedPrompt.includes('Validate all inputs'), 1066 'Prompt should include plain string requirements' 1067 ); 1068 }); 1069 }); 1070 1071 // ================================================================ 1072 // refactorCode() - complexity_issues as non-array 1073 // ================================================================ 1074 describe('DeveloperAgent - refactorCode() with non-array complexity_issues', () => { 1075 test('handles complexity_issues as plain string (not array)', async () => { 1076 agent.createCommit = async () => 'refactor-string-hash'; 1077 1078 let capturedPrompt = ''; 1079 mockSimpleLLMCall.mock.mockImplementationOnce(async (agentName, taskId, opts) => { 1080 capturedPrompt = opts.prompt; 1081 return JSON.stringify({ 1082 old_string: 'function foo() { return null; }', 1083 new_string: 'function foo() { return null ?? 0; }', 1084 changes: ['Simplified logic'], 1085 explanation: 'Refactored', 1086 }); 1087 }); 1088 1089 const task = insertTask('refactor_code', { 1090 file_path: 'src/score.js', 1091 reason: 'Reduce complexity', 1092 complexity_issues: 'Function too long, nesting too deep', // Plain string 1093 }); 1094 1095 await agent.refactorCode(task); 1096 1097 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1098 assert.strictEqual( 1099 updated.status, 1100 'completed', 1101 'Should complete with string complexity_issues' 1102 ); 1103 1104 assert.ok( 1105 capturedPrompt.includes('Function too long') || capturedPrompt.includes('complexity'), 1106 'Prompt should include complexity issues' 1107 ); 1108 }); 1109 }); 1110 1111 // ================================================================ 1112 // applyFeedback() - edge case: empty files_to_update skips file ops and commit 1113 // ================================================================ 1114 describe('DeveloperAgent - applyFeedback() with empty files list', () => { 1115 test('completes successfully when files_to_update is empty (no file ops, no commit)', async () => { 1116 const task = insertTask('apply_feedback', { 1117 feedback_from: 'qa', 1118 feedback_message: 'Looks good, no changes needed', 1119 files_to_update: [], 1120 }); 1121 1122 await agent.applyFeedback(task); 1123 1124 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1125 assert.strictEqual(updated.status, 'completed', 'Should complete with empty files_to_update'); 1126 1127 // No file reads should have happened 1128 assert.strictEqual( 1129 mockReadFile.mock.calls.length, 1130 0, 1131 'Should not read files when list is empty' 1132 ); 1133 assert.strictEqual( 1134 mockEditFile.mock.calls.length, 1135 0, 1136 'Should not edit files when list is empty' 1137 ); 1138 1139 // Should send answer back to feedback provider 1140 const answers = db.prepare("SELECT * FROM agent_messages WHERE to_agent = 'qa'").all(); 1141 assert.ok(answers.length > 0, 'Should send answer back to qa'); 1142 }); 1143 1144 test('applyFeedback with no files_to_update key defaults to empty and completes', async () => { 1145 const task = insertTask('apply_feedback', { 1146 feedback_from: 'architect', 1147 feedback_message: 'Architecture review complete', 1148 // No files_to_update key 1149 }); 1150 1151 await agent.applyFeedback(task); 1152 1153 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1154 assert.strictEqual(updated.status, 'completed'); 1155 }); 1156 }); 1157 1158 // ================================================================ 1159 // createImplementationPlan() - with requires_migration=true 1160 // ================================================================ 1161 describe('DeveloperAgent - createImplementationPlan() with migration', () => { 1162 test('plan step 1 includes migration folder when requires_migration=true', async () => { 1163 let capturedPlan = null; 1164 const origRequest = agent.requestArchitectApproval.bind(agent); 1165 agent.requestArchitectApproval = async (taskId, plan) => { 1166 capturedPlan = plan; 1167 return origRequest(taskId, plan); 1168 }; 1169 1170 const task = insertTask('implementation_plan', { 1171 design_proposal: { 1172 title: 'Add Conversations Table', 1173 files_affected: ['src/inbound/sms.js'], 1174 requires_migration: true, 1175 risks: ['Migration could fail'], 1176 estimated_effort: 3, 1177 }, 1178 }); 1179 1180 await agent.createImplementationPlan(task); 1181 1182 assert.ok(capturedPlan, 'Should receive plan'); 1183 // Step 1 should include the migrations folder 1184 const step1 = capturedPlan.steps[0]; 1185 assert.ok( 1186 step1.files.length > 0, 1187 'Step 1 should have migration files when requires_migration=true' 1188 ); 1189 assert.ok(step1.files[0].includes('migrations'), 'Should reference migrations folder'); 1190 1191 agent.requestArchitectApproval = origRequest; 1192 }); 1193 }); 1194 1195 // ================================================================ 1196 // fixBug() with contextFiles[0] fallback (uses _deps directly) 1197 // ================================================================ 1198 describe('DeveloperAgent - fixBug() using _deps directly', () => { 1199 test('fixBug happy path uses _deps.readFile, _deps.simpleLLMCall, _deps.editFile, _deps.runTestsForFile', async () => { 1200 // Use _deps injection directly instead of agent method overrides 1201 const origReadFile = _deps.readFile; 1202 const origGetFileContext = _deps.getFileContext; 1203 const origSimpleLLMCall = _deps.simpleLLMCall; 1204 const origEditFile = _deps.editFile; 1205 const origRunTestsForFile = _deps.runTestsForFile; 1206 1207 let readFileCalled = false; 1208 let llmCalled = false; 1209 let editFileCalled = false; 1210 let testsRan = false; 1211 1212 _deps.readFile = async () => { 1213 readFileCalled = true; 1214 return { content: 'function score() { return null; }', size: 40 }; 1215 }; 1216 _deps.getFileContext = async () => ({ imports: [], testFiles: [] }); 1217 _deps.simpleLLMCall = async () => { 1218 llmCalled = true; 1219 return JSON.stringify({ 1220 old_string: 'function score() { return null; }', 1221 new_string: 'function score() { return null ?? 0; }', 1222 explanation: 'Fixed null return', 1223 test_cases: [], 1224 }); 1225 }; 1226 _deps.editFile = async () => { 1227 editFileCalled = true; 1228 return { backupPath: '/tmp/backup.js', diff: 'diff' }; 1229 }; 1230 _deps.runTestsForFile = async () => { 1231 testsRan = true; 1232 return { success: true, stats: { pass: 5 }, failures: [], coverage: 88 }; 1233 }; 1234 1235 agent.createCommit = async () => 'deps-fix-hash'; 1236 1237 const task = insertTask('fix_bug', { 1238 error_type: 'null_pointer', 1239 error_message: 'score is null', 1240 file_path: 'src/score.js', 1241 stage: 'scoring', 1242 }); 1243 1244 await agent.fixBug(task); 1245 1246 assert.ok(readFileCalled, '_deps.readFile should be called'); 1247 assert.ok(llmCalled, '_deps.simpleLLMCall should be called'); 1248 assert.ok(editFileCalled, '_deps.editFile should be called'); 1249 assert.ok(testsRan, '_deps.runTestsForFile should be called'); 1250 1251 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1252 assert.strictEqual(updated.status, 'completed'); 1253 1254 // Restore 1255 _deps.readFile = origReadFile; 1256 _deps.getFileContext = origGetFileContext; 1257 _deps.simpleLLMCall = origSimpleLLMCall; 1258 _deps.editFile = origEditFile; 1259 _deps.runTestsForFile = origRunTestsForFile; 1260 }); 1261 1262 test('fixBug restores backup and fails task when tests fail after fix (using _deps)', async () => { 1263 const origReadFile = _deps.readFile; 1264 const origGetFileContext = _deps.getFileContext; 1265 const origSimpleLLMCall = _deps.simpleLLMCall; 1266 const origEditFile = _deps.editFile; 1267 const origRunTestsForFile = _deps.runTestsForFile; 1268 const origRestoreBackup = _deps.restoreBackup; 1269 1270 let backupRestored = false; 1271 1272 _deps.readFile = async () => ({ content: 'old code', size: 8 }); 1273 _deps.getFileContext = async () => ({ imports: [], testFiles: [] }); 1274 _deps.simpleLLMCall = async () => 1275 JSON.stringify({ 1276 old_string: 'old code', 1277 new_string: 'new code', 1278 explanation: 'Fixed', 1279 test_cases: [], 1280 }); 1281 _deps.editFile = async () => ({ backupPath: '/tmp/backup-test.js', diff: '' }); 1282 _deps.runTestsForFile = async () => ({ 1283 success: false, 1284 stats: { pass: 0, fail: 2 }, 1285 failures: [{ name: 'foo test', message: 'assertion failed' }], 1286 }); 1287 _deps.restoreBackup = async () => { 1288 backupRestored = true; 1289 }; 1290 1291 const task = insertTask('fix_bug', { 1292 error_type: 'api_error', 1293 error_message: 'API timeout', 1294 file_path: 'src/scrape.js', 1295 stage: 'serps', 1296 }); 1297 1298 await agent.fixBug(task); 1299 1300 assert.ok(backupRestored, 'Should restore backup when tests fail'); 1301 1302 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1303 assert.strictEqual(updated.status, 'failed'); 1304 assert.ok(updated.error_message.includes('tests did not pass')); 1305 1306 // Should have asked architect 1307 const archMsgs = db.prepare("SELECT * FROM agent_messages WHERE to_agent = 'architect'").all(); 1308 assert.ok(archMsgs.length > 0, 'Should ask architect when fix tests fail'); 1309 1310 // Restore 1311 _deps.readFile = origReadFile; 1312 _deps.getFileContext = origGetFileContext; 1313 _deps.simpleLLMCall = origSimpleLLMCall; 1314 _deps.editFile = origEditFile; 1315 _deps.runTestsForFile = origRunTestsForFile; 1316 _deps.restoreBackup = origRestoreBackup; 1317 }); 1318 }); 1319 1320 // ================================================================ 1321 // Miscellaneous edge cases 1322 // ================================================================ 1323 describe('DeveloperAgent - miscellaneous edge cases', () => { 1324 test('getTestFilePath handles agents subdirectory path', () => { 1325 const result = agent.getTestFilePath('src/agents/developer.js'); 1326 assert.strictEqual(result, 'tests/developer.test.js'); 1327 }); 1328 1329 test('getTestFilePath handles utils subdirectory path', () => { 1330 const result = agent.getTestFilePath('src/utils/error-handler.js'); 1331 assert.strictEqual(result, 'tests/error-handler.test.js'); 1332 }); 1333 1334 test('extractFilePath handles Files: with multiple comma-separated files', () => { 1335 const result = agent.extractFilePath('Files: src/score.js, src/capture.js, package.json', ''); 1336 assert.strictEqual(result, 'src/score.js'); 1337 }); 1338 1339 test('extractFilePath handles scripts/ directory prefix', () => { 1340 const result = agent.extractFilePath('Error in scripts/generate-report.js at line 45', ''); 1341 assert.strictEqual(result, 'scripts/generate-report.js'); 1342 }); 1343 1344 test('fixBug handles non-string error_message (converts to string preview)', async () => { 1345 // error_message as object (edge case from real-world usage) 1346 const task = insertTask('fix_bug', { 1347 error_type: 'database', 1348 error_message: { code: 'SQLITE_ERROR', message: 'no such table: sites' }, 1349 file_path: 'src/score.js', 1350 stage: 'scoring', 1351 }); 1352 1353 agent.createCommit = async () => 'obj-error-hash'; 1354 1355 // The code does: error_message.substring(0, 200) - but if error_message is object 1356 // it converts via String() fallback 1357 // Test that this doesn't throw outright 1358 await agent.fixBug(task); 1359 1360 // Either completes or fails - as long as it doesn't crash ungracefully 1361 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1362 assert.ok( 1363 ['completed', 'failed', 'blocked'].includes(updated.status), 1364 `Task should reach a terminal state, got: ${updated.status}` 1365 ); 1366 }); 1367 1368 test('checkCoverageBeforeCommit logs info when all files pass threshold', async () => { 1369 const task = insertTask('fix_bug', { error_message: 'test' }); 1370 1371 const origGetFileCoverage = agent.getFileCoverage.bind(agent); 1372 agent.getFileCoverage = async () => ({ 1373 'src/score.js': 95, 1374 }); 1375 1376 const result = await agent.checkCoverageBeforeCommit(['src/score.js'], task.id); 1377 1378 assert.strictEqual(result.canCommit, true); 1379 1380 // Should log info about passing coverage 1381 const infoLogs = db 1382 .prepare( 1383 "SELECT * FROM agent_logs WHERE log_level = 'info' AND message LIKE '%Coverage check passed%'" 1384 ) 1385 .all(); 1386 assert.ok(infoLogs.length > 0, 'Should log info when coverage passes'); 1387 1388 agent.getFileCoverage = origGetFileCoverage; 1389 }); 1390 1391 test('getFileCoverage logs info when starting coverage check', async () => { 1392 const origExecSync = _deps.execSync; 1393 const origReadFileCoverage = _deps.readFileCoverage; 1394 1395 _deps.execSync = mock.fn(() => ''); 1396 _deps.readFileCoverage = mock.fn(async () => 1397 JSON.stringify({ 'src/score.js': { lines: { pct: 90 } } }) 1398 ); 1399 1400 await agent.getFileCoverage(['src/score.js']); 1401 1402 const infoLogs = db 1403 .prepare( 1404 "SELECT * FROM agent_logs WHERE log_level = 'info' AND message LIKE '%Running coverage check%'" 1405 ) 1406 .all(); 1407 assert.ok(infoLogs.length > 0, 'Should log info when starting coverage check'); 1408 1409 _deps.execSync = origExecSync; 1410 _deps.readFileCoverage = origReadFileCoverage; 1411 }); 1412 });