/ tests / cron / classify-unknown-errors.test.js
classify-unknown-errors.test.js
  1  import { test, describe } from 'node:test';
  2  import assert from 'node:assert/strict';
  3  import Database from 'better-sqlite3';
  4  import { categorizeError } from '../../src/utils/error-categories.js';
  5  
  6  // We test the helper logic that classify-unknown-errors.js depends on,
  7  // without making actual LLM calls.
  8  
  9  function createTestDb() {
 10    const db = new Database(':memory:');
 11    db.exec(`
 12      CREATE TABLE sites (
 13        id INTEGER PRIMARY KEY,
 14        status TEXT,
 15        error_message TEXT,
 16        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 17        rescored_at DATETIME
 18      );
 19      CREATE TABLE messages (
 20        id INTEGER PRIMARY KEY,
 21        approval_status TEXT, delivery_status TEXT,
 22        error_message TEXT,
 23        read_at TEXT,
 24        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 25        message_type TEXT DEFAULT 'outreach',
 26        raw_payload TEXT
 27      );
 28      CREATE TABLE error_pattern_proposals (
 29        id INTEGER PRIMARY KEY AUTOINCREMENT,
 30        pattern TEXT NOT NULL,
 31        label TEXT NOT NULL,
 32        group_name TEXT NOT NULL CHECK(group_name IN ('terminal', 'retriable')),
 33        context TEXT NOT NULL CHECK(context IN ('site', 'outreach')),
 34        example_errors TEXT,
 35        occurrence_count INTEGER,
 36        status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'approved', 'rejected')),
 37        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 38        reviewed_at DATETIME,
 39        reviewed_by TEXT
 40      );
 41    `);
 42    return db;
 43  }
 44  
 45  describe('classify-unknown-errors helpers', () => {
 46    test('categorizeError correctly identifies unknown errors for classification', () => {
 47      // These should all fall through to 'unknown' so they get classified
 48      const unknownErrors = [
 49        'Some completely novel error message xyz',
 50        'Widget factory exception 42',
 51        'Unexpected token in JSON at position 0',
 52      ];
 53  
 54      for (const msg of unknownErrors) {
 55        const result = categorizeError(msg, 'site');
 56        assert.equal(result.group, 'unknown', `Expected unknown for: "${msg}"`);
 57      }
 58    });
 59  
 60    test('known errors are NOT returned as unknown (should not be re-classified)', () => {
 61      const knownErrors = [
 62        ['EACCES: permission denied', 'site'],
 63        ['outside business hours', 'outreach'],
 64        ['Social media platform', 'site'],
 65        ['userDataDir already in use', 'site'],
 66      ];
 67  
 68      for (const [msg, ctx] of knownErrors) {
 69        const result = categorizeError(msg, ctx);
 70        assert.notEqual(result.group, 'unknown', `Should not be unknown: "${msg}"`);
 71      }
 72    });
 73  
 74    test('error_pattern_proposals table accepts valid proposals', () => {
 75      const db = createTestDb();
 76  
 77      const insert = db.prepare(`
 78        INSERT INTO error_pattern_proposals (pattern, label, group_name, context, occurrence_count)
 79        VALUES (?, ?, ?, ?, ?)
 80      `);
 81  
 82      insert.run('ERR_SOCKET_TIMEOUT', 'Socket timeout', 'retriable', 'site', 42);
 83      insert.run('permanent ban', 'Permanently banned', 'terminal', 'outreach', 5);
 84  
 85      const rows = db.prepare('SELECT * FROM error_pattern_proposals').all();
 86      assert.equal(rows.length, 2);
 87      assert.equal(rows[0].status, 'pending');
 88      assert.equal(rows[0].group_name, 'retriable');
 89      assert.equal(rows[1].group_name, 'terminal');
 90  
 91      db.close();
 92    });
 93  
 94    test('error_pattern_proposals rejects invalid group_name', () => {
 95      const db = createTestDb();
 96  
 97      assert.throws(() => {
 98        db.prepare(
 99          `
100          INSERT INTO error_pattern_proposals (pattern, label, group_name, context)
101          VALUES ('x', 'label', 'invalid_group', 'site')
102        `
103        ).run();
104      });
105  
106      db.close();
107    });
108  
109    test('proposals can be approved and rejected', () => {
110      const db = createTestDb();
111      db.prepare(
112        `
113        INSERT INTO error_pattern_proposals (pattern, label, group_name, context)
114        VALUES ('test_pattern', 'Test', 'retriable', 'site')
115      `
116      ).run();
117  
118      const { id } = db.prepare('SELECT id FROM error_pattern_proposals').get();
119  
120      // Approve it
121      db.prepare(
122        `
123        UPDATE error_pattern_proposals
124        SET status='approved', reviewed_at=CURRENT_TIMESTAMP, reviewed_by='test'
125        WHERE id=?
126      `
127      ).run(id);
128  
129      const row = db.prepare('SELECT * FROM error_pattern_proposals WHERE id=?').get(id);
130      assert.equal(row.status, 'approved');
131      assert.ok(row.reviewed_by === 'test');
132  
133      db.close();
134    });
135  });