/ __quarantined_tests__ / agents / runner-coverage.test.js
runner-coverage.test.js
   1  /**
   2   * Agent Runner Coverage Tests
   3   *
   4   * Tests for src/agents/runner.js:
   5   * - runAgentCycle (sequential execution with agent mocks)
   6   * - getAgentStats (database queries)
   7   * - checkCircuitBreakers (circuit breaker logic)
   8   * - checkNightmareScenarios (alert detection)
   9   * - createSummaryCommit (git operations, mocked)
  10   * - runCompleteAgentCycle (orchestration)
  11   * - calculateTaskLimit (autoscaling logic via runCompleteAgentCycle with parallel mode)
  12   *
  13   * Strategy: Set DATABASE_PATH to a temp file BEFORE importing runner.js,
  14   * so the module-level `db` connects to our test database.
  15   * We reset the DB contents between tests by truncating tables.
  16   */
  17  
  18  import { describe, test, beforeEach, afterEach, mock } from 'node:test';
  19  import assert from 'node:assert/strict';
  20  import Database from 'better-sqlite3';
  21  import path from 'path';
  22  import { fileURLToPath } from 'url';
  23  import { unlinkSync, existsSync } from 'fs';
  24  
  25  const __filename = fileURLToPath(import.meta.url);
  26  const __dirname = path.dirname(__filename);
  27  
  28  // ── Set DATABASE_PATH BEFORE importing runner.js ───────────────────────────
  29  const TEST_DB_PATH = path.join('/tmp', `test-runner-coverage-${process.pid}.db`);
  30  process.env.DATABASE_PATH = TEST_DB_PATH;
  31  process.env.AGENT_SYSTEM_ENABLED = 'true';
  32  process.env.AGENT_AUTO_COMMIT = 'false';
  33  process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'false';
  34  process.env.AGENT_PARALLEL_EXECUTION = 'false';
  35  process.env.AGENT_IMMEDIATE_INVOCATION = 'false';
  36  process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
  37  
  38  // ── Test DB schema ─────────────────────────────────────────────────────────
  39  const TEST_SCHEMA = `
  40    CREATE TABLE IF NOT EXISTS agent_tasks (
  41      id INTEGER PRIMARY KEY AUTOINCREMENT,
  42      task_type TEXT NOT NULL,
  43      assigned_to TEXT NOT NULL,
  44      created_by TEXT,
  45      status TEXT DEFAULT 'pending',
  46      priority INTEGER DEFAULT 5,
  47      context_json TEXT,
  48      result_json TEXT,
  49      error_message TEXT,
  50      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  51      started_at DATETIME,
  52      completed_at DATETIME,
  53      retry_count INTEGER DEFAULT 0
  54    );
  55  
  56    CREATE TABLE IF NOT EXISTS agent_state (
  57      agent_name TEXT PRIMARY KEY,
  58      last_active DATETIME DEFAULT CURRENT_TIMESTAMP,
  59      current_task_id INTEGER,
  60      status TEXT DEFAULT 'idle',
  61      metrics_json TEXT
  62    );
  63  
  64    CREATE TABLE IF NOT EXISTS agent_logs (
  65      id INTEGER PRIMARY KEY AUTOINCREMENT,
  66      task_id INTEGER,
  67      agent_name TEXT NOT NULL,
  68      log_level TEXT,
  69      message TEXT,
  70      data_json TEXT,
  71      metadata_json TEXT,
  72      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  73    );
  74  `;
  75  
  76  // Create the test DB and schema before any mocks (delete stale file first)
  77  if (existsSync(TEST_DB_PATH)) unlinkSync(TEST_DB_PATH);
  78  const setupDb = new Database(TEST_DB_PATH);
  79  setupDb.exec(TEST_SCHEMA);
  80  setupDb.close();
  81  
  82  // ── Mock child_process BEFORE importing runner.js ──────────────────────────
  83  let gitStatusOutput = '';
  84  let gitBranchOutput = 'main';
  85  let gitCommitOutput = 'abc1234';
  86  let gitCheckoutShouldFail = false;
  87  let gitPushShouldFail = false;
  88  
  89  const execSyncMock = mock.fn(cmd => {
  90    if (cmd.includes('git status --porcelain')) return gitStatusOutput;
  91    if (cmd.includes('git rev-parse --abbrev-ref HEAD')) return gitBranchOutput;
  92    if (cmd.includes('git rev-parse --short HEAD')) return gitCommitOutput;
  93    if (cmd.includes('git checkout') || cmd.includes('git checkout -B')) {
  94      if (gitCheckoutShouldFail) throw new Error('git checkout failed');
  95      gitBranchOutput = 'autofix';
  96      return '';
  97    }
  98    if (cmd.includes('git push')) {
  99      if (gitPushShouldFail) throw new Error('git push failed');
 100      return '';
 101    }
 102    if (cmd.includes('git add')) return '';
 103    if (cmd.includes('git commit')) return '';
 104    return '';
 105  });
 106  
 107  mock.module('child_process', {
 108    namedExports: { execSync: execSyncMock },
 109  });
 110  
 111  // ── Mock agent classes ─────────────────────────────────────────────────────
 112  // Shared mutable state for agent task counts
 113  const agentTaskCounts = {
 114    monitor: 2,
 115    triage: 1,
 116    developer: 0,
 117    qa: 0,
 118    security: 0,
 119    architect: 0,
 120  };
 121  
 122  // Track which agents should throw
 123  const agentShouldThrow = {
 124    monitor: false,
 125    triage: false,
 126    developer: false,
 127    qa: false,
 128    security: false,
 129    architect: false,
 130  };
 131  
 132  function resetAgentBehavior(counts = {}, throws = {}) {
 133    agentTaskCounts.monitor = counts.monitor ?? 0;
 134    agentTaskCounts.triage = counts.triage ?? 0;
 135    agentTaskCounts.developer = counts.developer ?? 0;
 136    agentTaskCounts.qa = counts.qa ?? 0;
 137    agentTaskCounts.security = counts.security ?? 0;
 138    agentTaskCounts.architect = counts.architect ?? 0;
 139  
 140    agentShouldThrow.monitor = throws.monitor ?? false;
 141    agentShouldThrow.triage = throws.triage ?? false;
 142    agentShouldThrow.developer = throws.developer ?? false;
 143    agentShouldThrow.qa = throws.qa ?? false;
 144    agentShouldThrow.security = throws.security ?? false;
 145    agentShouldThrow.architect = throws.architect ?? false;
 146  }
 147  
 148  function makeMockClass(agentName) {
 149    return class {
 150      constructor() {
 151        this.agentName = agentName;
 152      }
 153      pollTasks(_limit) {
 154        const shouldThrow = Object.prototype.hasOwnProperty.call(agentShouldThrow, agentName)
 155          ? agentShouldThrow[agentName]
 156          : false;
 157        const taskCount = Object.prototype.hasOwnProperty.call(agentTaskCounts, agentName)
 158          ? agentTaskCounts[agentName]
 159          : 0;
 160  
 161        if (shouldThrow) {
 162          return Promise.reject(new Error(`${agentName} agent crash`));
 163        }
 164        return Promise.resolve(taskCount);
 165      }
 166    };
 167  }
 168  
 169  mock.module('../../src/agents/monitor.js', {
 170    namedExports: { MonitorAgent: makeMockClass('monitor') },
 171  });
 172  mock.module('../../src/agents/triage.js', {
 173    namedExports: { TriageAgent: makeMockClass('triage') },
 174  });
 175  mock.module('../../src/agents/developer.js', {
 176    namedExports: { DeveloperAgent: makeMockClass('developer') },
 177  });
 178  mock.module('../../src/agents/qa.js', {
 179    namedExports: { QAAgent: makeMockClass('qa') },
 180  });
 181  mock.module('../../src/agents/security.js', {
 182    namedExports: { SecurityAgent: makeMockClass('security') },
 183  });
 184  mock.module('../../src/agents/architect.js', {
 185    namedExports: { ArchitectAgent: makeMockClass('architect') },
 186  });
 187  
 188  // ── Import runner AFTER all mocks are registered ──────────────────────────
 189  const { runAgentCycle, getAgentStats, checkCircuitBreakers, runCompleteAgentCycle } =
 190    await import('../../src/agents/runner.js');
 191  
 192  // ── Helper: direct DB connection for test setup ───────────────────────────
 193  let testDb;
 194  
 195  function clearTables() {
 196    testDb.exec('DELETE FROM agent_tasks');
 197    testDb.exec('DELETE FROM agent_state');
 198    testDb.exec('DELETE FROM agent_logs');
 199  
 200    // Re-insert agent state rows
 201    const agents = ['monitor', 'triage', 'developer', 'qa', 'security', 'architect'];
 202    for (const name of agents) {
 203      testDb
 204        .prepare(`INSERT OR IGNORE INTO agent_state (agent_name, status) VALUES (?, 'idle')`)
 205        .run(name);
 206    }
 207  }
 208  
 209  beforeEach(() => {
 210    testDb = new Database(TEST_DB_PATH);
 211    process.env.AGENT_SYSTEM_ENABLED = 'true';
 212    process.env.AGENT_AUTO_COMMIT = 'false';
 213    process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'false';
 214    process.env.AGENT_PARALLEL_EXECUTION = 'false';
 215    process.env.AGENT_CIRCUIT_BREAKER_THRESHOLD = '0.3';
 216  
 217    gitStatusOutput = '';
 218    gitBranchOutput = 'main';
 219    gitCommitOutput = 'abc1234';
 220    gitCheckoutShouldFail = false;
 221    gitPushShouldFail = false;
 222  
 223    clearTables();
 224    resetAgentBehavior();
 225    execSyncMock.mock.resetCalls();
 226  });
 227  
 228  afterEach(() => {
 229    testDb.close();
 230  });
 231  
 232  // Cleanup after all tests
 233  afterEach(async () => {});
 234  
 235  // ── runAgentCycle ──────────────────────────────────────────────────────────
 236  describe('runAgentCycle - sequential execution', () => {
 237    test('returns disabled result when AGENT_SYSTEM_ENABLED=false', async () => {
 238      process.env.AGENT_SYSTEM_ENABLED = 'false';
 239      const result = await runAgentCycle();
 240      assert.equal(result.enabled, false);
 241      assert.equal(result.processed, 0);
 242    });
 243  
 244    test('returns summary with enabled=true when system is active', async () => {
 245      resetAgentBehavior({ monitor: 3, triage: 1 });
 246      const result = await runAgentCycle({ tasksPerAgent: 5 });
 247  
 248      assert.equal(result.enabled, true);
 249      assert.ok(result.timestamp);
 250      assert.ok(typeof result.agents === 'object');
 251      assert.equal(result.total_processed, 4);
 252      assert.equal(result.errors.length, 0);
 253    });
 254  
 255    test('records per-agent processed count and status', async () => {
 256      resetAgentBehavior({ monitor: 2, developer: 1 });
 257      const result = await runAgentCycle({ tasksPerAgent: 3 });
 258  
 259      assert.equal(result.agents.monitor.processed, 2);
 260      assert.equal(result.agents.monitor.status, 'success');
 261      assert.ok(result.agents.monitor.duration_ms >= 0);
 262      assert.equal(result.agents.developer.processed, 1);
 263      assert.equal(result.total_processed, 3);
 264    });
 265  
 266    test('continues running agents when one throws an error', async () => {
 267      resetAgentBehavior({ monitor: 2, triage: 1 }, { developer: true });
 268      const result = await runAgentCycle({ tasksPerAgent: 5 });
 269  
 270      assert.ok(result.agents.developer);
 271      assert.equal(result.agents.developer.status, 'failed');
 272      assert.equal(result.agents.developer.error, 'developer agent crash');
 273      assert.equal(result.errors.length, 1);
 274      assert.equal(result.errors[0].agent, 'developer');
 275  
 276      // Other agents should still have succeeded
 277      assert.equal(result.agents.monitor.status, 'success');
 278      assert.equal(result.agents.triage.status, 'success');
 279    });
 280  
 281    test('verbose=true completes without error when tasks processed', async () => {
 282      resetAgentBehavior({ monitor: 1 });
 283      const result = await runAgentCycle({ tasksPerAgent: 5, verbose: true });
 284      assert.equal(result.enabled, true);
 285      assert.equal(result.agents.monitor.processed, 1);
 286    });
 287  
 288    test('verbose=true completes without error when no tasks', async () => {
 289      resetAgentBehavior(); // all 0
 290      const result = await runAgentCycle({ tasksPerAgent: 5, verbose: true });
 291      assert.equal(result.total_processed, 0);
 292      assert.equal(result.enabled, true);
 293    });
 294  
 295    test('defaults to tasksPerAgent=5 when not specified', async () => {
 296      resetAgentBehavior({ monitor: 1 });
 297      const result = await runAgentCycle();
 298      assert.equal(result.enabled, true);
 299      assert.ok(result.agents.monitor);
 300    });
 301  
 302    test('handles multiple agent failures gracefully', async () => {
 303      resetAgentBehavior({}, { monitor: true, developer: true, qa: true });
 304      const result = await runAgentCycle({ tasksPerAgent: 5 });
 305  
 306      assert.equal(result.errors.length, 3);
 307      assert.equal(result.agents.monitor.status, 'failed');
 308      assert.equal(result.agents.developer.status, 'failed');
 309      assert.equal(result.agents.qa.status, 'failed');
 310  
 311      // Other agents should still succeed
 312      assert.equal(result.agents.triage.status, 'success');
 313      assert.equal(result.agents.security.status, 'success');
 314      assert.equal(result.agents.architect.status, 'success');
 315    });
 316  });
 317  
 318  // ── getAgentStats ──────────────────────────────────────────────────────────
 319  describe('getAgentStats - database queries', () => {
 320    test('returns empty agents array when no tasks in last 24h', () => {
 321      const stats = getAgentStats();
 322      assert.ok(Array.isArray(stats.agents));
 323      assert.equal(stats.agents.length, 0);
 324      assert.ok(stats.overall);
 325      assert.equal(stats.overall.total, 0);
 326      assert.equal(stats.overall.success_rate, 0);
 327      assert.equal(stats.overall.failure_rate, 0);
 328    });
 329  
 330    test('calculates correct success_rate and failure_rate', () => {
 331      const now = new Date().toISOString();
 332      // 3 completed, 1 failed for developer
 333      for (let i = 0; i < 3; i++) {
 334        testDb
 335          .prepare(
 336            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at, completed_at, started_at)
 337           VALUES ('fix_bug', 'developer', 'completed', ?, ?, ?)`
 338          )
 339          .run(now, now, now);
 340      }
 341      testDb
 342        .prepare(
 343          `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 344         VALUES ('fix_bug', 'developer', 'failed', ?)`
 345        )
 346        .run(now);
 347  
 348      const stats = getAgentStats();
 349      const devStats = stats.agents.find(a => a.agent === 'developer');
 350      assert.ok(devStats, 'developer stats should exist');
 351      assert.equal(devStats.total, 4);
 352      assert.equal(devStats.completed, 3);
 353      assert.equal(devStats.failed, 1);
 354      assert.ok(Math.abs(devStats.success_rate - 0.75) < 0.01);
 355      assert.ok(Math.abs(devStats.failure_rate - 0.25) < 0.01);
 356    });
 357  
 358    test('counts pending, running, and blocked tasks', () => {
 359      const now = new Date().toISOString();
 360      testDb
 361        .prepare(
 362          `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 363         VALUES ('run_tests', 'qa', 'pending', ?)`
 364        )
 365        .run(now);
 366      testDb
 367        .prepare(
 368          `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 369         VALUES ('run_tests', 'qa', 'running', ?)`
 370        )
 371        .run(now);
 372      testDb
 373        .prepare(
 374          `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 375         VALUES ('run_tests', 'qa', 'blocked', ?)`
 376        )
 377        .run(now);
 378  
 379      const stats = getAgentStats();
 380      const qaStats = stats.agents.find(a => a.agent === 'qa');
 381      assert.ok(qaStats);
 382      assert.equal(qaStats.total, 3);
 383      assert.equal(qaStats.pending, 1);
 384      assert.equal(qaStats.running, 1);
 385      assert.equal(qaStats.blocked, 1);
 386    });
 387  
 388    test('overall stats aggregate across multiple agents', () => {
 389      const now = new Date().toISOString();
 390      testDb
 391        .prepare(
 392          `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 393         VALUES ('fix_bug', 'developer', 'completed', ?)`
 394        )
 395        .run(now);
 396      testDb
 397        .prepare(
 398          `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 399         VALUES ('run_tests', 'qa', 'failed', ?)`
 400        )
 401        .run(now);
 402  
 403      const stats = getAgentStats();
 404      assert.equal(stats.overall.total, 2);
 405      assert.equal(stats.overall.completed, 1);
 406      assert.equal(stats.overall.failed, 1);
 407      assert.ok(Math.abs(stats.overall.success_rate - 0.5) < 0.01);
 408      assert.ok(Math.abs(stats.overall.failure_rate - 0.5) < 0.01);
 409    });
 410  
 411    test('excludes tasks older than 24 hours', () => {
 412      testDb
 413        .prepare(
 414          `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 415         VALUES ('fix_bug', 'developer', 'completed', datetime('now', '-2 days'))`
 416        )
 417        .run();
 418  
 419      const stats = getAgentStats();
 420      assert.equal(stats.overall.total, 0);
 421    });
 422  
 423    test('returns success_rate=0 when all tasks are pending (no completed)', () => {
 424      const now = new Date().toISOString();
 425      testDb
 426        .prepare(
 427          `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 428         VALUES ('fix_bug', 'developer', 'pending', ?)`
 429        )
 430        .run(now);
 431  
 432      const stats = getAgentStats();
 433      const devStats = stats.agents.find(a => a.agent === 'developer');
 434      assert.ok(devStats);
 435      assert.equal(devStats.success_rate, 0);
 436      assert.equal(devStats.failure_rate, 0);
 437    });
 438  });
 439  
 440  // ── checkCircuitBreakers ───────────────────────────────────────────────────
 441  describe('checkCircuitBreakers - circuit breaker logic', () => {
 442    test('returns empty array when no agents have high failure rate', () => {
 443      const blocked = checkCircuitBreakers();
 444      assert.ok(Array.isArray(blocked));
 445      assert.equal(blocked.length, 0);
 446    });
 447  
 448    test('triggers circuit breaker when failure rate > 30% with >= 10 tasks', () => {
 449      const now = new Date().toISOString();
 450      // 8 failed + 2 completed = 80% failure rate, 10 total
 451      for (let i = 0; i < 8; i++) {
 452        testDb
 453          .prepare(
 454            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 455           VALUES ('fix_bug', 'developer', 'failed', ?)`
 456          )
 457          .run(now);
 458      }
 459      for (let i = 0; i < 2; i++) {
 460        testDb
 461          .prepare(
 462            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 463           VALUES ('fix_bug', 'developer', 'completed', ?)`
 464          )
 465          .run(now);
 466      }
 467  
 468      const blocked = checkCircuitBreakers();
 469      const devBlocked = blocked.find(b => b.agent === 'developer');
 470      assert.ok(devBlocked, 'developer should be blocked');
 471      assert.ok(devBlocked.failure_rate > 0.3);
 472      assert.equal(devBlocked.total_tasks, 10);
 473    });
 474  
 475    test('does NOT trigger circuit breaker when fewer than 10 tasks total', () => {
 476      const now = new Date().toISOString();
 477      // 5 failed (100% failure rate), but < 10 total
 478      for (let i = 0; i < 5; i++) {
 479        testDb
 480          .prepare(
 481            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 482           VALUES ('fix_bug', 'developer', 'failed', ?)`
 483          )
 484          .run(now);
 485      }
 486  
 487      const blocked = checkCircuitBreakers();
 488      const devBlocked = blocked.find(b => b.agent === 'developer');
 489      assert.equal(devBlocked, undefined);
 490    });
 491  
 492    test('auto-recovers when cooldown expired and failure rate dropped', () => {
 493      // Set developer to blocked state with trigger time 2 hours ago (cooldown is 30 min)
 494      const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
 495      testDb
 496        .prepare(
 497          `INSERT OR REPLACE INTO agent_state (agent_name, status, metrics_json)
 498         VALUES ('developer', 'blocked', ?)`
 499        )
 500        .run(JSON.stringify({ circuit_breaker_triggered_at: twoHoursAgo }));
 501  
 502      // Add recent tasks with LOW failure rate (10% failure rate = 0.1 <= threshold 0.3)
 503      // so that developer appears in stats.agents AND failure_rate is below threshold
 504      const now = new Date().toISOString();
 505      for (let i = 0; i < 9; i++) {
 506        testDb
 507          .prepare(
 508            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at, completed_at, started_at)
 509           VALUES ('fix_bug', 'developer', 'completed', ?, ?, ?)`
 510          )
 511          .run(now, now, now);
 512      }
 513      testDb
 514        .prepare(
 515          `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 516         VALUES ('fix_bug', 'developer', 'failed', ?)`
 517        )
 518        .run(now);
 519  
 520      checkCircuitBreakers();
 521  
 522      // Check that developer was auto-recovered (runner sets to 'idle' on recovery)
 523      const devState = testDb
 524        .prepare('SELECT status FROM agent_state WHERE agent_name = ?')
 525        .get('developer');
 526      assert.equal(devState.status, 'idle');
 527    });
 528  
 529    test('does NOT auto-recover when cooldown has not expired', () => {
 530      // Set developer to blocked state with trigger time 5 minutes ago (within 30 min cooldown)
 531      const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
 532      testDb
 533        .prepare(
 534          `INSERT OR REPLACE INTO agent_state (agent_name, status, metrics_json)
 535         VALUES ('developer', 'blocked', ?)`
 536        )
 537        .run(JSON.stringify({ circuit_breaker_triggered_at: fiveMinutesAgo }));
 538  
 539      // Add recent tasks with LOW failure rate (so developer appears in stats)
 540      // but cooldown hasn't expired yet
 541      const now = new Date().toISOString();
 542      for (let i = 0; i < 10; i++) {
 543        testDb
 544          .prepare(
 545            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at, completed_at, started_at)
 546           VALUES ('fix_bug', 'developer', 'completed', ?, ?, ?)`
 547          )
 548          .run(now, now, now);
 549      }
 550  
 551      checkCircuitBreakers();
 552  
 553      // Developer should still be blocked (cooldown not expired)
 554      const devState = testDb
 555        .prepare('SELECT status FROM agent_state WHERE agent_name = ?')
 556        .get('developer');
 557      assert.notEqual(devState.status, 'active');
 558    });
 559  
 560    test('does NOT auto-recover when failure rate still above threshold', () => {
 561      // Blocked 2 hours ago (cooldown expired) but still failing at high rate
 562      const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
 563      testDb
 564        .prepare(
 565          `INSERT OR REPLACE INTO agent_state (agent_name, status, metrics_json)
 566         VALUES ('developer', 'blocked', ?)`
 567        )
 568        .run(JSON.stringify({ circuit_breaker_triggered_at: twoHoursAgo }));
 569  
 570      // Add recent failures (80% failure rate > 30% threshold)
 571      const now = new Date().toISOString();
 572      for (let i = 0; i < 8; i++) {
 573        testDb
 574          .prepare(
 575            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 576           VALUES ('fix_bug', 'developer', 'failed', ?)`
 577          )
 578          .run(now);
 579      }
 580      for (let i = 0; i < 2; i++) {
 581        testDb
 582          .prepare(
 583            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at, completed_at, started_at)
 584           VALUES ('fix_bug', 'developer', 'completed', ?, ?, ?)`
 585          )
 586          .run(now, now, now);
 587      }
 588  
 589      checkCircuitBreakers();
 590  
 591      // Should still be blocked since failure rate is high - not active
 592      const devState = testDb
 593        .prepare('SELECT status FROM agent_state WHERE agent_name = ?')
 594        .get('developer');
 595      assert.notEqual(devState.status, 'active');
 596    });
 597  
 598    test('handles agent with no metrics_json gracefully', () => {
 599      testDb
 600        .prepare(
 601          `INSERT OR REPLACE INTO agent_state (agent_name, status, metrics_json)
 602         VALUES ('developer', 'blocked', NULL)`
 603        )
 604        .run();
 605  
 606      // Should not crash
 607      const blocked = checkCircuitBreakers();
 608      assert.ok(Array.isArray(blocked));
 609    });
 610  
 611    test('respects custom threshold from AGENT_CIRCUIT_BREAKER_THRESHOLD env var', () => {
 612      process.env.AGENT_CIRCUIT_BREAKER_THRESHOLD = '0.5'; // 50% threshold
 613  
 614      const now = new Date().toISOString();
 615      // 6 failed, 4 completed = 60% failure rate - should trigger at 50% threshold
 616      for (let i = 0; i < 6; i++) {
 617        testDb
 618          .prepare(
 619            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 620           VALUES ('fix_bug', 'developer', 'failed', ?)`
 621          )
 622          .run(now);
 623      }
 624      for (let i = 0; i < 4; i++) {
 625        testDb
 626          .prepare(
 627            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 628           VALUES ('fix_bug', 'developer', 'completed', ?)`
 629          )
 630          .run(now);
 631      }
 632  
 633      const blocked = checkCircuitBreakers();
 634      const devBlocked = blocked.find(b => b.agent === 'developer');
 635      assert.ok(devBlocked, 'should be blocked with 60% failure and 50% threshold');
 636  
 637      process.env.AGENT_CIRCUIT_BREAKER_THRESHOLD = '0.3';
 638    });
 639  
 640    test('uses custom cooldown from AGENT_CIRCUIT_BREAKER_COOLDOWN env var', () => {
 641      process.env.AGENT_CIRCUIT_BREAKER_COOLDOWN = '120'; // 2 hour cooldown
 642  
 643      // Trigger time 90 minutes ago (within 2h cooldown, so should NOT auto-recover)
 644      const ninetyMinAgo = new Date(Date.now() - 90 * 60 * 1000).toISOString();
 645      testDb
 646        .prepare(
 647          `INSERT OR REPLACE INTO agent_state (agent_name, status, metrics_json)
 648         VALUES ('developer', 'blocked', ?)`
 649        )
 650        .run(JSON.stringify({ circuit_breaker_triggered_at: ninetyMinAgo }));
 651  
 652      // Add recent tasks with LOW failure rate (so developer appears in stats)
 653      const now = new Date().toISOString();
 654      for (let i = 0; i < 10; i++) {
 655        testDb
 656          .prepare(
 657            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at, completed_at, started_at)
 658           VALUES ('fix_bug', 'developer', 'completed', ?, ?, ?)`
 659          )
 660          .run(now, now, now);
 661      }
 662  
 663      checkCircuitBreakers();
 664  
 665      // Should NOT auto-recover since 90 min < 120 min cooldown
 666      const devState = testDb
 667        .prepare('SELECT status FROM agent_state WHERE agent_name = ?')
 668        .get('developer');
 669      assert.notEqual(devState.status, 'active');
 670  
 671      process.env.AGENT_CIRCUIT_BREAKER_COOLDOWN = '30';
 672    });
 673  });
 674  
 675  // ── runCompleteAgentCycle ──────────────────────────────────────────────────
 676  describe('runCompleteAgentCycle - orchestration', () => {
 677    test('aborts when critical nightmare scenario detected (cost runaway)', async () => {
 678      // Inject >66 log entries to push cost above $1/hour (66.7 * 0.015 = $1.0)
 679      for (let i = 0; i < 70; i++) {
 680        testDb
 681          .prepare(
 682            `INSERT INTO agent_logs (agent_name, log_level, message, created_at)
 683           VALUES ('monitor', 'INFO', 'agent initialized', datetime('now', '-30 minutes'))`
 684          )
 685          .run();
 686      }
 687  
 688      const result = await runCompleteAgentCycle();
 689      assert.equal(result.aborted, true);
 690      assert.ok(result.alerts);
 691      assert.ok(result.alerts.some(a => a.type === 'cost_runaway'));
 692    });
 693  
 694    test('runs sequential cycle when parallel flags are false', async () => {
 695      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'false';
 696      process.env.AGENT_PARALLEL_EXECUTION = 'false';
 697      resetAgentBehavior({ monitor: 1, triage: 0, developer: 0, qa: 0, security: 0, architect: 0 });
 698  
 699      const result = await runCompleteAgentCycle({ tasksPerAgent: 3 });
 700      assert.equal(result.enabled, true);
 701      assert.equal(result.total_processed, 1);
 702    });
 703  
 704    test('runs parallel cycle when AGENT_RUN_TYPES_IN_PARALLEL=true', async () => {
 705      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'true';
 706      resetAgentBehavior({ monitor: 2, triage: 1 });
 707  
 708      const result = await runCompleteAgentCycle({ tasksPerAgent: 3 });
 709      assert.equal(result.enabled, true);
 710      // Parallel run should have processed both agents
 711      assert.ok(result.total_processed >= 0);
 712  
 713      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'false';
 714    });
 715  
 716    test('runs parallel cycle when AGENT_PARALLEL_EXECUTION=true', async () => {
 717      process.env.AGENT_PARALLEL_EXECUTION = 'true';
 718      resetAgentBehavior({ monitor: 1 });
 719  
 720      const result = await runCompleteAgentCycle();
 721      assert.equal(result.enabled, true);
 722  
 723      process.env.AGENT_PARALLEL_EXECUTION = 'false';
 724    });
 725  
 726    test('includes circuit_breakers in summary when agent is blocked by high failure rate', async () => {
 727      const now = new Date().toISOString();
 728      // Trigger circuit breaker for developer
 729      for (let i = 0; i < 8; i++) {
 730        testDb
 731          .prepare(
 732            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 733           VALUES ('fix_bug', 'developer', 'failed', ?)`
 734          )
 735          .run(now);
 736      }
 737      for (let i = 0; i < 2; i++) {
 738        testDb
 739          .prepare(
 740            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 741           VALUES ('fix_bug', 'developer', 'completed', ?)`
 742          )
 743          .run(now);
 744      }
 745  
 746      resetAgentBehavior();
 747      const result = await runCompleteAgentCycle();
 748  
 749      if (result.circuit_breakers) {
 750        assert.ok(Array.isArray(result.circuit_breakers));
 751        const devCb = result.circuit_breakers.find(b => b.agent === 'developer');
 752        assert.ok(devCb);
 753      }
 754    });
 755  
 756    test('includes post-run alerts in summary', async () => {
 757      // Create stale task buildup for post-run nightmare detection
 758      for (let i = 0; i < 55; i++) {
 759        testDb
 760          .prepare(
 761            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 762           VALUES ('fix_bug', 'developer', 'pending', datetime('now', '-2 hours'))`
 763          )
 764          .run();
 765      }
 766  
 767      resetAgentBehavior();
 768      const result = await runCompleteAgentCycle();
 769  
 770      if (!result.aborted && result.alerts) {
 771        assert.ok(Array.isArray(result.alerts));
 772      }
 773    });
 774  
 775    test('creates summary commit when AGENT_AUTO_COMMIT=true with changes', async () => {
 776      process.env.AGENT_AUTO_COMMIT = 'true';
 777      process.env.AGENT_AUTO_COMMIT_BRANCH = 'autofix';
 778      gitStatusOutput = ' M src/test.js';
 779      gitBranchOutput = 'autofix';
 780      gitCommitOutput = 'def5678';
 781  
 782      resetAgentBehavior();
 783      const result = await runCompleteAgentCycle();
 784  
 785      if (!result.aborted) {
 786        assert.equal(result.commit, 'def5678');
 787        assert.equal(result.branch, 'autofix');
 788      }
 789  
 790      process.env.AGENT_AUTO_COMMIT = 'false';
 791    });
 792  
 793    test('skips commit when AGENT_AUTO_COMMIT is not set to true', async () => {
 794      process.env.AGENT_AUTO_COMMIT = 'false';
 795      resetAgentBehavior();
 796  
 797      const result = await runCompleteAgentCycle();
 798      assert.equal(result.commit, undefined);
 799    });
 800  
 801    test('skips commit when git status shows no changes', async () => {
 802      process.env.AGENT_AUTO_COMMIT = 'true';
 803      gitStatusOutput = ''; // no changes
 804  
 805      resetAgentBehavior();
 806      const result = await runCompleteAgentCycle();
 807      if (!result.aborted) {
 808        assert.equal(result.commit, undefined);
 809      }
 810  
 811      process.env.AGENT_AUTO_COMMIT = 'false';
 812    });
 813  
 814    test('switches to autofix branch when on different branch', async () => {
 815      process.env.AGENT_AUTO_COMMIT = 'true';
 816      process.env.AGENT_AUTO_COMMIT_BRANCH = 'autofix';
 817      gitStatusOutput = ' M src/test.js';
 818      gitBranchOutput = 'main'; // Different from autofix
 819      gitCommitOutput = 'newcommit';
 820  
 821      resetAgentBehavior();
 822      const result = await runCompleteAgentCycle();
 823  
 824      if (!result.aborted) {
 825        // Git checkout should have been called
 826        const calls = execSyncMock.mock.calls.map(c => c.arguments[0]);
 827        const hasCheckout = calls.some(cmd => cmd.includes('checkout'));
 828        assert.ok(hasCheckout, 'Should have called git checkout');
 829      }
 830  
 831      process.env.AGENT_AUTO_COMMIT = 'false';
 832    });
 833  
 834    test('returns null commit when git checkout fails', async () => {
 835      process.env.AGENT_AUTO_COMMIT = 'true';
 836      gitStatusOutput = ' M src/test.js';
 837      gitBranchOutput = 'main'; // triggers checkout
 838      gitCheckoutShouldFail = true;
 839  
 840      resetAgentBehavior();
 841      const result = await runCompleteAgentCycle();
 842  
 843      if (!result.aborted) {
 844        assert.equal(result.commit, undefined);
 845      }
 846  
 847      process.env.AGENT_AUTO_COMMIT = 'false';
 848    });
 849  
 850    test('handles git push failure gracefully when AGENT_AUTO_PUSH=true', async () => {
 851      process.env.AGENT_AUTO_COMMIT = 'true';
 852      process.env.AGENT_AUTO_PUSH = 'true';
 853      process.env.AGENT_AUTO_COMMIT_BRANCH = 'autofix';
 854      gitStatusOutput = ' M src/test.js';
 855      gitBranchOutput = 'autofix';
 856      gitCommitOutput = 'push123';
 857      gitPushShouldFail = true;
 858  
 859      resetAgentBehavior();
 860      // Should not throw even when push fails
 861      const result = await runCompleteAgentCycle();
 862      if (!result.aborted) {
 863        assert.equal(result.commit, 'push123'); // commit still created, push failed
 864      }
 865  
 866      process.env.AGENT_AUTO_COMMIT = 'false';
 867      process.env.AGENT_AUTO_PUSH = 'false';
 868    });
 869  });
 870  
 871  // ── Nightmare scenario detection ───────────────────────────────────────────
 872  describe('checkNightmareScenarios (via runCompleteAgentCycle pre-check)', () => {
 873    test('detects task loop when same fix_bug context repeated > 5 times in 1 hour', async () => {
 874      const ctx = JSON.stringify({ error: 'same error loop', file: 'test.js' });
 875      for (let i = 0; i < 6; i++) {
 876        testDb
 877          .prepare(
 878            `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, created_at)
 879           VALUES ('fix_bug', 'developer', 'pending', ?, datetime('now', '-30 minutes'))`
 880          )
 881          .run(ctx);
 882      }
 883  
 884      resetAgentBehavior();
 885      const result = await runCompleteAgentCycle();
 886  
 887      // Should not crash; may or may not abort depending on severity
 888      assert.ok(result !== undefined);
 889    });
 890  
 891    test('detects stale task buildup when > 50 tasks pending more than 1 hour', async () => {
 892      for (let i = 0; i < 55; i++) {
 893        testDb
 894          .prepare(
 895            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 896           VALUES ('fix_bug', 'developer', 'pending', datetime('now', '-2 hours'))`
 897          )
 898          .run();
 899      }
 900  
 901      resetAgentBehavior();
 902      const result = await runCompleteAgentCycle();
 903  
 904      // stale_task_buildup is 'high' severity (not critical), so pre-check doesn't abort
 905      // but it may appear in post-alerts
 906      assert.ok(result !== undefined);
 907    });
 908  
 909    test('detects circuit breaker cascade when 3+ agents are blocked', async () => {
 910      const agentNames = ['developer', 'qa', 'security'];
 911      for (const name of agentNames) {
 912        testDb
 913          .prepare(`INSERT OR REPLACE INTO agent_state (agent_name, status) VALUES (?, 'blocked')`)
 914          .run(name);
 915      }
 916  
 917      resetAgentBehavior();
 918      const result = await runCompleteAgentCycle();
 919  
 920      // circuit_breaker_cascade is 'critical' - should abort
 921      if (result.aborted) {
 922        const hasCascade =
 923          result.alerts && result.alerts.some(a => a.type === 'circuit_breaker_cascade');
 924        assert.ok(hasCascade);
 925      }
 926    });
 927  
 928    test('emergency shutdown when hourly cost > $5', async () => {
 929      // Need > 333 log entries (333 * 0.015 = $4.995, 334 * 0.015 = $5.01)
 930      for (let i = 0; i < 340; i++) {
 931        testDb
 932          .prepare(
 933            `INSERT INTO agent_logs (agent_name, log_level, message, created_at)
 934           VALUES ('monitor', 'INFO', 'agent initialized', datetime('now', '-30 minutes'))`
 935          )
 936          .run();
 937      }
 938  
 939      process.env.AGENT_SYSTEM_ENABLED = 'true';
 940      resetAgentBehavior();
 941      const result = await runCompleteAgentCycle();
 942  
 943      assert.equal(result.aborted, true);
 944      // Emergency shutdown should have set AGENT_SYSTEM_ENABLED to false
 945      assert.equal(process.env.AGENT_SYSTEM_ENABLED, 'false');
 946  
 947      // Restore for subsequent tests
 948      process.env.AGENT_SYSTEM_ENABLED = 'true';
 949    });
 950  });
 951  
 952  // ── Autoscaling via parallel mode ──────────────────────────────────────────
 953  describe('calculateTaskLimit autoscaling (via parallel runCompleteAgentCycle)', () => {
 954    test('uses base limit when queue depth is normal (< 10)', async () => {
 955      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'true';
 956      process.env.AGENT_MAX_TASKS_PER_CYCLE = '5';
 957  
 958      resetAgentBehavior({ monitor: 0 });
 959      const result = await runCompleteAgentCycle({ verbose: false });
 960      assert.equal(result.enabled, true);
 961  
 962      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'false';
 963    });
 964  
 965    test('autoscales to maxLimit=20 when queue depth > 20', async () => {
 966      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'true';
 967      process.env.AGENT_MAX_TASKS_PER_CYCLE = '5';
 968  
 969      // Insert 25 pending developer tasks
 970      for (let i = 0; i < 25; i++) {
 971        testDb
 972          .prepare(
 973            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
 974           VALUES ('fix_bug', 'developer', 'pending', datetime('now'))`
 975          )
 976          .run();
 977      }
 978  
 979      resetAgentBehavior({ developer: 5 });
 980      const result = await runCompleteAgentCycle({ verbose: true });
 981  
 982      if (!result.aborted && result.agents) {
 983        // developer task_limit should be 20 (maxLimit)
 984        if (result.agents.developer && result.agents.developer.task_limit !== undefined) {
 985          assert.equal(result.agents.developer.task_limit, 20);
 986        }
 987      }
 988  
 989      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'false';
 990    });
 991  
 992    test('autoscales to 2x base when queue depth is 11-20', async () => {
 993      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'true';
 994      process.env.AGENT_MAX_TASKS_PER_CYCLE = '5';
 995  
 996      // Insert 15 pending qa tasks
 997      for (let i = 0; i < 15; i++) {
 998        testDb
 999          .prepare(
1000            `INSERT INTO agent_tasks (task_type, assigned_to, status, created_at)
1001           VALUES ('run_tests', 'qa', 'pending', datetime('now'))`
1002          )
1003          .run();
1004      }
1005  
1006      resetAgentBehavior({ qa: 3 });
1007      const result = await runCompleteAgentCycle();
1008  
1009      if (!result.aborted && result.agents) {
1010        if (result.agents.qa && result.agents.qa.task_limit !== undefined) {
1011          // task_limit should be min(5*2, 20) = 10
1012          assert.equal(result.agents.qa.task_limit, 10);
1013        }
1014      }
1015  
1016      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'false';
1017    });
1018  
1019    test('parallel run handles agent failure gracefully', async () => {
1020      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'true';
1021  
1022      resetAgentBehavior({ monitor: 1 }, { developer: true });
1023      const result = await runCompleteAgentCycle();
1024  
1025      if (!result.aborted && result.agents) {
1026        const agentNames = Object.keys(result.agents);
1027        assert.ok(agentNames.length > 0);
1028      }
1029  
1030      process.env.AGENT_RUN_TYPES_IN_PARALLEL = 'false';
1031    });
1032  });