/ __quarantined_tests__ / agents / triage-known-errors.test.js
triage-known-errors.test.js
  1  /**
  2   * Triage Agent Known Errors Unit Tests
  3   *
  4   * Tests for the known error database functionality:
  5   * - checkKnownErrorDatabase()
  6   * - normalizeErrorMessage()
  7   * - calculateSimilarity()
  8   * - Integration with classifyErrorTask()
  9   */
 10  
 11  import { test, describe, beforeEach, afterEach } from 'node:test';
 12  import assert from 'node:assert';
 13  import Database from 'better-sqlite3';
 14  import { TriageAgent } from '../../src/agents/triage.js';
 15  import { resetDb as resetBaseDb } from '../../src/agents/base-agent.js';
 16  import { resetDb as resetTaskDb } from '../../src/agents/utils/task-manager.js';
 17  import { resetDb as resetMessageDb } from '../../src/agents/utils/message-manager.js';
 18  import fs from 'fs/promises';
 19  
 20  // Use temporary file database for tests
 21  let db;
 22  let agent;
 23  const TEST_DB_PATH = './tests/agents/test-triage-known-errors.db';
 24  
 25  beforeEach(async () => {
 26    // Close any existing database connection
 27    if (db) {
 28      try {
 29        db.close();
 30      } catch (e) {
 31        // Ignore
 32      }
 33    }
 34  
 35    // Remove existing test database if it exists
 36    try {
 37      await fs.unlink(TEST_DB_PATH);
 38    } catch (e) {
 39      // Ignore if file doesn't exist
 40    }
 41  
 42    // Wait a bit for file system
 43    await new Promise(resolve => setTimeout(resolve, 50));
 44  
 45    // Create temporary test database
 46    db = new Database(TEST_DB_PATH);
 47    process.env.DATABASE_PATH = TEST_DB_PATH;
 48    // Point TEL_DB_PATH at the test DB so tel.agent_tasks queries work via self-attach.
 49    process.env.TEL_DB_PATH = TEST_DB_PATH;
 50  
 51    // Create tables
 52    db.exec(`
 53      CREATE TABLE agent_tasks (
 54        id INTEGER PRIMARY KEY AUTOINCREMENT,
 55        task_type TEXT NOT NULL,
 56        assigned_to TEXT NOT NULL,
 57        created_by TEXT,
 58        status TEXT DEFAULT 'pending',
 59        priority INTEGER DEFAULT 5,
 60        context_json TEXT,
 61        result_json TEXT,
 62        parent_task_id INTEGER,
 63        error_message TEXT,
 64        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 65        started_at DATETIME,
 66        completed_at DATETIME,
 67        retry_count INTEGER DEFAULT 0
 68      );
 69  
 70      CREATE TABLE agent_messages (
 71        id INTEGER PRIMARY KEY AUTOINCREMENT,
 72        task_id INTEGER,
 73        from_agent TEXT NOT NULL,
 74        to_agent TEXT NOT NULL,
 75        message_type TEXT,
 76        content TEXT NOT NULL,
 77        metadata_json TEXT,
 78        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 79        read_at DATETIME
 80      );
 81  
 82      CREATE TABLE agent_logs (
 83        id INTEGER PRIMARY KEY AUTOINCREMENT,
 84        task_id INTEGER,
 85        agent_name TEXT NOT NULL,
 86        log_level TEXT,
 87        message TEXT,
 88        data_json TEXT,
 89        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 90      );
 91  
 92      CREATE TABLE agent_state (
 93        agent_name TEXT PRIMARY KEY,
 94        last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
 95        current_task_id INTEGER,
 96        status TEXT DEFAULT 'idle',
 97        metrics_json TEXT
 98      );
 99  
100      CREATE TABLE agent_outcomes (
101        id INTEGER PRIMARY KEY AUTOINCREMENT,
102        task_id INTEGER NOT NULL,
103        agent_name TEXT NOT NULL,
104        task_type TEXT NOT NULL,
105        outcome TEXT NOT NULL,
106        context_json TEXT,
107        result_json TEXT,
108        duration_ms INTEGER,
109        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
110      );
111  
112      CREATE TABLE IF NOT EXISTS cron_locks (
113        lock_key TEXT PRIMARY KEY,
114        description TEXT,
115        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
116      );
117    `);
118  
119    // Initialize agent
120    agent = new TriageAgent();
121    await agent.initialize();
122  });
123  
124  afterEach(async () => {
125    // Restore env vars
126    delete process.env.TEL_DB_PATH;
127  
128    // Reset all database connections first
129    resetBaseDb();
130    resetTaskDb();
131    resetMessageDb();
132  
133    if (db) {
134      db.close();
135    }
136    // Clean up test database
137    try {
138      await fs.unlink(TEST_DB_PATH);
139    } catch (e) {
140      // Ignore if file doesn't exist
141    }
142  });
143  
144  describe('TriageAgent - normalizeErrorMessage()', () => {
145    test('removes line and column numbers', () => {
146      const input = 'TypeError at file.js:179:45';
147      const normalized = agent.normalizeErrorMessage(input);
148  
149      assert.ok(!normalized.includes(':179'));
150      assert.ok(!normalized.includes(':45'));
151      assert.ok(normalized.includes('file.js'));
152    });
153  
154    test('removes file paths but keeps filename', () => {
155      const input = 'Error in /home/user/project/src/scoring.js';
156      const normalized = agent.normalizeErrorMessage(input);
157  
158      assert.ok(!normalized.includes('/home/user/project/src/'));
159      assert.ok(normalized.includes('scoring.js'));
160    });
161  
162    test('normalizes IDs', () => {
163      const input = 'Site id=12345 failed with task_id: 67890';
164      const normalized = agent.normalizeErrorMessage(input);
165  
166      assert.ok(normalized.includes('id=id'));
167      assert.ok(normalized.includes('task_id=id'));
168      assert.ok(!normalized.includes('12345'));
169      assert.ok(!normalized.includes('67890'));
170    });
171  
172    test('removes timestamps', () => {
173      const input = 'Error at 2024-01-15T10:30:45 failed';
174      const normalized = agent.normalizeErrorMessage(input);
175  
176      assert.ok(normalized.includes('timestamp'));
177      assert.ok(!normalized.includes('2024-01-15'));
178    });
179  
180    test('normalizes URLs', () => {
181      const input = 'Failed to fetch https://api.example.com/v1/data';
182      const normalized = agent.normalizeErrorMessage(input);
183  
184      assert.ok(normalized.includes('url'));
185      assert.ok(!normalized.includes('example.com'));
186    });
187  
188    test('normalizes large numbers', () => {
189      const input = 'Memory exceeded 1234567 bytes';
190      const normalized = agent.normalizeErrorMessage(input);
191  
192      assert.ok(normalized.includes('num'));
193      assert.ok(!normalized.includes('1234567'));
194    });
195  
196    test('converts to lowercase', () => {
197      const input = 'TypeError: Cannot Read Property';
198      const normalized = agent.normalizeErrorMessage(input);
199  
200      assert.strictEqual(normalized, normalized.toLowerCase());
201    });
202  
203    test('normalizes whitespace', () => {
204      const input = 'Error    with   multiple     spaces';
205      const normalized = agent.normalizeErrorMessage(input);
206  
207      assert.ok(!normalized.includes('  '));
208      assert.strictEqual(normalized.trim(), normalized);
209    });
210  
211    test('handles stack traces', () => {
212      const message = 'TypeError: Cannot read property';
213      const stack = 'at score.js:179:45\nat processTask.js:23:10';
214      const normalized = agent.normalizeErrorMessage(message, stack);
215  
216      assert.ok(!normalized.includes(':179'));
217      assert.ok(!normalized.includes(':23'));
218      assert.ok(normalized.includes('score.js'));
219      assert.ok(normalized.includes('processtask.js'));
220    });
221  });
222  
223  describe('TriageAgent - calculateSimilarity()', () => {
224    test('returns 1.0 for identical errors', () => {
225      const error = 'typeerror cannot read property score of null';
226      const similarity = agent.calculateSimilarity(error, error);
227  
228      assert.strictEqual(similarity, 1.0);
229    });
230  
231    test('returns 0.0 for completely different errors', () => {
232      const error1 = 'network timeout error';
233      const error2 = 'database constraint violation';
234      const similarity = agent.calculateSimilarity(error1, error2);
235  
236      assert.ok(similarity < 0.3); // Should be very low
237    });
238  
239    test('returns high similarity for similar errors', () => {
240      const error1 = 'typeerror cannot read property score of null at file.js';
241      const error2 = 'typeerror cannot read property score of null at other.js';
242      const similarity = agent.calculateSimilarity(error1, error2);
243  
244      assert.ok(similarity >= 0.7); // Should be considered similar
245    });
246  
247    test('handles word order differences', () => {
248      const error1 = 'database unique constraint failed';
249      const error2 = 'unique constraint failed database';
250      const similarity = agent.calculateSimilarity(error1, error2);
251  
252      // Jaccard similarity is 1.0 but Levenshtein reduces the combined score
253      // Implementation uses 50/50 blend, so same-word different-order scores ~0.7+
254      assert.ok(similarity >= 0.7, `Similarity ${similarity} should be >= 0.7`);
255    });
256  
257    test('returns partial similarity for overlapping errors', () => {
258      const error1 = 'api error rate limit exceeded timeout';
259      const error2 = 'api error rate limit exceeded';
260      const similarity = agent.calculateSimilarity(error1, error2);
261  
262      assert.ok(similarity > 0.5 && similarity < 1.0);
263    });
264  });
265  
266  describe('TriageAgent - checkKnownErrorDatabase()', () => {
267    test('returns null when no completed fix_bug tasks exist', async () => {
268      const result = agent.checkKnownErrorDatabase('New error never seen before');
269  
270      assert.strictEqual(result, null);
271    });
272  
273    test('returns null when no similar errors found', async () => {
274      // Create a completed fix_bug task with different error
275      db.prepare(
276        `
277        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, result_json, completed_at)
278        VALUES ('fix_bug', 'developer', 'completed', ?, ?, CURRENT_TIMESTAMP)
279      `
280      ).run(
281        JSON.stringify({
282          error_message: 'Network timeout error ENOTFOUND',
283          error_type: 'network',
284        }),
285        JSON.stringify({
286          fix_description: 'Added retry logic',
287          files_changed: ['src/scrape.js'],
288        })
289      );
290  
291      // Query with completely different error
292      const result = agent.checkKnownErrorDatabase('Database UNIQUE constraint failed');
293  
294      assert.strictEqual(result, null);
295    });
296  
297    test('finds similar error with normalized matching', async () => {
298      // Create a completed fix_bug task
299      db.prepare(
300        `
301        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, result_json, completed_at)
302        VALUES ('fix_bug', 'developer', 'completed', ?, ?, CURRENT_TIMESTAMP)
303      `
304      ).run(
305        JSON.stringify({
306          error_message: 'TypeError: Cannot read property score of null at scoring.js',
307          stack_trace: 'at processScore (src/scoring.js:179)',
308          error_type: 'null_pointer',
309        }),
310        JSON.stringify({
311          fix_description: 'Added null check: score?.value || 0',
312          files_changed: ['src/scoring.js'],
313          summary: 'Fixed null pointer by adding optional chaining',
314        })
315      );
316  
317      // Query with similar error (different line number and file)
318      const result = agent.checkKnownErrorDatabase(
319        'TypeError: Cannot read property score of null at rescoring.js',
320        'at processScore (src/rescoring.js:234)'
321      );
322  
323      assert.ok(result !== null);
324      assert.ok(result.similarity >= 0.7);
325      assert.strictEqual(result.error_type, 'null_pointer');
326      assert.ok(result.fix_description.includes('null check'));
327    });
328  
329    test('returns highest similarity match when multiple exist', async () => {
330      // Create two completed tasks with different similarities
331      db.prepare(
332        `
333        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, result_json, completed_at)
334        VALUES
335          ('fix_bug', 'developer', 'completed', ?, ?, CURRENT_TIMESTAMP),
336          ('fix_bug', 'developer', 'completed', ?, ?, CURRENT_TIMESTAMP)
337      `
338      ).run(
339        // Less similar error
340        JSON.stringify({
341          error_message: 'TypeError: Cannot read property of null',
342          error_type: 'null_pointer',
343        }),
344        JSON.stringify({
345          fix_description: 'Generic null check',
346        }),
347        // More similar error
348        JSON.stringify({
349          error_message: 'TypeError: Cannot read property "score" of null in scoring',
350          error_type: 'null_pointer',
351        }),
352        JSON.stringify({
353          fix_description: 'Specific score null check',
354        })
355      );
356  
357      const result = agent.checkKnownErrorDatabase(
358        'TypeError: Cannot read property "score" of null in scoring module'
359      );
360  
361      assert.ok(result !== null);
362      assert.ok(result.fix_description.includes('Specific score'));
363    });
364  
365    test('requires 70% similarity threshold', async () => {
366      // Create a completed task
367      db.prepare(
368        `
369        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, result_json, completed_at)
370        VALUES ('fix_bug', 'developer', 'completed', ?, ?, CURRENT_TIMESTAMP)
371      `
372      ).run(
373        JSON.stringify({
374          error_message: 'Database UNIQUE constraint failed on sites.domain',
375          error_type: 'database',
376        }),
377        JSON.stringify({
378          fix_description: 'Added INSERT OR IGNORE',
379        })
380      );
381  
382      // Query with partially similar error (should be below 70%)
383      const result = agent.checkKnownErrorDatabase('Database error occurred');
384  
385      assert.strictEqual(result, null); // Should not match due to low similarity
386    });
387  
388    test('handles malformed JSON gracefully', async () => {
389      // Insert task with invalid JSON
390      db.prepare(
391        `
392        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, result_json, completed_at)
393        VALUES ('fix_bug', 'developer', 'completed', 'invalid json', 'invalid json', CURRENT_TIMESTAMP)
394      `
395      ).run();
396  
397      // Should not throw, should return null
398      const result = agent.checkKnownErrorDatabase('Any error');
399  
400      assert.strictEqual(result, null);
401    });
402  
403    test('includes all relevant fix data in result', async () => {
404      db.prepare(
405        `
406        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, result_json, completed_at)
407        VALUES ('fix_bug', 'developer', 'completed', ?, ?, CURRENT_TIMESTAMP)
408      `
409      ).run(
410        JSON.stringify({
411          error_message: 'API rate limit exceeded status 429',
412          error_type: 'api_error',
413        }),
414        JSON.stringify({
415          fix_description: 'Implemented exponential backoff',
416          files_changed: ['src/utils/api-client.js', 'src/scrape.js'],
417          summary: 'Added rate limiting',
418        })
419      );
420  
421      const result = agent.checkKnownErrorDatabase('API rate limit exceeded status 429');
422  
423      assert.ok(result !== null);
424      assert.ok(result.task_id > 0);
425      assert.strictEqual(result.error_type, 'api_error');
426      assert.ok(result.fix_description.includes('backoff'));
427      assert.ok(Array.isArray(result.files_changed));
428      assert.strictEqual(result.files_changed.length, 2);
429      assert.ok(result.completed_at);
430      assert.ok(result.similarity >= 0.7);
431    });
432  });
433  
434  describe('TriageAgent - classifyErrorTask() with known errors', () => {
435    test('routes known error with lower priority', async () => {
436      // Create a completed fix_bug task
437      db.prepare(
438        `
439        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, result_json, completed_at, created_at)
440        VALUES ('fix_bug', 'developer', 'completed', ?, ?, datetime('now','-2 hours'), datetime('now','-2 hours'))
441      `
442      ).run(
443        JSON.stringify({
444          error_message: 'TypeError: Cannot read property "score" of null',
445          error_type: 'null_pointer',
446        }),
447        JSON.stringify({
448          fix_description: 'Added null check with optional chaining',
449          files_changed: ['src/score.js'], // Must be non-empty to trigger routing (not dismiss)
450        })
451      );
452  
453      // Create a classify_error task with similar error
454      const taskId = db
455        .prepare(
456          `
457        INSERT INTO agent_tasks (task_type, assigned_to, status, priority, context_json)
458        VALUES ('classify_error', 'triage', 'pending', 5, ?)
459      `
460        )
461        .run(
462          JSON.stringify({
463            error_message: 'TypeError: Cannot read property "score" of null at line 200',
464            stage: 'scoring',
465            frequency: 1,
466          })
467        ).lastInsertRowid;
468  
469      // Get the task
470      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
471      task.context_json = JSON.parse(task.context_json);
472  
473      // Process it
474      await agent.classifyErrorTask(task);
475  
476      // Verify developer task was created with priority 5
477      const devTasks = db
478        .prepare(
479          `
480        SELECT * FROM agent_tasks
481        WHERE assigned_to = 'developer' AND parent_task_id = ?
482      `
483        )
484        .all(taskId);
485  
486      assert.strictEqual(devTasks.length, 1);
487      assert.strictEqual(devTasks[0].task_type, 'fix_bug');
488      assert.strictEqual(devTasks[0].priority, 5); // Lower priority for known errors
489  
490      const devContext = JSON.parse(devTasks[0].context_json);
491      assert.strictEqual(devContext.severity, 'low'); // Known errors are low severity
492      assert.ok(devContext.known_fix); // Should include known fix data
493      assert.ok(devContext.known_fix.fix_description);
494    });
495  
496    test('logs when known error is detected', async () => {
497      // Create a completed fix_bug task with more specific error message
498      db.prepare(
499        `
500        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, result_json, completed_at)
501        VALUES ('fix_bug', 'developer', 'completed', ?, ?, CURRENT_TIMESTAMP)
502      `
503      ).run(
504        JSON.stringify({
505          error_message: 'UNIQUE constraint failed: sites.domain when inserting site record',
506          error_type: 'database',
507          stack_trace: 'at insertSite (src/serps.js:123)',
508        }),
509        JSON.stringify({
510          fix_description: 'Use INSERT OR IGNORE',
511        })
512      );
513  
514      const taskId = db
515        .prepare(
516          `
517        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
518        VALUES ('classify_error', 'triage', 'pending', ?)
519      `
520        )
521        .run(
522          JSON.stringify({
523            error_message: 'UNIQUE constraint failed: sites.domain when inserting duplicate',
524            stack_trace: 'at insertSite (src/serps.js:456)',
525            stage: 'serps',
526          })
527        ).lastInsertRowid;
528  
529      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
530      task.context_json = JSON.parse(task.context_json);
531  
532      await agent.classifyErrorTask(task);
533  
534      // Check logs
535      const logs = db
536        .prepare(
537          `
538        SELECT * FROM agent_logs
539        WHERE agent_name = 'triage' AND task_id = ?
540      `
541        )
542        .all(taskId);
543  
544      const knownErrorLog = logs.find(log => log.message.includes('Known error'));
545      assert.ok(knownErrorLog, 'Should log known error detection');
546  
547      const logData = JSON.parse(knownErrorLog.data_json);
548      assert.ok(logData.known_fix_task_id);
549      assert.ok(logData.similarity >= 0.7);
550    });
551  
552    test('falls back to normal classification when no known error found', async () => {
553      // Create a classify_error task with new error
554      const taskId = db
555        .prepare(
556          `
557        INSERT INTO agent_tasks (task_type, assigned_to, status, priority, context_json)
558        VALUES ('classify_error', 'triage', 'pending', 5, ?)
559      `
560        )
561        .run(
562          JSON.stringify({
563            error_message: 'Network ENOTFOUND api.production-service.io',
564            stage: 'serps',
565            frequency: 1,
566          })
567        ).lastInsertRowid;
568  
569      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
570      task.context_json = JSON.parse(task.context_json);
571  
572      await agent.classifyErrorTask(task);
573  
574      // Verify normal classification occurred
575      const architectTasks = db
576        .prepare(
577          `
578        SELECT * FROM agent_tasks
579        WHERE assigned_to = 'architect' AND parent_task_id = ?
580      `
581        )
582        .all(taskId);
583  
584      assert.strictEqual(architectTasks.length, 1); // Network errors go to architect
585      // Priority calculation: basePriority 6 + high severity boost 2 + serps stage boost 2 = 10
586      assert.strictEqual(architectTasks[0].priority, 10);
587  
588      const context = JSON.parse(architectTasks[0].context_json);
589      assert.strictEqual(context.error_type, 'network');
590      assert.ok(!context.known_fix); // No known fix
591    });
592  
593    test('completes triage task with known_fix in result', async () => {
594      // Create a completed fix_bug task
595      db.prepare(
596        `
597        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, result_json, completed_at)
598        VALUES ('fix_bug', 'developer', 'completed', ?, ?, CURRENT_TIMESTAMP)
599      `
600      ).run(
601        JSON.stringify({
602          error_message: 'Configuration error: OPENROUTER_API_KEY missing',
603          error_type: 'configuration',
604        }),
605        JSON.stringify({
606          fix_description: 'Set OPENROUTER_API_KEY in .env file',
607          files_changed: ['.env'], // Non-empty so it routes (not dismisses as known_operational)
608        })
609      );
610  
611      const taskId = db
612        .prepare(
613          `
614        INSERT INTO agent_tasks (task_type, assigned_to, status, context_json)
615        VALUES ('classify_error', 'triage', 'pending', ?)
616      `
617        )
618        .run(
619          JSON.stringify({
620            error_message: 'Configuration error: OPENROUTER_API_KEY missing',
621            stage: 'scoring',
622          })
623        ).lastInsertRowid;
624  
625      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
626      task.context_json = JSON.parse(task.context_json);
627  
628      await agent.classifyErrorTask(task);
629  
630      // Verify triage task completed with known_fix
631      const completedTask = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
632      assert.strictEqual(completedTask.status, 'completed');
633  
634      const result = JSON.parse(completedTask.result_json);
635      assert.strictEqual(result.classification, 'known_error');
636      assert.strictEqual(result.severity, 'low');
637      assert.ok(result.known_fix);
638      assert.ok(result.known_fix.fix_description);
639    });
640  });