/ __quarantined_tests__ / agents / qa-extended.test.js
qa-extended.test.js
   1  /**
   2   * Extended Tests for QA Agent
   3   *
   4   * Targets uncovered lines to boost coverage above 85%:
   5   * - generateTests LLM call path (lines 792-876)
   6   * - fixTestIssues: ReferenceError fix pattern (lines 981-982)
   7   * - fixTestIssues: async/await fix pattern (lines 994-998)
   8   * - fixTestIssues: error handler (lines 1024-1029)
   9   * - identifyUncoveredLines: detailed coverage extraction paths (lines 710-727)
  10   * - runTestFiles success path
  11   * - runTestPattern and runAllTests
  12   * - getFileCoverage with coverage file present
  13   */
  14  
  15  import { test, describe } from 'node:test';
  16  import assert from 'node:assert';
  17  import fs from 'fs/promises';
  18  import Database from 'better-sqlite3';
  19  import { resetDb as resetQaBaseDb } from '../../src/agents/base-agent.js';
  20  import { resetDb as resetQaTaskDb } from '../../src/agents/utils/task-manager.js';
  21  import { resetDb as resetQaMessageDb } from '../../src/agents/utils/message-manager.js';
  22  
  23  process.env.AGENT_IMMEDIATE_INVOCATION = 'false';
  24  process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
  25  
  26  // ============================================================
  27  // Helper: create isolated test DB + agent instance
  28  // ============================================================
  29  
  30  const SCHEMA_SQL = `
  31    CREATE TABLE IF NOT EXISTS agent_tasks (
  32      id INTEGER PRIMARY KEY AUTOINCREMENT, task_type TEXT NOT NULL,
  33      assigned_to TEXT NOT NULL, created_by TEXT, status TEXT DEFAULT 'pending',
  34      priority INTEGER DEFAULT 5, context_json TEXT, result_json TEXT,
  35      parent_task_id INTEGER, error_message TEXT,
  36      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  37      started_at DATETIME, completed_at DATETIME, retry_count INTEGER DEFAULT 0
  38    );
  39    CREATE TABLE IF NOT EXISTS agent_messages (
  40      id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER,
  41      from_agent TEXT NOT NULL, to_agent TEXT NOT NULL, message_type TEXT,
  42      content TEXT NOT NULL, metadata_json TEXT,
  43      created_at DATETIME DEFAULT CURRENT_TIMESTAMP, read_at DATETIME
  44    );
  45    CREATE TABLE IF NOT EXISTS agent_logs (
  46      id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER,
  47      agent_name TEXT NOT NULL, log_level TEXT, message TEXT, data_json TEXT,
  48      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  49    );
  50    CREATE TABLE IF NOT EXISTS agent_state (
  51      agent_name TEXT PRIMARY KEY, last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
  52      current_task_id INTEGER, status TEXT DEFAULT 'idle', metrics_json TEXT
  53    );
  54    CREATE TABLE IF NOT EXISTS agent_outcomes (
  55      id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL,
  56      agent_name TEXT NOT NULL, task_type TEXT NOT NULL, outcome TEXT NOT NULL,
  57      context_json TEXT, result_json TEXT, duration_ms INTEGER,
  58      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  59    );
  60    CREATE TABLE IF NOT EXISTS agent_llm_usage (
  61      id INTEGER PRIMARY KEY AUTOINCREMENT, agent_name TEXT NOT NULL,
  62      task_id INTEGER, model TEXT NOT NULL, prompt_tokens INTEGER NOT NULL,
  63      completion_tokens INTEGER NOT NULL, cost_usd DECIMAL(10,6) NOT NULL,
  64      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  65    );
  66    CREATE TABLE IF NOT EXISTS structured_logs (
  67      id INTEGER PRIMARY KEY AUTOINCREMENT, agent_name TEXT, task_id INTEGER,
  68      level TEXT, message TEXT, data_json TEXT,
  69      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  70    );
  71  `;
  72  
  73  async function createQaTestEnv(testDbPath) {
  74    resetQaBaseDb();
  75    resetQaTaskDb();
  76    resetQaMessageDb();
  77  
  78    try {
  79      await fs.unlink(testDbPath);
  80    } catch (_e) {
  81      /* ignore */
  82    }
  83  
  84    const db = new Database(testDbPath);
  85    db.exec(SCHEMA_SQL);
  86  
  87    process.env.DATABASE_PATH = testDbPath;
  88  
  89    const { QAAgent } = await import('../../src/agents/qa.js');
  90    const agent = new QAAgent();
  91    await agent.initialize();
  92  
  93    const cleanup = async () => {
  94      resetQaBaseDb();
  95      resetQaTaskDb();
  96      resetQaMessageDb();
  97      try {
  98        db.close();
  99      } catch (_e) {
 100        /* ignore */
 101      }
 102      try {
 103        await fs.unlink(testDbPath);
 104      } catch (_e) {
 105        /* ignore */
 106      }
 107    };
 108  
 109    return { db, agent, cleanup };
 110  }
 111  
 112  function insertQaTask(db, taskType, contextObj) {
 113    return db
 114      .prepare(
 115        `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 116         VALUES (?, 'qa', 'pending', ?) RETURNING id`
 117      )
 118      .get(taskType, contextObj !== undefined ? JSON.stringify(contextObj) : null).id;
 119  }
 120  
 121  function getQaTask(db, taskId) {
 122    const row = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 123    if (row && row.context_json && typeof row.context_json === 'string') {
 124      try {
 125        row.context_json = JSON.parse(row.context_json);
 126      } catch (_e) {
 127        /* ignore */
 128      }
 129    }
 130    return row;
 131  }
 132  
 133  // ============================================================
 134  // generateTests: LLM call path (lines 792-876)
 135  // ============================================================
 136  
 137  describe('QA Agent Extended - generateTests', () => {
 138    test('generateTests returns null when LLM call fails (no API key)', async () => {
 139      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-gt1.db');
 140      try {
 141        agent.log = async () => {};
 142  
 143        const uncoveredInfo = {
 144          uncoveredLines: [5, 10, 15],
 145          sourceCode: `function add(a, b) {
 146    if (a === null) {
 147      throw new Error('null a');
 148    }
 149    return a + b;
 150  }`,
 151          coveragePct: 50,
 152        };
 153  
 154        // Without a valid API key, callLLM will throw and generateTests returns null
 155        const result = await agent.generateTests('src/add.js', uncoveredInfo, null);
 156        // Either returns test code (if API available) or null (if no API key)
 157        assert.ok(result === null || typeof result === 'string', 'Should return null or string');
 158      } finally {
 159        await cleanup();
 160      }
 161    });
 162  
 163    test('generateTests uses testInstructions in prompt when provided', async () => {
 164      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-gt2.db');
 165      try {
 166        agent.log = async () => {};
 167  
 168        // We can't easily mock the ESM import, but we can verify the method is callable
 169        const uncoveredInfo = {
 170          uncoveredLines: [3],
 171          sourceCode: 'function test() { return null; }',
 172          coveragePct: 60,
 173        };
 174  
 175        const result = await agent.generateTests(
 176          'src/test-module.js',
 177          uncoveredInfo,
 178          'Focus on null handling tests'
 179        );
 180  
 181        // Result should be null (no API key in test env) or a string
 182        assert.ok(result === null || typeof result === 'string');
 183      } finally {
 184        await cleanup();
 185      }
 186    });
 187  
 188    test('generateTests handles uncoveredLines with multiple entries', async () => {
 189      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-gt3.db');
 190      try {
 191        agent.log = async () => {};
 192  
 193        const sourceCode = Array.from({ length: 20 }, (_, i) => `const line${i} = ${i};`).join('\n');
 194  
 195        const uncoveredInfo = {
 196          uncoveredLines: [1, 5, 10, 15, 20],
 197          sourceCode,
 198          coveragePct: 25,
 199        };
 200  
 201        // Should not throw, even with many uncovered lines
 202        const result = await agent.generateTests('src/multi-line.js', uncoveredInfo);
 203        assert.ok(result === null || typeof result === 'string');
 204      } finally {
 205        await cleanup();
 206      }
 207    });
 208  });
 209  
 210  // ============================================================
 211  // fixTestIssues: ReferenceError fix pattern (lines 979-988)
 212  // ============================================================
 213  
 214  describe('QA Agent Extended - fixTestIssues fix patterns', () => {
 215    test('fixTestIssues fixes ReferenceError by adding missing import', async () => {
 216      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-fti1.db');
 217      const tmpFile = './tests/agents/tmp-ref-error-fix.test.js';
 218      try {
 219        // Write a test file missing the 'assert' import
 220        await fs.writeFile(
 221          tmpFile,
 222          `import { test } from 'node:test';
 223  test('uses assert', () => {
 224    assert.ok(true);
 225  });
 226  `
 227        );
 228  
 229        // Mock runTestFiles to return success after fix
 230        const origRun = agent.runTestFiles;
 231        agent.runTestFiles = async () => ({ success: true, output: '1 passing', count: 1 });
 232  
 233        const result = await agent.fixTestIssues(tmpFile, {
 234          output: 'ReferenceError: assert is not defined\n  at test',
 235        });
 236  
 237        // Should have tried to add the import and return success from runTestFiles
 238        assert.ok(typeof result === 'boolean', 'Should return boolean');
 239        assert.strictEqual(result, true, 'Should return true when runTestFiles succeeds after fix');
 240  
 241        agent.runTestFiles = origRun;
 242      } finally {
 243        await cleanup();
 244        try {
 245          await fs.unlink(tmpFile);
 246        } catch (_e) {
 247          /* ignore */
 248        }
 249      }
 250    });
 251  
 252    test('fixTestIssues fixes assert.equal deprecation', async () => {
 253      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-fti2.db');
 254      const tmpFile = './tests/agents/tmp-assert-equal-fix.test.js';
 255      try {
 256        await fs.writeFile(
 257          tmpFile,
 258          `import { test } from 'node:test';
 259  import assert from 'node:assert';
 260  test('uses deprecated equal', () => {
 261    assert.equal(1, 1);
 262    assert.equal('a', 'a');
 263  });
 264  `
 265        );
 266  
 267        const origRun = agent.runTestFiles;
 268        agent.runTestFiles = async () => ({ success: true, output: '1 passing', count: 1 });
 269  
 270        const result = await agent.fixTestIssues(tmpFile, {
 271          output: 'assert.equal deprecation warning - use strictEqual instead',
 272        });
 273  
 274        assert.ok(typeof result === 'boolean');
 275  
 276        // Verify the file was updated (assert.equal -> assert.strictEqual)
 277        const updatedContent = await fs.readFile(tmpFile, 'utf8');
 278        assert.ok(
 279          updatedContent.includes('assert.strictEqual'),
 280          'Should replace assert.equal with assert.strictEqual'
 281        );
 282  
 283        agent.runTestFiles = origRun;
 284      } finally {
 285        await cleanup();
 286        try {
 287          await fs.unlink(tmpFile);
 288        } catch (_e) {
 289          /* ignore */
 290        }
 291      }
 292    });
 293  
 294    test('fixTestIssues fixes missing async when await used in non-async test', async () => {
 295      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-fti3.db');
 296      const tmpFile = './tests/agents/tmp-async-fix.test.js';
 297      try {
 298        await fs.writeFile(
 299          tmpFile,
 300          `import { test } from 'node:test';
 301  import assert from 'node:assert';
 302  test('sync test that uses await', () => {
 303    const result = await someAsyncFn();
 304    assert.ok(result);
 305  });
 306  `
 307        );
 308  
 309        const origRun = agent.runTestFiles;
 310        agent.runTestFiles = async () => ({ success: true, output: '1 passing', count: 1 });
 311  
 312        const result = await agent.fixTestIssues(tmpFile, {
 313          output: 'SyntaxError: await is only valid in async functions at test sync test',
 314        });
 315  
 316        assert.ok(typeof result === 'boolean');
 317        agent.runTestFiles = origRun;
 318      } finally {
 319        await cleanup();
 320        try {
 321          await fs.unlink(tmpFile);
 322        } catch (_e) {
 323          /* ignore */
 324        }
 325      }
 326    });
 327  
 328    test('fixTestIssues returns false when fix applied but tests still fail', async () => {
 329      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-fti4.db');
 330      const tmpFile = './tests/agents/tmp-fix-still-failing.test.js';
 331      try {
 332        await fs.writeFile(
 333          tmpFile,
 334          `import { test } from 'node:test';
 335  import assert from 'node:assert';
 336  test('still broken after fix', () => {
 337    assert.equal('a', 'b');
 338  });
 339  `
 340        );
 341  
 342        const origRun = agent.runTestFiles;
 343        // Simulate: fix applied (assert.equal found) but tests still fail after rerun
 344        agent.runTestFiles = async () => ({
 345          success: false,
 346          output: 'AssertionError: expected a to equal b',
 347          count: 0,
 348        });
 349  
 350        const result = await agent.fixTestIssues(tmpFile, {
 351          output: 'assert.equal deprecation - use strictEqual',
 352        });
 353  
 354        assert.strictEqual(result, false, 'Should return false when retest still fails');
 355        agent.runTestFiles = origRun;
 356      } finally {
 357        await cleanup();
 358        try {
 359          await fs.unlink(tmpFile);
 360        } catch (_e) {
 361          /* ignore */
 362        }
 363      }
 364    });
 365  
 366    test('fixTestIssues error handler returns false when readFile throws', async () => {
 367      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-fti5.db');
 368      try {
 369        agent.log = async () => {};
 370  
 371        // Pass a non-existent file path to trigger the error handler (lines 1024-1029)
 372        const result = await agent.fixTestIssues('/nonexistent/path/to/missing-test-file.test.js', {
 373          output: 'ReferenceError: assert is not defined',
 374        });
 375  
 376        assert.strictEqual(result, false, 'Should return false when file cannot be read');
 377      } finally {
 378        await cleanup();
 379      }
 380    });
 381  
 382    test('fixTestIssues returns false when no fix patterns match', async () => {
 383      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-fti6.db');
 384      const tmpFile = './tests/agents/tmp-no-pattern.test.js';
 385      try {
 386        await fs.writeFile(
 387          tmpFile,
 388          `import { test } from 'node:test';
 389  import assert from 'node:assert';
 390  test('passing test', () => { assert.ok(true); });
 391  `
 392        );
 393  
 394        const result = await agent.fixTestIssues(tmpFile, {
 395          output: 'Some completely unrecognized error pattern xyz abc 123',
 396        });
 397  
 398        assert.strictEqual(result, false, 'Should return false when no patterns match');
 399      } finally {
 400        await cleanup();
 401        try {
 402          await fs.unlink(tmpFile);
 403        } catch (_e) {
 404          /* ignore */
 405        }
 406      }
 407    });
 408  
 409    test('fixTestIssues: async fix does not change already-async test', async () => {
 410      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-fti7.db');
 411      const tmpFile = './tests/agents/tmp-already-async.test.js';
 412      try {
 413        // File already has async - the fix should not change it
 414        const originalCode = `import { test } from 'node:test';
 415  import assert from 'node:assert';
 416  test('already async test', async () => {
 417    const result = await Promise.resolve(1);
 418    assert.strictEqual(result, 1);
 419  });
 420  `;
 421        await fs.writeFile(tmpFile, originalCode);
 422  
 423        const origRun = agent.runTestFiles;
 424        agent.runTestFiles = async () => ({ success: true, output: '1 passing', count: 1 });
 425  
 426        const result = await agent.fixTestIssues(tmpFile, {
 427          output: 'await someValue at test',
 428        });
 429  
 430        // The async fix pattern checks if 'async (' exists - it does, so no change
 431        const afterContent = await fs.readFile(tmpFile, 'utf8');
 432        assert.strictEqual(afterContent, originalCode, 'Already-async code should not be modified');
 433  
 434        agent.runTestFiles = origRun;
 435      } finally {
 436        await cleanup();
 437        try {
 438          await fs.unlink(tmpFile);
 439        } catch (_e) {
 440          /* ignore */
 441        }
 442      }
 443    });
 444  });
 445  
 446  // ============================================================
 447  // identifyUncoveredLines: paths in coverage file handling
 448  // ============================================================
 449  
 450  describe('QA Agent Extended - identifyUncoveredLines', () => {
 451    test('returns null when coverage file does not exist', async () => {
 452      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-iucl1.db');
 453      try {
 454        agent.log = async () => {};
 455  
 456        // Force coverage-summary.json to not exist by using a file we know isn't in coverage
 457        const result = await agent.identifyUncoveredLines('src/completely-nonexistent-xyz.js');
 458        assert.strictEqual(result, null, 'Should return null when file not in coverage data');
 459      } finally {
 460        await cleanup();
 461      }
 462    });
 463  
 464    test('identifyUncoveredLines is an async function returning Promise', async () => {
 465      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-iucl2.db');
 466      try {
 467        agent.log = async () => {};
 468        const result = agent.identifyUncoveredLines('src/test.js');
 469        assert.ok(result instanceof Promise, 'Should return a Promise');
 470        await result.catch(() => {}); // catch any errors
 471      } finally {
 472        await cleanup();
 473      }
 474    });
 475  });
 476  
 477  // ============================================================
 478  // runTestFiles: success/failure paths
 479  // ============================================================
 480  
 481  describe('QA Agent Extended - runTestFiles', () => {
 482    test('runTestFiles returns success structure', async () => {
 483      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-rtf1.db');
 484      try {
 485        // We test with a real valid test file - this should pass
 486        const result = await agent.runTestFiles(['tests/agents/qa.test.js']);
 487        assert.ok(typeof result === 'object', 'Should return object');
 488        assert.ok('success' in result, 'Should have success field');
 489        assert.ok('output' in result, 'Should have output field');
 490        assert.ok(typeof result.success === 'boolean', 'Success should be boolean');
 491      } finally {
 492        await cleanup();
 493      }
 494    });
 495  
 496    test('runTestFiles returns correct structure for any file', async () => {
 497      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-rtf2.db');
 498      try {
 499        const result = await agent.runTestFiles(['/nonexistent/test/file.test.js']);
 500        assert.ok(typeof result === 'object', 'Should return object');
 501        assert.ok('success' in result, 'Should have success field');
 502        assert.ok('output' in result, 'Should have output field');
 503        assert.ok(typeof result.success === 'boolean', 'success should be boolean');
 504        assert.ok(typeof result.output === 'string', 'output should be string');
 505      } finally {
 506        await cleanup();
 507      }
 508    });
 509  });
 510  
 511  // ============================================================
 512  // runTestPattern: method exists and returns correct structure
 513  // ============================================================
 514  
 515  describe('QA Agent Extended - runTestPattern', () => {
 516    test('runTestPattern is callable and returns object', async () => {
 517      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-rtp1.db');
 518      try {
 519        // Run a real pattern (may succeed or fail - either is acceptable)
 520        const result = await agent.runTestPattern('nonexistent-pattern-xyz');
 521        assert.ok(typeof result === 'object', 'Should return object');
 522        assert.ok('success' in result, 'Should have success field');
 523        assert.ok('output' in result, 'Should have output field');
 524      } finally {
 525        await cleanup();
 526      }
 527    });
 528  });
 529  
 530  // ============================================================
 531  // runAllTests: method exists and returns correct structure
 532  // ============================================================
 533  
 534  describe('QA Agent Extended - runAllTests', () => {
 535    test('runAllTests is callable and returns object', async () => {
 536      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-rat1.db');
 537      try {
 538        // Don't actually run all tests - just verify it returns the right structure
 539        // Patch execSync to avoid actually running npm test
 540        const origAgent = agent;
 541        const origRunFiles = agent.runTestFiles;
 542        agent.runTestFiles = async () => ({ success: true, output: 'mocked', count: 0 });
 543  
 544        // runAllTests calls execSync internally - call it but it's acceptable to fail
 545        const result = await agent.runAllTests().catch(err => ({
 546          success: false,
 547          output: err.message,
 548          count: 0,
 549        }));
 550  
 551        assert.ok(typeof result === 'object', 'Should return object');
 552        agent.runTestFiles = origRunFiles;
 553      } finally {
 554        await cleanup();
 555      }
 556    });
 557  });
 558  
 559  // ============================================================
 560  // getFileCoverage: various scenarios
 561  // ============================================================
 562  
 563  describe('QA Agent Extended - getFileCoverage', () => {
 564    test('returns 0 for files not in coverage data', async () => {
 565      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-gfc1.db');
 566      try {
 567        agent.log = async () => {};
 568        const coverage = await agent.getFileCoverage(['src/absolutely-nonexistent-xyz.js']);
 569        assert.ok(typeof coverage === 'object');
 570        assert.strictEqual(coverage['src/absolutely-nonexistent-xyz.js'], 0);
 571      } finally {
 572        await cleanup();
 573      }
 574    });
 575  
 576    test('handles empty files array returning empty object', async () => {
 577      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-gfc2.db');
 578      try {
 579        agent.log = async () => {};
 580        const coverage = await agent.getFileCoverage([]);
 581        assert.deepStrictEqual(coverage, {});
 582      } finally {
 583        await cleanup();
 584      }
 585    });
 586  
 587    test('handles multiple files gracefully', async () => {
 588      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-gfc3.db');
 589      try {
 590        agent.log = async () => {};
 591        const files = ['src/nonexistent1.js', 'src/nonexistent2.js', 'src/nonexistent3.js'];
 592        const coverage = await agent.getFileCoverage(files);
 593        assert.ok(typeof coverage === 'object');
 594        assert.strictEqual(Object.keys(coverage).length, 3);
 595        for (const file of files) {
 596          assert.strictEqual(typeof coverage[file], 'number');
 597          assert.ok(coverage[file] >= 0 && coverage[file] <= 100);
 598        }
 599      } finally {
 600        await cleanup();
 601      }
 602    });
 603  });
 604  
 605  // ============================================================
 606  // processTask: error propagation
 607  // ============================================================
 608  
 609  test('QA Agent Extended - processTask propagates errors for required-context tasks', async () => {
 610    const { db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-pt1.db');
 611    try {
 612      // Tasks that require context - passing null context_json should throw
 613      const requiresContextTypes = ['verify_fix', 'write_test', 'check_coverage', 'run_tests'];
 614  
 615      for (const taskType of requiresContextTypes) {
 616        const taskId = db
 617          .prepare(
 618            `INSERT INTO agent_tasks (task_type, assigned_to, status)
 619             VALUES (?, 'qa', 'pending')`
 620          )
 621          .run(taskType).lastInsertRowid;
 622  
 623        const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 624        // context_json is null
 625  
 626        await assert.rejects(
 627          async () => agent.processTask(task),
 628          err => {
 629            assert.ok(err.message.length > 0);
 630            return true;
 631          },
 632          `${taskType} should throw when context_json is missing`
 633        );
 634      }
 635    } finally {
 636      await cleanup();
 637    }
 638  });
 639  
 640  // ============================================================
 641  // writeTest: integration tests with mocked internal methods
 642  // ============================================================
 643  
 644  test('QA Agent Extended - writeTest succeeds and creates coverage commit', async () => {
 645    const { db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-wt1.db');
 646    const tmpTestFile = './tests/agents/tmp-write-test-ext-new.test.js';
 647    try {
 648      try {
 649        await fs.unlink(tmpTestFile);
 650      } catch (_e) {
 651        /* ignore */
 652      }
 653  
 654      const taskId = insertQaTask(db, 'write_test', {
 655        files_to_test: ['src/scoring.js'],
 656        current_coverage: 30,
 657        target_coverage: 80,
 658      });
 659      const task = getQaTask(db, taskId);
 660  
 661      // Mock all IO operations
 662      agent.identifyUncoveredLines = async () => ({
 663        uncoveredLines: [10, 20, 30],
 664        sourceCode: 'function score(site) {\n  if (!site) { return null; }\n  return site.score;\n}',
 665        coveragePct: 30,
 666      });
 667      agent.generateTests = async () =>
 668        "import { test } from 'node:test';\nimport assert from 'node:assert';\ntest('score handles null', () => { assert.ok(true); });\n";
 669      agent.getTestFile = () => tmpTestFile;
 670      agent.fileExists = async () => false; // New file
 671      agent.runTestFiles = async () => ({ success: true, output: '1 passing', count: 1 });
 672      agent.getFileCoverage = async files => {
 673        const r = {};
 674        files.forEach(f => (r[f] = 82));
 675        return r;
 676      };
 677  
 678      await agent.writeTest(task);
 679  
 680      const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 681      assert.ok(['completed', 'failed'].includes(updated.status));
 682  
 683      if (updated.status === 'completed') {
 684        const result = JSON.parse(updated.result_json || '{}');
 685        assert.ok(Array.isArray(result.tests_written));
 686      }
 687    } finally {
 688      await cleanup();
 689      try {
 690        await fs.unlink(tmpTestFile);
 691      } catch (_e) {
 692        /* ignore */
 693      }
 694    }
 695  });
 696  
 697  test('QA Agent Extended - writeTest handles git commit error gracefully', async () => {
 698    const { db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-wt2.db');
 699    const tmpTestFile = './tests/agents/tmp-write-test-git-err.test.js';
 700    try {
 701      const taskId = insertQaTask(db, 'write_test', {
 702        files_to_test: ['src/enrich.js'],
 703        current_coverage: 40,
 704      });
 705      const task = getQaTask(db, taskId);
 706  
 707      agent.identifyUncoveredLines = async () => ({
 708        uncoveredLines: [5],
 709        sourceCode: 'function enrich() { return {}; }',
 710        coveragePct: 40,
 711      });
 712      agent.generateTests = async () =>
 713        "import { test } from 'node:test';\ntest('enrich works', () => {});\n";
 714      agent.getTestFile = () => tmpTestFile;
 715      agent.fileExists = async () => false;
 716      agent.runTestFiles = async () => ({ success: true, output: '1 passing', count: 1 });
 717      agent.getFileCoverage = async files => {
 718        const r = {};
 719        files.forEach(f => (r[f] = 85));
 720        return r;
 721      };
 722  
 723      // Task should complete even if git commit fails (git errors are caught internally)
 724      await agent.writeTest(task);
 725  
 726      const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 727      assert.ok(['completed', 'failed'].includes(updated.status));
 728    } finally {
 729      await cleanup();
 730      try {
 731        await fs.unlink(tmpTestFile);
 732      } catch (_e) {
 733        /* ignore */
 734      }
 735    }
 736  });
 737  
 738  // ============================================================
 739  // verifyFix: complete path with coverage at exactly 80%
 740  // ============================================================
 741  
 742  test('QA Agent Extended - verifyFix at exactly 80% coverage threshold passes', async () => {
 743    const { db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-vf1.db');
 744    try {
 745      const taskId = insertQaTask(db, 'verify_fix', {
 746        files_changed: ['src/scrape.js'],
 747      });
 748      const task = getQaTask(db, taskId);
 749  
 750      agent.fileExists = async f => f.endsWith('.test.js');
 751      agent.runTestFiles = async () => ({ success: true, output: '5 passing', count: 5 });
 752      agent.getFileCoverage = async files => {
 753        const r = {};
 754        files.forEach(f => (r[f] = 80)); // Exactly 80%
 755        return r;
 756      };
 757  
 758      await agent.verifyFix(task);
 759  
 760      const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 761      // At exactly 80%, it should pass (< 80 fails, >= 80 passes)
 762      assert.strictEqual(updated.status, 'completed', 'Should complete at exactly 80% coverage');
 763    } finally {
 764      await cleanup();
 765    }
 766  });
 767  
 768  test('QA Agent Extended - verifyFix with coverage at 79% blocks', async () => {
 769    const { db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-vf2.db');
 770    try {
 771      const taskId = insertQaTask(db, 'verify_fix', {
 772        files_changed: ['src/score.js'],
 773      });
 774      const task = getQaTask(db, taskId);
 775  
 776      agent.fileExists = async f => f.endsWith('.test.js');
 777      agent.runTestFiles = async () => ({ success: true, output: '3 passing', count: 3 });
 778      agent.getFileCoverage = async files => {
 779        const r = {};
 780        files.forEach(f => (r[f] = 79)); // Just below 80%
 781        return r;
 782      };
 783      agent.askQuestion = async () => {};
 784  
 785      await agent.verifyFix(task);
 786  
 787      const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 788      assert.strictEqual(updated.status, 'blocked', 'Should block at 79% coverage (below 80%)');
 789    } finally {
 790      await cleanup();
 791    }
 792  });
 793  
 794  // ============================================================
 795  // checkCoverage: with files that have high coverage
 796  // ============================================================
 797  
 798  test('QA Agent Extended - checkCoverage all files above threshold returns all_meet_threshold=true', async () => {
 799    const { db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-cc1.db');
 800    try {
 801      const taskId = insertQaTask(db, 'check_coverage', {
 802        files: ['src/logger.js', 'src/rate-limiter.js'],
 803      });
 804      const task = getQaTask(db, taskId);
 805  
 806      agent.getFileCoverage = async files => {
 807        const r = {};
 808        files.forEach(f => (r[f] = 90));
 809        return r;
 810      };
 811  
 812      await agent.checkCoverage(task);
 813  
 814      const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 815      assert.strictEqual(updated.status, 'completed');
 816      const result = JSON.parse(updated.result_json || '{}');
 817      assert.strictEqual(result.all_meet_threshold, true);
 818      assert.strictEqual(result.below_threshold.length, 0);
 819    } finally {
 820      await cleanup();
 821    }
 822  });
 823  
 824  // ============================================================
 825  // exploratoryTest: with detailed test areas
 826  // ============================================================
 827  
 828  test('QA Agent Extended - exploratoryTest with detailed areas completes correctly', async () => {
 829    const { db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-et1.db');
 830    try {
 831      const taskId = insertQaTask(db, 'exploratory_testing', {
 832        feature: 'Email outreach',
 833        files: ['src/outreach/email.js'],
 834        test_areas: ['deliverability', 'rate limiting', 'error handling', 'retry logic'],
 835      });
 836      const task = getQaTask(db, taskId);
 837  
 838      await agent.exploratoryTest(task);
 839  
 840      const updated = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 841      assert.strictEqual(updated.status, 'completed');
 842      const result = JSON.parse(updated.result_json || '{}');
 843      assert.strictEqual(result.manual_testing_required, true);
 844      assert.strictEqual(result.exploratory_testing_performed, false);
 845      assert.ok(result.recommendation.length > 0);
 846    } finally {
 847      await cleanup();
 848    }
 849  });
 850  
 851  // ============================================================
 852  // approximateUncoveredLines: additional edge cases
 853  // ============================================================
 854  
 855  describe('QA Agent Extended - approximateUncoveredLines edge cases', () => {
 856    test('handles code with only return null (no other patterns)', async () => {
 857      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-aul1.db');
 858      try {
 859        const code = `function maybeNull(x) {
 860    if (!x) {
 861      return null;
 862    }
 863    return x.value;
 864  }`;
 865        const result = agent.approximateUncoveredLines(code, 60);
 866        assert.ok(result.uncoveredLines.includes(3)); // return null line
 867        assert.strictEqual(result.coveragePct, 60);
 868        assert.strictEqual(result.sourceCode, code);
 869      } finally {
 870        await cleanup();
 871      }
 872    });
 873  
 874    test('handles code with multiple catch blocks', async () => {
 875      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-aul2.db');
 876      try {
 877        const code = `async function fetchData(url) {
 878    try {
 879      const response = await fetch(url);
 880      return response.json();
 881    } catch (error) {
 882      logger.error(error);
 883      return null;
 884    }
 885  }
 886  
 887  async function processData(data) {
 888    try {
 889      return transform(data);
 890    } catch (err) {
 891      return null;
 892    }
 893  }`;
 894        const result = agent.approximateUncoveredLines(code, 40);
 895        // Should identify both catch lines and null returns
 896        assert.ok(result.uncoveredLines.length > 0);
 897        assert.ok(Array.isArray(result.uncoveredLines));
 898      } finally {
 899        await cleanup();
 900      }
 901    });
 902  
 903    test('handles code with block comments spanning multiple lines', async () => {
 904      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-aul3.db');
 905      try {
 906        const code = `/**
 907   * This is a JSDoc comment
 908   * spanning multiple lines
 909   */
 910  function documented() {
 911    return true;
 912  }`;
 913        const result = agent.approximateUncoveredLines(code, 70);
 914        // Comment lines should not be included
 915        assert.ok(!result.uncoveredLines.includes(1));
 916        assert.ok(!result.uncoveredLines.includes(2));
 917        assert.ok(!result.uncoveredLines.includes(3));
 918        assert.ok(!result.uncoveredLines.includes(4));
 919      } finally {
 920        await cleanup();
 921      }
 922    });
 923  });
 924  
 925  // ============================================================
 926  // mergeTests: additional cases
 927  // ============================================================
 928  
 929  describe('QA Agent Extended - mergeTests additional cases', () => {
 930    test('mergeTests handles tests without trailing semicolons', async () => {
 931      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-mt1.db');
 932      try {
 933        const existing = `import { test } from 'node:test';
 934  test('first', () => {});`;
 935  
 936        const newTests = `import { test } from 'node:test';
 937  test('second test with content', () => {
 938    const x = 1 + 2;
 939    console.log(x);
 940  });`;
 941  
 942        const result = await agent.mergeTests(existing, newTests);
 943        assert.ok(typeof result === 'string');
 944        assert.ok(result.includes('first'));
 945      } finally {
 946        await cleanup();
 947      }
 948    });
 949  
 950    test('mergeTests handles existing tests with no describe block', async () => {
 951      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-mt2.db');
 952      try {
 953        const existing = `import { test } from 'node:test';
 954  import assert from 'node:assert';
 955  test('top-level test A', () => { assert.ok(true); });`;
 956  
 957        const newTests = `import { test } from 'node:test';
 958  import assert from 'node:assert';
 959  test('top-level test B', () => { assert.ok(true); });`;
 960  
 961        const result = await agent.mergeTests(existing, newTests);
 962        assert.ok(typeof result === 'string');
 963        assert.ok(result.includes('top-level test A'));
 964      } finally {
 965        await cleanup();
 966      }
 967    });
 968  });
 969  
 970  // ============================================================
 971  // addMissingImport: CommonJS patterns
 972  // ============================================================
 973  
 974  describe('QA Agent Extended - addMissingImport CommonJS patterns', () => {
 975    test('handles CJS require for assert', async () => {
 976      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-ami1.db');
 977      try {
 978        const cjsCode = `const test = require('node:test');
 979  test('my test', () => { assert.ok(true); });`;
 980  
 981        const result = agent.addMissingImport(cjsCode, 'assert');
 982        assert.ok(result.includes('require'));
 983        assert.ok(result.includes('assert'));
 984      } finally {
 985        await cleanup();
 986      }
 987    });
 988  
 989    test('handles CJS require for Database', async () => {
 990      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-ami2.db');
 991      try {
 992        const cjsCode = `const test = require('node:test');
 993  test('db test', () => { new Database('./test.db'); });`;
 994  
 995        const result = agent.addMissingImport(cjsCode, 'Database');
 996        assert.ok(result.includes('require'));
 997        assert.ok(result.includes('better-sqlite3'));
 998      } finally {
 999        await cleanup();
1000      }
1001    });
1002  
1003    test('handles ESM import for it identifier', async () => {
1004      const { db: _db, agent, cleanup } = await createQaTestEnv('./test-qa-ext-ami3.db');
1005      try {
1006        const esmCode = `import { test } from 'node:test';
1007  test('first', () => {});`;
1008        const result = agent.addMissingImport(esmCode, 'it');
1009        assert.ok(result.includes('it'));
1010        assert.ok(result.includes("from 'node:test'"));
1011      } finally {
1012        await cleanup();
1013      }
1014    });
1015  });