qa.test.js
   1  /**
   2   * Tests for QA Agent
   3   */
   4  
   5  import { test, describe, before, beforeEach, afterEach } from 'node:test';
   6  import assert from 'node:assert';
   7  import fs from 'fs/promises';
   8  import { QAAgent } from '../../src/agents/qa.js';
   9  import Database from 'better-sqlite3';
  10  import { resetDb as resetQaBaseDb } from '../../src/agents/base-agent.js';
  11  import { resetDb as resetQaTaskDb } from '../../src/agents/utils/task-manager.js';
  12  import { resetDb as resetQaMessageDb } from '../../src/agents/utils/message-manager.js';
  13  
  14  describe('QA Agent', () => {
  15    let agent;
  16  
  17    before(async () => {
  18      // Create agent without full initialization to test utility methods
  19      agent = new QAAgent();
  20    });
  21  
  22    describe('getTestFile', () => {
  23      test('converts src file to test file path', () => {
  24        const result = agent.getTestFile('src/scoring.js');
  25        assert.strictEqual(result, 'tests/scoring.test.js');
  26      });
  27  
  28      test('handles nested src paths', () => {
  29        const result = agent.getTestFile('src/agents/qa.js');
  30        assert.strictEqual(result, 'tests/qa.test.js');
  31      });
  32  
  33      test('handles utils paths', () => {
  34        const result = agent.getTestFile('src/utils/logger.js');
  35        assert.strictEqual(result, 'tests/logger.test.js');
  36      });
  37    });
  38  
  39    describe('fileExists', () => {
  40      test('returns true for existing file', async () => {
  41        const exists = await agent.fileExists('package.json');
  42        assert.strictEqual(exists, true);
  43      });
  44  
  45      test('returns false for non-existing file', async () => {
  46        const exists = await agent.fileExists('nonexistent-file.txt');
  47        assert.strictEqual(exists, false);
  48      });
  49    });
  50  
  51    describe('approximateUncoveredLines', () => {
  52      test('identifies error handlers', () => {
  53        const sourceCode = `
  54  function test() {
  55    try {
  56      doSomething();
  57    } catch (error) {
  58      logger.error(error);
  59      return null;
  60    }
  61  }
  62  `;
  63  
  64        const result = agent.approximateUncoveredLines(sourceCode, 50);
  65  
  66        assert.ok(result.uncoveredLines.includes(5)); // catch line
  67        assert.ok(result.uncoveredLines.includes(7)); // return null line
  68        assert.strictEqual(result.coveragePct, 50);
  69      });
  70  
  71      test('identifies else branches', () => {
  72        const sourceCode = `
  73  function test(x) {
  74    if (x > 0) {
  75      return true;
  76    } else {
  77      return false;
  78    }
  79  }
  80  `;
  81  
  82        const result = agent.approximateUncoveredLines(sourceCode, 60);
  83  
  84        assert.ok(result.uncoveredLines.includes(5)); // else line
  85        assert.ok(result.uncoveredLines.includes(6)); // return false
  86      });
  87  
  88      test('skips comments and empty lines', () => {
  89        const sourceCode = `
  90  // This is a comment
  91  /* Block comment */
  92  
  93  function test() {
  94    return true;
  95  }
  96  `;
  97  
  98        const result = agent.approximateUncoveredLines(sourceCode, 80);
  99  
 100        // Should not include comment lines or empty lines
 101        assert.ok(!result.uncoveredLines.includes(2)); // Comment
 102        assert.ok(!result.uncoveredLines.includes(3)); // Block comment
 103        assert.ok(!result.uncoveredLines.includes(4)); // Empty line
 104      });
 105    });
 106  
 107    describe('mergeTests', () => {
 108      test('merges new tests with existing tests', async () => {
 109        const existingTests = `import { test } from 'node:test';
 110  import assert from 'node:assert';
 111  
 112  describe('module', () => {
 113    test('existing test', () => {
 114      assert.ok(true);
 115    });
 116  });`;
 117  
 118        const newTests = `import { test, describe } from 'node:test';
 119  import assert from 'node:assert';
 120  
 121  describe('module', () => {
 122    test('new test', () => {
 123      assert.ok(true);
 124    });
 125  });`;
 126  
 127        const result = await agent.mergeTests(existingTests, newTests);
 128  
 129        assert.ok(result.includes('existing test'));
 130        assert.ok(result.includes('new test'));
 131      });
 132  
 133      test('avoids duplicate test names', async () => {
 134        // Safe-append: appends all content (only deduplicates imports, not test names).
 135        // Preserving existing tests is the priority; verification happens via runTestFiles.
 136        const existingTests = `import { test } from 'node:test';
 137  import assert from 'node:assert';
 138  
 139  describe('module', () => {
 140    test('same test name', () => {
 141      assert.ok(true);
 142    });
 143  });`;
 144  
 145        const newTests = `import { test, describe } from 'node:test';
 146  import assert from 'node:assert';
 147  
 148  describe('module', () => {
 149    test('same test name', () => {
 150      assert.ok(false);
 151    });
 152  });`;
 153  
 154        const result = await agent.mergeTests(existingTests, newTests);
 155  
 156        // Existing test is preserved
 157        assert.ok(result.includes('same test name'));
 158        assert.ok(result.startsWith(existingTests.trimEnd()));
 159        // Result is longer (new content was appended)
 160        assert.ok(result.length > existingTests.length);
 161      });
 162  
 163      test('returns new tests if no existing structure', async () => {
 164        // When existing file is empty, returns new tests directly (no separator)
 165        const existingTests = '';
 166        const newTests = `import { test } from 'node:test';
 167  
 168  test('new test', () => {
 169    assert.ok(true);
 170  });`;
 171  
 172        const result = await agent.mergeTests(existingTests, newTests);
 173  
 174        assert.strictEqual(result, newTests);
 175      });
 176    });
 177  
 178    describe('getFileCoverage', () => {
 179      test('reads coverage from coverage-summary.json', async () => {
 180        // This test requires actual coverage data to be present
 181        // Skip if no coverage directory
 182        try {
 183          await fs.access('coverage/coverage-summary.json');
 184        } catch (e) {
 185          // Skip test if no coverage
 186          return;
 187        }
 188  
 189        const coverage = await agent.getFileCoverage(['src/agents/qa.js']);
 190  
 191        assert.ok(coverage['src/agents/qa.js'] !== undefined);
 192        assert.ok(typeof coverage['src/agents/qa.js'] === 'number');
 193        assert.ok(coverage['src/agents/qa.js'] >= 0);
 194        assert.ok(coverage['src/agents/qa.js'] <= 100);
 195      });
 196  
 197      test('returns 0 for missing files', async () => {
 198        const coverage = await agent.getFileCoverage(['nonexistent-file.js']);
 199  
 200        assert.strictEqual(coverage['nonexistent-file.js'], 0);
 201      });
 202  
 203      test('handles multiple files', async () => {
 204        // Skip if no coverage
 205        try {
 206          await fs.access('coverage/coverage-summary.json');
 207        } catch (e) {
 208          return;
 209        }
 210  
 211        const coverage = await agent.getFileCoverage([
 212          'src/agents/qa.js',
 213          'src/agents/base-agent.js',
 214        ]);
 215  
 216        assert.ok(Object.keys(coverage).length === 2);
 217      });
 218    });
 219  
 220    describe('runTestFiles', () => {
 221      test('returns result with success and output', async () => {
 222        // This would make an actual npm test call
 223        // Just verify the method exists and returns correct structure
 224        assert.ok(typeof agent.runTestFiles === 'function');
 225      });
 226    });
 227  
 228    describe('addMissingImport', () => {
 229      test('adds node:test import for test identifier', () => {
 230        const code = `test('my test', async (t) => {\n  // test code\n});`;
 231        const result = agent.addMissingImport(code, 'test');
 232  
 233        assert.ok(result.includes("import { test } from 'node:test';"));
 234        assert.ok(result.includes("test('my test'"));
 235      });
 236  
 237      test('adds node:assert import for assert identifier', () => {
 238        const code = `test('my test', () => {\n  // use assert\n});`;
 239        const result = agent.addMissingImport(code, 'assert');
 240  
 241        assert.ok(result.includes("import assert from 'node:assert';"));
 242      });
 243  
 244      test('adds to existing import if module already imported', () => {
 245        const code = `import { test } from 'node:test';\n\ntest('my test', () => {});`;
 246        const result = agent.addMissingImport(code, 'describe');
 247  
 248        assert.ok(result.includes("import { test, describe } from 'node:test';"));
 249        assert.strictEqual(result.match(/import.*from 'node:test'/g).length, 1); // Only one import line
 250      });
 251  
 252      test('detects ESM vs CommonJS correctly', () => {
 253        const esmCode = `import { test } from 'node:test';\ntest('my test', () => {});`;
 254        const cjsCode = `const test = require('node:test');\ntest('my test', () => {});`;
 255  
 256        const esmResult = agent.addMissingImport(esmCode, 'assert');
 257        const cjsResult = agent.addMissingImport(cjsCode, 'assert');
 258  
 259        assert.ok(esmResult.includes('import'));
 260        assert.ok(cjsResult.includes('require'));
 261      });
 262  
 263      test('handles named imports correctly', () => {
 264        const code = `test('my test', () => {});`;
 265        const result = agent.addMissingImport(code, 'strictEqual');
 266  
 267        assert.ok(result.includes("import { strictEqual } from 'node:assert';"));
 268      });
 269  
 270      test('handles default imports correctly', () => {
 271        const code = `test('my test', () => {});`;
 272        const result = agent.addMissingImport(code, 'Database');
 273  
 274        assert.ok(result.includes("import Database from 'better-sqlite3';"));
 275        assert.ok(!result.includes('{ Database }')); // Not a named import
 276      });
 277  
 278      test('inserts after last import in ESM', () => {
 279        const code = `import { test } from 'node:test';\nimport assert from 'node:assert';\n\ntest('my test', () => {});`;
 280        const result = agent.addMissingImport(code, 'readFile');
 281  
 282        const lines = result.split('\n');
 283        const readFileImportIndex = lines.findIndex(line =>
 284          line.includes("import { readFile } from 'fs/promises'")
 285        );
 286        const lastImportIndex = 1; // assert import
 287  
 288        assert.ok(readFileImportIndex === lastImportIndex + 1); // After last import
 289      });
 290  
 291      test('adds comment for unknown identifiers', () => {
 292        const code = `test('my test', () => {});`;
 293        const result = agent.addMissingImport(code, 'unknownIdentifier');
 294  
 295        assert.ok(result.includes('// TODO: Add import for unknownIdentifier (unknown module)'));
 296      });
 297  
 298      test('does not duplicate imports', () => {
 299        const code = `import { test } from 'node:test';\n\ntest('my test', () => {});`;
 300        const result = agent.addMissingImport(code, 'test');
 301  
 302        // Should return unchanged since 'test' is already imported
 303        assert.strictEqual(result, code);
 304      });
 305  
 306      test('handles CommonJS named imports', () => {
 307        const code = `const test = require('node:test');\ntest('my test', () => {});`;
 308        const result = agent.addMissingImport(code, 'strictEqual');
 309  
 310        assert.ok(result.includes("const { strictEqual } = require('node:assert');"));
 311      });
 312  
 313      test('handles CommonJS default imports', () => {
 314        const code = `const test = require('node:test');\ntest('my test', () => {});`;
 315        const result = agent.addMissingImport(code, 'Database');
 316  
 317        assert.ok(result.includes("const Database = require('better-sqlite3');"));
 318      });
 319  
 320      test('handles fs/promises imports', () => {
 321        const code = `test('my test', async () => {});`;
 322        const result = agent.addMissingImport(code, 'readFile');
 323  
 324        assert.ok(result.includes("import { readFile } from 'fs/promises';"));
 325      });
 326  
 327      test('handles path imports', () => {
 328        const code = `test('my test', () => {});`;
 329        const result = agent.addMissingImport(code, 'join');
 330  
 331        assert.ok(result.includes("import { join } from 'path';"));
 332      });
 333    });
 334  });
 335  
 336  // ============================================================
 337  // ADDITIONAL TESTS TO BOOST QA COVERAGE
 338  // ============================================================
 339  
 340  const QA_INTEGRATION_DB = './tests/agents/test-qa-integration.db';
 341  
 342  function createQaTestDb() {
 343    const db = new Database(QA_INTEGRATION_DB);
 344    db.exec(`
 345      CREATE TABLE IF NOT EXISTS agent_tasks (
 346        id INTEGER PRIMARY KEY AUTOINCREMENT, task_type TEXT NOT NULL,
 347        assigned_to TEXT NOT NULL, created_by TEXT, status TEXT DEFAULT 'pending',
 348        priority INTEGER DEFAULT 5, context_json TEXT, result_json TEXT,
 349        parent_task_id INTEGER, error_message TEXT,
 350        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 351        started_at DATETIME, completed_at DATETIME, retry_count INTEGER DEFAULT 0
 352      );
 353      CREATE TABLE IF NOT EXISTS agent_messages (
 354        id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER,
 355        from_agent TEXT NOT NULL, to_agent TEXT NOT NULL, message_type TEXT,
 356        content TEXT NOT NULL, metadata_json TEXT,
 357        created_at DATETIME DEFAULT CURRENT_TIMESTAMP, read_at DATETIME
 358      );
 359      CREATE TABLE IF NOT EXISTS agent_logs (
 360        id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER,
 361        agent_name TEXT NOT NULL, log_level TEXT, message TEXT, data_json TEXT,
 362        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 363      );
 364      CREATE TABLE IF NOT EXISTS agent_state (
 365        agent_name TEXT PRIMARY KEY, last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
 366        current_task_id INTEGER, status TEXT DEFAULT 'idle', metrics_json TEXT
 367      );
 368      CREATE TABLE IF NOT EXISTS agent_outcomes (
 369        id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL,
 370        agent_name TEXT NOT NULL, task_type TEXT NOT NULL, outcome TEXT NOT NULL,
 371        context_json TEXT, result_json TEXT, duration_ms INTEGER,
 372        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 373      );
 374      CREATE TABLE IF NOT EXISTS agent_llm_usage (
 375        id INTEGER PRIMARY KEY AUTOINCREMENT, agent_name TEXT NOT NULL,
 376        task_id INTEGER, model TEXT NOT NULL, prompt_tokens INTEGER NOT NULL,
 377        completion_tokens INTEGER NOT NULL, cost_usd DECIMAL(10,6) NOT NULL,
 378        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 379      );
 380      CREATE TABLE IF NOT EXISTS structured_logs (
 381        id INTEGER PRIMARY KEY AUTOINCREMENT, agent_name TEXT, task_id INTEGER,
 382        level TEXT, message TEXT, data_json TEXT,
 383        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 384      );
 385    `);
 386    return db;
 387  }
 388  
 389  async function setupQaIntegration() {
 390    resetQaBaseDb();
 391    resetQaTaskDb();
 392    resetQaMessageDb();
 393    try {
 394      await fs.unlink(QA_INTEGRATION_DB);
 395    } catch (_e) {
 396      /* ignore */
 397    }
 398    const qdb = createQaTestDb();
 399    process.env.DATABASE_PATH = QA_INTEGRATION_DB;
 400    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 401    process.env.AGENT_IMMEDIATE_INVOCATION = 'false';
 402    const { QAAgent: QAFresh } = await import('../../src/agents/qa.js');
 403    const qagent = new QAFresh();
 404    await qagent.initialize();
 405    return { qdb, qagent };
 406  }
 407  
 408  async function teardownQaIntegration(qdb) {
 409    resetQaBaseDb();
 410    resetQaTaskDb();
 411    resetQaMessageDb();
 412    try {
 413      qdb.close();
 414    } catch (_e) {
 415      /* ignore */
 416    }
 417    try {
 418      await fs.unlink(QA_INTEGRATION_DB);
 419    } catch (_e) {
 420      /* ignore */
 421    }
 422  }
 423  
 424  test('QA Agent Integration - processTask throws for verify_fix with missing context_json', async () => {
 425    const { qdb, qagent } = await setupQaIntegration();
 426    try {
 427      const taskId = qdb
 428        .prepare(
 429          `INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES ('verify_fix', 'qa', 'pending')`
 430        )
 431        .run().lastInsertRowid;
 432      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 433  
 434      await assert.rejects(
 435        async () => qagent.processTask(task),
 436        err => {
 437          assert.ok(err.message.length > 0);
 438          return true;
 439        }
 440      );
 441    } finally {
 442      await teardownQaIntegration(qdb);
 443    }
 444  });
 445  
 446  test('QA Agent Integration - check_coverage completes with empty files', async () => {
 447    const { qdb, qagent } = await setupQaIntegration();
 448    try {
 449      const taskId = qdb
 450        .prepare(
 451          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('check_coverage', 'qa', 'pending', ?)`
 452        )
 453        .run(JSON.stringify({ files: [] })).lastInsertRowid;
 454      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 455      task.context_json = JSON.parse(task.context_json);
 456  
 457      await qagent.checkCoverage(task);
 458  
 459      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 460      assert.strictEqual(updated.status, 'completed');
 461      const result = JSON.parse(updated.result_json);
 462      assert.strictEqual(result.all_meet_threshold, true);
 463    } finally {
 464      await teardownQaIntegration(qdb);
 465    }
 466  });
 467  
 468  test('QA Agent Integration - checkCoverage identifies files below 80%', async () => {
 469    const { qdb, qagent } = await setupQaIntegration();
 470    try {
 471      const taskId = qdb
 472        .prepare(
 473          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('check_coverage', 'qa', 'pending', ?)`
 474        )
 475        .run(JSON.stringify({ files: ['src/low-cov.js'] })).lastInsertRowid;
 476      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 477      task.context_json = JSON.parse(task.context_json);
 478  
 479      const orig = qagent.getFileCoverage;
 480      qagent.getFileCoverage = async files => {
 481        const r = {};
 482        files.forEach(f => (r[f] = 50));
 483        return r;
 484      };
 485  
 486      await qagent.checkCoverage(task);
 487  
 488      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 489      assert.strictEqual(updated.status, 'completed');
 490      const result = JSON.parse(updated.result_json);
 491      assert.strictEqual(result.all_meet_threshold, false);
 492      assert.ok(result.below_threshold.length > 0);
 493  
 494      qagent.getFileCoverage = orig;
 495    } finally {
 496      await teardownQaIntegration(qdb);
 497    }
 498  });
 499  
 500  test('QA Agent Integration - run_tests with test_files array', async () => {
 501    const { qdb, qagent } = await setupQaIntegration();
 502    try {
 503      const taskId = qdb
 504        .prepare(
 505          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('run_tests', 'qa', 'pending', ?)`
 506        )
 507        .run(JSON.stringify({ test_files: ['tests/agents/qa.test.js'] })).lastInsertRowid;
 508      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 509      task.context_json = JSON.parse(task.context_json);
 510  
 511      const orig = qagent.runTestFiles;
 512      qagent.runTestFiles = async () => ({ success: true, output: '10 passing', count: 10 });
 513  
 514      await qagent.runTests(task);
 515  
 516      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 517      assert.strictEqual(updated.status, 'completed');
 518      const result = JSON.parse(updated.result_json);
 519      assert.strictEqual(result.success, true);
 520  
 521      qagent.runTestFiles = orig;
 522    } finally {
 523      await teardownQaIntegration(qdb);
 524    }
 525  });
 526  
 527  test('QA Agent Integration - run_tests with pattern', async () => {
 528    const { qdb, qagent } = await setupQaIntegration();
 529    try {
 530      const taskId = qdb
 531        .prepare(
 532          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('run_tests', 'qa', 'pending', ?)`
 533        )
 534        .run(JSON.stringify({ pattern: 'qa' })).lastInsertRowid;
 535      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 536      task.context_json = JSON.parse(task.context_json);
 537  
 538      const orig = qagent.runTestPattern;
 539      qagent.runTestPattern = async p => ({ success: true, output: `Pattern: ${p}` });
 540  
 541      await qagent.runTests(task);
 542  
 543      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 544      assert.strictEqual(updated.status, 'completed');
 545  
 546      qagent.runTestPattern = orig;
 547    } finally {
 548      await teardownQaIntegration(qdb);
 549    }
 550  });
 551  
 552  test('QA Agent Integration - run_tests with no params runs all', async () => {
 553    const { qdb, qagent } = await setupQaIntegration();
 554    try {
 555      const taskId = qdb
 556        .prepare(
 557          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('run_tests', 'qa', 'pending', ?)`
 558        )
 559        .run(JSON.stringify({})).lastInsertRowid;
 560      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 561      task.context_json = JSON.parse(task.context_json);
 562  
 563      const orig = qagent.runAllTests;
 564      qagent.runAllTests = async () => ({ success: true, output: 'All passing' });
 565  
 566      await qagent.runTests(task);
 567  
 568      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 569      assert.strictEqual(updated.status, 'completed');
 570  
 571      qagent.runAllTests = orig;
 572    } finally {
 573      await teardownQaIntegration(qdb);
 574    }
 575  });
 576  
 577  test('QA Agent Integration - exploratory_testing completes with manual flag', async () => {
 578    const { qdb, qagent } = await setupQaIntegration();
 579    try {
 580      const taskId = qdb
 581        .prepare(
 582          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('exploratory_testing', 'qa', 'pending', ?)`
 583        )
 584        .run(
 585          JSON.stringify({ feature: 'Auth', files: ['src/auth.js'], test_areas: ['Login'] })
 586        ).lastInsertRowid;
 587      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 588      task.context_json = JSON.parse(task.context_json);
 589  
 590      await qagent.exploratoryTest(task);
 591  
 592      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 593      assert.strictEqual(updated.status, 'completed');
 594      const result = JSON.parse(updated.result_json);
 595      assert.strictEqual(result.manual_testing_required, true);
 596    } finally {
 597      await teardownQaIntegration(qdb);
 598    }
 599  });
 600  
 601  test('QA Agent Integration - delegates fix_bug to correct agent', async () => {
 602    const { qdb, qagent } = await setupQaIntegration();
 603    try {
 604      const taskId = qdb
 605        .prepare(
 606          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('fix_bug', 'qa', 'pending', ?)`
 607        )
 608        .run(JSON.stringify({ error_message: 'Error', stack_trace: '' })).lastInsertRowid;
 609      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 610      task.context_json = JSON.parse(task.context_json);
 611  
 612      await qagent.processTask(task);
 613  
 614      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 615      assert.ok(['completed', 'failed', 'pending'].includes(updated.status));
 616    } finally {
 617      await teardownQaIntegration(qdb);
 618    }
 619  });
 620  
 621  test('QA Agent Integration - delegates implement_feature to correct agent', async () => {
 622    const { qdb, qagent } = await setupQaIntegration();
 623    try {
 624      const taskId = qdb
 625        .prepare(
 626          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('implement_feature', 'qa', 'pending', ?)`
 627        )
 628        .run(JSON.stringify({ feature_description: 'Add auth' })).lastInsertRowid;
 629      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 630      task.context_json = JSON.parse(task.context_json);
 631  
 632      await qagent.processTask(task);
 633  
 634      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 635      assert.ok(['completed', 'failed', 'pending'].includes(updated.status));
 636    } finally {
 637      await teardownQaIntegration(qdb);
 638    }
 639  });
 640  
 641  test('QA Agent Integration - delegates unknown task type', async () => {
 642    const { qdb, qagent } = await setupQaIntegration();
 643    try {
 644      const taskId = qdb
 645        .prepare(
 646          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('some_unknown_xyz', 'qa', 'pending', ?)`
 647        )
 648        .run(JSON.stringify({ data: 'test' })).lastInsertRowid;
 649      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 650      task.context_json = JSON.parse(task.context_json);
 651  
 652      await qagent.processTask(task);
 653  
 654      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 655      assert.ok(['completed', 'failed', 'pending'].includes(updated.status));
 656    } finally {
 657      await teardownQaIntegration(qdb);
 658    }
 659  });
 660  
 661  test('QA Agent Integration - verifyFix blocks when no test files found', async () => {
 662    const { qdb, qagent } = await setupQaIntegration();
 663    try {
 664      const taskId = qdb
 665        .prepare(
 666          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('verify_fix', 'qa', 'pending', ?)`
 667        )
 668        .run(JSON.stringify({ files_changed: ['src/nonexistent-module-xyz.js'] })).lastInsertRowid;
 669      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 670      task.context_json = JSON.parse(task.context_json);
 671  
 672      await qagent.verifyFix(task);
 673  
 674      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 675      assert.strictEqual(updated.status, 'blocked');
 676      const writeTasks = qdb
 677        .prepare(`SELECT * FROM agent_tasks WHERE task_type = 'write_test' AND parent_task_id = ?`)
 678        .all(taskId);
 679      assert.ok(writeTasks.length > 0);
 680    } finally {
 681      await teardownQaIntegration(qdb);
 682    }
 683  });
 684  
 685  test('QA Agent Integration - verifyFix blocks when tests fail', async () => {
 686    const { qdb, qagent } = await setupQaIntegration();
 687    try {
 688      const taskId = qdb
 689        .prepare(
 690          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('verify_fix', 'qa', 'pending', ?)`
 691        )
 692        .run(JSON.stringify({ files_changed: ['src/scoring.js'] })).lastInsertRowid;
 693      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 694      task.context_json = JSON.parse(task.context_json);
 695  
 696      const origExists = qagent.fileExists;
 697      qagent.fileExists = async f => f.endsWith('.test.js');
 698      const origRun = qagent.runTestFiles;
 699      qagent.runTestFiles = async () => ({ success: false, output: 'AssertionError: expected' });
 700  
 701      await qagent.verifyFix(task);
 702  
 703      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 704      assert.strictEqual(updated.status, 'blocked');
 705  
 706      qagent.fileExists = origExists;
 707      qagent.runTestFiles = origRun;
 708    } finally {
 709      await teardownQaIntegration(qdb);
 710    }
 711  });
 712  
 713  test('QA Agent Integration - verifyFix completes when tests pass and coverage >= 80%', async () => {
 714    const { qdb, qagent } = await setupQaIntegration();
 715    try {
 716      const taskId = qdb
 717        .prepare(
 718          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('verify_fix', 'qa', 'pending', ?)`
 719        )
 720        .run(JSON.stringify({ files_changed: ['src/scoring.js'] })).lastInsertRowid;
 721      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 722      task.context_json = JSON.parse(task.context_json);
 723  
 724      const origExists = qagent.fileExists;
 725      qagent.fileExists = async f => f.endsWith('.test.js');
 726      const origRun = qagent.runTestFiles;
 727      qagent.runTestFiles = async () => ({ success: true, output: '10 passing', count: 10 });
 728      const origCov = qagent.getFileCoverage;
 729      qagent.getFileCoverage = async files => {
 730        const r = {};
 731        files.forEach(f => (r[f] = 85));
 732        return r;
 733      };
 734  
 735      await qagent.verifyFix(task);
 736  
 737      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 738      assert.strictEqual(updated.status, 'completed');
 739      const result = JSON.parse(updated.result_json);
 740      assert.strictEqual(result.tests_passed, true);
 741  
 742      qagent.fileExists = origExists;
 743      qagent.runTestFiles = origRun;
 744      qagent.getFileCoverage = origCov;
 745    } finally {
 746      await teardownQaIntegration(qdb);
 747    }
 748  });
 749  
 750  test('QA Agent Integration - verifyFix blocks and creates write_test when coverage below 80%', async () => {
 751    const { qdb, qagent } = await setupQaIntegration();
 752    try {
 753      const taskId = qdb
 754        .prepare(
 755          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES ('verify_fix', 'qa', 'pending', ?)`
 756        )
 757        .run(JSON.stringify({ files_changed: ['src/scoring.js'] })).lastInsertRowid;
 758      const task = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 759      task.context_json = JSON.parse(task.context_json);
 760  
 761      const origExists = qagent.fileExists;
 762      qagent.fileExists = async f => f.endsWith('.test.js');
 763      const origRun = qagent.runTestFiles;
 764      qagent.runTestFiles = async () => ({ success: true, output: '5 passing' });
 765      const origCov = qagent.getFileCoverage;
 766      qagent.getFileCoverage = async files => {
 767        const r = {};
 768        files.forEach(f => (r[f] = 60));
 769        return r;
 770      };
 771  
 772      await qagent.verifyFix(task);
 773  
 774      const updated = qdb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 775      assert.strictEqual(updated.status, 'blocked');
 776      const writeTasks = qdb
 777        .prepare(`SELECT * FROM agent_tasks WHERE task_type = 'write_test' AND parent_task_id = ?`)
 778        .all(taskId);
 779      assert.ok(writeTasks.length > 0);
 780  
 781      qagent.fileExists = origExists;
 782      qagent.runTestFiles = origRun;
 783      qagent.getFileCoverage = origCov;
 784    } finally {
 785      await teardownQaIntegration(qdb);
 786    }
 787  });
 788  
 789  describe('QA Agent - approximateUncoveredLines extra cases', () => {
 790    let qaAgent2;
 791  
 792    before(async () => {
 793      qaAgent2 = new QAAgent();
 794    });
 795  
 796    test('handles source code with switch statements', () => {
 797      const sourceCode = `
 798  function test(x) {
 799    switch(x) {
 800      case 1:
 801        return 'one';
 802      default:
 803        return 'other';
 804    }
 805  }
 806  `;
 807      const result = qaAgent2.approximateUncoveredLines(sourceCode, 40);
 808      assert.ok(result !== undefined);
 809      assert.strictEqual(result.coveragePct, 40);
 810    });
 811  
 812    test('handles empty source code', () => {
 813      const result = qaAgent2.approximateUncoveredLines('', 0);
 814      assert.ok(result !== undefined);
 815      assert.strictEqual(result.coveragePct, 0);
 816    });
 817  
 818    test('handles code with only whitespace and comments', () => {
 819      const sourceCode = `
 820  // This is a comment
 821  /* Another comment */
 822  `;
 823      const result = qaAgent2.approximateUncoveredLines(sourceCode, 50);
 824      assert.ok(!result.uncoveredLines.includes(2));
 825      assert.ok(!result.uncoveredLines.includes(3));
 826    });
 827  
 828    test('identifies uncovered lines from low coverage percentage', () => {
 829      const sourceCode = `
 830  function test(x) {
 831    if (x === null) {
 832      throw new Error('null input');
 833    }
 834    return x;
 835  }
 836  `;
 837      const result = qaAgent2.approximateUncoveredLines(sourceCode, 30);
 838      assert.ok(result.uncoveredLines.length > 0);
 839    });
 840  });
 841  
 842  describe('QA Agent - runTestFiles, runTestPattern, runAllTests', () => {
 843    let qaAgent3;
 844  
 845    before(async () => {
 846      qaAgent3 = new QAAgent();
 847    });
 848  
 849    test('runTestFiles method exists', () => {
 850      assert.ok(typeof qaAgent3.runTestFiles === 'function');
 851    });
 852  
 853    test('runTestPattern method exists', () => {
 854      assert.ok(typeof qaAgent3.runTestPattern === 'function');
 855    });
 856  
 857    test('runAllTests method exists', () => {
 858      assert.ok(typeof qaAgent3.runAllTests === 'function');
 859    });
 860  });
 861  
 862  describe('QA Agent - fixTestIssues', () => {
 863    let qaAgent4;
 864  
 865    before(async () => {
 866      qaAgent4 = new QAAgent();
 867    });
 868  
 869    test('returns false when error output has no known patterns', async () => {
 870      const tmpFile = './tests/agents/tmp-fix-qa-test.js';
 871      await fs.writeFile(
 872        tmpFile,
 873        `import { test } from 'node:test';
 874  import assert from 'node:assert';
 875  test('ok', () => { assert.ok(true); });
 876  `
 877      );
 878  
 879      const result = await qaAgent4.fixTestIssues(tmpFile, { output: 'no patterns here xyz' });
 880      assert.strictEqual(result, false);
 881  
 882      await fs.unlink(tmpFile).catch(() => {});
 883    });
 884  
 885    test('fixes assert.equal occurrences in test file', async () => {
 886      const tmpFile = './tests/agents/tmp-fix-equal-test.js';
 887      await fs.writeFile(
 888        tmpFile,
 889        `import { test } from 'node:test';
 890  import assert from 'node:assert';
 891  test('uses old assert', () => { assert.equal(1, 1); });
 892  `
 893      );
 894  
 895      const orig = qaAgent4.runTestFiles;
 896      qaAgent4.runTestFiles = async () => ({ success: true, output: 'passing' });
 897  
 898      const result = await qaAgent4.fixTestIssues(tmpFile, {
 899        output: 'assert.equal deprecation warning',
 900      });
 901      assert.ok(typeof result === 'boolean');
 902  
 903      qaAgent4.runTestFiles = orig;
 904      await fs.unlink(tmpFile).catch(() => {});
 905    });
 906  });
 907  
 908  describe('QA Agent - getTestFile extra paths', () => {
 909    let qaAgent5;
 910  
 911    before(async () => {
 912      qaAgent5 = new QAAgent();
 913    });
 914  
 915    test('handles src/stages paths', () => {
 916      const result = qaAgent5.getTestFile('src/stages/scoring.js');
 917      assert.ok(result.endsWith('.test.js'));
 918    });
 919  
 920    test('handles outreach paths', () => {
 921      const result = qaAgent5.getTestFile('src/outreach/email.js');
 922      assert.ok(result.endsWith('.test.js'));
 923    });
 924  
 925    test('handles contacts paths', () => {
 926      const result = qaAgent5.getTestFile('src/contacts/prioritize.js');
 927      assert.ok(result.endsWith('.test.js'));
 928    });
 929  });
 930  
 931  // ============================================================
 932  // ADDITIONAL TESTS - generateTests, identifyUncoveredLines, writeTest
 933  // ============================================================
 934  
 935  const QA_EXTENDED_DB = './tests/agents/test-qa-extended.db';
 936  
 937  function createExtendedDb() {
 938    const db = new Database(QA_EXTENDED_DB);
 939    db.exec(`
 940      CREATE TABLE IF NOT EXISTS agent_tasks (
 941        id INTEGER PRIMARY KEY AUTOINCREMENT, task_type TEXT NOT NULL,
 942        assigned_to TEXT NOT NULL, created_by TEXT, status TEXT DEFAULT 'pending',
 943        priority INTEGER DEFAULT 5, context_json TEXT, result_json TEXT,
 944        parent_task_id INTEGER, error_message TEXT,
 945        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 946        started_at DATETIME, completed_at DATETIME, retry_count INTEGER DEFAULT 0
 947      );
 948      CREATE TABLE IF NOT EXISTS agent_messages (
 949        id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER,
 950        from_agent TEXT NOT NULL, to_agent TEXT NOT NULL, message_type TEXT,
 951        content TEXT NOT NULL, metadata_json TEXT,
 952        created_at DATETIME DEFAULT CURRENT_TIMESTAMP, read_at DATETIME
 953      );
 954      CREATE TABLE IF NOT EXISTS agent_logs (
 955        id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER,
 956        agent_name TEXT NOT NULL, log_level TEXT, message TEXT, data_json TEXT,
 957        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 958      );
 959      CREATE TABLE IF NOT EXISTS agent_state (
 960        agent_name TEXT PRIMARY KEY, last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
 961        current_task_id INTEGER, status TEXT DEFAULT 'idle', metrics_json TEXT
 962      );
 963      CREATE TABLE IF NOT EXISTS agent_outcomes (
 964        id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL,
 965        agent_name TEXT NOT NULL, task_type TEXT NOT NULL, outcome TEXT NOT NULL,
 966        context_json TEXT, result_json TEXT, duration_ms INTEGER,
 967        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 968      );
 969      CREATE TABLE IF NOT EXISTS agent_llm_usage (
 970        id INTEGER PRIMARY KEY AUTOINCREMENT, agent_name TEXT NOT NULL,
 971        task_id INTEGER, model TEXT NOT NULL, prompt_tokens INTEGER NOT NULL,
 972        completion_tokens INTEGER NOT NULL, cost_usd DECIMAL(10,6) NOT NULL,
 973        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 974      );
 975      CREATE TABLE IF NOT EXISTS structured_logs (
 976        id INTEGER PRIMARY KEY AUTOINCREMENT, agent_name TEXT, task_id INTEGER,
 977        level TEXT, message TEXT, data_json TEXT,
 978        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 979      );
 980    `);
 981    return db;
 982  }
 983  
 984  async function setupExtended() {
 985    resetQaBaseDb();
 986    resetQaTaskDb();
 987    resetQaMessageDb();
 988    try {
 989      await fs.unlink(QA_EXTENDED_DB);
 990    } catch (_e) {
 991      /* ignore */
 992    }
 993    const edb = createExtendedDb();
 994    process.env.DATABASE_PATH = QA_EXTENDED_DB;
 995    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 996    process.env.AGENT_IMMEDIATE_INVOCATION = 'false';
 997    const { QAAgent: QAFresh2 } = await import('../../src/agents/qa.js');
 998    const eagent = new QAFresh2();
 999    await eagent.initialize();
1000    return { edb, eagent };
1001  }
1002  
1003  async function teardownExtended(edb) {
1004    resetQaBaseDb();
1005    resetQaTaskDb();
1006    resetQaMessageDb();
1007    try {
1008      edb.close();
1009    } catch (_e) {
1010      /* ignore */
1011    }
1012    try {
1013      await fs.unlink(QA_EXTENDED_DB);
1014    } catch (_e) {
1015      /* ignore */
1016    }
1017  }
1018  
1019  // --- identifyUncoveredLines tests ---
1020  
1021  describe('QA Agent - identifyUncoveredLines', () => {
1022    let qaIdenAgent;
1023  
1024    before(async () => {
1025      qaIdenAgent = new QAAgent();
1026      qaIdenAgent.log = async () => {};
1027    });
1028  
1029    test('returns null when coverage-summary.json does not exist', async () => {
1030      const result = await qaIdenAgent.identifyUncoveredLines('src/nonexistent-module-12345.js');
1031      assert.strictEqual(result, null);
1032    });
1033  
1034    test('identifyUncoveredLines is an async function', () => {
1035      const result = qaIdenAgent.identifyUncoveredLines('src/test.js');
1036      assert.ok(result instanceof Promise);
1037      result.catch(() => {}); // prevent unhandled rejection
1038    });
1039  
1040    test('identifyUncoveredLines method exists and accepts a file path', () => {
1041      assert.strictEqual(typeof qaIdenAgent.identifyUncoveredLines, 'function');
1042      assert.strictEqual(qaIdenAgent.identifyUncoveredLines.length, 1);
1043    });
1044  });
1045  
1046  // --- generateTests tests ---
1047  
1048  describe('QA Agent - generateTests', () => {
1049    let qaGenAgent;
1050  
1051    before(async () => {
1052      qaGenAgent = new QAAgent();
1053      qaGenAgent.log = async () => {};
1054    });
1055  
1056    test('generateTests is a function with correct arity', () => {
1057      assert.strictEqual(typeof qaGenAgent.generateTests, 'function');
1058      assert.strictEqual(qaGenAgent.generateTests.length, 3);
1059    });
1060  
1061    test('generateTests returns null when LLM throws', async () => {
1062      // Create a subclass-like object where callLLM throws to test error path
1063      // We cannot mock ESM imports directly here, but we can test the method signature
1064      assert.ok(qaGenAgent.generateTests !== undefined);
1065    });
1066  });
1067  
1068  // --- writeTest integration tests ---
1069  
1070  test('QA Agent Integration - writeTest with empty files_to_test completes', async () => {
1071    const { edb, eagent } = await setupExtended();
1072    try {
1073      const taskId = edb
1074        .prepare(
1075          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1076        )
1077        .run('write_test', 'qa', 'pending', JSON.stringify({ files_to_test: [] })).lastInsertRowid;
1078      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1079      task.context_json = JSON.parse(task.context_json);
1080  
1081      await eagent.writeTest(task);
1082  
1083      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1084      assert.ok(['completed', 'failed'].includes(updated.status));
1085    } finally {
1086      await teardownExtended(edb);
1087    }
1088  });
1089  
1090  test('QA Agent Integration - writeTest skips when identifyUncoveredLines returns null', async () => {
1091    const { edb, eagent } = await setupExtended();
1092    try {
1093      const taskId = edb
1094        .prepare(
1095          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1096        )
1097        .run(
1098          'write_test',
1099          'qa',
1100          'pending',
1101          JSON.stringify({ files_to_test: ['src/some-module.js'] })
1102        ).lastInsertRowid;
1103      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1104      task.context_json = JSON.parse(task.context_json);
1105  
1106      eagent.identifyUncoveredLines = async () => null;
1107  
1108      await eagent.writeTest(task);
1109  
1110      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1111      assert.ok(['completed', 'failed'].includes(updated.status));
1112    } finally {
1113      await teardownExtended(edb);
1114    }
1115  });
1116  
1117  test('QA Agent Integration - writeTest skips when no uncovered lines', async () => {
1118    const { edb, eagent } = await setupExtended();
1119    try {
1120      const taskId = edb
1121        .prepare(
1122          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1123        )
1124        .run(
1125          'write_test',
1126          'qa',
1127          'pending',
1128          JSON.stringify({ files_to_test: ['src/covered-module.js'] })
1129        ).lastInsertRowid;
1130      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1131      task.context_json = JSON.parse(task.context_json);
1132  
1133      eagent.identifyUncoveredLines = async () => ({
1134        uncoveredLines: [],
1135        sourceCode: 'function foo() { return 1; }',
1136        coveragePct: 100,
1137      });
1138  
1139      await eagent.writeTest(task);
1140  
1141      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1142      assert.ok(['completed', 'failed'].includes(updated.status));
1143    } finally {
1144      await teardownExtended(edb);
1145    }
1146  });
1147  
1148  test('QA Agent Integration - writeTest skips when generateTests returns null', async () => {
1149    const { edb, eagent } = await setupExtended();
1150    try {
1151      const taskId = edb
1152        .prepare(
1153          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1154        )
1155        .run(
1156          'write_test',
1157          'qa',
1158          'pending',
1159          JSON.stringify({ files_to_test: ['src/some-module.js'] })
1160        ).lastInsertRowid;
1161      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1162      task.context_json = JSON.parse(task.context_json);
1163  
1164      eagent.identifyUncoveredLines = async () => ({
1165        uncoveredLines: [5, 10],
1166        sourceCode: 'function foo() { return 1; }',
1167        coveragePct: 50,
1168      });
1169      eagent.generateTests = async () => null;
1170  
1171      await eagent.writeTest(task);
1172  
1173      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1174      assert.ok(['completed', 'failed'].includes(updated.status));
1175    } finally {
1176      await teardownExtended(edb);
1177    }
1178  });
1179  
1180  test('QA Agent Integration - writeTest creates new test file when test file does not exist', async () => {
1181    const { edb, eagent } = await setupExtended();
1182    const tmpTestFile = './tests/agents/tmp-new-write-test-gen.test.js';
1183    try {
1184      try {
1185        await fs.unlink(tmpTestFile);
1186      } catch (_e) {
1187        /* ignore */
1188      }
1189  
1190      const taskId = edb
1191        .prepare(
1192          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1193        )
1194        .run(
1195          'write_test',
1196          'qa',
1197          'pending',
1198          JSON.stringify({
1199            files_to_test: ['src/tmp-write-test-gen.js'],
1200            current_coverage: 40,
1201            target_coverage: 80,
1202          })
1203        ).lastInsertRowid;
1204      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1205      task.context_json = JSON.parse(task.context_json);
1206  
1207      eagent.identifyUncoveredLines = async () => ({
1208        uncoveredLines: [5, 10],
1209        sourceCode: 'function foo() { return 1; }',
1210        coveragePct: 40,
1211      });
1212      eagent.generateTests = async () =>
1213        "import { test } from 'node:test';\nimport assert from 'node:assert';\ntest('gen test', () => { assert.ok(true); });\n";
1214      eagent.getTestFile = () => tmpTestFile;
1215      eagent.fileExists = async f => !f.endsWith('tmp-new-write-test-gen.test.js');
1216      eagent.runTestFiles = async () => ({ success: true, output: '1 passing', count: 1 });
1217      eagent.getFileCoverage = async files => {
1218        const r = {};
1219        files.forEach(f => (r[f] = 85));
1220        return r;
1221      };
1222  
1223      await eagent.writeTest(task);
1224  
1225      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1226      assert.ok(['completed', 'failed'].includes(updated.status));
1227    } finally {
1228      await teardownExtended(edb);
1229      try {
1230        await fs.unlink(tmpTestFile);
1231      } catch (_e) {
1232        /* ignore */
1233      }
1234    }
1235  });
1236  
1237  test('QA Agent Integration - writeTest merges with existing test file', async () => {
1238    const { edb, eagent } = await setupExtended();
1239    const tmpTestFile = './tests/agents/tmp-merge-write-test.test.js';
1240    try {
1241      const { writeFile } = await import('fs/promises');
1242      await writeFile(
1243        tmpTestFile,
1244        "import { test } from 'node:test';\nimport assert from 'node:assert';\ntest('existing', () => { assert.ok(true); });\n"
1245      );
1246  
1247      const taskId = edb
1248        .prepare(
1249          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1250        )
1251        .run(
1252          'write_test',
1253          'qa',
1254          'pending',
1255          JSON.stringify({
1256            file: 'src/existing-module.js',
1257            current_coverage: 50,
1258            target_coverage: 80,
1259          })
1260        ).lastInsertRowid;
1261      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1262      task.context_json = JSON.parse(task.context_json);
1263  
1264      eagent.identifyUncoveredLines = async () => ({
1265        uncoveredLines: [5],
1266        sourceCode: 'function foo() { return 1; }',
1267        coveragePct: 50,
1268      });
1269      eagent.generateTests = async () => "test('merged test', () => { assert.ok(true); });";
1270      eagent.getTestFile = () => tmpTestFile;
1271      eagent.fileExists = async () => true;
1272      eagent.runTestFiles = async () => ({ success: true, output: '1 passing', count: 1 });
1273      eagent.getFileCoverage = async files => {
1274        const r = {};
1275        files.forEach(f => (r[f] = 85));
1276        return r;
1277      };
1278  
1279      await eagent.writeTest(task);
1280  
1281      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1282      assert.ok(['completed', 'failed'].includes(updated.status));
1283    } finally {
1284      await teardownExtended(edb);
1285      try {
1286        await fs.unlink(tmpTestFile);
1287      } catch (_e) {
1288        /* ignore */
1289      }
1290    }
1291  });
1292  
1293  test('QA Agent Integration - writeTest handles test failure then failed fix', async () => {
1294    const { edb, eagent } = await setupExtended();
1295    const tmpTestFile = './tests/agents/tmp-fail-fix-test.test.js';
1296    try {
1297      const { writeFile } = await import('fs/promises');
1298      await writeFile(
1299        tmpTestFile,
1300        "import { test } from 'node:test';\ntest('placeholder', () => {});\n"
1301      );
1302  
1303      const taskId = edb
1304        .prepare(
1305          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1306        )
1307        .run(
1308          'write_test',
1309          'qa',
1310          'pending',
1311          JSON.stringify({ files_to_test: ['src/fail-test-module.js'] })
1312        ).lastInsertRowid;
1313      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1314      task.context_json = JSON.parse(task.context_json);
1315  
1316      eagent.identifyUncoveredLines = async () => ({
1317        uncoveredLines: [5],
1318        sourceCode: 'function foo() { return 1; }',
1319        coveragePct: 30,
1320      });
1321      eagent.generateTests = async () => "test('broken', () => { undefinedVar; });";
1322      eagent.getTestFile = () => tmpTestFile;
1323      eagent.fileExists = async () => false;
1324      eagent.runTestFiles = async () => ({
1325        success: false,
1326        output: 'ReferenceError: undefinedVar is not defined',
1327      });
1328      eagent.fixTestIssues = async () => false;
1329  
1330      await eagent.writeTest(task);
1331  
1332      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1333      assert.ok(['completed', 'failed'].includes(updated.status));
1334    } finally {
1335      await teardownExtended(edb);
1336      try {
1337        await fs.unlink(tmpTestFile);
1338      } catch (_e) {
1339        /* ignore */
1340      }
1341    }
1342  });
1343  
1344  test('QA Agent Integration - writeTest fails when all files throw exceptions', async () => {
1345    const { edb, eagent } = await setupExtended();
1346    try {
1347      const taskId = edb
1348        .prepare(
1349          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1350        )
1351        .run(
1352          'write_test',
1353          'qa',
1354          'pending',
1355          JSON.stringify({ files_to_test: ['src/error-module.js'] })
1356        ).lastInsertRowid;
1357      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1358      task.context_json = JSON.parse(task.context_json);
1359  
1360      eagent.identifyUncoveredLines = async () => {
1361        throw new Error('Coverage tool crashed');
1362      };
1363  
1364      await eagent.writeTest(task);
1365  
1366      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1367      assert.strictEqual(updated.status, 'failed');
1368    } finally {
1369      await teardownExtended(edb);
1370    }
1371  });
1372  
1373  test('QA Agent Integration - processTask routes write_test', async () => {
1374    const { edb, eagent } = await setupExtended();
1375    try {
1376      const taskId = edb
1377        .prepare(
1378          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1379        )
1380        .run('write_test', 'qa', 'pending', JSON.stringify({ files_to_test: [] })).lastInsertRowid;
1381      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1382  
1383      eagent.identifyUncoveredLines = async () => null;
1384      await eagent.processTask(task);
1385  
1386      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1387      assert.ok(['completed', 'failed'].includes(updated.status));
1388    } finally {
1389      await teardownExtended(edb);
1390    }
1391  });
1392  
1393  test('QA Agent Integration - processTask routes check_coverage', async () => {
1394    const { edb, eagent } = await setupExtended();
1395    try {
1396      const taskId = edb
1397        .prepare(
1398          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1399        )
1400        .run(
1401          'check_coverage',
1402          'qa',
1403          'pending',
1404          JSON.stringify({ files: ['src/test-mod.js'] })
1405        ).lastInsertRowid;
1406      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1407  
1408      eagent.getFileCoverage = async files => {
1409        const r = {};
1410        files.forEach(f => (r[f] = 90));
1411        return r;
1412      };
1413      await eagent.processTask(task);
1414  
1415      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1416      assert.strictEqual(updated.status, 'completed');
1417    } finally {
1418      await teardownExtended(edb);
1419    }
1420  });
1421  
1422  test('QA Agent Integration - processTask routes run_tests', async () => {
1423    const { edb, eagent } = await setupExtended();
1424    try {
1425      const taskId = edb
1426        .prepare(
1427          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1428        )
1429        .run(
1430          'run_tests',
1431          'qa',
1432          'pending',
1433          JSON.stringify({ test_files: ['tests/test.js'] })
1434        ).lastInsertRowid;
1435      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1436  
1437      eagent.runTestFiles = async () => ({ success: true, output: '3 passing', count: 3 });
1438      await eagent.processTask(task);
1439  
1440      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1441      assert.strictEqual(updated.status, 'completed');
1442    } finally {
1443      await teardownExtended(edb);
1444    }
1445  });
1446  
1447  test('QA Agent Integration - processTask routes exploratory_testing', async () => {
1448    const { edb, eagent } = await setupExtended();
1449    try {
1450      const taskId = edb
1451        .prepare(
1452          'INSERT INTO agent_tasks (task_type, assigned_to, status, context_json) VALUES (?, ?, ?, ?)'
1453        )
1454        .run(
1455          'exploratory_testing',
1456          'qa',
1457          'pending',
1458          JSON.stringify({ feature: 'Search' })
1459        ).lastInsertRowid;
1460      const task = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1461  
1462      await eagent.processTask(task);
1463  
1464      const updated = edb.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1465      assert.strictEqual(updated.status, 'completed');
1466      const result = JSON.parse(updated.result_json);
1467      assert.strictEqual(result.manual_testing_required, true);
1468    } finally {
1469      await teardownExtended(edb);
1470    }
1471  });
1472  
1473  // --- getFileCoverage edge cases ---
1474  
1475  describe('QA Agent - getFileCoverage error handling', () => {
1476    let covAgent;
1477  
1478    before(async () => {
1479      covAgent = new QAAgent();
1480      covAgent.log = async () => {};
1481    });
1482  
1483    test('returns 0 for all files when coverage file read fails', async () => {
1484      const result = await covAgent.getFileCoverage([
1485        'src/nonexistent-abc123.js',
1486        'src/nonexistent-xyz456.js',
1487      ]);
1488      assert.strictEqual(typeof result, 'object');
1489      for (const val of Object.values(result)) {
1490        assert.strictEqual(typeof val, 'number');
1491      }
1492    });
1493  
1494    test('returns empty object for empty files array', async () => {
1495      const result = await covAgent.getFileCoverage([]);
1496      assert.deepStrictEqual(result, {});
1497    });
1498  
1499    test('handles absolute file paths gracefully', async () => {
1500      const result = await covAgent.getFileCoverage(['/absolute/path/to/file.js']);
1501      assert.ok(typeof result === 'object');
1502      assert.strictEqual(result['/absolute/path/to/file.js'], 0);
1503    });
1504  });
1505  
1506  // --- mergeTests edge cases ---
1507  
1508  describe('QA Agent - mergeTests edge cases', () => {
1509    let mergeAgent;
1510  
1511    before(async () => {
1512      mergeAgent = new QAAgent();
1513    });
1514  
1515    test('returns existing when all new content is duplicate imports', async () => {
1516      // After commit 5b1f42d9, mergeTests uses safe-append (import deduplication only).
1517      // When newTests consists entirely of already-imported statements, testsToAppend
1518      // is empty and the function returns existingTests unchanged.
1519      const existingTests = `import { test } from 'node:test';
1520  import assert from 'node:assert';
1521  describe('mod', () => {
1522    test('existing test', () => { assert.ok(true); });
1523  });`;
1524  
1525      // newTests only contains imports that already exist in existingTests
1526      const newTests = `import { test } from 'node:test';
1527  import assert from 'node:assert';`;
1528  
1529      const result = await mergeAgent.mergeTests(existingTests, newTests);
1530      // Result should be existingTests unchanged (no new code appended)
1531      assert.strictEqual(result, existingTests);
1532      assert.ok(result.includes('existing test'));
1533    });
1534  
1535    test('merges multiple unique tests from new tests', async () => {
1536      const existingTests = `import { test } from 'node:test';
1537  describe('mod', () => {
1538    test('first test', () => {});
1539  });`;
1540  
1541      const newTests = `import { test, describe } from 'node:test';
1542  describe('mod', () => {
1543    test('second test', () => {});
1544    test('third test', () => {});
1545  });`;
1546  
1547      const result = await mergeAgent.mergeTests(existingTests, newTests);
1548      assert.ok(result.includes('second test'));
1549      assert.ok(result.includes('third test'));
1550      assert.ok(result.includes('first test'));
1551    });
1552  
1553    test('handles new tests when existing file has no describe block', async () => {
1554      const existingTests = `import { test } from 'node:test';
1555  test('existing top-level', () => {});`;
1556  
1557      const newTests = `import { test } from 'node:test';
1558  test('top-level test', () => {});`;
1559  
1560      const result = await mergeAgent.mergeTests(existingTests, newTests);
1561      assert.ok(typeof result === 'string');
1562      assert.ok(result.length > 0);
1563    });
1564  });
1565  
1566  // --- approximateUncoveredLines additional patterns ---
1567  
1568  describe('QA Agent - approximateUncoveredLines additional patterns', () => {
1569    let approxAgent;
1570  
1571    before(async () => {
1572      approxAgent = new QAAgent();
1573    });
1574  
1575    test('identifies throw new Error lines', () => {
1576      const sourceCode = `
1577  function validate(x) {
1578    if (!x) {
1579      throw new Error('x is required');
1580    }
1581    return x;
1582  }
1583  `;
1584      const result = approxAgent.approximateUncoveredLines(sourceCode, 50);
1585      assert.ok(result.uncoveredLines.includes(4));
1586    });
1587  
1588    test('identifies return false lines', () => {
1589      const sourceCode = `
1590  function check(x) {
1591    if (x < 0) {
1592      return false;
1593    }
1594    return true;
1595  }
1596  `;
1597      const result = approxAgent.approximateUncoveredLines(sourceCode, 60);
1598      assert.ok(result.uncoveredLines.includes(4));
1599    });
1600  
1601    test('identifies default case in switch', () => {
1602      const sourceCode = `
1603  function handle(cmd) {
1604    switch(cmd) {
1605      case 'start': return 1;
1606      default:
1607        return 0;
1608    }
1609  }
1610  `;
1611      const result = approxAgent.approximateUncoveredLines(sourceCode, 50);
1612      assert.ok(result.uncoveredLines.some(l => l >= 5));
1613    });
1614  
1615    test('returns sourceCode in result object', () => {
1616      const sourceCode = 'function foo() { return 1; }';
1617      const result = approxAgent.approximateUncoveredLines(sourceCode, 75);
1618      assert.strictEqual(result.sourceCode, sourceCode);
1619    });
1620  
1621    test('returns coveragePct in result object', () => {
1622      const sourceCode = 'function foo() { return 1; }';
1623      const result = approxAgent.approximateUncoveredLines(sourceCode, 42.5);
1624      assert.strictEqual(result.coveragePct, 42.5);
1625    });
1626  
1627    test('handles complex code with multiple uncovered patterns', () => {
1628      const sourceCode = `
1629  async function process(data) {
1630    try {
1631      const result = await fetch(data);
1632      if (result.ok) {
1633        return result.json();
1634      } else {
1635        return null;
1636      }
1637    } catch (error) {
1638      throw new Error('fetch failed');
1639    }
1640  }
1641  `;
1642      const result = approxAgent.approximateUncoveredLines(sourceCode, 30);
1643      assert.ok(result.uncoveredLines.length > 0);
1644      for (const lineNum of result.uncoveredLines) {
1645        assert.ok(Number.isInteger(lineNum) && lineNum > 0);
1646      }
1647    });
1648  });
1649  
1650  // --- addMissingImport additional coverage ---
1651  
1652  describe('QA Agent - addMissingImport additional identifiers', () => {
1653    let addImpAgent;
1654  
1655    before(async () => {
1656      addImpAgent = new QAAgent();
1657    });
1658  
1659    test('handles describe identifier (node:test)', () => {
1660      const code = "import { test } from 'node:test';\ntest('my test', () => {});";
1661      const result = addImpAgent.addMissingImport(code, 'describe');
1662      assert.ok(result.includes('describe'));
1663      assert.ok(result.includes("from 'node:test'"));
1664    });
1665  
1666    test('handles before identifier (node:test)', () => {
1667      const code = "import { test } from 'node:test';\ntest('my test', () => {});";
1668      const result = addImpAgent.addMissingImport(code, 'before');
1669      assert.ok(result.includes('before'));
1670    });
1671  
1672    test('handles after identifier (node:test)', () => {
1673      const code = "import { test } from 'node:test';\ntest('my test', () => {});";
1674      const result = addImpAgent.addMissingImport(code, 'after');
1675      assert.ok(result.includes('after'));
1676    });
1677  
1678    test('handles beforeEach identifier (node:test)', () => {
1679      const code = "import { test } from 'node:test';\ntest('my test', () => {});";
1680      const result = addImpAgent.addMissingImport(code, 'beforeEach');
1681      assert.ok(result.includes('beforeEach'));
1682    });
1683  
1684    test('handles afterEach identifier (node:test)', () => {
1685      const code = "import { test } from 'node:test';\ntest('my test', () => {});";
1686      const result = addImpAgent.addMissingImport(code, 'afterEach');
1687      assert.ok(result.includes('afterEach'));
1688    });
1689  
1690    test('handles rejects (node:assert named)', () => {
1691      const code = "test('my test', async () => {});";
1692      const result = addImpAgent.addMissingImport(code, 'rejects');
1693      assert.ok(result.includes("from 'node:assert'"));
1694    });
1695  
1696    test('handles ok (node:assert named)', () => {
1697      const code = "test('my test', () => {});";
1698      const result = addImpAgent.addMissingImport(code, 'ok');
1699      assert.ok(result.includes("from 'node:assert'"));
1700    });
1701  
1702    test('handles throws (node:assert named)', () => {
1703      const code = "test('my test', () => {});";
1704      const result = addImpAgent.addMissingImport(code, 'throws');
1705      assert.ok(result.includes("from 'node:assert'"));
1706    });
1707  
1708    test('handles writeFile (fs/promises named)', () => {
1709      const code = "test('my test', async () => {});";
1710      const result = addImpAgent.addMissingImport(code, 'writeFile');
1711      assert.ok(result.includes("from 'fs/promises'"));
1712    });
1713  
1714    test('handles unlink (fs/promises named)', () => {
1715      const code = "test('my test', async () => {});";
1716      const result = addImpAgent.addMissingImport(code, 'unlink');
1717      assert.ok(result.includes("from 'fs/promises'"));
1718    });
1719  
1720    test('handles mkdir (fs/promises named)', () => {
1721      const code = "test('my test', async () => {});";
1722      const result = addImpAgent.addMissingImport(code, 'mkdir');
1723      assert.ok(result.includes("from 'fs/promises'"));
1724    });
1725  
1726    test('handles resolve (path named)', () => {
1727      const code = "test('my test', () => {});";
1728      const result = addImpAgent.addMissingImport(code, 'resolve');
1729      assert.ok(result.includes("from 'path'"));
1730    });
1731  
1732    test('handles dirname (path named)', () => {
1733      const code = "test('my test', () => {});";
1734      const result = addImpAgent.addMissingImport(code, 'dirname');
1735      assert.ok(result.includes("from 'path'"));
1736    });
1737  
1738    test('handles basename (path named)', () => {
1739      const code = "test('my test', () => {});";
1740      const result = addImpAgent.addMissingImport(code, 'basename');
1741      assert.ok(result.includes("from 'path'"));
1742    });
1743  
1744    test('does not duplicate named import when already present', () => {
1745      const code = "import { test, describe } from 'node:test';\ntest('my test', () => {});";
1746      const result = addImpAgent.addMissingImport(code, 'describe');
1747      const importLines = result.split('\n').filter(l => l.includes("from 'node:test'"));
1748      assert.strictEqual(importLines.length, 1);
1749    });
1750  
1751    test('inserts at start when no imports present at all', () => {
1752      const code = "// no imports\ntest('my test', () => {});";
1753      const result = addImpAgent.addMissingImport(code, 'test');
1754      const lines = result.split('\n');
1755      assert.ok(lines[0].includes("import { test } from 'node:test'"));
1756    });
1757  });