/ __quarantined_tests__ / agents / triage.test.js
triage.test.js
   1  /**
   2   * Triage Agent Unit Tests
   3   *
   4   * Tests error classification, severity determination, routing, and priority calculation.
   5   */
   6  
   7  import { test, describe, beforeEach, afterEach } from 'node:test';
   8  import assert from 'node:assert';
   9  import Database from 'better-sqlite3';
  10  import { TriageAgent } from '../../src/agents/triage.js';
  11  import { resetDb as resetBaseDb } from '../../src/agents/base-agent.js';
  12  import { resetDb as resetTaskDb } from '../../src/agents/utils/task-manager.js';
  13  import { resetDb as resetMessageDb } from '../../src/agents/utils/message-manager.js';
  14  import fs from 'fs/promises';
  15  
  16  // Use temporary file database for tests
  17  let db;
  18  let agent;
  19  const TEST_DB_PATH = './tests/agents/test-triage.db';
  20  
  21  beforeEach(async () => {
  22    // Remove existing test database if it exists
  23    try {
  24      await fs.unlink(TEST_DB_PATH);
  25    } catch (e) {
  26      // Ignore if file doesn't exist
  27    }
  28  
  29    // Create temporary test database
  30    db = new Database(TEST_DB_PATH);
  31    process.env.DATABASE_PATH = TEST_DB_PATH;
  32    // Point TEL_DB_PATH at the test DB so tel.agent_tasks queries work via self-attach.
  33    process.env.TEL_DB_PATH = TEST_DB_PATH;
  34    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
  35    process.env.AGENT_IMMEDIATE_INVOCATION = 'false';
  36  
  37    // Create tables
  38    db.exec(`
  39      CREATE TABLE agent_tasks (
  40        id INTEGER PRIMARY KEY AUTOINCREMENT,
  41        task_type TEXT NOT NULL,
  42        assigned_to TEXT NOT NULL,
  43        created_by TEXT,
  44        status TEXT DEFAULT 'pending',
  45        priority INTEGER DEFAULT 5,
  46        context_json TEXT,
  47        result_json TEXT,
  48        parent_task_id INTEGER,
  49        error_message TEXT,
  50        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  51        started_at DATETIME,
  52        completed_at DATETIME,
  53        retry_count INTEGER DEFAULT 0
  54      );
  55  
  56      CREATE TABLE agent_messages (
  57        id INTEGER PRIMARY KEY AUTOINCREMENT,
  58        task_id INTEGER,
  59        from_agent TEXT NOT NULL,
  60        to_agent TEXT NOT NULL,
  61        message_type TEXT,
  62        content TEXT NOT NULL,
  63        metadata_json TEXT,
  64        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  65        read_at DATETIME
  66      );
  67  
  68      CREATE TABLE agent_logs (
  69        id INTEGER PRIMARY KEY AUTOINCREMENT,
  70        task_id INTEGER,
  71        agent_name TEXT NOT NULL,
  72        log_level TEXT,
  73        message TEXT,
  74        data_json TEXT,
  75        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  76      );
  77  
  78      CREATE TABLE agent_state (
  79        agent_name TEXT PRIMARY KEY,
  80        last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
  81        current_task_id INTEGER,
  82        status TEXT DEFAULT 'idle',
  83        metrics_json TEXT
  84      );
  85      CREATE TABLE agent_outcomes (
  86        id INTEGER PRIMARY KEY AUTOINCREMENT,
  87        task_id INTEGER NOT NULL,
  88        agent_name TEXT NOT NULL,
  89        task_type TEXT NOT NULL,
  90        outcome TEXT NOT NULL,
  91        context_json TEXT,
  92        result_json TEXT,
  93        duration_ms INTEGER,
  94        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  95      );
  96  
  97      CREATE TABLE agent_llm_usage (
  98        id INTEGER PRIMARY KEY AUTOINCREMENT,
  99        agent_name TEXT NOT NULL,
 100        task_id INTEGER,
 101        model TEXT NOT NULL,
 102        prompt_tokens INTEGER NOT NULL,
 103        completion_tokens INTEGER NOT NULL,
 104        cost_usd DECIMAL(10, 6) NOT NULL,
 105        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 106      );
 107  
 108      CREATE TABLE structured_logs (
 109        id INTEGER PRIMARY KEY AUTOINCREMENT,
 110        agent_name TEXT,
 111        task_id INTEGER,
 112        level TEXT,
 113        message TEXT,
 114        data_json TEXT,
 115        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 116      );
 117  
 118    `);
 119  
 120    // Initialize agent
 121    agent = new TriageAgent();
 122    await agent.initialize();
 123  });
 124  
 125  afterEach(async () => {
 126    // Restore env vars
 127    delete process.env.TEL_DB_PATH;
 128  
 129    // Reset all database connections first
 130    resetBaseDb();
 131    resetTaskDb();
 132    resetMessageDb();
 133  
 134    if (db) {
 135      db.close();
 136    }
 137    // Clean up test database
 138    try {
 139      await fs.unlink(TEST_DB_PATH);
 140    } catch (e) {
 141      // Ignore if file doesn't exist
 142    }
 143  });
 144  
 145  describe('TriageAgent - Error Classification', () => {
 146    test('classifies null pointer errors correctly', () => {
 147      const result = agent.classifyError('TypeError: Cannot read property "score" of null');
 148  
 149      assert.strictEqual(result.type, 'null_pointer');
 150      assert.strictEqual(result.severity, 'medium');
 151      assert.strictEqual(result.assignee, 'developer');
 152    });
 153  
 154    test('classifies network errors correctly', () => {
 155      const result = agent.classifyError('Error: ENOTFOUND api.example.com');
 156  
 157      assert.strictEqual(result.type, 'network');
 158      assert.strictEqual(result.assignee, 'architect');
 159    });
 160  
 161    test('classifies database constraint errors correctly', () => {
 162      const result = agent.classifyError('UNIQUE constraint failed: messages.site_id');
 163  
 164      assert.strictEqual(result.type, 'database');
 165      assert.strictEqual(result.severity, 'high');
 166    });
 167  
 168    test('classifies API errors correctly', () => {
 169      const result = agent.classifyError('API error: status 429 - rate limit exceeded');
 170  
 171      assert.strictEqual(result.type, 'api_error');
 172    });
 173  
 174    test('classifies security errors correctly', () => {
 175      const result = agent.classifyError('Unauthorized: invalid API signature');
 176  
 177      assert.strictEqual(result.type, 'security');
 178      assert.strictEqual(result.severity, 'critical');
 179      assert.strictEqual(result.basePriority, 10);
 180    });
 181  
 182    test('classifies configuration errors correctly', () => {
 183      const result = agent.classifyError('Error: OPENROUTER_API_KEY environment variable required');
 184  
 185      assert.strictEqual(result.type, 'configuration');
 186      assert.strictEqual(result.severity, 'high');
 187    });
 188  
 189    test('defaults to unknown for unrecognized errors', () => {
 190      const result = agent.classifyError('Something weird happened');
 191  
 192      assert.strictEqual(result.type, 'unknown');
 193      assert.strictEqual(result.severity, 'medium');
 194    });
 195  });
 196  
 197  describe('TriageAgent - Severity Determination', () => {
 198    test('marks security errors as critical', () => {
 199      const { severity, basePriority } = agent.determineSeverity(
 200        'Unauthorized access detected',
 201        'scoring',
 202        1
 203      );
 204  
 205      assert.strictEqual(severity, 'critical');
 206      assert.strictEqual(basePriority, 10);
 207    });
 208  
 209    test('marks early pipeline stage errors as high severity', () => {
 210      const { severity } = agent.determineSeverity('Random error', 'serps', 1);
 211  
 212      assert.strictEqual(severity, 'high');
 213    });
 214  
 215    test('marks high frequency errors as high severity', () => {
 216      const { severity } = agent.determineSeverity(
 217        'Random error',
 218        'outreach',
 219        15 // > 10 occurrences
 220      );
 221  
 222      assert.strictEqual(severity, 'high');
 223    });
 224  
 225    test('marks data loss risks as high severity', () => {
 226      const { severity } = agent.determineSeverity('Database transaction rollback', 'scoring', 1);
 227  
 228      assert.strictEqual(severity, 'high');
 229    });
 230  });
 231  
 232  describe('TriageAgent - Routing Logic', () => {
 233    test('routes security errors to security agent', () => {
 234      const assignee = agent.routeToAgent('security', 'critical', {});
 235  
 236      assert.strictEqual(assignee, 'security');
 237    });
 238  
 239    test('routes network errors to architect', () => {
 240      const assignee = agent.routeToAgent('network', 'medium', {});
 241  
 242      assert.strictEqual(assignee, 'architect');
 243    });
 244  
 245    test('routes database schema changes to architect', () => {
 246      const assignee = agent.routeToAgent('database', 'high', {
 247        schema_change_needed: true,
 248      });
 249  
 250      assert.strictEqual(assignee, 'architect');
 251    });
 252  
 253    test('routes test failures to qa', () => {
 254      const assignee = agent.routeToAgent('unknown', 'medium', {
 255        test_failure: true,
 256      });
 257  
 258      assert.strictEqual(assignee, 'qa');
 259    });
 260  
 261    test('routes most errors to developer by default', () => {
 262      const assignee = agent.routeToAgent('null_pointer', 'medium', {});
 263  
 264      assert.strictEqual(assignee, 'developer');
 265    });
 266  });
 267  
 268  describe('TriageAgent - Priority Calculation', () => {
 269    test('calculates high priority for critical security errors', () => {
 270      const task = { id: 1, created_at: new Date().toISOString() };
 271      const priority = agent.calculatePriority(task, {
 272        frequency: 1,
 273        severity: 'critical',
 274        stage: 'scoring',
 275        error_type: 'security',
 276      });
 277  
 278      // basePriority 5 - 2 (critical) - 2 (scoring stage) = 1 (capped)
 279      assert.ok(priority >= 1 && priority <= 10);
 280      // Critical errors should have high priority (low number = high priority)
 281      assert.ok(priority <= 3);
 282    });
 283  
 284    test('boosts priority for early pipeline stages', () => {
 285      const task = { id: 1, created_at: new Date().toISOString() };
 286      const scoringPriority = agent.calculatePriority(task, {
 287        frequency: 5,
 288        severity: 'medium',
 289        stage: 'scoring',
 290      });
 291      const outreachPriority = agent.calculatePriority(task, {
 292        frequency: 5,
 293        severity: 'medium',
 294        stage: 'outreach',
 295      });
 296  
 297      // Scoring (critical stage) should have lower priority number (higher priority)
 298      assert.ok(scoringPriority <= outreachPriority);
 299    });
 300  
 301    test('boosts priority for high frequency errors', () => {
 302      const task = { id: 1, created_at: new Date().toISOString() };
 303      const highFreqPriority = agent.calculatePriority(task, {
 304        frequency: 150,
 305        severity: 'medium',
 306        stage: 'outreach',
 307      });
 308      const lowFreqPriority = agent.calculatePriority(task, {
 309        frequency: 1,
 310        severity: 'medium',
 311        stage: 'outreach',
 312      });
 313  
 314      // High frequency should have higher priority (lower number)
 315      assert.ok(highFreqPriority <= lowFreqPriority);
 316    });
 317  
 318    test('reduces priority for low severity errors', () => {
 319      const task = { id: 1, created_at: new Date().toISOString() };
 320      const lowSeverityPriority = agent.calculatePriority(task, {
 321        frequency: 1,
 322        severity: 'low',
 323        stage: 'outreach',
 324      });
 325  
 326      // Low severity should be closer to max (5 is baseline, medium reduces by 1)
 327      assert.ok(lowSeverityPriority >= 4); // Low severity = no reduction from medium
 328    });
 329  
 330    test('caps priority at minimum 1', () => {
 331      const task = { id: 1, created_at: new Date().toISOString() };
 332      const priority = agent.calculatePriority(task, {
 333        frequency: 200,
 334        severity: 'critical',
 335        stage: 'scoring',
 336        error_type: 'database',
 337      });
 338      assert.ok(priority >= 1);
 339    });
 340  
 341    test('caps priority at maximum 10', () => {
 342      const task = { id: 1, created_at: new Date().toISOString() };
 343      const priority = agent.calculatePriority(task, {});
 344      assert.ok(priority <= 10);
 345    });
 346  
 347    test('uses calculatePriorityFromClassification for old-style single-object calls', () => {
 348      // calculatePriorityFromClassification is used internally in classifyErrorTask
 349      const priority = agent.calculatePriorityFromClassification({
 350        type: 'security',
 351        severity: 'critical',
 352        basePriority: 10,
 353        stage: 'scoring',
 354        frequency: 1,
 355      });
 356      // basePriority 10 + critical boost 5 = 15, capped at 10
 357      assert.strictEqual(priority, 10);
 358    });
 359  });
 360  
 361  describe('TriageAgent - Suggested Fixes', () => {
 362    test('suggests null check for null pointer errors', () => {
 363      const suggestion = agent.suggestFix('null_pointer', 'Cannot read property');
 364  
 365      assert.match(suggestion, /optional chaining/i);
 366    });
 367  
 368    test('suggests INSERT OR IGNORE for UNIQUE constraint errors', () => {
 369      const suggestion = agent.suggestFix('database', 'UNIQUE constraint failed');
 370  
 371      assert.match(suggestion, /INSERT OR IGNORE/i);
 372    });
 373  
 374    test('suggests retry logic for network errors', () => {
 375      const suggestion = agent.suggestFix('network', 'ENOTFOUND');
 376  
 377      assert.match(suggestion, /retryWithBackoff/i);
 378    });
 379  
 380    test('suggests rate limiting for 429 errors', () => {
 381      const suggestion = agent.suggestFix('api_error', 'status 429 - rate limit');
 382  
 383      assert.match(suggestion, /rate limiting/i);
 384    });
 385  
 386    test('suggests checking .env for configuration errors', () => {
 387      const suggestion = agent.suggestFix('configuration', 'missing API_KEY');
 388  
 389      assert.match(suggestion, /\.env/i);
 390    });
 391  });
 392  
 393  describe('TriageAgent - Task Processing', () => {
 394    test('processes classify_error task and creates developer task', async () => {
 395      // Create a classify_error task
 396      const taskId = db
 397        .prepare(
 398          `
 399        INSERT INTO agent_tasks (task_type, assigned_to, status, priority, context_json)
 400        VALUES ('classify_error', 'triage', 'pending', 5, ?)
 401      `
 402        )
 403        .run(
 404          JSON.stringify({
 405            error_message: 'TypeError: Cannot read property "score" of null',
 406            stack_trace: 'at Object.<anonymous> (src/scoring.js:179:45)',
 407            stage: 'scoring',
 408            frequency: 1,
 409          })
 410        ).lastInsertRowid;
 411  
 412      // Get the task
 413      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 414      task.context_json = JSON.parse(task.context_json);
 415  
 416      // Process it
 417      await agent.classifyErrorTask(task);
 418  
 419      // Verify developer task was created
 420      const devTasks = db
 421        .prepare(
 422          `
 423        SELECT * FROM agent_tasks
 424        WHERE assigned_to = 'developer' AND parent_task_id = ?
 425      `
 426        )
 427        .all(taskId);
 428  
 429      assert.strictEqual(devTasks.length, 1);
 430      assert.strictEqual(devTasks[0].task_type, 'fix_bug');
 431  
 432      const devContext = JSON.parse(devTasks[0].context_json);
 433      assert.strictEqual(devContext.error_type, 'null_pointer');
 434      assert.strictEqual(devContext.severity, 'medium');
 435  
 436      // Verify original task completed
 437      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 438      assert.strictEqual(completedTask.status, 'completed');
 439    });
 440  
 441    test('logs error classification details', async () => {
 442      const taskId = db
 443        .prepare(
 444          `
 445        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 446        VALUES ('classify_error', 'triage', 'pending', ?)
 447      `
 448        )
 449        .run(
 450          JSON.stringify({
 451            error_message: 'ENOTFOUND api.openrouter.ai',
 452            stage: 'serps',
 453            frequency: 1,
 454          })
 455        ).lastInsertRowid;
 456  
 457      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 458      task.context_json = JSON.parse(task.context_json);
 459  
 460      await agent.classifyErrorTask(task);
 461  
 462      // Check logs
 463      const logs = db
 464        .prepare(
 465          `
 466        SELECT * FROM agent_logs
 467        WHERE agent_name = 'triage' AND task_id = ?
 468      `
 469        )
 470        .all(taskId);
 471  
 472      assert.ok(logs.length > 0);
 473      const classifiedLog = logs.find(log => log.message.includes('classified'));
 474      assert.ok(classifiedLog);
 475    });
 476  });
 477  
 478  describe('TriageAgent - Priority Scoring', () => {
 479    test('assigns highest priority (1) to critical high-frequency errors', () => {
 480      const task = {
 481        id: 1,
 482        created_at: new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(), // 48 hours old
 483      };
 484  
 485      const context = {
 486        frequency: 150, // Very frequent
 487        severity: 'critical',
 488        stage: 'scoring',
 489        error_type: 'database_connection',
 490      };
 491  
 492      const priority = agent.calculatePriority(task, context);
 493      assert.strictEqual(priority, 1); // Max reduction: 5 - 3 (freq) - 2 (severity) - 1 (age) - 2 (stage) - 1 (type) = -4 -> clamped to 1
 494    });
 495  
 496    test('assigns medium priority (5) to recent low-frequency errors', () => {
 497      const task = {
 498        id: 1,
 499        created_at: new Date().toISOString(), // Just created
 500      };
 501  
 502      const context = {
 503        frequency: 1,
 504        severity: 'low',
 505        stage: 'outreach',
 506        error_type: 'formatting',
 507      };
 508  
 509      const priority = agent.calculatePriority(task, context);
 510      assert.strictEqual(priority, 5); // No reductions
 511    });
 512  
 513    test('assigns higher priority to pipeline-critical stages', () => {
 514      const task = {
 515        id: 1,
 516        created_at: new Date().toISOString(),
 517      };
 518  
 519      const scoringContext = {
 520        frequency: 5,
 521        severity: 'medium',
 522        stage: 'scoring', // Critical stage
 523      };
 524  
 525      const outreachContext = {
 526        frequency: 5,
 527        severity: 'medium',
 528        stage: 'outreach', // Non-critical stage
 529      };
 530  
 531      const scoringPriority = agent.calculatePriority(task, scoringContext);
 532      const outreachPriority = agent.calculatePriority(task, outreachContext);
 533  
 534      assert.ok(scoringPriority < outreachPriority); // Lower number = higher priority
 535    });
 536  
 537    test('increases priority for older errors', () => {
 538      const oldTask = {
 539        id: 1,
 540        created_at: new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(), // 48 hours old
 541      };
 542  
 543      const newTask = {
 544        id: 2,
 545        created_at: new Date().toISOString(), // Just created
 546      };
 547  
 548      // Use context that won't hit the priority floor (1) so age difference is visible
 549      // outreach is non-critical (no -2 stage reduction), low frequency (no freq reduction)
 550      const context = {
 551        frequency: 1, // No frequency reduction (> 1 threshold not met)
 552        severity: 'medium',
 553        stage: 'outreach', // Non-critical stage = no reduction
 554      };
 555  
 556      const oldPriority = agent.calculatePriority(oldTask, context);
 557      const newPriority = agent.calculatePriority(newTask, context);
 558  
 559      // Old task gets age reduction (-1), new task doesn't
 560      // So oldPriority (score=3) < newPriority (score=4)
 561      assert.ok(
 562        oldPriority < newPriority,
 563        `Expected older task priority (${oldPriority}) < newer task priority (${newPriority})`
 564      );
 565    });
 566  
 567    test('clamps priority to valid range (1-10)', () => {
 568      const task = {
 569        id: 1,
 570        created_at: new Date().toISOString(),
 571      };
 572  
 573      // Try to go below 1
 574      const maxContext = {
 575        frequency: 200,
 576        severity: 'critical',
 577        stage: 'scoring',
 578        error_type: 'security_breach',
 579      };
 580  
 581      // Try to go above 10 (minimal context)
 582      const minContext = {};
 583  
 584      const maxPriority = agent.calculatePriority(task, maxContext);
 585      const minPriority = agent.calculatePriority(task, minContext);
 586  
 587      assert.ok(maxPriority >= 1 && maxPriority <= 10);
 588      assert.ok(minPriority >= 1 && minPriority <= 10);
 589    });
 590  });
 591  
 592  describe('TriageAgent - Error Similarity', () => {
 593    test('detects identical errors (100% similarity)', () => {
 594      const error1 = 'TypeError: Cannot read property "length" of undefined';
 595      const error2 = 'TypeError: Cannot read property "length" of undefined';
 596  
 597      const similarity = agent.calculateSimilarity(error1, error2);
 598      assert.strictEqual(similarity, 1.0);
 599    });
 600  
 601    test('detects similar errors with different specifics (high similarity)', () => {
 602      const error1 = agent.normalizeErrorMessage(
 603        'Site 12345 failed: Database error at line 45',
 604        'at score.js:45:10'
 605      );
 606      const error2 = agent.normalizeErrorMessage(
 607        'Site 67890 failed: Database error at line 78',
 608        'at score.js:78:15'
 609      );
 610  
 611      const similarity = agent.calculateSimilarity(error1, error2);
 612      assert.ok(similarity > 0.7); // Should be similar after normalization
 613    });
 614  
 615    test('detects different error types (low similarity)', () => {
 616      const error1 = 'TypeError: Cannot read property "length" of undefined';
 617      const error2 = 'ReferenceError: fetch is not defined';
 618  
 619      const similarity = agent.calculateSimilarity(error1, error2);
 620      assert.ok(similarity < 0.5); // Different error types
 621    });
 622  
 623    test('handles empty strings gracefully', () => {
 624      const similarity1 = agent.calculateSimilarity('', 'some error');
 625      const similarity2 = agent.calculateSimilarity('some error', '');
 626      const similarity3 = agent.calculateSimilarity('', '');
 627  
 628      assert.strictEqual(similarity1, 0);
 629      assert.strictEqual(similarity2, 0);
 630      assert.strictEqual(similarity3, 1); // Both empty = identical
 631    });
 632  
 633    test('uses hybrid Jaccard + Levenshtein approach', () => {
 634      // Jaccard catches word-level similarity
 635      const jaccard1 = 'database connection timeout';
 636      const jaccard2 = 'database connection failed';
 637  
 638      // Levenshtein catches character-level similarity
 639      const levenshtein1 = 'Cannot read property length';
 640      const levenshtein2 = 'Cannot read property width';
 641  
 642      const jaccardSim = agent.calculateSimilarity(jaccard1, jaccard2);
 643      const levenshteinSim = agent.calculateSimilarity(levenshtein1, levenshtein2);
 644  
 645      // Both should show high similarity (>0.6) due to hybrid approach
 646      assert.ok(jaccardSim > 0.6);
 647      assert.ok(levenshteinSim > 0.6);
 648    });
 649  
 650    test('normalizes errors before comparison', () => {
 651      // These should be very similar after normalization
 652      const error1 = 'Site 12345 at /path/to/file.js:179:45 failed';
 653      const error2 = 'Site 67890 at /different/path/file.js:250:30 failed';
 654  
 655      const norm1 = agent.normalizeErrorMessage(error1, '');
 656      const norm2 = agent.normalizeErrorMessage(error2, '');
 657  
 658      const similarity = agent.calculateSimilarity(norm1, norm2);
 659      assert.ok(similarity > 0.8); // Should be very similar after normalization
 660    });
 661  });
 662  
 663  // ============================================================
 664  // EXTENDED COVERAGE TESTS (added to boost coverage to 85%+)
 665  // ============================================================
 666  
 667  describe('TriageAgent - classifyError (additional types)', () => {
 668    test('classifies agent system errors as agent_system_error', () => {
 669      const result = agent.classifyError('Unknown task type: bootstrap_monitor');
 670      assert.strictEqual(result.type, 'agent_system_error');
 671      assert.strictEqual(result.severity, 'low');
 672      assert.strictEqual(result.assignee, 'architect');
 673      assert.strictEqual(result.is_agent_error, true);
 674    });
 675  
 676    test('classifies performance/timeout errors correctly', () => {
 677      const result = agent.classifyError('Operation timeout: slow query exceeded 30s');
 678      assert.strictEqual(result.type, 'performance');
 679      assert.strictEqual(result.assignee, 'architect');
 680    });
 681  
 682    test('classifies integration errors for Resend', () => {
 683      const result = agent.classifyError('Resend API returned 500 internal error');
 684      assert.strictEqual(result.type, 'integration');
 685      assert.strictEqual(result.assignee, 'developer');
 686    });
 687  
 688    test('classifies integration errors for Twilio', () => {
 689      const result = agent.classifyError('Twilio error: invalid phone number');
 690      assert.strictEqual(result.type, 'integration');
 691    });
 692  
 693    test('classifies circuit breaker errors correctly', () => {
 694      const result = agent.classifyError('breaker open: too many failures');
 695      assert.strictEqual(result.type, 'circuit_breaker');
 696      assert.strictEqual(result.assignee, 'architect');
 697      assert.strictEqual(result.severity, 'high');
 698    });
 699  
 700    test('classifies validation errors as low severity', () => {
 701      const result = agent.classifyError('invalid input: schema mismatch detected');
 702      assert.strictEqual(result.type, 'validation');
 703      assert.strictEqual(result.severity, 'low');
 704      assert.strictEqual(result.assignee, 'developer');
 705    });
 706  
 707    test('classifies memory leak as performance issue', () => {
 708      const result = agent.classifyError('heap out of memory - memory leak detected');
 709      assert.strictEqual(result.type, 'performance');
 710    });
 711  
 712    test('classifies ZenRows API errors as integration', () => {
 713      const result = agent.classifyError('zenrows connection refused');
 714      assert.strictEqual(result.type, 'integration');
 715    });
 716  });
 717  
 718  describe('TriageAgent - routeTask', () => {
 719    test('routes to qa when task description mentions test', async () => {
 720      const taskId = db
 721        .prepare(
 722          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 723         VALUES ('route_task', 'triage', 'pending', ?)`
 724        )
 725        .run(
 726          JSON.stringify({
 727            task_description: 'Write tests for the new login module',
 728            task_type: 'write_tests',
 729            context: { files: ['src/login.js'] },
 730          })
 731        ).lastInsertRowid;
 732  
 733      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 734      task.context_json = JSON.parse(task.context_json);
 735  
 736      await agent.routeTask(task);
 737  
 738      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 739      assert.strictEqual(completedTask.status, 'completed');
 740      const result = JSON.parse(completedTask.result_json);
 741      assert.strictEqual(result.routed_to, 'qa');
 742    });
 743  
 744    test('routes to security when task description mentions audit', async () => {
 745      const taskId = db
 746        .prepare(
 747          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 748         VALUES ('route_task', 'triage', 'pending', ?)`
 749        )
 750        .run(
 751          JSON.stringify({
 752            task_description: 'Security audit for authentication module',
 753            task_type: 'security_audit',
 754          })
 755        ).lastInsertRowid;
 756  
 757      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 758      task.context_json = JSON.parse(task.context_json);
 759  
 760      await agent.routeTask(task);
 761  
 762      const result = JSON.parse(
 763        db.prepare('SELECT result_json FROM agent_tasks WHERE id = ?').get(taskId).result_json
 764      );
 765      assert.strictEqual(result.routed_to, 'security');
 766    });
 767  
 768    test('routes to architect when task description mentions architecture', async () => {
 769      const taskId = db
 770        .prepare(
 771          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 772         VALUES ('route_task', 'triage', 'pending', ?)`
 773        )
 774        .run(
 775          JSON.stringify({
 776            task_description: 'Design the new microservices architecture',
 777            task_type: 'design',
 778          })
 779        ).lastInsertRowid;
 780  
 781      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 782      task.context_json = JSON.parse(task.context_json);
 783  
 784      await agent.routeTask(task);
 785  
 786      const result = JSON.parse(
 787        db.prepare('SELECT result_json FROM agent_tasks WHERE id = ?').get(taskId).result_json
 788      );
 789      assert.strictEqual(result.routed_to, 'architect');
 790    });
 791  
 792    test('routes to developer by default for general tasks', async () => {
 793      const taskId = db
 794        .prepare(
 795          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 796         VALUES ('route_task', 'triage', 'pending', ?)`
 797        )
 798        .run(
 799          JSON.stringify({
 800            task_description: 'Implement the new pricing feature',
 801            task_type: 'implement_feature',
 802          })
 803        ).lastInsertRowid;
 804  
 805      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 806      task.context_json = JSON.parse(task.context_json);
 807  
 808      await agent.routeTask(task);
 809  
 810      const result = JSON.parse(
 811        db.prepare('SELECT result_json FROM agent_tasks WHERE id = ?').get(taskId).result_json
 812      );
 813      assert.strictEqual(result.routed_to, 'developer');
 814    });
 815  
 816    test('routes to developer when context is empty', async () => {
 817      const taskId = db
 818        .prepare(
 819          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 820         VALUES ('route_task', 'triage', 'pending', ?)`
 821        )
 822        .run(JSON.stringify({})).lastInsertRowid;
 823  
 824      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 825      task.context_json = JSON.parse(task.context_json);
 826  
 827      await agent.routeTask(task);
 828  
 829      const result = JSON.parse(
 830        db.prepare('SELECT result_json FROM agent_tasks WHERE id = ?').get(taskId).result_json
 831      );
 832      assert.strictEqual(result.routed_to, 'developer');
 833    });
 834  });
 835  
 836  describe('TriageAgent - prioritizeTasks', () => {
 837    test('prioritizes tasks with null priority and updates DB', async () => {
 838      // Insert pending tasks with null priority
 839      db.prepare(
 840        `INSERT INTO agent_tasks (task_type, assigned_to, status, priority, context_json)
 841         VALUES ('fix_bug', 'developer', 'pending', NULL, ?)`
 842      ).run(
 843        JSON.stringify({
 844          error_message: 'TypeError',
 845          frequency: 5,
 846          severity: 'high',
 847          stage: 'scoring',
 848        })
 849      );
 850  
 851      db.prepare(
 852        `INSERT INTO agent_tasks (task_type, assigned_to, status, priority, context_json)
 853         VALUES ('fix_bug', 'developer', 'pending', NULL, ?)`
 854      ).run(
 855        JSON.stringify({
 856          error_message: 'Network timeout',
 857          frequency: 1,
 858          severity: 'low',
 859          stage: 'outreach',
 860        })
 861      );
 862  
 863      // Create the prioritize_tasks task
 864      const taskId = db
 865        .prepare(
 866          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 867         VALUES ('prioritize_tasks', 'triage', 'pending', ?)`
 868        )
 869        .run(JSON.stringify({})).lastInsertRowid;
 870  
 871      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 872      task.context_json = JSON.parse(task.context_json);
 873  
 874      await agent.prioritizeTasks(task);
 875  
 876      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 877      assert.strictEqual(completedTask.status, 'completed');
 878      const result = JSON.parse(completedTask.result_json);
 879      assert.ok(result.tasks_prioritized > 0, 'Should have prioritized some tasks');
 880    });
 881  
 882    test('handles empty pending task queue gracefully', async () => {
 883      // No pending tasks with null priority
 884      const taskId = db
 885        .prepare(
 886          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 887         VALUES ('prioritize_tasks', 'triage', 'pending', ?)`
 888        )
 889        .run(JSON.stringify({})).lastInsertRowid;
 890  
 891      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 892      task.context_json = JSON.parse(task.context_json);
 893  
 894      await agent.prioritizeTasks(task);
 895  
 896      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 897      assert.strictEqual(completedTask.status, 'completed');
 898      const result = JSON.parse(completedTask.result_json);
 899      assert.strictEqual(result.tasks_prioritized, 0);
 900      assert.match(result.message, /No pending tasks/);
 901    });
 902  
 903    test('handles tasks with malformed context_json gracefully', async () => {
 904      db.prepare(
 905        `INSERT INTO agent_tasks (task_type, assigned_to, status, priority, context_json)
 906         VALUES ('fix_bug', 'developer', 'pending', NULL, ?)`
 907      ).run('{invalid json}');
 908  
 909      const taskId = db
 910        .prepare(
 911          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 912         VALUES ('prioritize_tasks', 'triage', 'pending', ?)`
 913        )
 914        .run(JSON.stringify({})).lastInsertRowid;
 915  
 916      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 917      task.context_json = JSON.parse(task.context_json);
 918  
 919      // Should not throw even with malformed context
 920      await assert.doesNotReject(() => agent.prioritizeTasks(task));
 921    });
 922  });
 923  
 924  describe('TriageAgent - processTask routing (all task types)', () => {
 925    test('processes route_task type', async () => {
 926      const taskId = db
 927        .prepare(
 928          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 929         VALUES ('route_task', 'triage', 'pending', ?)`
 930        )
 931        .run(
 932          JSON.stringify({
 933            task_description: 'Implement new feature',
 934            task_type: 'implement_feature',
 935          })
 936        ).lastInsertRowid;
 937  
 938      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 939      task.context_json = JSON.parse(task.context_json);
 940  
 941      await agent.processTask(task);
 942  
 943      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 944      assert.strictEqual(completedTask.status, 'completed');
 945    });
 946  
 947    test('processes prioritize_tasks type', async () => {
 948      const taskId = db
 949        .prepare(
 950          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 951         VALUES ('prioritize_tasks', 'triage', 'pending', ?)`
 952        )
 953        .run(JSON.stringify({})).lastInsertRowid;
 954  
 955      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 956      task.context_json = JSON.parse(task.context_json);
 957  
 958      await agent.processTask(task);
 959  
 960      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 961      assert.strictEqual(completedTask.status, 'completed');
 962    });
 963  
 964    test('delegates implement_feature to correct agent', async () => {
 965      const taskId = db
 966        .prepare(
 967          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 968         VALUES ('implement_feature', 'triage', 'pending', ?)`
 969        )
 970        .run(JSON.stringify({ description: 'New feature' })).lastInsertRowid;
 971  
 972      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 973      task.context_json = JSON.parse(task.context_json);
 974  
 975      await agent.processTask(task);
 976  
 977      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 978      assert.strictEqual(completedTask.status, 'completed');
 979      const result = JSON.parse(completedTask.result_json || '{}');
 980      assert.strictEqual(result.delegated, true, 'implement_feature should be delegated');
 981    });
 982  
 983    test('delegates fix_bug to correct agent', async () => {
 984      const taskId = db
 985        .prepare(
 986          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
 987         VALUES ('fix_bug', 'triage', 'pending', ?)`
 988        )
 989        .run(JSON.stringify({ error_message: 'Some bug to fix' })).lastInsertRowid;
 990  
 991      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 992      task.context_json = JSON.parse(task.context_json);
 993  
 994      await agent.processTask(task);
 995  
 996      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
 997      assert.strictEqual(completedTask.status, 'completed');
 998    });
 999  
1000    test('delegates unknown task type', async () => {
1001      const taskId = db
1002        .prepare(
1003          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
1004         VALUES ('some_unknown_type', 'triage', 'pending', ?)`
1005        )
1006        .run(JSON.stringify({})).lastInsertRowid;
1007  
1008      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1009      task.context_json = JSON.parse(task.context_json);
1010  
1011      await agent.processTask(task);
1012  
1013      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1014      assert.strictEqual(completedTask.status, 'completed');
1015    });
1016  
1017    test('parses string context_json before processing', async () => {
1018      const taskId = db
1019        .prepare(
1020          `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
1021         VALUES ('prioritize_tasks', 'triage', 'pending', ?)`
1022        )
1023        .run('{"fromString": true}').lastInsertRowid;
1024  
1025      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1026      // context_json is still a string here - processTask should parse it
1027  
1028      await agent.processTask(task);
1029  
1030      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
1031      assert.strictEqual(completedTask.status, 'completed');
1032    });
1033  });
1034  
1035  describe('TriageAgent - suggestFix (additional types)', () => {
1036    test('suggests timeout fix for performance timeout errors', () => {
1037      const suggestion = agent.suggestFix('performance', 'timeout exceeded');
1038      assert.match(suggestion, /timeout/i);
1039    });
1040  
1041    test('suggests circuit breaker fix for circuit_breaker errors', () => {
1042      const suggestion = agent.suggestFix('circuit_breaker', 'breaker open');
1043      assert.match(suggestion, /circuit.breaker/i);
1044    });
1045  
1046    test('suggests security review for security errors', () => {
1047      const suggestion = agent.suggestFix('security', 'unauthorized access');
1048      assert.match(suggestion, /security|review/i);
1049    });
1050  
1051    test('returns generic suggestion for unknown error types', () => {
1052      const suggestion = agent.suggestFix('unknown_type', 'some weird error');
1053      assert.ok(suggestion.length > 0);
1054    });
1055  
1056    test('suggests API key check for 401 api errors', () => {
1057      const suggestion = agent.suggestFix('api_error', 'status 401 unauthorized');
1058      assert.match(suggestion, /API key/i);
1059    });
1060  });
1061  
1062  describe('TriageAgent - routeToAgent (additional cases)', () => {
1063    test('routes agent_system_error to developer (default - routeToAgent has no special case)', () => {
1064      // agent_system_error falls through to developer in routeToAgent
1065      // (the architect assignee comes from classifyError's own assignee field, not routeToAgent)
1066      const assignee = agent.routeToAgent('agent_system_error', 'low', {});
1067      assert.strictEqual(assignee, 'developer');
1068    });
1069  
1070    test('routes performance errors to architect', () => {
1071      const assignee = agent.routeToAgent('performance', 'medium', {});
1072      assert.strictEqual(assignee, 'architect');
1073    });
1074  
1075    test('routes circuit_breaker errors to architect', () => {
1076      const assignee = agent.routeToAgent('circuit_breaker', 'high', {});
1077      assert.strictEqual(assignee, 'architect');
1078    });
1079  
1080    test('routes integration errors to developer', () => {
1081      const assignee = agent.routeToAgent('integration', 'medium', {});
1082      assert.strictEqual(assignee, 'developer');
1083    });
1084  
1085    test('routes api_error to developer', () => {
1086      const assignee = agent.routeToAgent('api_error', 'medium', {});
1087      assert.strictEqual(assignee, 'developer');
1088    });
1089  });