/ __quarantined_tests__ / agents / context-builder.test.js
context-builder.test.js
   1  /**
   2   * Tests for Context Builder
   3   *
   4   * Verifies task history integration for agent learning.
   5   */
   6  
   7  import { test } from 'node:test';
   8  import assert from 'node:assert/strict';
   9  import Database from 'better-sqlite3';
  10  import fs from 'fs/promises';
  11  import path from 'path';
  12  import { fileURLToPath } from 'url';
  13  import { buildAgentContext, clearCache, resetDb } from '../../src/agents/utils/context-builder.js';
  14  import {
  15    createAgentTask,
  16    completeTask,
  17    failTask,
  18    resetDb as resetTaskManagerDb,
  19  } from '../../src/agents/utils/task-manager.js';
  20  
  21  const __filename = fileURLToPath(import.meta.url);
  22  const __dirname = path.dirname(__filename);
  23  
  24  // Test database path
  25  const testDbPath = path.join(__dirname, '..', 'test-context-builder.db');
  26  
  27  // Initialize test database using migration files (agent tables are NOT in schema.sql)
  28  async function initTestDb() {
  29    // Remove old test DB
  30    try {
  31      await fs.unlink(testDbPath);
  32    } catch (e) {
  33      // Ignore if doesn't exist
  34    }
  35  
  36    const db = new Database(testDbPath);
  37    db.pragma('foreign_keys = ON');
  38  
  39    // Load tables from migration files (agent tables are not in schema.sql)
  40    const migrationsDir = path.join(__dirname, '..', '..', 'db', 'migrations');
  41    const migrationFiles = [
  42      '047-create-agent-system.sql',
  43      '052-create-agent-llm-usage.sql',
  44      '053-create-agent-outcomes.sql',
  45    ];
  46  
  47    for (const migFile of migrationFiles) {
  48      try {
  49        const sql = await fs.readFile(path.join(migrationsDir, migFile), 'utf8');
  50        db.exec(sql);
  51      } catch (e) {
  52        // Ignore missing migration files
  53      }
  54    }
  55  
  56    db.close();
  57  }
  58  
  59  // Cleanup test database
  60  async function cleanupTestDb() {
  61    try {
  62      await fs.unlink(testDbPath);
  63    } catch (e) {
  64      // Ignore
  65    }
  66  }
  67  
  68  test('Context Builder - buildAgentContext with no history', async t => {
  69    await initTestDb();
  70    process.env.DATABASE_PATH = testDbPath;
  71    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
  72  
  73    try {
  74      const context = await buildAgentContext('developer', ['base.md', 'developer.md']);
  75  
  76      assert.ok(context.fullContext, 'Should have full context');
  77      assert.ok(context.baseContext, 'Should have base context');
  78      assert.ok(context.historyContext, 'Should have history context');
  79      assert.strictEqual(context.metadata.historyStats.recentSuccesses, 0, 'No successes yet');
  80      assert.strictEqual(context.metadata.historyStats.recentFailures, 0, 'No failures yet');
  81      assert.strictEqual(context.metadata.historyStats.relatedTasks, 0, 'No related tasks yet');
  82      assert.ok(
  83        context.historyContext.includes('No historical task data available'),
  84        'Should indicate no history'
  85      );
  86    } finally {
  87      delete process.env.DATABASE_PATH;
  88      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
  89      resetDb();
  90      resetTaskManagerDb();
  91      clearCache();
  92      await cleanupTestDb();
  93    }
  94  });
  95  
  96  test('Context Builder - buildAgentContext with successful tasks', async t => {
  97    await initTestDb();
  98    process.env.DATABASE_PATH = testDbPath;
  99    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 100  
 101    try {
 102      // Create and complete a successful task
 103      const taskId = await createAgentTask({
 104        task_type: 'fix_bug',
 105        assigned_to: 'developer',
 106        context: {
 107          error_type: 'null_pointer',
 108          file_path: 'src/scoring.js',
 109        },
 110      });
 111  
 112      completeTask(taskId, {
 113        files_changed: ['src/scoring.js'],
 114        approach: 'Added null check before accessing property',
 115      });
 116  
 117      // Record outcome
 118      const db = new Database(testDbPath);
 119      db.prepare(
 120        `
 121        INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, duration_ms, result_json)
 122        VALUES (?, ?, ?, ?, ?, ?)
 123      `
 124      ).run(
 125        taskId,
 126        'developer',
 127        'fix_bug',
 128        'success',
 129        1500,
 130        JSON.stringify({ approach: 'Added null check before accessing property' })
 131      );
 132      db.close();
 133  
 134      // Clear cache to force reload
 135      clearCache();
 136  
 137      // Build context
 138      const context = await buildAgentContext('developer', ['base.md', 'developer.md']);
 139  
 140      assert.strictEqual(context.metadata.historyStats.recentSuccesses, 1, 'Should have 1 success');
 141      assert.ok(
 142        context.historyContext.includes('Recent Successful Approaches'),
 143        'Should include success section'
 144      );
 145      assert.ok(context.historyContext.includes('fix_bug'), 'Should mention task type');
 146      assert.ok(context.historyContext.includes('src/scoring.js'), 'Should mention affected file');
 147    } finally {
 148      delete process.env.DATABASE_PATH;
 149      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 150      resetDb();
 151      resetTaskManagerDb();
 152      clearCache();
 153      await cleanupTestDb();
 154    }
 155  });
 156  
 157  test('Context Builder - buildAgentContext with failed tasks', async t => {
 158    await initTestDb();
 159    process.env.DATABASE_PATH = testDbPath;
 160    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 161  
 162    try {
 163      // Create and fail a task
 164      const taskId = await createAgentTask({
 165        task_type: 'fix_bug',
 166        assigned_to: 'developer',
 167        context: {
 168          error_type: 'api_timeout',
 169          file_path: 'src/outreach/sms.js',
 170        },
 171      });
 172  
 173      failTask(taskId, 'Twilio API timeout after 30 seconds');
 174  
 175      // Record outcome
 176      const db = new Database(testDbPath);
 177      db.prepare(
 178        `
 179        INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, duration_ms, context_json)
 180        VALUES (?, ?, ?, ?, ?, ?)
 181      `
 182      ).run(
 183        taskId,
 184        'developer',
 185        'fix_bug',
 186        'failure',
 187        30000,
 188        JSON.stringify({
 189          error_type: 'api_timeout',
 190          error: 'Twilio API timeout after 30 seconds',
 191        })
 192      );
 193      db.close();
 194  
 195      // Clear cache
 196      clearCache();
 197  
 198      // Build context
 199      const context = await buildAgentContext('developer', ['base.md', 'developer.md']);
 200  
 201      assert.strictEqual(context.metadata.historyStats.recentFailures, 1, 'Should have 1 failure');
 202      assert.ok(
 203        context.historyContext.includes('Past Failures to Avoid'),
 204        'Should include failure section'
 205      );
 206      assert.ok(context.historyContext.includes('api_timeout'), 'Should mention error type');
 207    } finally {
 208      delete process.env.DATABASE_PATH;
 209      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 210      resetDb();
 211      resetTaskManagerDb();
 212      clearCache();
 213      await cleanupTestDb();
 214    }
 215  });
 216  
 217  test('Context Builder - buildAgentContext with related tasks', async t => {
 218    await initTestDb();
 219    process.env.DATABASE_PATH = testDbPath;
 220    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 221  
 222    try {
 223      // Create a successful task for file X
 224      const task1 = await createAgentTask({
 225        task_type: 'fix_bug',
 226        assigned_to: 'developer',
 227        context: {
 228          error_type: 'null_pointer',
 229          file_path: 'src/capture.js',
 230        },
 231      });
 232  
 233      completeTask(task1, {
 234        files_changed: ['src/capture.js'],
 235        approach: 'Added null check',
 236      });
 237  
 238      // Record outcome
 239      const db = new Database(testDbPath);
 240      db.prepare(
 241        `
 242        INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, result_json)
 243        VALUES (?, ?, ?, ?, ?)
 244      `
 245      ).run(
 246        task1,
 247        'developer',
 248        'fix_bug',
 249        'success',
 250        JSON.stringify({ approach: 'Added null check' })
 251      );
 252      db.close();
 253  
 254      // Clear cache
 255      clearCache();
 256  
 257      // Build context with current task for same file
 258      const currentTask = {
 259        context_json: {
 260          error_type: 'undefined_reference',
 261          file_path: 'src/capture.js',
 262        },
 263      };
 264  
 265      const context = await buildAgentContext('developer', ['base.md', 'developer.md'], currentTask);
 266  
 267      assert.strictEqual(context.metadata.historyStats.relatedTasks, 1, 'Should have 1 related task');
 268      assert.ok(
 269        context.historyContext.includes('Related Tasks'),
 270        'Should include related tasks section'
 271      );
 272      assert.ok(context.historyContext.includes('src/capture.js'), 'Should mention the related file');
 273    } finally {
 274      delete process.env.DATABASE_PATH;
 275      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 276      resetDb();
 277      resetTaskManagerDb();
 278      clearCache();
 279      await cleanupTestDb();
 280    }
 281  });
 282  
 283  test('Context Builder - caching behavior', async t => {
 284    await initTestDb();
 285    process.env.DATABASE_PATH = testDbPath;
 286    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 287  
 288    try {
 289      // Create a task
 290      const taskId = await createAgentTask({
 291        task_type: 'fix_bug',
 292        assigned_to: 'developer',
 293      });
 294  
 295      completeTask(taskId, { files_changed: ['test.js'] });
 296  
 297      // First call - should query DB
 298      const context1 = await buildAgentContext('developer', ['base.md', 'developer.md']);
 299  
 300      // Second call - should use cache
 301      const context2 = await buildAgentContext('developer', ['base.md', 'developer.md']);
 302  
 303      assert.strictEqual(
 304        context1.metadata.historyStats.recentSuccesses,
 305        context2.metadata.historyStats.recentSuccesses,
 306        'Cached results should match'
 307      );
 308  
 309      // Clear cache
 310      clearCache();
 311  
 312      // Third call - should re-query DB
 313      const context3 = await buildAgentContext('developer', ['base.md', 'developer.md']);
 314  
 315      assert.strictEqual(
 316        context1.metadata.historyStats.recentSuccesses,
 317        context3.metadata.historyStats.recentSuccesses,
 318        'Results after cache clear should still match'
 319      );
 320    } finally {
 321      delete process.env.DATABASE_PATH;
 322      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 323      resetDb();
 324      resetTaskManagerDb();
 325      clearCache();
 326      await cleanupTestDb();
 327    }
 328  });
 329  
 330  test('Context Builder - disabled via env var', async t => {
 331    await initTestDb();
 332    process.env.DATABASE_PATH = testDbPath;
 333    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 334    process.env.AGENT_ENABLE_TASK_HISTORY = 'false';
 335  
 336    try {
 337      const context = await buildAgentContext('developer', ['base.md', 'developer.md']);
 338  
 339      assert.strictEqual(context.historyContext, null, 'History should be null when disabled');
 340      assert.strictEqual(context.historyTokens, 0, 'History tokens should be 0');
 341      assert.ok(
 342        context.fullContext === context.baseContext,
 343        'Full context should equal base context'
 344      );
 345    } finally {
 346      delete process.env.DATABASE_PATH;
 347      delete process.env.AGENT_ENABLE_TASK_HISTORY;
 348      resetDb();
 349      resetTaskManagerDb();
 350      clearCache();
 351      await cleanupTestDb();
 352    }
 353  });
 354  
 355  test('Context Builder - token estimation', async t => {
 356    await initTestDb();
 357    process.env.DATABASE_PATH = testDbPath;
 358    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 359  
 360    try {
 361      const context = await buildAgentContext('developer', ['base.md', 'developer.md']);
 362  
 363      assert.ok(context.totalTokens > 0, 'Should estimate total tokens');
 364      assert.ok(context.historyTokens >= 0, 'Should estimate history tokens');
 365      assert.ok(
 366        context.totalTokens >= context.historyTokens,
 367        'Total tokens should be >= history tokens'
 368      );
 369    } finally {
 370      delete process.env.DATABASE_PATH;
 371      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 372      resetDb();
 373      resetTaskManagerDb();
 374      clearCache();
 375      await cleanupTestDb();
 376    }
 377  });
 378  
 379  test('Context Builder - mixed success/failure history', async t => {
 380    await initTestDb();
 381    process.env.DATABASE_PATH = testDbPath;
 382    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 383  
 384    try {
 385      // Create 3 successful tasks
 386      for (let i = 0; i < 3; i++) {
 387        const taskId = await createAgentTask({
 388          task_type: 'fix_bug',
 389          assigned_to: 'developer',
 390        });
 391        completeTask(taskId, { approach: `Fix ${i}` });
 392  
 393        const db = new Database(testDbPath);
 394        db.prepare(
 395          `
 396          INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome)
 397          VALUES (?, ?, ?, ?)
 398        `
 399        ).run(taskId, 'developer', 'fix_bug', 'success');
 400        db.close();
 401      }
 402  
 403      // Create 2 failed tasks
 404      for (let i = 0; i < 2; i++) {
 405        const taskId = await createAgentTask({
 406          task_type: 'fix_bug',
 407          assigned_to: 'developer',
 408        });
 409        failTask(taskId, `Error ${i}`);
 410  
 411        const db = new Database(testDbPath);
 412        db.prepare(
 413          `
 414          INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome)
 415          VALUES (?, ?, ?, ?)
 416        `
 417        ).run(taskId, 'developer', 'fix_bug', 'failure');
 418        db.close();
 419      }
 420  
 421      // Clear cache
 422      clearCache();
 423  
 424      // Build context
 425      const context = await buildAgentContext('developer', ['base.md', 'developer.md']);
 426  
 427      assert.strictEqual(context.metadata.historyStats.recentSuccesses, 3, 'Should have 3 successes');
 428      assert.strictEqual(context.metadata.historyStats.recentFailures, 2, 'Should have 2 failures');
 429      assert.ok(
 430        context.historyContext.includes('Recent Successful Approaches'),
 431        'Should include successes'
 432      );
 433      assert.ok(context.historyContext.includes('Past Failures to Avoid'), 'Should include failures');
 434    } finally {
 435      delete process.env.DATABASE_PATH;
 436      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 437      resetDb();
 438      resetTaskManagerDb();
 439      clearCache();
 440      await cleanupTestDb();
 441    }
 442  });
 443  
 444  test('Context Builder - resetDb handles close error gracefully', async t => {
 445    // resetDb should work when db is open and when db is null
 446    await initTestDb();
 447    process.env.DATABASE_PATH = testDbPath;
 448    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 449  
 450    try {
 451      // First call triggers db initialization
 452      await buildAgentContext('developer', ['base.md']);
 453  
 454      // resetDb when db is open - exercises db.close() path (lines 24-28)
 455      resetDb();
 456      // Second resetDb when db is null - exercises null guard
 457      resetDb();
 458      assert.ok(true, 'resetDb completed without error both times');
 459    } finally {
 460      delete process.env.DATABASE_PATH;
 461      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 462      resetDb();
 463      resetTaskManagerDb();
 464      clearCache();
 465      await cleanupTestDb();
 466    }
 467  });
 468  
 469  test('Context Builder - formatSuccessfulTask uses file_path when no files_changed', async t => {
 470    await initTestDb();
 471    process.env.DATABASE_PATH = testDbPath;
 472    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 473  
 474    try {
 475      const taskId = await createAgentTask({
 476        task_type: 'write_tests',
 477        assigned_to: 'developer',
 478        context: { file_path: 'src/score.js' },
 479      });
 480      completeTask(taskId, { file_path: 'src/score.js', action_taken: 'Wrote unit tests' });
 481  
 482      // result_json has file_path (not files_changed) - covers lines 344-346
 483      const db = new Database(testDbPath);
 484      db.prepare(
 485        `INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, result_json)
 486         VALUES (?, ?, ?, ?, ?)`
 487      ).run(
 488        taskId,
 489        'developer',
 490        'write_tests',
 491        'success',
 492        JSON.stringify({ file_path: 'src/score.js', action_taken: 'Wrote unit tests' })
 493      );
 494      db.close();
 495  
 496      clearCache();
 497  
 498      const context = await buildAgentContext('developer', ['base.md']);
 499      assert.ok(context.historyContext.includes('src/score.js'), 'Should show file_path from result');
 500      assert.ok(
 501        context.historyContext.includes('Wrote unit tests'),
 502        'Should show action_taken as Approach'
 503      );
 504    } finally {
 505      delete process.env.DATABASE_PATH;
 506      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 507      resetDb();
 508      resetTaskManagerDb();
 509      clearCache();
 510      await cleanupTestDb();
 511    }
 512  });
 513  
 514  test('Context Builder - related task insight branches (what worked / what failed)', async t => {
 515    await initTestDb();
 516    process.env.DATABASE_PATH = testDbPath;
 517    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 518  
 519    try {
 520      // Task completed with approach - covers extractRelatedInsight lines 395-397
 521      const taskId1 = await createAgentTask({
 522        task_type: 'fix_bug',
 523        assigned_to: 'qa',
 524        context: { file_path: 'src/scrape.js', error_type: 'timeout' },
 525      });
 526      completeTask(taskId1, { approach: 'Increased timeout to 30 seconds' });
 527  
 528      const db = new Database(testDbPath);
 529      db.prepare(
 530        `INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, result_json)
 531         VALUES (?, ?, ?, ?, ?)`
 532      ).run(
 533        taskId1,
 534        'qa',
 535        'fix_bug',
 536        'success',
 537        JSON.stringify({ approach: 'Increased timeout to 30 seconds' })
 538      );
 539  
 540      // Failed task with context.error - covers extractRelatedInsight lines 398-400
 541      const taskId2 = await createAgentTask({
 542        task_type: 'fix_bug',
 543        assigned_to: 'qa',
 544        context: { file_path: 'src/scrape.js', error_type: 'timeout', error: 'Connection refused' },
 545      });
 546      failTask(taskId2, 'Connection refused after retry');
 547  
 548      db.prepare(
 549        `INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, context_json)
 550         VALUES (?, ?, ?, ?, ?)`
 551      ).run(
 552        taskId2,
 553        'qa',
 554        'fix_bug',
 555        'failure',
 556        JSON.stringify({ file_path: 'src/scrape.js', error: 'Connection refused' })
 557      );
 558      db.close();
 559  
 560      clearCache();
 561  
 562      const currentTask = {
 563        context_json: { file_path: 'src/scrape.js', error_type: 'timeout' },
 564      };
 565  
 566      const context = await buildAgentContext('qa', ['base.md'], currentTask);
 567      assert.ok(context.historyContext.includes('Related Tasks'), 'Should have related tasks');
 568      // insight branches should render content (lines 422-424)
 569      assert.ok(
 570        context.historyContext.includes('scrape') || context.historyContext.includes('timeout'),
 571        'Should show related task details'
 572      );
 573      assert.ok(typeof context.metadata.historyStats.relatedTasks === 'number');
 574    } finally {
 575      delete process.env.DATABASE_PATH;
 576      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 577      resetDb();
 578      resetTaskManagerDb();
 579      clearCache();
 580      await cleanupTestDb();
 581    }
 582  });
 583  
 584  test('Context Builder - extractFilePathFromContext from error_message and stack_trace', async t => {
 585    await initTestDb();
 586    process.env.DATABASE_PATH = testDbPath;
 587    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 588  
 589    try {
 590      // Task context with error_message containing a JS file path - covers lines 454-457
 591      const taskId1 = await createAgentTask({
 592        task_type: 'fix_bug',
 593        assigned_to: 'developer',
 594        context: {
 595          error_type: 'parse_error',
 596          error_message: 'SyntaxError at /home/user/code/src/capture.js:42:3',
 597        },
 598      });
 599      completeTask(taskId1, { approach: 'Fixed syntax error' });
 600  
 601      // Task context with stack_trace containing a JS file path - covers lines 459-462
 602      const taskId2 = await createAgentTask({
 603        task_type: 'fix_bug',
 604        assigned_to: 'developer',
 605        context: {
 606          error_type: 'parse_error',
 607          stack_trace: 'Error\n  at Object.<anonymous> (/home/user/code/src/scrape.js:10:5)',
 608        },
 609      });
 610      completeTask(taskId2, { approach: 'Fixed stack issue' });
 611  
 612      clearCache();
 613  
 614      const currentTask = {
 615        context_json: { error_type: 'parse_error' },
 616      };
 617  
 618      const context = await buildAgentContext('developer', ['base.md'], currentTask);
 619      assert.ok(context.historyContext, 'Should build context with error_message/stack_trace tasks');
 620      assert.ok(typeof context.metadata.historyStats.relatedTasks === 'number');
 621    } finally {
 622      delete process.env.DATABASE_PATH;
 623      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 624      resetDb();
 625      resetTaskManagerDb();
 626      clearCache();
 627      await cleanupTestDb();
 628    }
 629  });
 630  
 631  test('Context Builder - invalid JSON in task records is handled gracefully', async t => {
 632    await initTestDb();
 633    process.env.DATABASE_PATH = testDbPath;
 634    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 635  
 636    try {
 637      // Insert tasks with malformed JSON directly to trigger tryParseJSON catch (lines 507-509)
 638      const db = new Database(testDbPath);
 639      db.prepare(
 640        `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, completed_at)
 641         VALUES (?, ?, ?, ?, datetime('now'))`
 642      ).run('fix_bug', 'developer', 'completed', '{not valid json}');
 643  
 644      db.prepare(
 645        `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, context_json, completed_at)
 646         VALUES (?, ?, ?, ?, ?, datetime('now'))`
 647      ).run('fix_bug', 'developer', 'failed', 'Something went wrong', '{bad json here}');
 648      db.close();
 649  
 650      clearCache();
 651  
 652      // Should not throw - tryParseJSON returns null on invalid JSON
 653      const context = await buildAgentContext('developer', ['base.md']);
 654      assert.ok(context.fullContext, 'Should build context even with malformed JSON in tasks');
 655    } finally {
 656      delete process.env.DATABASE_PATH;
 657      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 658      resetDb();
 659      resetTaskManagerDb();
 660      clearCache();
 661      await cleanupTestDb();
 662    }
 663  });
 664  
 665  test('Context Builder - cache hit on second call (TTL not expired)', async t => {
 666    await initTestDb();
 667    process.env.DATABASE_PATH = testDbPath;
 668    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 669  
 670    try {
 671      const taskId = await createAgentTask({
 672        task_type: 'fix_bug',
 673        assigned_to: 'developer',
 674      });
 675      completeTask(taskId, { files_changed: ['src/test.js'] });
 676  
 677      clearCache();
 678  
 679      // First call populates cache
 680      const ctx1 = await buildAgentContext('developer', ['base.md']);
 681      // Second call hits cache (covers getCached return path, lines 518-528 hit branch)
 682      const ctx2 = await buildAgentContext('developer', ['base.md']);
 683  
 684      assert.strictEqual(
 685        ctx1.metadata.historyStats.recentSuccesses,
 686        ctx2.metadata.historyStats.recentSuccesses,
 687        'Cached result should match original'
 688      );
 689    } finally {
 690      delete process.env.DATABASE_PATH;
 691      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 692      resetDb();
 693      resetTaskManagerDb();
 694      clearCache();
 695      await cleanupTestDb();
 696    }
 697  });
 698  
 699  test('Context Builder - extractFilePathFromContext uses filePath field', async t => {
 700    await initTestDb();
 701    process.env.DATABASE_PATH = testDbPath;
 702    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 703  
 704    try {
 705      // Create a task where context only has 'filePath' (camelCase) - not 'file_path'
 706      // This exercises extractFilePathFromContext lines 442 and 448-450
 707      const taskId = await createAgentTask({
 708        task_type: 'fix_bug',
 709        assigned_to: 'developer',
 710        context: { error_type: 'null_ref', filePath: 'src/enrich.js' },
 711      });
 712      completeTask(taskId, { approach: 'Added null guard' });
 713  
 714      clearCache();
 715  
 716      // Current task with only error_type (no file_path/file), but existing task has filePath
 717      // getRelatedTasks will use error_type to match, then formatRelatedTask will find filePath
 718      const currentTask = {
 719        context_json: { error_type: 'null_ref' },
 720      };
 721  
 722      const context = await buildAgentContext('developer', ['base.md'], currentTask);
 723      assert.ok(context.historyContext, 'Should build context');
 724      assert.ok(typeof context.metadata.historyStats.relatedTasks === 'number');
 725    } finally {
 726      delete process.env.DATABASE_PATH;
 727      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 728      resetDb();
 729      resetTaskManagerDb();
 730      clearCache();
 731      await cleanupTestDb();
 732    }
 733  });
 734  
 735  test('Context Builder - extractFilePathFromContext uses affected_file and files_changed', async t => {
 736    await initTestDb();
 737    process.env.DATABASE_PATH = testDbPath;
 738    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 739  
 740    try {
 741      // Task with affected_file in context
 742      const taskId1 = await createAgentTask({
 743        task_type: 'fix_bug',
 744        assigned_to: 'developer',
 745        context: { error_type: 'io_error', affected_file: 'src/capture.js' },
 746      });
 747      completeTask(taskId1, { approach: 'Fixed IO' });
 748  
 749      // Task with files_changed array in context
 750      const taskId2 = await createAgentTask({
 751        task_type: 'fix_bug',
 752        assigned_to: 'developer',
 753        context: { error_type: 'io_error', files_changed: ['src/score.js', 'src/rescore.js'] },
 754      });
 755      completeTask(taskId2, { approach: 'Fixed scoring' });
 756  
 757      clearCache();
 758  
 759      const currentTask = {
 760        context_json: { error_type: 'io_error' },
 761      };
 762  
 763      const context = await buildAgentContext('developer', ['base.md'], currentTask);
 764      assert.ok(context.historyContext, 'Should build context with affected_file/files_changed');
 765      assert.ok(typeof context.metadata.historyStats.relatedTasks === 'number');
 766    } finally {
 767      delete process.env.DATABASE_PATH;
 768      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 769      resetDb();
 770      resetTaskManagerDb();
 771      clearCache();
 772      await cleanupTestDb();
 773    }
 774  });
 775  
 776  test('Context Builder - related task with failed status (outcome === failed branch)', async t => {
 777    await initTestDb();
 778    process.env.DATABASE_PATH = testDbPath;
 779    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 780  
 781    try {
 782      // Insert a task directly with status='failed' so task.outcome is null, task.status is 'failed'
 783      // This makes formatRelatedTask compute outcome='failed' (task.outcome || task.status)
 784      // Then extractRelatedInsight checks outcome === 'failed' && context?.error -> lines 398-400
 785      const db = new Database(testDbPath);
 786      db.prepare(
 787        `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, context_json, completed_at)
 788         VALUES (?, ?, ?, ?, ?, datetime('now'))`
 789      ).run(
 790        'fix_bug',
 791        'developer',
 792        'failed',
 793        'Network error',
 794        JSON.stringify({ file_path: 'src/outreach.js', error_type: 'net_err', error: 'ECONNREFUSED' })
 795      );
 796      db.close();
 797  
 798      clearCache();
 799  
 800      // Current task with matching file_path and error_type to trigger related lookup
 801      const currentTask = {
 802        context_json: { file_path: 'src/outreach.js', error_type: 'net_err' },
 803      };
 804  
 805      const context = await buildAgentContext('developer', ['base.md'], currentTask);
 806      assert.ok(context.historyContext, 'Should build context');
 807      // The insight branch for failed status with context.error should be exercised
 808      assert.ok(typeof context.metadata.historyStats.relatedTasks === 'number');
 809    } finally {
 810      delete process.env.DATABASE_PATH;
 811      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 812      resetDb();
 813      resetTaskManagerDb();
 814      clearCache();
 815      await cleanupTestDb();
 816    }
 817  });
 818  
 819  test('Context Builder - extractFilePathFromContext in current task context (filePath camelCase)', async t => {
 820    await initTestDb();
 821    process.env.DATABASE_PATH = testDbPath;
 822    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 823  
 824    try {
 825      // Insert a completed task so there's something to correlate against
 826      const taskId = await createAgentTask({
 827        task_type: 'fix_bug',
 828        assigned_to: 'developer',
 829        context: { filePath: 'src/enrich.js', error_type: 'io' },
 830      });
 831      completeTask(taskId, { approach: 'Fixed' });
 832  
 833      clearCache();
 834  
 835      // Current task context has 'filePath' (camelCase), NOT 'file_path' or 'file'
 836      // This causes extractFilePathFromContext to be called and find filePath at line 442/449-450
 837      const currentTask = {
 838        context_json: { filePath: 'src/enrich.js' },
 839      };
 840  
 841      const context = await buildAgentContext('developer', ['base.md'], currentTask);
 842      assert.ok(context.historyContext, 'Should build context');
 843      // Related tasks should be found via filePath extraction
 844      assert.ok(typeof context.metadata.historyStats.relatedTasks === 'number');
 845    } finally {
 846      delete process.env.DATABASE_PATH;
 847      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 848      resetDb();
 849      resetTaskManagerDb();
 850      clearCache();
 851      await cleanupTestDb();
 852    }
 853  });
 854  
 855  test('Context Builder - extractFilePathFromContext in current task context (affected_file)', async t => {
 856    await initTestDb();
 857    process.env.DATABASE_PATH = testDbPath;
 858    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 859  
 860    try {
 861      const taskId = await createAgentTask({
 862        task_type: 'fix_bug',
 863        assigned_to: 'developer',
 864        context: { affected_file: 'src/score.js' },
 865      });
 866      completeTask(taskId, { approach: 'Fixed scorer' });
 867  
 868      clearCache();
 869  
 870      // Current task with affected_file (no file_path/file) -> extractFilePathFromContext at line 443/449-450
 871      const currentTask = {
 872        context_json: { affected_file: 'src/score.js' },
 873      };
 874  
 875      const context = await buildAgentContext('developer', ['base.md'], currentTask);
 876      assert.ok(context.historyContext, 'Should build context');
 877      assert.ok(typeof context.metadata.historyStats.relatedTasks === 'number');
 878    } finally {
 879      delete process.env.DATABASE_PATH;
 880      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 881      resetDb();
 882      resetTaskManagerDb();
 883      clearCache();
 884      await cleanupTestDb();
 885    }
 886  });
 887  
 888  test('Context Builder - extractFilePathFromContext from current task error_message', async t => {
 889    await initTestDb();
 890    process.env.DATABASE_PATH = testDbPath;
 891    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 892  
 893    try {
 894      // Insert a stored task with a matching path in context
 895      const taskId = await createAgentTask({
 896        task_type: 'fix_bug',
 897        assigned_to: 'developer',
 898        context: { error_message: 'Error at /home/user/src/capture.js:42' },
 899      });
 900      completeTask(taskId, { approach: 'Fixed it' });
 901  
 902      clearCache();
 903  
 904      // Current task context has error_message with a JS path (no file_path/file/filePath etc.)
 905      // This triggers extractFilePathFromContext to match via error_message regex (lines 454-457)
 906      const currentTask = {
 907        context_json: { error_message: 'Failed at /home/user/src/capture.js:10:3' },
 908      };
 909  
 910      const context = await buildAgentContext('developer', ['base.md'], currentTask);
 911      assert.ok(context.historyContext, 'Should build context');
 912      assert.ok(typeof context.metadata.historyStats.relatedTasks === 'number');
 913    } finally {
 914      delete process.env.DATABASE_PATH;
 915      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 916      resetDb();
 917      resetTaskManagerDb();
 918      clearCache();
 919      await cleanupTestDb();
 920    }
 921  });
 922  
 923  test('Context Builder - extractFilePathFromContext from current task stack_trace', async t => {
 924    await initTestDb();
 925    process.env.DATABASE_PATH = testDbPath;
 926    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 927  
 928    try {
 929      // Stored task that relates via file path
 930      const taskId = await createAgentTask({
 931        task_type: 'fix_bug',
 932        assigned_to: 'developer',
 933        context: { stack_trace: 'at fn (/home/user/src/scrape.js:5:3)' },
 934      });
 935      completeTask(taskId, { approach: 'Fixed scrape' });
 936  
 937      clearCache();
 938  
 939      // Current task context has stack_trace with a JS path (no other file fields)
 940      // This triggers lines 459-462 in extractFilePathFromContext
 941      const currentTask = {
 942        context_json: { stack_trace: 'Error\n  at handler (/home/user/src/scrape.js:20:7)' },
 943      };
 944  
 945      const context = await buildAgentContext('developer', ['base.md'], currentTask);
 946      assert.ok(context.historyContext, 'Should build context');
 947      assert.ok(typeof context.metadata.historyStats.relatedTasks === 'number');
 948    } finally {
 949      delete process.env.DATABASE_PATH;
 950      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 951      resetDb();
 952      resetTaskManagerDb();
 953      clearCache();
 954      await cleanupTestDb();
 955    }
 956  });
 957  
 958  test('Context Builder - cache TTL expiry evicts stale entry (lines 524-526)', async t => {
 959    // Use mock.timers to advance Date.now() past CACHE_TTL_MS (30 minutes).
 960    // IMPORTANT: mock.timers.enable() must be called BEFORE the first buildAgentContext call
 961    // so that the timestamp stored in the cache uses the mocked time base.
 962    // After ticking 31 minutes, Date.now() returns a value > (timestamp + TTL),
 963    // which triggers getCached lines 523-525 (delete + return null -> cache miss).
 964    const { mock } = await import('node:test');
 965  
 966    await initTestDb();
 967    process.env.DATABASE_PATH = testDbPath;
 968    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 969  
 970    // Enable Date mocking BEFORE making any calls so timestamps are consistent
 971    mock.timers.enable({ apis: ['Date'] });
 972  
 973    try {
 974      clearCache();
 975  
 976      // First call: cache entries are stored with mocked Date.now() as timestamp
 977      const ctx1 = await buildAgentContext('developer', ['base.md']);
 978      assert.ok(ctx1.fullContext, 'First call should succeed');
 979  
 980      // Advance time past 30-minute TTL (CACHE_TTL_MS = 30 * 60 * 1000)
 981      mock.timers.tick(31 * 60 * 1000); // advance 31 minutes
 982  
 983      // Second call: getCached finds entries with timestamp now > 31 minutes old
 984      // -> Date.now() - timestamp > CACHE_TTL_MS is true
 985      // -> lines 524-526 execute: taskHistoryCache.delete(key); return null;
 986      // -> cache miss -> re-queries DB
 987      const ctx2 = await buildAgentContext('developer', ['base.md']);
 988      assert.ok(ctx2.fullContext, 'Second call after TTL expiry should succeed');
 989      assert.ok(
 990        ctx2.metadata.historyStats.recentSuccesses >= 0,
 991        'Should have valid stats after cache eviction'
 992      );
 993    } finally {
 994      mock.timers.reset();
 995      delete process.env.DATABASE_PATH;
 996      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 997      resetDb();
 998      resetTaskManagerDb();
 999      clearCache();
1000      await cleanupTestDb();
1001    }
1002  });
1003  
1004  test('Context Builder - formatFailedTask returns null when no error_message (line 366)', async t => {
1005    await initTestDb();
1006    process.env.DATABASE_PATH = testDbPath;
1007    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
1008  
1009    try {
1010      // Insert a task with status='failed' but NO error_message
1011      // This makes formatFailedTask return null (line 366: if (!task.error_message) return null)
1012      const db = new Database(testDbPath);
1013      db.prepare(
1014        `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, context_json, completed_at)
1015         VALUES (?, ?, ?, ?, ?, datetime('now'))`
1016      ).run('fix_bug', 'developer', 'failed', null, JSON.stringify({ file_path: 'src/test.js' }));
1017      db.close();
1018  
1019      clearCache();
1020  
1021      // buildAgentContext fetches failed tasks; formatFailedTask is called with this record
1022      // Since error_message is null, formatFailedTask returns null -> filtered out from display
1023      const context = await buildAgentContext('developer', ['base.md']);
1024      assert.ok(context.fullContext, 'Should build context even with null error_message task');
1025      // The failed task with null error_message should not appear in history context
1026      // (formatFailedTask returns null -> filtered from list -> recentFailures stays 0 or
1027      //  the task shows as failed but has no message to format)
1028      assert.ok(typeof context.metadata.historyStats.recentFailures === 'number');
1029    } finally {
1030      delete process.env.DATABASE_PATH;
1031      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
1032      resetDb();
1033      resetTaskManagerDb();
1034      clearCache();
1035      await cleanupTestDb();
1036    }
1037  });
1038  
1039  test('Context Builder - getRelatedTasks with currentTask having null context_json (line 203)', async t => {
1040    await initTestDb();
1041    process.env.DATABASE_PATH = testDbPath;
1042    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
1043  
1044    try {
1045      clearCache();
1046  
1047      // currentTask with null context_json triggers early return at line 203
1048      const currentTask = { context_json: null };
1049      const context = await buildAgentContext('developer', ['base.md'], currentTask);
1050      assert.ok(context.fullContext, 'Should build context with null context_json task');
1051      assert.strictEqual(
1052        context.metadata.historyStats.relatedTasks,
1053        0,
1054        'Should have 0 related tasks when context_json is null'
1055      );
1056    } finally {
1057      delete process.env.DATABASE_PATH;
1058      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
1059      resetDb();
1060      resetTaskManagerDb();
1061      clearCache();
1062      await cleanupTestDb();
1063    }
1064  });
1065  
1066  test('Context Builder - getRelatedTasks returns [] when no filePath and no errorType (line 209)', async t => {
1067    await initTestDb();
1068    process.env.DATABASE_PATH = testDbPath;
1069    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
1070  
1071    try {
1072      clearCache();
1073  
1074      // currentTask has context_json with no file_path, file, filePath, affected_file,
1075      // files_changed, error_message, stack_trace, or error_type fields.
1076      // extractFilePathFromContext returns null, errorType is undefined -> both falsy
1077      // -> getRelatedTasks returns [] at line 209
1078      const currentTask = { context_json: { some_other_field: 'value' } };
1079      const context = await buildAgentContext('developer', ['base.md'], currentTask);
1080      assert.ok(context.fullContext, 'Should build context when no path/error_type');
1081      assert.strictEqual(
1082        context.metadata.historyStats.relatedTasks,
1083        0,
1084        'Should have 0 related tasks when no file path or error type'
1085      );
1086    } finally {
1087      delete process.env.DATABASE_PATH;
1088      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
1089      resetDb();
1090      resetTaskManagerDb();
1091      clearCache();
1092      await cleanupTestDb();
1093    }
1094  });
1095  
1096  test('Context Builder - formatFailedTask with context having only file (not file_path) (line 382)', async t => {
1097    await initTestDb();
1098    process.env.DATABASE_PATH = testDbPath;
1099    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
1100  
1101    try {
1102      // Insert a failed task where context uses 'file' key (not 'file_path')
1103      // This exercises the `context.file_path || context.file` branch at line 382
1104      const db = new Database(testDbPath);
1105      db.prepare(
1106        `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, context_json, completed_at)
1107         VALUES (?, ?, ?, ?, ?, datetime('now'))`
1108      ).run(
1109        'fix_bug',
1110        'developer',
1111        'failed',
1112        'Operation failed due to network error',
1113        JSON.stringify({ file: 'src/outreach/sms.js', error_type: 'network' })
1114      );
1115      db.close();
1116  
1117      clearCache();
1118  
1119      const context = await buildAgentContext('developer', ['base.md']);
1120      assert.ok(context.fullContext, 'Should build context');
1121      // The failed task uses 'file' key -> line 382 branch `context.file_path || context.file`
1122      // evaluates to context.file -> 'src/outreach/sms.js' -> adds File line to output
1123      assert.ok(typeof context.metadata.historyStats.recentFailures === 'number');
1124    } finally {
1125      delete process.env.DATABASE_PATH;
1126      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
1127      resetDb();
1128      resetTaskManagerDb();
1129      clearCache();
1130      await cleanupTestDb();
1131    }
1132  });