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