/ tests / agents / process-guardian.test.js
process-guardian.test.js
  1  /**
  2   * Process Guardian Tests
  3   *
  4   * Tests the Tier 1 process guardian that runs as a direct cron function.
  5   * Verifies pipeline service checks, clearance cycle detection, and
  6   * circuit breaker monitoring.
  7   */
  8  
  9  import { test, mock, beforeEach, afterEach } from 'node:test';
 10  import assert from 'node:assert/strict';
 11  import Database from 'better-sqlite3';
 12  import { createPgMock } from '../helpers/pg-mock.js'; // eslint-disable-line no-unused-vars
 13  
 14  function setupTestDatabase() {
 15    const db = new Database(':memory:');
 16  
 17    db.exec(`
 18      CREATE TABLE IF NOT EXISTS system_health (
 19        id INTEGER PRIMARY KEY AUTOINCREMENT,
 20        check_type TEXT NOT NULL,
 21        status TEXT NOT NULL CHECK (status IN ('ok', 'warning', 'critical')),
 22        details TEXT,
 23        action_taken TEXT,
 24        created_at TEXT DEFAULT (datetime('now'))
 25      );
 26  
 27      CREATE TABLE IF NOT EXISTS agent_tasks (
 28        id INTEGER PRIMARY KEY AUTOINCREMENT,
 29        task_type TEXT NOT NULL,
 30        assigned_to TEXT NOT NULL,
 31        created_by TEXT,
 32        priority INTEGER DEFAULT 5,
 33        status TEXT DEFAULT 'pending',
 34        context_json TEXT,
 35        parent_task_id INTEGER,
 36        result_json TEXT,
 37        error_message TEXT,
 38        retry_count INTEGER DEFAULT 0,
 39        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 40        started_at TIMESTAMP,
 41        completed_at TIMESTAMP
 42      );
 43  
 44      CREATE TABLE IF NOT EXISTS settings (
 45        key TEXT PRIMARY KEY,
 46        value TEXT,
 47        description TEXT,
 48        updated_at TEXT DEFAULT (datetime('now'))
 49      );
 50    `);
 51  
 52    return db;
 53  }
 54  
 55  test('Process Guardian', async t => {
 56    let db;
 57  
 58    beforeEach(() => {
 59      db = setupTestDatabase();
 60    });
 61  
 62    afterEach(() => {
 63      if (db?.open) db.close();
 64    });
 65  
 66    await t.test('runProcessGuardian returns summary with all check results', async () => {
 67      // We can't easily test the actual systemctl calls, but we can verify the
 68      // function structure by importing and checking the module exports
 69      const mod = await import('../../src/cron/process-guardian.js');
 70      assert.ok(
 71        typeof mod.runProcessGuardian === 'function',
 72        'runProcessGuardian should be exported'
 73      );
 74    });
 75  
 76    await t.test('system_health table accepts health check records', () => {
 77      db.prepare(
 78        `INSERT INTO system_health (check_type, status, details, action_taken)
 79         VALUES (?, ?, ?, ?)`
 80      ).run('pipeline_service', 'ok', JSON.stringify({ service_status: 'active' }), null);
 81  
 82      db.prepare(
 83        `INSERT INTO system_health (check_type, status, details, action_taken)
 84         VALUES (?, ?, ?, ?)`
 85      ).run(
 86        'circuit_breaker',
 87        'critical',
 88        JSON.stringify({ breaker_open_errors_last_hour: 5 }),
 89        null
 90      );
 91  
 92      const rows = db.prepare('SELECT * FROM system_health ORDER BY id').all();
 93      assert.equal(rows.length, 2);
 94      assert.equal(rows[0].check_type, 'pipeline_service');
 95      assert.equal(rows[0].status, 'ok');
 96      assert.equal(rows[1].check_type, 'circuit_breaker');
 97      assert.equal(rows[1].status, 'critical');
 98    });
 99  
100    await t.test('circuit breaker detection counts breaker-open errors', () => {
101      // Insert some agent tasks with breaker errors
102      for (let i = 0; i < 5; i++) {
103        db.prepare(
104          `INSERT INTO agent_tasks (task_type, assigned_to, context_json, created_at)
105           VALUES (?, ?, ?, datetime('now', '-30 minutes'))`
106        ).run('fix_bug', 'developer', JSON.stringify({ error: 'Breaker is open' }));
107      }
108  
109      const row = db
110        .prepare(
111          `SELECT COUNT(*) as count
112           FROM agent_tasks
113           WHERE created_at >= datetime('now', '-1 hour')
114             AND context_json LIKE '%Breaker is open%'`
115        )
116        .get();
117  
118      assert.equal(row.count, 5);
119      assert.ok(row.count > 3, 'Should detect circuit breaker firing');
120    });
121  
122    await t.test('clearance cycle detection uses previous system_health record', () => {
123      // Simulate previous check showing clearance was running
124      db.prepare(
125        `INSERT INTO system_health (check_type, status, details, created_at)
126         VALUES (?, ?, ?, datetime('now', '-2 minutes'))`
127      ).run('clearance_cycle', 'ok', JSON.stringify({ clearance_running: true, was_running: false }));
128  
129      const lastRow = db
130        .prepare(
131          `SELECT details FROM system_health
132           WHERE check_type = 'clearance_cycle'
133           ORDER BY created_at DESC LIMIT 1`
134        )
135        .get();
136  
137      const details = JSON.parse(lastRow.details);
138      assert.equal(details.clearance_running, true, 'Should record clearance was running');
139    });
140  
141    await t.test('old system_health records can be cleaned up', () => {
142      // Insert old records
143      for (let i = 0; i < 5; i++) {
144        db.prepare(
145          `INSERT INTO system_health (check_type, status, details, created_at)
146           VALUES (?, ?, ?, datetime('now', '-10 days'))`
147        ).run('pipeline_service', 'ok', '{}');
148      }
149  
150      // Insert recent records
151      db.prepare(
152        `INSERT INTO system_health (check_type, status, details)
153         VALUES (?, ?, ?)`
154      ).run('pipeline_service', 'ok', '{}');
155  
156      const result = db
157        .prepare(`DELETE FROM system_health WHERE created_at < datetime('now', '-7 days')`)
158        .run();
159  
160      assert.equal(result.changes, 5, 'Should delete 5 old records');
161  
162      const remaining = db.prepare('SELECT COUNT(*) as count FROM system_health').get();
163      assert.equal(remaining.count, 1, 'Should keep 1 recent record');
164    });
165  });