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 });