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