structured-logger.test.js
1 /** 2 * Tests for src/agents/utils/structured-logger.js 3 * 4 * Covers: 5 * - Constructor validation (agentName required) 6 * - log() with all valid levels 7 * - log() with invalid level throws 8 * - writeToDatabase catch block (DB error gracefully handled) 9 * - writeToConsole: error/warn/log console method selection 10 * - Convenience methods: debug, info, warn, error 11 * - setTaskId 12 * - resetDb (close + null reset) 13 */ 14 15 import { test, describe, before, after } from 'node:test'; 16 import assert from 'node:assert/strict'; 17 import Database from 'better-sqlite3'; 18 import { tmpdir } from 'os'; 19 import { join } from 'path'; 20 import { existsSync, unlinkSync } from 'fs'; 21 22 const TEST_DB = join(tmpdir(), `structured-logger-${Date.now()}.db`); 23 24 let db; 25 26 before(() => { 27 process.env.DATABASE_PATH = TEST_DB; 28 db = new Database(TEST_DB); 29 db.exec(` 30 CREATE TABLE IF NOT EXISTS agent_logs ( 31 id INTEGER PRIMARY KEY AUTOINCREMENT, 32 task_id INTEGER, 33 agent_name TEXT NOT NULL, 34 log_level TEXT, 35 message TEXT, 36 data_json TEXT, 37 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 38 ); 39 `); 40 }); 41 42 after(() => { 43 try { 44 db.close(); 45 } catch { 46 /* ignore */ 47 } 48 if (existsSync(TEST_DB)) { 49 try { 50 unlinkSync(TEST_DB); 51 } catch { 52 /* ignore */ 53 } 54 } 55 delete process.env.DATABASE_PATH; 56 }); 57 58 const { StructuredLogger, resetDb } = await import('../../src/agents/utils/structured-logger.js'); 59 60 describe('StructuredLogger', () => { 61 describe('constructor', () => { 62 test('creates logger with agentName', () => { 63 const logger = new StructuredLogger('developer'); 64 assert.equal(logger.agentName, 'developer'); 65 assert.equal(logger.taskId, null); 66 }); 67 68 test('creates logger with agentName + taskId', () => { 69 const logger = new StructuredLogger('qa', 42); 70 assert.equal(logger.agentName, 'qa'); 71 assert.equal(logger.taskId, 42); 72 }); 73 74 test('throws if agentName is missing', () => { 75 assert.throws(() => new StructuredLogger(), /agentName is required/); 76 }); 77 78 test('throws if agentName is empty string', () => { 79 assert.throws(() => new StructuredLogger(''), /agentName is required/); 80 }); 81 }); 82 83 describe('log() — valid levels', () => { 84 test('logs at debug level', () => { 85 const logger = new StructuredLogger('test-agent'); 86 assert.doesNotThrow(() => logger.log('debug', 'debug message', { key: 'val' })); 87 }); 88 89 test('logs at info level', () => { 90 const logger = new StructuredLogger('test-agent'); 91 assert.doesNotThrow(() => logger.log('info', 'info message')); 92 }); 93 94 test('logs at warn level', () => { 95 const logger = new StructuredLogger('test-agent'); 96 assert.doesNotThrow(() => logger.log('warn', 'warn message')); 97 }); 98 99 test('logs at error level', () => { 100 const logger = new StructuredLogger('test-agent'); 101 assert.doesNotThrow(() => logger.log('error', 'error message')); 102 }); 103 }); 104 105 describe('log() — invalid level', () => { 106 test('throws for invalid log level', () => { 107 const logger = new StructuredLogger('test-agent'); 108 assert.throws(() => logger.log('critical', 'msg'), /Invalid log level/); 109 }); 110 111 test('throws for uppercase level', () => { 112 const logger = new StructuredLogger('test-agent'); 113 assert.throws(() => logger.log('INFO', 'msg'), /Invalid log level/); 114 }); 115 }); 116 117 describe('database writes', () => { 118 test('writes log entry to agent_logs table', () => { 119 resetDb(); // force fresh connection with test DB 120 const logger = new StructuredLogger('db-test', 99); 121 logger.info('Testing DB write', { extra: 'data' }); 122 123 const row = db 124 .prepare( 125 `SELECT * FROM agent_logs WHERE agent_name = 'db-test' AND message = 'Testing DB write'` 126 ) 127 .get(); 128 assert.ok(row, 'should find log row in DB'); 129 assert.equal(row.log_level, 'info'); 130 assert.equal(row.task_id, 99); 131 assert.ok(row.data_json, 'should have data_json'); 132 const data = JSON.parse(row.data_json); 133 assert.equal(data.extra, 'data'); 134 }); 135 136 test('handles null task_id correctly', () => { 137 const logger = new StructuredLogger('null-task-agent'); 138 logger.debug('no task'); 139 const row = db.prepare(`SELECT * FROM agent_logs WHERE agent_name = 'null-task-agent'`).get(); 140 assert.ok(row); 141 assert.equal(row.task_id, null); 142 }); 143 144 test('logs without context stores only task_id in data_json', () => { 145 const logger = new StructuredLogger('no-context-agent'); 146 logger.warn('plain message'); 147 const row = db 148 .prepare(`SELECT * FROM agent_logs WHERE agent_name = 'no-context-agent'`) 149 .get(); 150 assert.ok(row); 151 // fullContext always includes task_id, so data_json is always non-null 152 // but task_id=null is serialised as {"task_id":null} 153 const parsed = JSON.parse(row.data_json); 154 assert.equal(parsed.task_id, null); 155 }); 156 }); 157 158 describe('convenience methods', () => { 159 test('debug() calls log("debug", ...)', () => { 160 const logger = new StructuredLogger('conv-test'); 161 assert.doesNotThrow(() => logger.debug('debug msg')); 162 }); 163 164 test('info() calls log("info", ...)', () => { 165 const logger = new StructuredLogger('conv-test'); 166 assert.doesNotThrow(() => logger.info('info msg')); 167 }); 168 169 test('warn() calls log("warn", ...)', () => { 170 const logger = new StructuredLogger('conv-test'); 171 assert.doesNotThrow(() => logger.warn('warn msg')); 172 }); 173 174 test('error() calls log("error", ...)', () => { 175 const logger = new StructuredLogger('conv-test'); 176 assert.doesNotThrow(() => logger.error('error msg')); 177 }); 178 }); 179 180 describe('setTaskId', () => { 181 test('updates taskId after construction', () => { 182 const logger = new StructuredLogger('task-setter'); 183 assert.equal(logger.taskId, null); 184 logger.setTaskId(123); 185 assert.equal(logger.taskId, 123); 186 }); 187 }); 188 189 describe('resetDb', () => { 190 test('closes and resets the db connection', () => { 191 resetDb(); 192 // After reset, calling resetDb again should not throw 193 assert.doesNotThrow(() => resetDb()); 194 }); 195 196 test('reconnects after reset when a new log is written', () => { 197 resetDb(); 198 const logger = new StructuredLogger('post-reset'); 199 assert.doesNotThrow(() => logger.info('after reset')); 200 }); 201 }); 202 });