/ __quarantined_tests__ / agents / task-manager-coverage2.test.js
task-manager-coverage2.test.js
  1  /**
  2   * task-manager-coverage2.test.js
  3   *
  4   * Covers previously uncovered paths in src/agents/utils/task-manager.js:
  5   *   - getTaskById with malformed context_json (lines 500-511)
  6   *   - getTaskById with malformed result_json (lines 500-511)
  7   *   - spawnAgentAsync pool-limit path (lines 670-689)
  8   *   - spawnAgentAsync rate-limit active path (lines 691-707)
  9   *   - spawnAgentAsync full spawn path (lines 708-738)
 10   *
 11   * Uses AGENT_SPAWN_LOAD_THRESHOLD=10 to bypass the CPU load guard so
 12   * subsequent branches are reachable in CI/test environments.
 13   */
 14  
 15  import { test, describe, beforeEach, afterEach } from 'node:test';
 16  import assert from 'node:assert/strict';
 17  import Database from 'better-sqlite3';
 18  import { mkdtempSync, rmSync } from 'fs';
 19  import { tmpdir } from 'os';
 20  import { join } from 'path';
 21  
 22  import {
 23    getTaskById,
 24    spawnAgentAsync,
 25    resetDb,
 26    resetDbConnection,
 27  } from '../../src/agents/utils/task-manager.js';
 28  
 29  // ─── Schema ───────────────────────────────────────────────────────────────────
 30  
 31  const SCHEMA = `
 32    CREATE TABLE agent_tasks (
 33      id INTEGER PRIMARY KEY AUTOINCREMENT,
 34      task_type TEXT NOT NULL,
 35      assigned_to TEXT NOT NULL,
 36      created_by TEXT,
 37      status TEXT DEFAULT 'pending',
 38      priority INTEGER DEFAULT 5,
 39      context_json TEXT,
 40      result_json TEXT,
 41      parent_task_id INTEGER,
 42      error_message TEXT,
 43      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 44      started_at DATETIME,
 45      completed_at DATETIME,
 46      retry_count INTEGER DEFAULT 0
 47    );
 48    CREATE TABLE agent_logs (
 49      id INTEGER PRIMARY KEY AUTOINCREMENT,
 50      task_id INTEGER,
 51      agent_name TEXT NOT NULL,
 52      log_level TEXT,
 53      message TEXT NOT NULL,
 54      data_json TEXT,
 55      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
 56    );
 57    CREATE TABLE agent_state (
 58      agent_name TEXT PRIMARY KEY,
 59      status TEXT DEFAULT 'idle',
 60      current_task_id INTEGER,
 61      last_heartbeat DATETIME,
 62      last_active DATETIME,
 63      config_json TEXT
 64    );
 65    CREATE TABLE cron_locks (
 66      lock_key TEXT PRIMARY KEY,
 67      description TEXT,
 68      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
 69    );
 70  `;
 71  
 72  // ─── Lifecycle ────────────────────────────────────────────────────────────────
 73  
 74  let testDir;
 75  let dbPath;
 76  let db;
 77  
 78  beforeEach(() => {
 79    testDir = mkdtempSync(join(tmpdir(), 'tm-cov2-'));
 80    dbPath = join(testDir, 'test.db');
 81    db = new Database(dbPath);
 82    db.pragma('foreign_keys = OFF');
 83    db.exec(SCHEMA);
 84  
 85    process.env.DATABASE_PATH = dbPath;
 86    process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 87    // Override CPU load threshold so load guard never triggers in test environment
 88    process.env.AGENT_SPAWN_LOAD_THRESHOLD = '10';
 89  
 90    resetDb();
 91    resetDbConnection();
 92  });
 93  
 94  afterEach(() => {
 95    delete process.env.AGENT_SPAWN_LOAD_THRESHOLD;
 96    delete process.env.DATABASE_PATH;
 97    delete process.env.AGENT_REALTIME_NOTIFICATIONS;
 98    try {
 99      db.close();
100    } catch {
101      /* already closed */
102    }
103    resetDb();
104    resetDbConnection();
105    try {
106      rmSync(testDir, { recursive: true });
107    } catch {
108      /* ignore cleanup errors */
109    }
110  });
111  
112  // ─── getTaskById: malformed JSON paths ───────────────────────────────────────
113  
114  describe('getTaskById - malformed JSON (lines 500-511)', () => {
115    test('returns null context_json when stored JSON is malformed', () => {
116      const r = db
117        .prepare(
118          "INSERT INTO agent_tasks (task_type, assigned_to, context_json) VALUES ('fix_bug', 'developer', 'invalid{json')"
119        )
120        .run();
121      const task = getTaskById(r.lastInsertRowid);
122      assert.ok(task !== null, 'task should be found by ID');
123      assert.strictEqual(task.context_json, null, 'malformed context_json should become null');
124    });
125  
126    test('returns null result_json when stored JSON is malformed', () => {
127      const r = db
128        .prepare(
129          "INSERT INTO agent_tasks (task_type, assigned_to, result_json) VALUES ('run_tests', 'qa', '{bad:json:')"
130        )
131        .run();
132      const task = getTaskById(r.lastInsertRowid);
133      assert.ok(task !== null, 'task should be found by ID');
134      assert.strictEqual(task.result_json, null, 'malformed result_json should become null');
135    });
136  
137    test('returns null context_json and null result_json when both are malformed', () => {
138      const r = db
139        .prepare(
140          "INSERT INTO agent_tasks (task_type, assigned_to, context_json, result_json) VALUES ('fix_bug', 'developer', 'not-json', '[bad')"
141        )
142        .run();
143      const task = getTaskById(r.lastInsertRowid);
144      assert.strictEqual(task.context_json, null);
145      assert.strictEqual(task.result_json, null);
146    });
147  
148    test('parses valid JSON normally (regression check)', () => {
149      const r = db
150        .prepare(
151          "INSERT INTO agent_tasks (task_type, assigned_to, context_json, result_json) VALUES ('fix_bug', 'developer', '{\"key\":1}', '{\"ok\":true}')"
152        )
153        .run();
154      const task = getTaskById(r.lastInsertRowid);
155      assert.deepStrictEqual(task.context_json, { key: 1 });
156      assert.deepStrictEqual(task.result_json, { ok: true });
157    });
158  });
159  
160  // ─── spawnAgentAsync: pool-limit path (lines 670-689) ────────────────────────
161  
162  describe('spawnAgentAsync - pool limit path (lines 670-689)', () => {
163    test('returns early when agent pool is at limit (runningCount >= typeMax)', () => {
164      // 'security' has no POOL_DEFAULTS entry → typeMax = 1
165      // Insert one active security agent → runningCount=1 >= typeMax=1 → pool full
166      db.prepare(
167        "INSERT INTO agent_state (agent_name, status, last_active) VALUES ('security', 'working', datetime('now'))"
168      ).run();
169  
170      // Should not throw — pool-limit branch returns silently
171      assert.doesNotThrow(() => spawnAgentAsync('security', 42));
172    });
173  
174    test('pool limit with triage agent (typeMax=1)', () => {
175      db.prepare(
176        "INSERT INTO agent_state (agent_name, status, last_active) VALUES ('triage', 'working', datetime('now'))"
177      ).run();
178  
179      assert.doesNotThrow(() => spawnAgentAsync('triage', 43));
180    });
181  
182    test('pool limit with architect agent (typeMax=1)', () => {
183      db.prepare(
184        "INSERT INTO agent_state (agent_name, status, last_active) VALUES ('architect', 'working', datetime('now'))"
185      ).run();
186  
187      assert.doesNotThrow(() => spawnAgentAsync('architect', 44));
188    });
189  });
190  
191  // ─── spawnAgentAsync: rate-limit path (lines 691-707) ────────────────────────
192  
193  describe('spawnAgentAsync - rate limit path (lines 691-707)', () => {
194    test('returns early when global spawn rate limit is active', () => {
195      // Insert a very recent spawn record (within 60 seconds)
196      db.prepare(
197        "INSERT INTO cron_locks (lock_key, updated_at) VALUES ('agent_spawn_ratelimit', datetime('now'))"
198      ).run();
199  
200      // No agent_state for triage → pool OK; rate limit triggers early return
201      assert.doesNotThrow(() => spawnAgentAsync('triage', 50));
202    });
203  
204    test('rate limit with fresh spawn record (5 seconds ago)', () => {
205      db.prepare(
206        "INSERT INTO cron_locks (lock_key, updated_at) VALUES ('agent_spawn_ratelimit', datetime('now', '-5 seconds'))"
207      ).run();
208  
209      assert.doesNotThrow(() => spawnAgentAsync('security', 51));
210    });
211  });
212  
213  // ─── spawnAgentAsync: full spawn path (lines 708-738) ────────────────────────
214  
215  describe('spawnAgentAsync - full spawn path (lines 708-738)', () => {
216    test('proceeds to spawn when load OK, pool OK, and no rate limit', () => {
217      // No agent_state entry for 'monitor' → runningCount=0 < typeMax=1 → pool OK
218      // No cron_locks entry → rate limit OK
219      // AGENT_SPAWN_LOAD_THRESHOLD=10 → load check passes
220  
221      // Should not throw — spawn call is made (child runs asynchronously)
222      assert.doesNotThrow(() => spawnAgentAsync('monitor', 60));
223    });
224  
225    test('proceeds to spawn when rate limit is expired (>60 seconds old)', () => {
226      // Old rate limit record (expired)
227      db.prepare(
228        "INSERT INTO cron_locks (lock_key, updated_at) VALUES ('agent_spawn_ratelimit', datetime('now', '-120 seconds'))"
229      ).run();
230  
231      // Rate limit expired → should proceed to actual spawn
232      assert.doesNotThrow(() => spawnAgentAsync('triage', 61));
233    });
234  
235    test('spawn path with developer agent (typeMax=3, pool available)', () => {
236      // developer has typeMax=3, no entries → runningCount=0 < 3 → pool OK
237      // No rate limit → proceed to spawn
238      assert.doesNotThrow(() => spawnAgentAsync('developer', 62));
239    });
240  
241    test('spawn path with qa agent (typeMax=3, one running)', () => {
242      // qa has typeMax=3
243      // Insert one active qa entry → runningCount=1 < 3 → pool OK
244      db.prepare(
245        "INSERT INTO agent_state (agent_name, status, last_active) VALUES ('qa', 'working', datetime('now'))"
246      ).run();
247  
248      assert.doesNotThrow(() => spawnAgentAsync('qa', 63));
249    });
250  });