/ __quarantined_tests__ / agents / structured-logging.test.js
structured-logging.test.js
  1  /**
  2   * Tests for Structured Logging System
  3   *
  4   * Validates StructuredLogger, agent_outcomes tracking, and learning from past outcomes.
  5   */
  6  
  7  import { test } from 'node:test';
  8  import assert from 'node:assert';
  9  import Database from 'better-sqlite3';
 10  import { mkdtempSync, rmSync } from 'fs';
 11  import { tmpdir } from 'os';
 12  import { join } from 'path';
 13  import StructuredLogger, {
 14    resetDb as resetLoggerDb,
 15  } from '../../src/agents/utils/structured-logger.js';
 16  import { BaseAgent, resetDb as resetBaseDb } from '../../src/agents/base-agent.js';
 17  import { resetDb as resetTaskManagerDb } from '../../src/agents/utils/task-manager.js';
 18  
 19  // Test agent implementation
 20  class TestAgent extends BaseAgent {
 21    constructor() {
 22      super('developer', ['base.md', 'developer.md']);
 23    }
 24  
 25    // Override initialize to skip context loading in tests
 26    async initialize() {
 27      if (this.isInitialized) return;
 28  
 29      // Set dummy context for tests
 30      this.context = 'Test context';
 31      this.contextMetadata = { sizeKB: 0, files: [], context: 'Test context' };
 32  
 33      // Update agent_state
 34      this.updateAgentState('idle');
 35  
 36      this.isInitialized = true;
 37    }
 38  
 39    async processTask(task) {
 40      // Simple test task processing
 41      await this.log('info', 'Processing test task', { task_type: task.task_type });
 42  
 43      if (task.task_type === 'fail_task') {
 44        throw new Error('Intentional test failure');
 45      }
 46  
 47      await this.completeTask(task.id, { success: true });
 48    }
 49  }
 50  
 51  let testDir;
 52  let dbPath;
 53  let db;
 54  
 55  test('Structured Logging System', async t => {
 56    await t.beforeEach(() => {
 57      // Reset database connections first
 58      resetLoggerDb();
 59      resetBaseDb();
 60      resetTaskManagerDb();
 61  
 62      // Create temporary directory and database
 63      testDir = mkdtempSync(join(tmpdir(), 'agent-test-'));
 64      dbPath = join(testDir, 'test.db');
 65      process.env.DATABASE_PATH = dbPath;
 66  
 67      // Initialize database
 68      db = new Database(dbPath);
 69      db.pragma('foreign_keys = ON');
 70      db.pragma('journal_mode = WAL'); // Better concurrency
 71  
 72      // Create required tables
 73      db.exec(`
 74        CREATE TABLE agent_logs (
 75          id INTEGER PRIMARY KEY AUTOINCREMENT,
 76          task_id INTEGER,
 77          agent_name TEXT NOT NULL,
 78          log_level TEXT CHECK(log_level IN ('debug', 'info', 'warn', 'error')),
 79          message TEXT NOT NULL,
 80          data_json TEXT,
 81          created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 82        );
 83  
 84        CREATE TABLE agent_outcomes (
 85          id INTEGER PRIMARY KEY AUTOINCREMENT,
 86          task_id INTEGER NOT NULL,
 87          agent_name TEXT NOT NULL,
 88          task_type TEXT NOT NULL,
 89          outcome TEXT NOT NULL CHECK(outcome IN ('success', 'failure')),
 90          context_json TEXT,
 91          result_json TEXT,
 92          duration_ms INTEGER,
 93          created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 94        );
 95  
 96        CREATE TABLE agent_tasks (
 97          id INTEGER PRIMARY KEY AUTOINCREMENT,
 98          task_type TEXT NOT NULL,
 99          assigned_to TEXT NOT NULL,
100          created_by TEXT,
101          status TEXT DEFAULT 'pending',
102          priority INTEGER DEFAULT 5,
103          context_json TEXT,
104          result_json TEXT,
105          parent_task_id INTEGER,
106          error_message TEXT,
107          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
108          started_at DATETIME,
109          completed_at DATETIME,
110          retry_count INTEGER DEFAULT 0
111        );
112  
113        CREATE TABLE agent_state (
114          agent_name TEXT PRIMARY KEY,
115          last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
116          current_task_id INTEGER,
117          status TEXT DEFAULT 'idle',
118          metrics_json TEXT
119        );
120      `);
121  
122      // Initialize agent state
123      db.prepare("INSERT INTO agent_state (agent_name, status) VALUES (?, 'idle')").run('developer');
124    });
125  
126    await t.afterEach(() => {
127      // Reset database connections first to release locks
128      resetLoggerDb();
129      resetBaseDb();
130      resetTaskManagerDb();
131  
132      // Close database and clean up
133      if (db) {
134        try {
135          db.close();
136        } catch (e) {
137          // Ignore
138        }
139        db = null;
140      }
141  
142      if (testDir) {
143        try {
144          rmSync(testDir, { recursive: true, force: true });
145        } catch (e) {
146          // Ignore
147        }
148        testDir = null;
149      }
150  
151      // Clear the env var
152      delete process.env.DATABASE_PATH;
153    });
154  
155    await t.test('StructuredLogger - basic functionality', () => {
156      const logger = new StructuredLogger('developer', 123);
157  
158      // Log at different levels
159      logger.info('Test info message', { action: 'test' });
160      logger.warn('Test warning', { warning_type: 'test' });
161      logger.error('Test error', { error_type: 'test' });
162      logger.debug('Test debug', { debug_data: 'test' });
163  
164      // Verify logs written to database
165      const logs = db.prepare('SELECT * FROM agent_logs ORDER BY id ASC').all();
166  
167      assert.strictEqual(logs.length, 4);
168      assert.strictEqual(logs[0].log_level, 'info');
169      assert.strictEqual(logs[0].message, 'Test info message');
170      assert.strictEqual(logs[0].agent_name, 'developer');
171      assert.strictEqual(logs[0].task_id, 123);
172  
173      assert.strictEqual(logs[1].log_level, 'warn');
174      assert.strictEqual(logs[2].log_level, 'error');
175      assert.strictEqual(logs[3].log_level, 'debug');
176    });
177  
178    await t.test('StructuredLogger - JSON context', () => {
179      const logger = new StructuredLogger('qa', 456);
180  
181      logger.info('Task started', {
182        file: 'src/test.js',
183        action: 'verify_fix',
184        metadata: { version: '1.0' },
185      });
186  
187      const log = db.prepare('SELECT * FROM agent_logs WHERE task_id = 456').get();
188  
189      assert.ok(log);
190      assert.ok(log.data_json);
191  
192      const data = JSON.parse(log.data_json);
193      assert.strictEqual(data.file, 'src/test.js');
194      assert.strictEqual(data.action, 'verify_fix');
195      assert.deepStrictEqual(data.metadata, { version: '1.0' });
196    });
197  
198    await t.test('StructuredLogger - without task ID', () => {
199      const logger = new StructuredLogger('security');
200  
201      logger.info('Global security scan started');
202  
203      const log = db.prepare('SELECT * FROM agent_logs WHERE agent_name = ?').get('security');
204  
205      assert.ok(log);
206      assert.strictEqual(log.task_id, null);
207      assert.strictEqual(log.message, 'Global security scan started');
208    });
209  
210    await t.test('StructuredLogger - setTaskId', () => {
211      const logger = new StructuredLogger('architect');
212  
213      logger.info('Agent initialized', { status: 'ready' });
214  
215      // Set task ID after creation
216      logger.setTaskId(789);
217      logger.info('Task assigned', { task_type: 'design_review' });
218  
219      const logs = db
220        .prepare('SELECT * FROM agent_logs WHERE agent_name = ? ORDER BY id')
221        .all('architect');
222  
223      assert.strictEqual(logs.length, 2);
224      assert.strictEqual(logs[0].task_id, null); // Before setTaskId
225      assert.strictEqual(logs[1].task_id, 789); // After setTaskId
226    });
227  
228    await t.test('StructuredLogger - invalid log level', () => {
229      const logger = new StructuredLogger('developer');
230  
231      assert.throws(() => {
232        logger.log('invalid', 'Test message');
233      }, /Invalid log level/);
234    });
235  
236    await t.test('BaseAgent - recordOutcome success', async () => {
237      const agent = new TestAgent();
238      await agent.initialize();
239  
240      // Create a test task
241      const taskId = db
242        .prepare(
243          `
244        INSERT INTO agent_tasks (task_type, assigned_to, created_by, status)
245        VALUES (?, ?, ?, ?)
246      `
247        )
248        .run('fix_bug', 'developer', 'triage', 'completed').lastInsertRowid;
249  
250      // Record successful outcome
251      await agent.recordOutcome(
252        taskId,
253        'success',
254        { error_type: 'null_pointer', duration_ms: 1500 },
255        { files_changed: ['src/test.js'], approach: 'Added null check' }
256      );
257  
258      // Verify outcome recorded
259      const outcome = db.prepare('SELECT * FROM agent_outcomes WHERE task_id = ?').get(taskId);
260  
261      assert.ok(outcome);
262      assert.strictEqual(outcome.agent_name, 'developer');
263      assert.strictEqual(outcome.task_type, 'fix_bug');
264      assert.strictEqual(outcome.outcome, 'success');
265      assert.strictEqual(outcome.duration_ms, 1500);
266  
267      const context = JSON.parse(outcome.context_json);
268      assert.strictEqual(context.error_type, 'null_pointer');
269  
270      const result = JSON.parse(outcome.result_json);
271      assert.deepStrictEqual(result.files_changed, ['src/test.js']);
272    });
273  
274    await t.test('BaseAgent - recordOutcome failure', async () => {
275      const agent = new TestAgent();
276      await agent.initialize();
277  
278      // Create a test task
279      const taskId = db
280        .prepare(
281          `
282        INSERT INTO agent_tasks (task_type, assigned_to, created_by, status)
283        VALUES (?, ?, ?, ?)
284      `
285        )
286        .run('fix_bug', 'developer', 'triage', 'failed').lastInsertRowid;
287  
288      // Record failure outcome
289      await agent.recordOutcome(
290        taskId,
291        'failure',
292        {
293          error_type: 'database',
294          error: 'UNIQUE constraint failed',
295          duration_ms: 500,
296        },
297        {}
298      );
299  
300      // Verify outcome recorded
301      const outcome = db.prepare('SELECT * FROM agent_outcomes WHERE task_id = ?').get(taskId);
302  
303      assert.ok(outcome);
304      assert.strictEqual(outcome.outcome, 'failure');
305      assert.strictEqual(outcome.duration_ms, 500);
306  
307      const context = JSON.parse(outcome.context_json);
308      assert.strictEqual(context.error_type, 'database');
309      assert.strictEqual(context.error, 'UNIQUE constraint failed');
310    });
311  
312    await t.test('BaseAgent - recordOutcome invalid outcome', async () => {
313      const agent = new TestAgent();
314      await agent.initialize();
315  
316      const taskId = db
317        .prepare(
318          `
319        INSERT INTO agent_tasks (task_type, assigned_to, created_by, status)
320        VALUES (?, ?, ?, ?)
321      `
322        )
323        .run('fix_bug', 'developer', 'triage', 'completed').lastInsertRowid;
324  
325      await assert.rejects(async () => {
326        await agent.recordOutcome(taskId, 'invalid', {}, {});
327      }, /Invalid outcome/);
328    });
329  
330    await t.test('BaseAgent - learnFromPastOutcomes no data', async () => {
331      const agent = new TestAgent();
332      await agent.initialize();
333  
334      const insights = await agent.learnFromPastOutcomes('fix_bug');
335  
336      assert.strictEqual(insights.total_outcomes, 0);
337      assert.strictEqual(insights.success_rate, 0);
338      assert.strictEqual(insights.task_type, 'fix_bug');
339      assert.ok(insights.insights.includes('No historical data'));
340    });
341  
342    await t.test('BaseAgent - learnFromPastOutcomes with data', async () => {
343      const agent = new TestAgent();
344      await agent.initialize();
345  
346      // Create test tasks and outcomes
347      const outcomes = [
348        { outcome: 'success', error_type: 'null_pointer', duration: 1000 },
349        { outcome: 'success', error_type: 'null_pointer', duration: 1200 },
350        { outcome: 'failure', error_type: 'database', duration: 500 },
351        { outcome: 'success', error_type: 'network', duration: 2000 },
352        { outcome: 'failure', error_type: 'database', duration: 600 },
353      ];
354  
355      for (const o of outcomes) {
356        const taskId = db
357          .prepare(
358            `
359          INSERT INTO agent_tasks (task_type, assigned_to, created_by, status)
360          VALUES (?, ?, ?, ?)
361        `
362          )
363          .run('fix_bug', 'developer', 'triage', 'completed').lastInsertRowid;
364  
365        db.prepare(
366          `
367          INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, context_json, duration_ms)
368          VALUES (?, ?, ?, ?, ?, ?)
369        `
370        ).run(
371          taskId,
372          'developer',
373          'fix_bug',
374          o.outcome,
375          JSON.stringify({ error_type: o.error_type }),
376          o.duration
377        );
378      }
379  
380      // Learn from outcomes
381      const insights = await agent.learnFromPastOutcomes('fix_bug');
382  
383      assert.strictEqual(insights.total_outcomes, 5);
384      assert.strictEqual(insights.success_count, 3);
385      assert.strictEqual(insights.failure_count, 2);
386      assert.strictEqual(insights.success_rate, 60);
387  
388      // Check average duration (1000 + 1200 + 500 + 2000 + 600) / 5 = 1060
389      assert.strictEqual(insights.avg_duration_ms, 1060);
390  
391      // Check context patterns
392      assert.ok(insights.context_patterns);
393      assert.ok(insights.context_patterns.null_pointer);
394      assert.strictEqual(insights.context_patterns.null_pointer.total, 2);
395      assert.strictEqual(insights.context_patterns.null_pointer.successes, 2);
396      assert.strictEqual(insights.context_patterns.null_pointer.success_rate, 100);
397  
398      assert.ok(insights.context_patterns.database);
399      assert.strictEqual(insights.context_patterns.database.total, 2);
400      assert.strictEqual(insights.context_patterns.database.successes, 0);
401      assert.strictEqual(insights.context_patterns.database.success_rate, 0);
402  
403      assert.ok(insights.recommendations);
404      assert.ok(Array.isArray(insights.recommendations));
405    });
406  
407    await t.test('BaseAgent - executeTask records outcomes automatically', async () => {
408      const agent = new TestAgent();
409      await agent.initialize();
410  
411      // Create a successful task
412      const successTaskId = db
413        .prepare(
414          `
415        INSERT INTO agent_tasks (task_type, assigned_to, created_by, status)
416        VALUES (?, ?, ?, ?)
417      `
418        )
419        .run('test_success', 'developer', 'system', 'pending').lastInsertRowid;
420  
421      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(successTaskId);
422  
423      // Execute task (should succeed)
424      await agent.executeTask(task);
425  
426      // Verify success outcome recorded
427      const successOutcome = db
428        .prepare('SELECT * FROM agent_outcomes WHERE task_id = ?')
429        .get(successTaskId);
430  
431      assert.ok(successOutcome);
432      assert.strictEqual(successOutcome.outcome, 'success');
433      assert.strictEqual(successOutcome.task_type, 'test_success');
434      assert.ok(successOutcome.duration_ms > 0);
435    });
436  
437    await t.test('BaseAgent - executeTask records failure outcomes', async () => {
438      const agent = new TestAgent();
439      await agent.initialize();
440  
441      // Create a task that will fail
442      const failTaskId = db
443        .prepare(
444          `
445        INSERT INTO agent_tasks (task_type, assigned_to, created_by, status)
446        VALUES (?, ?, ?, ?)
447      `
448        )
449        .run('fail_task', 'developer', 'system', 'pending').lastInsertRowid;
450  
451      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(failTaskId);
452  
453      // Execute task (should fail)
454      await assert.rejects(async () => {
455        await agent.executeTask(task);
456      }, /Intentional test failure/);
457  
458      // Verify failure outcome recorded
459      const failOutcome = db
460        .prepare('SELECT * FROM agent_outcomes WHERE task_id = ?')
461        .get(failTaskId);
462  
463      assert.ok(failOutcome);
464      assert.strictEqual(failOutcome.outcome, 'failure');
465      assert.strictEqual(failOutcome.task_type, 'fail_task');
466  
467      const context = JSON.parse(failOutcome.context_json);
468      assert.strictEqual(context.error, 'Intentional test failure');
469      assert.ok(context.stack);
470    });
471  
472    await t.test('BaseAgent - structured logger integration', async () => {
473      const agent = new TestAgent();
474      await agent.initialize();
475  
476      // Create a task
477      const taskId = db
478        .prepare(
479          `
480        INSERT INTO agent_tasks (task_type, assigned_to, created_by, status)
481        VALUES (?, ?, ?, ?)
482      `
483        )
484        .run('test_logging', 'developer', 'system', 'pending').lastInsertRowid;
485  
486      const task = db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
487  
488      // Execute task
489      await agent.executeTask(task);
490  
491      // Verify structured logs created
492      const logs = db.prepare('SELECT * FROM agent_logs WHERE task_id = ? ORDER BY id').all(taskId);
493  
494      assert.ok(logs.length > 0);
495  
496      // Should have at least: Task started, Processing test task, Task completed
497      const messages = logs.map(l => l.message);
498      assert.ok(messages.some(m => m.includes('Task started')));
499      assert.ok(messages.some(m => m.includes('Processing test task')));
500      assert.ok(messages.some(m => m.includes('Task completed')));
501  
502      // All logs should have proper structure
503      for (const log of logs) {
504        assert.strictEqual(log.agent_name, 'developer');
505        assert.strictEqual(log.task_id, taskId);
506        assert.ok(['debug', 'info', 'warn', 'error'].includes(log.log_level));
507      }
508    });
509  });