/ __quarantined_tests__ / agents / structured-logger.test.js
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  });