/ __quarantined_tests__ / agents / developer-coverage2.test.js
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  });