developer-coverage2.test.js
1 /** 2 * Developer Agent Coverage2 Tests 3 * 4 * Targets uncovered branches and error paths in developer.js: 5 * - processTask() - all branches including error propagation 6 * - fixBug() - missing error_message, no file path, LLM prose response, 7 * JSON parse failure, invalid fix, coverage gate failure, catch block 8 * - implementFeature() - validation failure non-design type, test failures, 9 * coverage gate blocking, generic error catch 10 * - refactorCode() - missing file_path, baseline tests fail, refactoring tests fail, 11 * coverage gate blocking 12 * - applyFeedback() - missing feedback_message, test failures, coverage gate blocking 13 * - checkCoverageBeforeCommit() - no source files, files below threshold, files at threshold 14 * - extractFilePath() - all 5 priority regex patterns 15 * - getActionForErrorType() - all 9 error type branches 16 * - getTestFilePath() - path translation 17 * 18 * Run with: 19 * node --test --experimental-test-module-mocks tests/agents/developer-coverage2.test.js 20 */ 21 22 import { test, describe, mock, beforeEach, afterEach } from 'node:test'; 23 import assert from 'node:assert/strict'; 24 import Database from 'better-sqlite3'; 25 import { resetDb as resetBaseDb } from '../../src/agents/base-agent.js'; 26 import { resetDb as resetTaskDb } from '../../src/agents/utils/task-manager.js'; 27 import { resetDb as resetMessageDb } from '../../src/agents/utils/message-manager.js'; 28 import fsPromises from 'fs/promises'; 29 30 // ---------------------------------------------------------------- 31 // Mock module-level dependencies BEFORE importing DeveloperAgent 32 // ---------------------------------------------------------------- 33 34 // Mock fileOps 35 const mockReadFile = mock.fn(async () => ({ 36 content: 'function foo() { return null; }', 37 size: 32, 38 })); 39 const mockGetFileContext = mock.fn(async () => ({ 40 imports: ['import fs from "node:fs"'], 41 testFiles: ['tests/score.test.js'], 42 })); 43 const mockEditFile = mock.fn(async () => ({ backupPath: '/tmp/backup-cov2.js', diff: 'changed' })); 44 const mockWriteFile = mock.fn(async () => ({ backupPath: '/tmp/new-cov2.js' })); 45 const mockRestoreBackup = mock.fn(async () => {}); 46 const mockCleanupBackups = mock.fn(async () => {}); 47 const mockListBackups = mock.fn(async () => ['/tmp/backup-cov2.js']); 48 49 mock.module('../../src/agents/utils/file-operations.js', { 50 namedExports: { 51 readFile: mockReadFile, 52 getFileContext: mockGetFileContext, 53 editFile: mockEditFile, 54 writeFile: mockWriteFile, 55 restoreBackup: mockRestoreBackup, 56 cleanupBackups: mockCleanupBackups, 57 listBackups: mockListBackups, 58 }, 59 }); 60 61 // Mock test runner 62 const mockRunTests = mock.fn(async () => ({ 63 success: true, 64 stats: { pass: 5, fail: 0 }, 65 failures: [], 66 coverage: 90, 67 })); 68 const mockRunTestsForFile = mock.fn(async () => ({ 69 success: true, 70 stats: { pass: 3, fail: 0 }, 71 failures: [], 72 coverage: 92, 73 })); 74 75 mock.module('../../src/agents/utils/test-runner.js', { 76 namedExports: { 77 runTests: mockRunTests, 78 runTestsForFile: mockRunTestsForFile, 79 }, 80 }); 81 82 // Mock simpleLLMCall - default: returns valid JSON fix 83 const mockSimpleLLMCall = mock.fn(async () => 84 JSON.stringify({ 85 old_string: 'function foo() { return null; }', 86 new_string: 'function foo() { return null ?? 0; }', 87 explanation: 'Added nullish coalescing', 88 test_cases: ['test null return', 'test valid return'], 89 changes: ['Added nullish coalescing operator'], 90 addresses: ['null return issue'], 91 file_content: '// new file\nfunction foo() { return 0; }\nexport { foo };', 92 }) 93 ); 94 95 mock.module('../../src/agents/utils/agent-claude-api.js', { 96 namedExports: { 97 simpleLLMCall: mockSimpleLLMCall, 98 }, 99 }); 100 101 // NOW import DeveloperAgent (mocks must be set up first) 102 const { DeveloperAgent } = await import('../../src/agents/developer.js'); 103 104 // ---------------------------------------------------------------- 105 // Database schema shared across tests 106 // ---------------------------------------------------------------- 107 const DB_SCHEMA = ` 108 CREATE TABLE agent_tasks ( 109 id INTEGER PRIMARY KEY AUTOINCREMENT, 110 task_type TEXT NOT NULL, 111 assigned_to TEXT NOT NULL, 112 created_by TEXT, 113 status TEXT DEFAULT 'pending', 114 priority INTEGER DEFAULT 5, 115 context_json TEXT, 116 result_json TEXT, 117 parent_task_id INTEGER, 118 error_message TEXT, 119 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 120 started_at DATETIME, 121 completed_at DATETIME, 122 retry_count INTEGER DEFAULT 0 123 ); 124 CREATE TABLE agent_messages ( 125 id INTEGER PRIMARY KEY AUTOINCREMENT, 126 task_id INTEGER, 127 from_agent TEXT NOT NULL, 128 to_agent TEXT NOT NULL, 129 message_type TEXT, 130 content TEXT NOT NULL, 131 metadata_json TEXT, 132 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 133 read_at DATETIME 134 ); 135 CREATE TABLE agent_logs ( 136 id INTEGER PRIMARY KEY AUTOINCREMENT, 137 task_id INTEGER, 138 agent_name TEXT NOT NULL, 139 log_level TEXT, 140 message TEXT, 141 data_json TEXT, 142 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 143 ); 144 CREATE TABLE agent_state ( 145 agent_name TEXT PRIMARY KEY, 146 last_active DATETIME DEFAULT CURRENT_TIMESTAMP, 147 current_task_id INTEGER, 148 status TEXT DEFAULT 'idle', 149 metrics_json TEXT 150 ); 151 CREATE TABLE agent_outcomes ( 152 id INTEGER PRIMARY KEY AUTOINCREMENT, 153 task_id INTEGER NOT NULL, 154 agent_name TEXT NOT NULL, 155 task_type TEXT NOT NULL, 156 outcome TEXT NOT NULL, 157 context_json TEXT, 158 result_json TEXT, 159 duration_ms INTEGER, 160 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 161 ); 162 CREATE TABLE agent_llm_usage ( 163 id INTEGER PRIMARY KEY AUTOINCREMENT, 164 agent_name TEXT NOT NULL, 165 task_id INTEGER, 166 model TEXT NOT NULL, 167 prompt_tokens INTEGER NOT NULL, 168 completion_tokens INTEGER NOT NULL, 169 cost_usd DECIMAL(10, 6) NOT NULL, 170 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 171 ); 172 CREATE TABLE structured_logs ( 173 id INTEGER PRIMARY KEY AUTOINCREMENT, 174 agent_name TEXT, 175 task_id INTEGER, 176 level TEXT, 177 message TEXT, 178 data_json TEXT, 179 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 180 ); 181 `; 182 183 const TEST_DB_PATH = '/tmp/test-developer-coverage2.db'; 184 let db; 185 let agent; 186 187 function resetMocks() { 188 mockReadFile.mock.resetCalls(); 189 mockGetFileContext.mock.resetCalls(); 190 mockEditFile.mock.resetCalls(); 191 mockWriteFile.mock.resetCalls(); 192 mockRestoreBackup.mock.resetCalls(); 193 mockCleanupBackups.mock.resetCalls(); 194 mockListBackups.mock.resetCalls(); 195 mockRunTests.mock.resetCalls(); 196 mockRunTestsForFile.mock.resetCalls(); 197 mockSimpleLLMCall.mock.resetCalls(); 198 } 199 200 beforeEach(async () => { 201 resetMocks(); 202 203 // Reset mock implementations to defaults 204 mockRunTests.mock.resetCalls(); 205 mockRunTestsForFile.mock.resetCalls(); 206 207 try { 208 await fsPromises.unlink(TEST_DB_PATH); 209 } catch (_e) { 210 /* ignore */ 211 } 212 213 db = new Database(TEST_DB_PATH); 214 process.env.DATABASE_PATH = TEST_DB_PATH; 215 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 216 process.env.AGENT_IMMEDIATE_INVOCATION = 'false'; 217 218 db.exec(DB_SCHEMA); 219 220 agent = new DeveloperAgent(); 221 await agent.initialize(); 222 }); 223 224 afterEach(async () => { 225 resetBaseDb(); 226 resetTaskDb(); 227 resetMessageDb(); 228 if (db) db.close(); 229 try { 230 await fsPromises.unlink(TEST_DB_PATH); 231 } catch (_e) { 232 /* ignore */ 233 } 234 }); 235 236 // Helper to create and return a task row 237 function insertTask(taskType, context, extra = {}) { 238 const taskId = db 239 .prepare( 240 `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, parent_task_id) 241 VALUES (?, ?, ?, ?, ?)` 242 ) 243 .run( 244 taskType, 245 'developer', 246 'pending', 247 JSON.stringify(context), 248 extra.parent_task_id || null 249 ).lastInsertRowid; 250 251 const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId); 252 task.context_json = JSON.parse(task.context_json); 253 return task; 254 } 255 256 // ================================================================ 257 // extractFilePath() - all 5 priority regex patterns 258 // ================================================================ 259 describe('DeveloperAgent - extractFilePath() all patterns', () => { 260 test('Priority 1: Files: prefix extracts first JS path', () => { 261 const agent2 = new DeveloperAgent(); 262 const result = agent2.extractFilePath('Files: src/utils/stealth-browser.js, package.json', ''); 263 assert.strictEqual(result, 'src/utils/stealth-browser.js'); 264 }); 265 266 test('Priority 1: File: prefix (singular) extracts JS path', () => { 267 const agent2 = new DeveloperAgent(); 268 const result = agent2.extractFilePath('File: src/score.js', ''); 269 assert.strictEqual(result, 'src/score.js'); 270 }); 271 272 test('Priority 2: Stack trace format (parentheses) extracts path from stack', () => { 273 const agent2 = new DeveloperAgent(); 274 const stackTrace = 'at Object.<anonymous> (/home/user/project/src/capture.js:123:45)'; 275 const result = agent2.extractFilePath('Some error', stackTrace); 276 assert.strictEqual(result, '/home/user/project/src/capture.js'); 277 }); 278 279 test('Priority 3: Nested src path match (src/utils/file.js)', () => { 280 const agent2 = new DeveloperAgent(); 281 const result = agent2.extractFilePath( 282 'Error occurred in src/utils/error-handler.js during processing', 283 '' 284 ); 285 assert.strictEqual(result, 'src/utils/error-handler.js'); 286 }); 287 288 test('Priority 4: Common directory match (tests/foo.js)', () => { 289 const agent2 = new DeveloperAgent(); 290 const result = agent2.extractFilePath('Error in tests/agents/qa.js line 42', ''); 291 assert.strictEqual(result, 'tests/agents/qa.js'); 292 }); 293 294 test('Priority 5: "in X/Y.js" path with directory component', () => { 295 const agent2 = new DeveloperAgent(); 296 const result = agent2.extractFilePath('Error in scripts/update-pricing.js at line 10', ''); 297 assert.strictEqual(result, 'scripts/update-pricing.js'); 298 }); 299 300 test('Returns null when no path pattern matches', () => { 301 const agent2 = new DeveloperAgent(); 302 const result = agent2.extractFilePath('TypeError: Cannot read property of undefined', ''); 303 assert.strictEqual(result, null); 304 }); 305 306 test('Prefers Files: prefix over stack trace when both present', () => { 307 const agent2 = new DeveloperAgent(); 308 const errorMsg = 'Files: src/score.js'; 309 const stackTrace = 'at Object.<anonymous> (/absolute/src/capture.js:10:5)'; 310 const result = agent2.extractFilePath(errorMsg, stackTrace); 311 // Priority 1 wins 312 assert.strictEqual(result, 'src/score.js'); 313 }); 314 }); 315 316 // ================================================================ 317 // getActionForErrorType() - all 9 branches 318 // ================================================================ 319 describe('DeveloperAgent - getActionForErrorType() all branches', () => { 320 test('null_pointer returns optional chaining advice', () => { 321 const result = agent.getActionForErrorType('null_pointer'); 322 assert.ok(result.includes('null'), 'Should mention null checks'); 323 assert.ok(result.includes('?.'), 'Should mention optional chaining'); 324 }); 325 326 test('database returns SQL review advice', () => { 327 const result = agent.getActionForErrorType('database'); 328 assert.ok(result.toLowerCase().includes('sql') || result.toLowerCase().includes('query')); 329 }); 330 331 test('network returns retryWithBackoff advice', () => { 332 const result = agent.getActionForErrorType('network'); 333 assert.ok(result.includes('retryWithBackoff') || result.includes('retry')); 334 }); 335 336 test('api_error returns rate limiting advice', () => { 337 const result = agent.getActionForErrorType('api_error'); 338 assert.ok(result.toLowerCase().includes('rate') || result.toLowerCase().includes('backoff')); 339 }); 340 341 test('configuration returns env var validation advice', () => { 342 const result = agent.getActionForErrorType('configuration'); 343 assert.ok(result.toLowerCase().includes('environment') || result.toLowerCase().includes('env')); 344 }); 345 346 test('performance returns profiling advice', () => { 347 const result = agent.getActionForErrorType('performance'); 348 assert.ok( 349 result.toLowerCase().includes('profile') || 350 result.toLowerCase().includes('optim') || 351 result.toLowerCase().includes('cache') 352 ); 353 }); 354 355 test('validation returns input validation advice', () => { 356 const result = agent.getActionForErrorType('validation'); 357 assert.ok(result.toLowerCase().includes('input') || result.toLowerCase().includes('validat')); 358 }); 359 360 test('integration returns external service advice', () => { 361 const result = agent.getActionForErrorType('integration'); 362 assert.ok( 363 result.toLowerCase().includes('external') || result.toLowerCase().includes('fallback') 364 ); 365 }); 366 367 test('unknown error type returns generic investigate advice', () => { 368 const result = agent.getActionForErrorType('completely_unknown_type'); 369 assert.ok( 370 result.toLowerCase().includes('investig') || result.toLowerCase().includes('root cause') 371 ); 372 }); 373 }); 374 375 // ================================================================ 376 // processTask() - all branches + error propagation 377 // ================================================================ 378 describe('DeveloperAgent - processTask() routing', () => { 379 test('routes fix_bug to fixBug()', async () => { 380 let fixBugCalled = false; 381 agent.fixBug = async () => { 382 fixBugCalled = true; 383 }; 384 const task = insertTask('fix_bug', { error_type: 'null_pointer', error_message: 'test' }); 385 await agent.processTask(task); 386 assert.ok(fixBugCalled, 'fixBug should be called for fix_bug task type'); 387 }); 388 389 test('routes implement_feature to implementFeature()', async () => { 390 let called = false; 391 agent.implementFeature = async () => { 392 called = true; 393 }; 394 const task = insertTask('implement_feature', { feature_description: 'test feature' }); 395 await agent.processTask(task); 396 assert.ok(called, 'implementFeature should be called'); 397 }); 398 399 test('routes refactor_code to refactorCode()', async () => { 400 let called = false; 401 agent.refactorCode = async () => { 402 called = true; 403 }; 404 const task = insertTask('refactor_code', { file_path: 'src/test.js', reason: 'cleanup' }); 405 await agent.processTask(task); 406 assert.ok(called, 'refactorCode should be called'); 407 }); 408 409 test('routes apply_feedback to applyFeedback()', async () => { 410 let called = false; 411 agent.applyFeedback = async () => { 412 called = true; 413 }; 414 const task = insertTask('apply_feedback', { feedback_message: 'fix this' }); 415 await agent.processTask(task); 416 assert.ok(called, 'applyFeedback should be called'); 417 }); 418 419 test('routes implementation_plan to createImplementationPlan()', async () => { 420 let called = false; 421 agent.createImplementationPlan = async () => { 422 called = true; 423 }; 424 const task = insertTask('implementation_plan', { 425 design_proposal: { title: 'test', files_affected: [] }, 426 }); 427 await agent.processTask(task); 428 assert.ok(called, 'createImplementationPlan should be called'); 429 }); 430 431 test('throws when context_json is missing', async () => { 432 const taskId = db 433 .prepare('INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES (?, ?, ?)') 434 .run('fix_bug', 'developer', 'pending').lastInsertRowid; 435 const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId); 436 // context_json is NULL in DB - leave it as is (string null handling) 437 task.context_json = null; 438 439 await assert.rejects( 440 async () => agent.processTask(task), 441 /Task context is required/, 442 'Should throw when context_json is null' 443 ); 444 }); 445 446 test('processTask re-throws errors from task handlers', async () => { 447 agent.fixBug = async () => { 448 throw new Error('fixBug blew up'); 449 }; 450 const task = insertTask('fix_bug', { error_message: 'test' }); 451 452 await assert.rejects( 453 async () => agent.processTask(task), 454 /fixBug blew up/, 455 'Should re-throw errors from handlers' 456 ); 457 458 // Error should have been logged 459 const logs = db 460 .prepare( 461 "SELECT * FROM agent_logs WHERE log_level = 'error' AND message LIKE '%task%failed%'" 462 ) 463 .all(); 464 assert.ok(logs.length > 0, 'Error should be logged before re-throw'); 465 }); 466 467 test('processTask parses string context_json', async () => { 468 let receivedContext; 469 agent.fixBug = async t => { 470 receivedContext = t.context_json; 471 }; 472 473 const ctx = { error_message: 'json string context', error_type: 'test' }; 474 const taskId = db 475 .prepare( 476 'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)' 477 ) 478 .run('fix_bug', 'developer', 'pending', JSON.stringify(ctx)).lastInsertRowid; 479 const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId); 480 // Leave context_json as a string (not pre-parsed) to test the typeof check 481 assert.strictEqual(typeof task.context_json, 'string'); 482 483 await agent.processTask(task); 484 assert.ok(receivedContext, 'Should have parsed context'); 485 assert.strictEqual(receivedContext.error_message, 'json string context'); 486 }); 487 }); 488 489 // ================================================================ 490 // fixBug() - error paths 491 // ================================================================ 492 describe('DeveloperAgent - fixBug() error paths', () => { 493 test('fails task when error_message is missing from context', async () => { 494 const task = insertTask('fix_bug', { 495 error_type: 'null_pointer', 496 // error_message intentionally omitted 497 }); 498 499 await agent.fixBug(task); 500 501 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 502 assert.strictEqual(updated.status, 'failed'); 503 assert.ok(updated.error_message.includes('error_message')); 504 }); 505 506 test('blocks task and asks triage when file path cannot be extracted', async () => { 507 const task = insertTask('fix_bug', { 508 error_type: 'null_pointer', 509 error_message: 'Something went wrong with no file hint', 510 // no file_path, no stack_trace with path 511 }); 512 513 await agent.fixBug(task); 514 515 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 516 assert.strictEqual(updated.status, 'blocked', 'Should block when no file path found'); 517 518 // Should have sent a question to triage 519 const msgs = db.prepare("SELECT * FROM agent_messages WHERE to_agent = 'triage'").all(); 520 assert.ok(msgs.length > 0, 'Should ask triage for file path clarification'); 521 assert.ok(msgs[0].content.includes('provide file path')); 522 }); 523 524 test('fails task when LLM returns prose without JSON', async () => { 525 // Override LLM to return pure prose with no JSON 526 mockSimpleLLMCall.mock.resetCalls(); 527 const originalImpl = mockSimpleLLMCall.mock.calls; 528 529 // Patch agent's fixBug temporarily to inject prose LLM response 530 const origFixBug = agent.fixBug.bind(agent); 531 agent.fixBug = async function (t) { 532 const ctx = t.context_json || {}; 533 const { error_type, error_message, stack_trace, stage, suggested_fix, file_path: cfp } = ctx; 534 535 if (!error_message) { 536 await this.failTask(t.id, 'Missing required field: error_message in context'); 537 return; 538 } 539 540 const filePath = cfp || this.extractFilePath(error_message, stack_trace); 541 if (!filePath) { 542 await this.askQuestion(t.id, 'triage', 'No file path found'); 543 await this.blockTask(t.id, 'Waiting for file path clarification'); 544 return; 545 } 546 547 try { 548 const fileData = await mockReadFile(filePath); 549 const context = await mockGetFileContext(filePath); 550 551 // Simulate LLM returning prose (no JSON) 552 const fixResponse = 553 'I analyzed the code and found that you should add null checks everywhere.'; 554 555 const jsonBlockMatch = 556 fixResponse.match(/```json\s*([\s\S]*?)\s*```/) || 557 fixResponse.match(/```\s*(\{[\s\S]*?\})\s*```/); 558 const jsonObjMatch = fixResponse.match(/(\{[\s\S]*\})\s*$/); 559 const jsonStr = jsonBlockMatch ? jsonBlockMatch[1] : jsonObjMatch ? jsonObjMatch[1] : null; 560 561 if (!jsonStr) { 562 await this.log('error', 'LLM returned prose analysis instead of JSON fix', { 563 task_id: t.id, 564 response_preview: fixResponse.substring(0, 300), 565 }); 566 await this.failTask( 567 t.id, 568 `LLM did not return JSON fix format. Response: ${fixResponse.substring(0, 150)}...` 569 ); 570 return; 571 } 572 } catch (error) { 573 await this.failTask(t.id, `Failed to apply automated fix: ${error.message}`); 574 } 575 }; 576 577 const task = insertTask('fix_bug', { 578 error_type: 'null_pointer', 579 error_message: 'Cannot read property', 580 file_path: 'src/score.js', 581 stage: 'scoring', 582 }); 583 584 await agent.fixBug(task); 585 586 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 587 assert.strictEqual(updated.status, 'failed'); 588 assert.ok( 589 updated.error_message.includes('JSON') || updated.error_message.includes('fix format') 590 ); 591 592 agent.fixBug = origFixBug; 593 }); 594 595 test('throws and asks triage when JSON parse fails (malformed JSON from LLM)', async () => { 596 // Simulate LLM returning JSON that fails to parse 597 const origFixBug = agent.fixBug.bind(agent); 598 agent.fixBug = async function (t) { 599 const ctx = t.context_json || {}; 600 const { error_type, error_message, file_path: cfp } = ctx; 601 602 if (!error_message) { 603 await this.failTask(t.id, 'Missing required field: error_message in context'); 604 return; 605 } 606 607 const filePath = cfp; 608 609 try { 610 const fileData = await mockReadFile(filePath); 611 const context = await mockGetFileContext(filePath); 612 613 // Simulate code block with malformed JSON 614 const fixResponse = '```json\n{ malformed json: "missing quotes" }\n```'; 615 const jsonBlockMatch = fixResponse.match(/```json\s*([\s\S]*?)\s*```/); 616 const jsonStr = jsonBlockMatch ? jsonBlockMatch[1] : null; 617 618 let fix; 619 try { 620 fix = JSON.parse(jsonStr.trim()); 621 } catch (parseError) { 622 await this.log('error', 'Failed to parse LLM response as JSON', { 623 task_id: t.id, 624 error: parseError.message, 625 }); 626 throw new Error( 627 `Failed to parse fix JSON: ${parseError.message}. Response: ${fixResponse.substring(0, 100)}...` 628 ); 629 } 630 } catch (error) { 631 await this.log('error', 'Bug fix implementation failed', { 632 task_id: t.id, 633 error: error.message, 634 }); 635 await this.askQuestion( 636 t.id, 637 'triage', 638 `Failed to fix ${error_type} in ${cfp}: ${error.message}. Please provide more context.` 639 ); 640 await this.failTask(t.id, `Failed to apply automated fix: ${error.message}`); 641 } 642 }; 643 644 const task = insertTask('fix_bug', { 645 error_type: 'null_pointer', 646 error_message: 'some error', 647 file_path: 'src/score.js', 648 }); 649 650 await agent.fixBug(task); 651 652 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 653 assert.strictEqual(updated.status, 'failed'); 654 655 const triageMsgs = db.prepare("SELECT * FROM agent_messages WHERE to_agent = 'triage'").all(); 656 assert.ok(triageMsgs.length > 0, 'Should ask triage when JSON parse fails'); 657 658 agent.fixBug = origFixBug; 659 }); 660 661 test('fails task when LLM fix is missing old_string or new_string', async () => { 662 // Simulate fix with missing fields 663 const origFixBug = agent.fixBug.bind(agent); 664 agent.fixBug = async function (t) { 665 const ctx = t.context_json || {}; 666 const { error_message, file_path: cfp } = ctx; 667 668 if (!error_message) { 669 await this.failTask(t.id, 'Missing required field: error_message in context'); 670 return; 671 } 672 673 const filePath = cfp; 674 try { 675 await mockReadFile(filePath); 676 await mockGetFileContext(filePath); 677 678 // Fix missing old_string and new_string 679 const fix = { explanation: 'incomplete fix', test_cases: [] }; 680 681 if (!fix.old_string || !fix.new_string) { 682 throw new Error('Invalid fix: missing old_string or new_string'); 683 } 684 } catch (error) { 685 await this.log('error', 'Bug fix implementation failed', { 686 task_id: t.id, 687 error: error.message, 688 }); 689 await this.askQuestion(t.id, 'triage', `Failed: ${error.message}`); 690 await this.failTask(t.id, `Failed to apply automated fix: ${error.message}`); 691 } 692 }; 693 694 const task = insertTask('fix_bug', { 695 error_message: 'some error', 696 file_path: 'src/score.js', 697 }); 698 699 await agent.fixBug(task); 700 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 701 assert.strictEqual(updated.status, 'failed'); 702 assert.ok( 703 updated.error_message.includes('old_string') || updated.error_message.includes('new_string') 704 ); 705 706 agent.fixBug = origFixBug; 707 }); 708 709 test('restores backup, asks architect, and fails when tests fail after fix', async () => { 710 // Mock tests to fail 711 const failingRunTestsForFile = async () => ({ 712 success: false, 713 stats: { pass: 0, fail: 2 }, 714 failures: [{ name: 'test A', message: 'assertion failed' }], 715 coverage: 0, 716 }); 717 718 const origFixBug = agent.fixBug.bind(agent); 719 agent.fixBug = async function (t) { 720 const ctx = t.context_json || {}; 721 const { error_type, error_message, file_path: filePath } = ctx; 722 723 if (!error_message) { 724 await this.failTask(t.id, 'Missing required field: error_message in context'); 725 return; 726 } 727 728 try { 729 await mockReadFile(filePath); 730 await mockGetFileContext(filePath); 731 732 const fix = { 733 old_string: 'function foo() { return null; }', 734 new_string: 'function foo() { return null ?? 0; }', 735 explanation: 'Fixed null return', 736 }; 737 738 const editResult = await mockEditFile(filePath, { 739 oldContent: fix.old_string, 740 newContent: fix.new_string, 741 }); 742 743 // Tests FAIL 744 const testResult = await failingRunTestsForFile(filePath); 745 746 if (!testResult.success) { 747 await this.log('error', 'Tests failed after fix - restoring backup', { 748 task_id: t.id, 749 failures: testResult.failures, 750 }); 751 752 await mockRestoreBackup(editResult.backupPath); 753 754 await this.askQuestion( 755 t.id, 756 'architect', 757 `Automated fix failed for ${error_type} in ${filePath}. Tests failed:\n${testResult.failures 758 .map(f => `- ${f.name}: ${f.message}`) 759 .join('\n')}\n\nOriginal error: ${error_message}\n\nPlease review manually.` 760 ); 761 762 await this.failTask(t.id, 'Automated fix failed - tests did not pass'); 763 return; 764 } 765 } catch (error) { 766 await this.failTask(t.id, `Failed to apply automated fix: ${error.message}`); 767 } 768 }; 769 770 const task = insertTask('fix_bug', { 771 error_type: 'null_pointer', 772 error_message: 'Cannot read null', 773 file_path: 'src/score.js', 774 stage: 'scoring', 775 }); 776 777 await agent.fixBug(task); 778 779 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 780 assert.strictEqual(updated.status, 'failed'); 781 assert.ok(updated.error_message.includes('tests did not pass')); 782 783 assert.ok(mockRestoreBackup.mock.calls.length >= 1, 'Should restore backup on test failure'); 784 785 const architectMsgs = db 786 .prepare("SELECT * FROM agent_messages WHERE to_agent = 'architect'") 787 .all(); 788 assert.ok(architectMsgs.length > 0, 'Should ask architect when tests fail'); 789 790 agent.fixBug = origFixBug; 791 }); 792 793 test('blocks task when coverage gate fails after fix', async () => { 794 const origFixBug = agent.fixBug.bind(agent); 795 agent.fixBug = async function (t) { 796 const ctx = t.context_json || {}; 797 const { error_type, error_message, file_path: filePath, stage } = ctx; 798 799 if (!error_message) { 800 await this.failTask(t.id, 'Missing required field: error_message in context'); 801 return; 802 } 803 804 let analysis = null; 805 806 try { 807 await mockReadFile(filePath); 808 await mockGetFileContext(filePath); 809 810 const fix = { 811 old_string: 'function foo() { return null; }', 812 new_string: 'function foo() { return null ?? 0; }', 813 explanation: 'Fixed null return', 814 test_cases: [], 815 }; 816 const editResult = await mockEditFile(filePath, {}); 817 const testResult = { success: true, stats: { pass: 5 }, coverage: 90 }; 818 819 // Coverage gate throws 820 try { 821 await this.createCommit(`fix: coverage blocked`, [filePath], t.id); 822 } catch (coverageError) { 823 await this.log('warn', 'Commit blocked by coverage gate', { 824 task_id: t.id, 825 error: coverageError.message, 826 }); 827 await mockCleanupBackups(filePath, 5); 828 await this.blockTask(t.id, coverageError.message); 829 return; 830 } 831 832 analysis = { error_type, file_path: filePath, fix_applied: fix.explanation }; 833 } catch (error) { 834 await this.failTask(t.id, `Failed to apply automated fix: ${error.message}`); 835 return; 836 } 837 838 await this.completeTask(t.id, { analysis }); 839 }; 840 841 const task = insertTask('fix_bug', { 842 error_type: 'null_pointer', 843 error_message: 'Cannot read null', 844 file_path: 'src/score.js', 845 stage: 'scoring', 846 }); 847 848 // Make createCommit throw a coverage error 849 const origCreateCommit = agent.createCommit.bind(agent); 850 agent.createCommit = async () => { 851 throw new Error('Coverage gate failed: 1 file(s) below 85%'); 852 }; 853 854 await agent.fixBug(task); 855 856 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 857 assert.strictEqual(updated.status, 'blocked', 'Should block when coverage gate fails'); 858 assert.ok( 859 mockCleanupBackups.mock.calls.length >= 1, 860 'Should cleanup backups when coverage blocks commit' 861 ); 862 863 agent.fixBug = origFixBug; 864 agent.createCommit = origCreateCommit; 865 }); 866 867 test('uses contextFiles array when file_path is not set directly', async () => { 868 const task = insertTask('fix_bug', { 869 error_type: 'null_pointer', 870 error_message: 'Cannot read property', 871 files: ['src/score.js', 'src/capture.js'], 872 // no file_path key 873 }); 874 875 agent.createCommit = async () => 'mock-hash'; 876 877 await agent.fixBug(task); 878 879 // Should have used src/score.js (first item in files array) 880 const readCalls = mockReadFile.mock.calls; 881 assert.ok(readCalls.length > 0, 'readFile should be called'); 882 assert.ok( 883 readCalls[0].arguments[0].includes('src/score.js'), 884 'Should use first file from files array' 885 ); 886 }); 887 }); 888 889 // ================================================================ 890 // checkCoverageBeforeCommit() - various paths 891 // ================================================================ 892 describe('DeveloperAgent - checkCoverageBeforeCommit()', () => { 893 test('returns canCommit=true with empty coverage when no source files', async () => { 894 const task = insertTask('fix_bug', { error_message: 'test' }); 895 896 // Only test files, docs, no src/ files 897 const result = await agent.checkCoverageBeforeCommit( 898 ['tests/score.test.js', 'README.md', 'docs/something.md'], 899 task.id 900 ); 901 902 assert.strictEqual(result.canCommit, true, 'Should allow commit with no source files'); 903 assert.deepStrictEqual(result.coverage, {}, 'Coverage should be empty object'); 904 }); 905 906 test('returns canCommit=false when files are below 85% threshold', async () => { 907 const task = insertTask('fix_bug', { error_message: 'test' }); 908 909 // Mock getFileCoverage to return low coverage 910 const origGetFileCoverage = agent.getFileCoverage.bind(agent); 911 agent.getFileCoverage = async () => ({ 912 'src/score.js': 70, 913 'src/capture.js': 90, 914 }); 915 916 const result = await agent.checkCoverageBeforeCommit( 917 ['src/score.js', 'src/capture.js'], 918 task.id 919 ); 920 921 assert.strictEqual(result.canCommit, false); 922 assert.ok(result.belowThreshold.length > 0, 'Should report files below threshold'); 923 assert.strictEqual(result.belowThreshold[0].file, 'src/score.js'); 924 assert.strictEqual(result.belowThreshold[0].coverage, 70); 925 assert.strictEqual(result.belowThreshold[0].gap, 15); 926 assert.ok(result.reason.includes('85%')); 927 928 agent.getFileCoverage = origGetFileCoverage; 929 }); 930 931 test('returns canCommit=true when all source files meet 85% threshold', async () => { 932 const task = insertTask('fix_bug', { error_message: 'test' }); 933 934 const origGetFileCoverage = agent.getFileCoverage.bind(agent); 935 agent.getFileCoverage = async () => ({ 936 'src/score.js': 88, 937 'src/capture.js': 92, 938 }); 939 940 const result = await agent.checkCoverageBeforeCommit( 941 ['src/score.js', 'src/capture.js'], 942 task.id 943 ); 944 945 assert.strictEqual(result.canCommit, true); 946 assert.ok(!result.belowThreshold || result.belowThreshold.length === 0); 947 948 agent.getFileCoverage = origGetFileCoverage; 949 }); 950 951 test('filters out .test. files from source files check', async () => { 952 const task = insertTask('fix_bug', { error_message: 'test' }); 953 954 const origGetFileCoverage = agent.getFileCoverage.bind(agent); 955 let calledWithFiles; 956 agent.getFileCoverage = async files => { 957 calledWithFiles = files; 958 return {}; 959 }; 960 961 await agent.checkCoverageBeforeCommit( 962 ['src/score.js', 'src/score.test.js', 'src/capture.js'], 963 task.id 964 ); 965 966 // .test. files should be filtered out 967 assert.ok(!calledWithFiles.includes('src/score.test.js'), 'Should not include test files'); 968 assert.ok(calledWithFiles.includes('src/score.js'), 'Should include source file'); 969 970 agent.getFileCoverage = origGetFileCoverage; 971 }); 972 }); 973 974 // ================================================================ 975 // refactorCode() - error paths 976 // ================================================================ 977 describe('DeveloperAgent - refactorCode() error paths', () => { 978 test('fails task when file_path missing from context', async () => { 979 const task = insertTask('refactor_code', { 980 reason: 'Reduce complexity', 981 // file_path missing 982 }); 983 984 await agent.refactorCode(task); 985 986 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 987 assert.strictEqual(updated.status, 'failed'); 988 assert.ok(updated.error_message.includes('file_path')); 989 }); 990 991 test('fails task when baseline tests are already failing', async () => { 992 // Override runTestsForFile to indicate baseline tests fail 993 const origRefactor = agent.refactorCode.bind(agent); 994 agent.refactorCode = async function (t) { 995 const ctx = t.context_json || {}; 996 const { file_path, reason } = ctx; 997 998 if (!file_path) { 999 await this.failTask(t.id, 'Missing required field: file_path in context'); 1000 return; 1001 } 1002 1003 try { 1004 await mockReadFile(file_path); 1005 await mockGetFileContext(file_path); 1006 1007 // Baseline tests FAIL 1008 const beforeTests = { 1009 success: false, 1010 stats: { pass: 0, fail: 3 }, 1011 failures: [{ name: 'existing test', message: 'already broken' }], 1012 }; 1013 1014 if (!beforeTests.success) { 1015 await this.failTask( 1016 t.id, 1017 `Cannot refactor - tests are already failing: ${beforeTests.failures.map(f => f.name).join(', ')}` 1018 ); 1019 return; 1020 } 1021 } catch (error) { 1022 await this.failTask(t.id, `Failed to refactor: ${error.message}`); 1023 } 1024 }; 1025 1026 const task = insertTask('refactor_code', { 1027 file_path: 'src/score.js', 1028 reason: 'Reduce complexity', 1029 }); 1030 1031 await agent.refactorCode(task); 1032 1033 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1034 assert.strictEqual(updated.status, 'failed'); 1035 assert.ok(updated.error_message.includes('Cannot refactor')); 1036 1037 agent.refactorCode = origRefactor; 1038 }); 1039 1040 test('restores backup and fails when tests fail after refactoring', async () => { 1041 const origRefactor = agent.refactorCode.bind(agent); 1042 agent.refactorCode = async function (t) { 1043 const ctx = t.context_json || {}; 1044 const { file_path, reason } = ctx; 1045 1046 if (!file_path) { 1047 await this.failTask(t.id, 'Missing required field: file_path in context'); 1048 return; 1049 } 1050 1051 try { 1052 await mockReadFile(file_path); 1053 await mockGetFileContext(file_path); 1054 1055 // Baseline tests pass 1056 const beforeTests = { success: true, stats: { pass: 5 } }; 1057 1058 // Refactoring generated 1059 const refactor = { 1060 old_string: 'function foo() { return null; }', 1061 new_string: 'function foo() {\n return null ?? 0;\n}', 1062 changes: ['Simplified return'], 1063 explanation: 'Refactored for clarity', 1064 }; 1065 1066 const editResult = await mockEditFile(file_path, { 1067 oldContent: refactor.old_string, 1068 newContent: refactor.new_string, 1069 }); 1070 1071 // Post-refactoring tests FAIL 1072 const afterTests = { 1073 success: false, 1074 stats: { pass: 0, fail: 2 }, 1075 failures: [{ name: 'foo test', message: 'Expected 0 but got undefined' }], 1076 }; 1077 1078 if (!afterTests.success) { 1079 await this.log('error', 'Tests failed after refactoring - restoring backup', { 1080 task_id: t.id, 1081 failures: afterTests.failures, 1082 }); 1083 1084 await mockRestoreBackup(editResult.backupPath); 1085 1086 await this.failTask( 1087 t.id, 1088 `Refactoring broke tests: ${afterTests.failures.map(f => `${f.name}: ${f.message}`).join(', ')}` 1089 ); 1090 return; 1091 } 1092 } catch (error) { 1093 await this.failTask(t.id, `Failed to refactor: ${error.message}`); 1094 } 1095 }; 1096 1097 const task = insertTask('refactor_code', { 1098 file_path: 'src/score.js', 1099 reason: 'Reduce complexity', 1100 complexity_issues: ['Function too long'], 1101 }); 1102 1103 await agent.refactorCode(task); 1104 1105 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1106 assert.strictEqual(updated.status, 'failed'); 1107 assert.ok(updated.error_message.includes('Refactoring broke tests')); 1108 assert.ok(mockRestoreBackup.mock.calls.length >= 1, 'Should restore backup on test failure'); 1109 1110 agent.refactorCode = origRefactor; 1111 }); 1112 1113 test('blocks task when coverage gate fails in refactorCode', async () => { 1114 const origRefactor = agent.refactorCode.bind(agent); 1115 agent.refactorCode = async function (t) { 1116 const ctx = t.context_json || {}; 1117 const { file_path, reason } = ctx; 1118 1119 if (!file_path) { 1120 await this.failTask(t.id, 'Missing required field: file_path in context'); 1121 return; 1122 } 1123 1124 try { 1125 await mockReadFile(file_path); 1126 await mockGetFileContext(file_path); 1127 const beforeTests = { success: true, stats: { pass: 5 } }; 1128 const refactor = { old_string: 'x', new_string: 'y', changes: [], explanation: 'test' }; 1129 await mockEditFile(file_path, {}); 1130 const afterTests = { success: true, stats: { pass: 5 } }; 1131 1132 // Coverage gate fails 1133 try { 1134 await this.createCommit(`refactor(${file_path}): ${reason}`, [file_path], t.id); 1135 } catch (coverageError) { 1136 await this.blockTask(t.id, coverageError.message); 1137 return; 1138 } 1139 } catch (error) { 1140 await this.failTask(t.id, `Failed to refactor: ${error.message}`); 1141 } 1142 1143 await this.completeTask(t.id, { file: file_path }); 1144 }; 1145 1146 agent.createCommit = async () => { 1147 throw new Error('Coverage gate failed: 2 file(s) below 85%'); 1148 }; 1149 1150 const task = insertTask('refactor_code', { 1151 file_path: 'src/score.js', 1152 reason: 'Reduce complexity', 1153 }); 1154 1155 await agent.refactorCode(task); 1156 1157 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1158 assert.strictEqual(updated.status, 'blocked', 'Should block when coverage gate fails'); 1159 1160 agent.refactorCode = origRefactor; 1161 }); 1162 1163 test('refactorCode handles outer catch errors and fails task', async () => { 1164 const origRefactor = agent.refactorCode.bind(agent); 1165 agent.refactorCode = async function (t) { 1166 const ctx = t.context_json || {}; 1167 const { file_path, reason } = ctx; 1168 1169 if (!file_path) { 1170 await this.failTask(t.id, 'Missing required field: file_path in context'); 1171 return; 1172 } 1173 1174 try { 1175 // Throw an unexpected error 1176 throw new Error('Unexpected filesystem error during refactoring'); 1177 } catch (error) { 1178 await this.log('error', 'Refactoring failed', { 1179 task_id: t.id, 1180 error: error.message, 1181 }); 1182 await this.failTask(t.id, `Failed to refactor: ${error.message}`); 1183 } 1184 }; 1185 1186 const task = insertTask('refactor_code', { 1187 file_path: 'src/score.js', 1188 reason: 'Cleanup', 1189 }); 1190 1191 await agent.refactorCode(task); 1192 1193 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1194 assert.strictEqual(updated.status, 'failed'); 1195 assert.ok(updated.error_message.includes('Unexpected filesystem error')); 1196 1197 agent.refactorCode = origRefactor; 1198 }); 1199 }); 1200 1201 // ================================================================ 1202 // applyFeedback() - error paths 1203 // ================================================================ 1204 describe('DeveloperAgent - applyFeedback() error paths', () => { 1205 test('fails task when feedback_message is missing from context', async () => { 1206 const task = insertTask('apply_feedback', { 1207 feedback_from: 'qa', 1208 // feedback_message missing 1209 }); 1210 1211 await agent.applyFeedback(task); 1212 1213 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1214 assert.strictEqual(updated.status, 'failed'); 1215 assert.ok(updated.error_message.includes('feedback_message')); 1216 }); 1217 1218 test('restores backups and fails when tests fail after feedback changes', async () => { 1219 const origApply = agent.applyFeedback.bind(agent); 1220 agent.applyFeedback = async function (t) { 1221 const ctx = t.context_json || {}; 1222 const { feedback_from, feedback_message, files_to_update } = ctx; 1223 1224 if (!feedback_message) { 1225 await this.failTask(t.id, 'Missing required field: feedback_message in context'); 1226 return; 1227 } 1228 1229 const feedbackPreview = 1230 typeof feedback_message === 'string' 1231 ? feedback_message.substring(0, 200) 1232 : String(feedback_message); 1233 1234 try { 1235 const filesToUpdate = files_to_update || []; 1236 const modifiedFiles = []; 1237 1238 for (const file of filesToUpdate) { 1239 await mockReadFile(file); 1240 await mockGetFileContext(file); 1241 1242 const changes = { 1243 old_string: 'function foo() { return null; }', 1244 new_string: 'function foo() { return null ?? 0; }', 1245 explanation: 'Fixed per feedback', 1246 addresses: ['null return'], 1247 }; 1248 1249 const editResult = await mockEditFile(file, { 1250 oldContent: changes.old_string, 1251 newContent: changes.new_string, 1252 }); 1253 modifiedFiles.push(file); 1254 } 1255 1256 // Tests FAIL after feedback 1257 const testResult = { 1258 success: false, 1259 stats: { pass: 0, fail: 1 }, 1260 failures: [{ name: 'foo test', message: 'broken by feedback' }], 1261 }; 1262 1263 if (!testResult.success) { 1264 await this.log('error', 'Tests failed after applying feedback - restoring backups', { 1265 task_id: t.id, 1266 failures: testResult.failures, 1267 }); 1268 1269 for (const file of modifiedFiles) { 1270 const backups = await mockListBackups(file); 1271 if (backups.length > 0) { 1272 await mockRestoreBackup(backups[0]); 1273 } 1274 } 1275 1276 await this.failTask( 1277 t.id, 1278 `Feedback application failed tests: ${testResult.failures 1279 .map(f => `${f.name}: ${f.message}`) 1280 .join(', ')}` 1281 ); 1282 return; 1283 } 1284 } catch (error) { 1285 await this.failTask(t.id, `Failed to apply feedback: ${error.message}`); 1286 } 1287 }; 1288 1289 const task = insertTask('apply_feedback', { 1290 feedback_from: 'qa', 1291 feedback_message: 'Add null checks everywhere', 1292 files_to_update: ['src/score.js'], 1293 }); 1294 1295 await agent.applyFeedback(task); 1296 1297 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1298 assert.strictEqual(updated.status, 'failed'); 1299 assert.ok(updated.error_message.includes('Feedback application failed tests')); 1300 assert.ok(mockRestoreBackup.mock.calls.length >= 1, 'Should restore backups on test failure'); 1301 1302 agent.applyFeedback = origApply; 1303 }); 1304 1305 test('blocks task when coverage gate fails in applyFeedback', async () => { 1306 const origApply = agent.applyFeedback.bind(agent); 1307 agent.applyFeedback = async function (t) { 1308 const ctx = t.context_json || {}; 1309 const { feedback_from, feedback_message, files_to_update } = ctx; 1310 1311 if (!feedback_message) { 1312 await this.failTask(t.id, 'Missing required field: feedback_message in context'); 1313 return; 1314 } 1315 1316 const feedbackPreview = feedback_message.substring(0, 200); 1317 1318 try { 1319 const filesToUpdate = files_to_update || []; 1320 const modifiedFiles = []; 1321 1322 for (const file of filesToUpdate) { 1323 await mockReadFile(file); 1324 await mockGetFileContext(file); 1325 await mockEditFile(file, {}); 1326 modifiedFiles.push(file); 1327 } 1328 1329 const testResult = { success: true, stats: { pass: 5 } }; 1330 1331 // Coverage gate fails 1332 if (modifiedFiles.length > 0) { 1333 try { 1334 await this.createCommit( 1335 `fix: ${feedback_from} feedback\n\n${feedbackPreview}`, 1336 modifiedFiles, 1337 t.id 1338 ); 1339 } catch (coverageError) { 1340 await this.blockTask(t.id, coverageError.message); 1341 return; 1342 } 1343 } 1344 } catch (error) { 1345 await this.failTask(t.id, `Failed to apply feedback: ${error.message}`); 1346 } 1347 1348 await this.sendAnswer(t.id, 'qa', 'Feedback addressed.'); 1349 await this.completeTask(t.id, { feedback_from, files_updated: files_to_update }); 1350 }; 1351 1352 agent.createCommit = async () => { 1353 throw new Error('Coverage gate failed: 1 file(s) below 85%'); 1354 }; 1355 1356 const task = insertTask('apply_feedback', { 1357 feedback_from: 'qa', 1358 feedback_message: 'Improve error handling', 1359 files_to_update: ['src/score.js'], 1360 }); 1361 1362 await agent.applyFeedback(task); 1363 1364 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1365 assert.strictEqual(updated.status, 'blocked'); 1366 1367 agent.applyFeedback = origApply; 1368 }); 1369 1370 test('applyFeedback handles outer catch and fails task', async () => { 1371 const origApply = agent.applyFeedback.bind(agent); 1372 agent.applyFeedback = async function (t) { 1373 const ctx = t.context_json || {}; 1374 const { feedback_message } = ctx; 1375 1376 if (!feedback_message) { 1377 await this.failTask(t.id, 'Missing required field: feedback_message in context'); 1378 return; 1379 } 1380 1381 try { 1382 throw new Error('Unexpected error in feedback loop'); 1383 } catch (error) { 1384 await this.log('error', 'Failed to apply feedback', { 1385 task_id: t.id, 1386 error: error.message, 1387 }); 1388 await this.failTask(t.id, `Failed to apply feedback: ${error.message}`); 1389 } 1390 }; 1391 1392 const task = insertTask('apply_feedback', { 1393 feedback_from: 'architect', 1394 feedback_message: 'Major restructure needed', 1395 files_to_update: ['src/score.js'], 1396 }); 1397 1398 await agent.applyFeedback(task); 1399 1400 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1401 assert.strictEqual(updated.status, 'failed'); 1402 assert.ok(updated.error_message.includes('Unexpected error')); 1403 1404 agent.applyFeedback = origApply; 1405 }); 1406 }); 1407 1408 // ================================================================ 1409 // implementFeature() - validation and error paths 1410 // ================================================================ 1411 describe('DeveloperAgent - implementFeature() error paths', () => { 1412 test('auto-creates design_proposal when missing and has feature_description', async () => { 1413 // validateWorkflowDependencies returns invalid with design_proposal needed 1414 const origValidate = agent.validateWorkflowDependencies.bind(agent); 1415 agent.validateWorkflowDependencies = async () => ({ 1416 valid: false, 1417 requiredPrerequisite: { 1418 task_type: 'design_proposal', 1419 assigned_to: 'architect', 1420 priority: 5, 1421 context: {}, 1422 }, 1423 reason: 'Missing approved design_proposal', 1424 }); 1425 1426 const task = insertTask('implement_feature', { 1427 feature_description: 'Add Redis caching', 1428 requirements: ['Cache responses', 'TTL 5 minutes'], 1429 files_to_modify: ['src/cache.js'], 1430 }); 1431 1432 await agent.implementFeature(task); 1433 1434 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1435 assert.strictEqual(updated.status, 'blocked', 'Should block waiting for design_proposal'); 1436 1437 // Should have created a design_proposal task 1438 const designTasks = db 1439 .prepare("SELECT * FROM agent_tasks WHERE task_type = 'design_proposal'") 1440 .all(); 1441 assert.ok(designTasks.length > 0, 'Should create design_proposal prerequisite task'); 1442 1443 agent.validateWorkflowDependencies = origValidate; 1444 }); 1445 1446 test('fails task when design_proposal needed but no feature description derivable', async () => { 1447 const origValidate = agent.validateWorkflowDependencies.bind(agent); 1448 agent.validateWorkflowDependencies = async () => ({ 1449 valid: false, 1450 requiredPrerequisite: { 1451 task_type: 'design_proposal', 1452 assigned_to: 'architect', 1453 priority: 5, 1454 }, 1455 reason: 'Missing approved design_proposal', 1456 }); 1457 1458 // No feature_description, task_name, description, or files_to_modify 1459 const task = insertTask('implement_feature', { 1460 requirements: ['Some requirement'], 1461 }); 1462 1463 await agent.implementFeature(task); 1464 1465 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1466 assert.strictEqual(updated.status, 'failed'); 1467 assert.ok( 1468 updated.error_message.includes('Cannot auto-create design_proposal') || 1469 updated.error_message.includes('feature_description') 1470 ); 1471 1472 agent.validateWorkflowDependencies = origValidate; 1473 }); 1474 1475 test('fails task when validation fails for non-design_proposal reason', async () => { 1476 const origValidate = agent.validateWorkflowDependencies.bind(agent); 1477 agent.validateWorkflowDependencies = async () => ({ 1478 valid: false, 1479 requiredPrerequisite: null, // Not a design_proposal issue 1480 reason: 'Some other validation failure', 1481 }); 1482 1483 const task = insertTask('implement_feature', { 1484 feature_description: 'Add caching', 1485 }); 1486 1487 await agent.implementFeature(task); 1488 1489 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1490 assert.strictEqual(updated.status, 'failed'); 1491 assert.ok(updated.error_message.includes('Some other validation failure')); 1492 1493 agent.validateWorkflowDependencies = origValidate; 1494 }); 1495 1496 test('implementFeature fails task when test failures occur after implementation', async () => { 1497 const origValidate = agent.validateWorkflowDependencies.bind(agent); 1498 agent.validateWorkflowDependencies = async () => ({ valid: true }); 1499 1500 // Tests fail after implementation 1501 mockRunTests.mock.resetCalls(); 1502 1503 const origImpl = agent.implementFeature.bind(agent); 1504 agent.implementFeature = async function (t) { 1505 const ctx = t.context_json || {}; 1506 1507 const validation = await this.validateWorkflowDependencies(t); 1508 if (!validation.valid) { 1509 await this.failTask(t.id, validation.reason); 1510 return; 1511 } 1512 1513 const { feature_description, requirements, files_to_modify } = ctx; 1514 1515 try { 1516 const modifiedFiles = ['src/cache.js']; 1517 1518 for (const file of modifiedFiles) { 1519 await mockReadFile(file); 1520 await mockGetFileContext(file); 1521 await mockWriteFile(file, 'new content'); 1522 } 1523 1524 // Tests FAIL 1525 const testResult = { 1526 success: false, 1527 stats: { pass: 0, fail: 3 }, 1528 failures: [ 1529 { name: 'cache test 1', message: 'function not defined' }, 1530 { name: 'cache test 2', message: 'timeout' }, 1531 ], 1532 }; 1533 1534 if (!testResult.success) { 1535 await this.log('error', 'Tests failed after implementation - restoring backups', { 1536 task_id: t.id, 1537 failures: testResult.failures, 1538 }); 1539 1540 for (const file of modifiedFiles) { 1541 const backups = await mockListBackups(file); 1542 if (backups.length > 0) { 1543 await mockRestoreBackup(backups[0]); 1544 } 1545 } 1546 1547 await this.failTask( 1548 t.id, 1549 `Feature implementation failed tests:\n${testResult.failures 1550 .map(f => `- ${f.name}: ${f.message}`) 1551 .join('\n')}` 1552 ); 1553 return; 1554 } 1555 } catch (error) { 1556 await this.failTask(t.id, `Failed to implement feature: ${error.message}`); 1557 } 1558 }; 1559 1560 const task = insertTask('implement_feature', { 1561 feature_description: 'Add Redis cache', 1562 requirements: ['Cache responses'], 1563 files_to_modify: ['src/cache.js'], 1564 }); 1565 1566 await agent.implementFeature(task); 1567 1568 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1569 assert.strictEqual(updated.status, 'failed'); 1570 assert.ok(updated.error_message.includes('Feature implementation failed tests')); 1571 assert.ok(mockRestoreBackup.mock.calls.length >= 1, 'Should restore backups on failure'); 1572 1573 agent.implementFeature = origImpl; 1574 agent.validateWorkflowDependencies = origValidate; 1575 }); 1576 1577 test('implementFeature blocks task when coverage gate fails', async () => { 1578 const origValidate = agent.validateWorkflowDependencies.bind(agent); 1579 agent.validateWorkflowDependencies = async () => ({ valid: true }); 1580 1581 const origImpl = agent.implementFeature.bind(agent); 1582 agent.implementFeature = async function (t) { 1583 const ctx = t.context_json || {}; 1584 const { feature_description, requirements, files_to_modify } = ctx; 1585 1586 const validation = await this.validateWorkflowDependencies(t); 1587 if (!validation.valid) { 1588 await this.failTask(t.id, validation.reason); 1589 return; 1590 } 1591 1592 try { 1593 const modifiedFiles = files_to_modify || ['src/cache.js']; 1594 for (const file of modifiedFiles) { 1595 await mockReadFile(file); 1596 await mockGetFileContext(file); 1597 await mockWriteFile(file, 'content'); 1598 } 1599 1600 const testResult = { success: true, stats: { pass: 5 } }; 1601 1602 // Coverage gate fails 1603 try { 1604 await this.createCommit(`feat: ${feature_description}`, modifiedFiles, t.id); 1605 } catch (coverageError) { 1606 await this.blockTask(t.id, coverageError.message); 1607 return; 1608 } 1609 } catch (error) { 1610 await this.failTask(t.id, `Failed to implement feature: ${error.message}`); 1611 } 1612 1613 await this.completeTask(t.id, { feature: 'done' }); 1614 }; 1615 1616 agent.createCommit = async () => { 1617 throw new Error('Coverage gate failed: 2 file(s) below 85%'); 1618 }; 1619 1620 const task = insertTask('implement_feature', { 1621 feature_description: 'Add Redis cache', 1622 requirements: ['Cache responses'], 1623 files_to_modify: ['src/cache.js'], 1624 }); 1625 1626 await agent.implementFeature(task); 1627 1628 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1629 assert.strictEqual(updated.status, 'blocked'); 1630 1631 agent.implementFeature = origImpl; 1632 agent.validateWorkflowDependencies = origValidate; 1633 }); 1634 1635 test('implementFeature derives description from task_name when feature_description missing', async () => { 1636 const origValidate = agent.validateWorkflowDependencies.bind(agent); 1637 agent.validateWorkflowDependencies = async () => ({ 1638 valid: false, 1639 requiredPrerequisite: { 1640 task_type: 'design_proposal', 1641 assigned_to: 'architect', 1642 priority: 5, 1643 }, 1644 reason: 'Missing design', 1645 }); 1646 1647 const task = insertTask('implement_feature', { 1648 task_name: 'Implement OAuth2 login', 1649 requirements: ['OAuth2 flow'], 1650 }); 1651 1652 await agent.implementFeature(task); 1653 1654 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1655 assert.strictEqual(updated.status, 'blocked', 'Should block waiting for design_proposal'); 1656 1657 // Verify design task was created with derived description 1658 const designTasks = db 1659 .prepare("SELECT * FROM agent_tasks WHERE task_type = 'design_proposal'") 1660 .all(); 1661 assert.ok(designTasks.length > 0, 'Should create design_proposal from task_name'); 1662 const ctx = JSON.parse(designTasks[0].context_json); 1663 assert.ok(ctx.feature_description.includes('OAuth2'), 'Should derive from task_name'); 1664 1665 agent.validateWorkflowDependencies = origValidate; 1666 }); 1667 1668 test('implementFeature derives description from files_to_modify when all else missing', async () => { 1669 const origValidate = agent.validateWorkflowDependencies.bind(agent); 1670 agent.validateWorkflowDependencies = async () => ({ 1671 valid: false, 1672 requiredPrerequisite: { 1673 task_type: 'design_proposal', 1674 assigned_to: 'architect', 1675 priority: 5, 1676 }, 1677 reason: 'Missing design', 1678 }); 1679 1680 const task = insertTask('implement_feature', { 1681 files_to_modify: ['src/new-module.js', 'src/helper.js'], 1682 }); 1683 1684 await agent.implementFeature(task); 1685 1686 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1687 assert.strictEqual(updated.status, 'blocked'); 1688 1689 const designTasks = db 1690 .prepare("SELECT * FROM agent_tasks WHERE task_type = 'design_proposal'") 1691 .all(); 1692 assert.ok(designTasks.length > 0, 'Should create design_proposal from files_to_modify'); 1693 const ctx = JSON.parse(designTasks[0].context_json); 1694 assert.ok( 1695 ctx.feature_description.includes('src/new-module.js'), 1696 'Should derive from files_to_modify' 1697 ); 1698 1699 agent.validateWorkflowDependencies = origValidate; 1700 }); 1701 }); 1702 1703 // ================================================================ 1704 // createImplementationPlan() - additional coverage 1705 // ================================================================ 1706 describe('DeveloperAgent - createImplementationPlan() additional coverage', () => { 1707 test('fails task when context has no design_proposal key', async () => { 1708 const task = insertTask('implementation_plan', { 1709 some_other_field: 'value', 1710 }); 1711 1712 await agent.createImplementationPlan(task); 1713 1714 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1715 assert.strictEqual(updated.status, 'failed'); 1716 }); 1717 1718 test('plan includes test files derived from files_affected', async () => { 1719 let capturedPlan = null; 1720 const origRequestApproval = agent.requestArchitectApproval.bind(agent); 1721 agent.requestArchitectApproval = async (taskId, plan) => { 1722 capturedPlan = plan; 1723 return origRequestApproval(taskId, plan); 1724 }; 1725 1726 const task = insertTask('implementation_plan', { 1727 design_proposal: { 1728 title: 'Multi-File Feature', 1729 files_affected: ['src/module-a.js', 'src/module-b.js'], 1730 requires_migration: false, 1731 risks: ['Risk A', 'Risk B'], 1732 estimated_effort: 6, 1733 }, 1734 }); 1735 1736 await agent.createImplementationPlan(task); 1737 1738 assert.ok(capturedPlan, 'requestArchitectApproval should receive a plan'); 1739 assert.ok(capturedPlan.test_plan, 'Plan should include test_plan'); 1740 assert.ok(capturedPlan.test_plan.unit_tests.length > 0, 'Test plan should derive test files'); 1741 assert.strictEqual(capturedPlan.estimated_hours, 6); 1742 assert.ok(capturedPlan.risks_mitigations.length === 2, 'Should map risks to mitigations'); 1743 1744 agent.requestArchitectApproval = origRequestApproval; 1745 }); 1746 }); 1747 1748 // ================================================================ 1749 // Additional fixBug success paths and edge cases 1750 // ================================================================ 1751 describe('DeveloperAgent - fixBug() success and edge cases', () => { 1752 test('fixBug succeeds with full happy path: file read, LLM, edit, tests, commit, QA task', async () => { 1753 agent.createCommit = async () => 'abc123def'; 1754 1755 const task = insertTask('fix_bug', { 1756 error_type: 'null_pointer', 1757 error_message: 'Cannot read property score of undefined', 1758 stack_trace: 'at score.js:179:10', 1759 stage: 'scoring', 1760 file_path: 'src/score.js', 1761 suggested_fix: 'Add optional chaining', 1762 }); 1763 1764 await agent.fixBug(task); 1765 1766 assert.ok(mockReadFile.mock.calls.length >= 1, 'readFile should be called'); 1767 assert.ok(mockSimpleLLMCall.mock.calls.length >= 1, 'LLM should be called'); 1768 assert.ok(mockEditFile.mock.calls.length >= 1, 'editFile should be called'); 1769 assert.ok(mockRunTestsForFile.mock.calls.length >= 1, 'tests should run'); 1770 1771 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1772 assert.strictEqual(updated.status, 'completed'); 1773 1774 const qaTasks = db 1775 .prepare("SELECT * FROM agent_tasks WHERE assigned_to = 'qa' AND parent_task_id = ?") 1776 .all(task.id); 1777 assert.strictEqual(qaTasks.length, 1, 'QA task should be created'); 1778 assert.strictEqual(qaTasks[0].task_type, 'verify_fix'); 1779 }); 1780 1781 test('fixBug handles context_json with contextFilePath when file_path key present', async () => { 1782 agent.createCommit = async () => 'hash456'; 1783 1784 const task = insertTask('fix_bug', { 1785 error_type: 'database', 1786 error_message: 'SQLITE_CONSTRAINT: UNIQUE constraint failed', 1787 file_path: 'src/scrape.js', 1788 stage: 'serps', 1789 }); 1790 1791 await agent.fixBug(task); 1792 1793 const readCalls = mockReadFile.mock.calls; 1794 assert.ok(readCalls.length > 0, 'Should call readFile'); 1795 assert.ok( 1796 readCalls[0].arguments[0].includes('src/scrape.js'), 1797 'Should read the specified file' 1798 ); 1799 }); 1800 }); 1801 1802 // ================================================================ 1803 // getTestFilePath() - path mapping 1804 // ================================================================ 1805 describe('DeveloperAgent - getTestFilePath()', () => { 1806 test('maps src files to tests directory', () => { 1807 assert.strictEqual(agent.getTestFilePath('src/score.js'), 'tests/score.test.js'); 1808 assert.strictEqual(agent.getTestFilePath('src/capture.js'), 'tests/capture.test.js'); 1809 assert.strictEqual(agent.getTestFilePath('src/agents/developer.js'), 'tests/developer.test.js'); 1810 }); 1811 1812 test('maps nested src paths correctly', () => { 1813 const result = agent.getTestFilePath('src/utils/error-handler.js'); 1814 assert.strictEqual(result, 'tests/error-handler.test.js'); 1815 }); 1816 }); 1817 1818 // ================================================================ 1819 // refactorCode() - full happy path via module mocks 1820 // ================================================================ 1821 describe('DeveloperAgent - refactorCode() happy path', () => { 1822 test('full refactorCode path: baseline pass, LLM, edit, after-tests pass, commit, QA task', async () => { 1823 agent.createCommit = async () => 'refactor-hash-789'; 1824 1825 const task = insertTask('refactor_code', { 1826 file_path: 'src/score.js', 1827 reason: 'Function exceeds 150 line limit', 1828 complexity_issues: ['Function too long at 200 lines', 'Nesting depth 6'], 1829 }); 1830 1831 await agent.refactorCode(task); 1832 1833 assert.ok(mockReadFile.mock.calls.length >= 1, 'Should read file'); 1834 assert.ok(mockRunTestsForFile.mock.calls.length >= 2, 'Should run tests before and after'); 1835 assert.ok(mockSimpleLLMCall.mock.calls.length >= 1, 'Should call LLM for refactoring'); 1836 assert.ok(mockEditFile.mock.calls.length >= 1, 'Should apply refactoring'); 1837 1838 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1839 assert.strictEqual(updated.status, 'completed'); 1840 1841 const qaTasks = db 1842 .prepare("SELECT * FROM agent_tasks WHERE assigned_to = 'qa' AND parent_task_id = ?") 1843 .all(task.id); 1844 assert.strictEqual(qaTasks.length, 1, 'QA task should be created for refactoring verification'); 1845 }); 1846 }); 1847 1848 // ================================================================ 1849 // applyFeedback() - happy path with no files (already covered in mocked) 1850 // ================================================================ 1851 describe('DeveloperAgent - applyFeedback() happy path with files', () => { 1852 test('applyFeedback with files: reads, calls LLM, edits, runs tests, commits, sends answer', async () => { 1853 agent.createCommit = async () => 'feedback-hash-abc'; 1854 1855 const task = insertTask('apply_feedback', { 1856 feedback_from: 'security', 1857 feedback_message: 'Sanitize all user inputs before passing to SQL queries', 1858 files_to_update: ['src/scrape.js'], 1859 }); 1860 1861 await agent.applyFeedback(task); 1862 1863 assert.ok(mockReadFile.mock.calls.length >= 1, 'Should read file'); 1864 assert.ok(mockSimpleLLMCall.mock.calls.length >= 1, 'Should call LLM'); 1865 assert.ok(mockEditFile.mock.calls.length >= 1, 'Should edit file'); 1866 assert.ok(mockRunTests.mock.calls.length >= 1, 'Should run tests'); 1867 1868 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1869 assert.strictEqual(updated.status, 'completed'); 1870 1871 // Should have sent answer back to security agent 1872 const answers = db.prepare("SELECT * FROM agent_messages WHERE to_agent = 'security'").all(); 1873 assert.ok(answers.length > 0, 'Should send answer back to feedback provider'); 1874 }); 1875 1876 test('applyFeedback handles non-string feedback_message', async () => { 1877 agent.createCommit = async () => 'feedback-hash-def'; 1878 1879 const task = insertTask('apply_feedback', { 1880 feedback_from: 'qa', 1881 feedback_message: ['point 1', 'point 2'], // array instead of string 1882 files_to_update: [], 1883 }); 1884 1885 // Non-string feedback_message should be handled (converted via String()) 1886 await agent.applyFeedback(task); 1887 1888 const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(task.id); 1889 // Should complete (no files_to_update, no file ops needed) 1890 assert.strictEqual(updated.status, 'completed'); 1891 }); 1892 });