/ __quarantined_tests__ / agents / immediate-invocation.test.js
immediate-invocation.test.js
  1  /**
  2   * Tests for immediate agent invocation (event-driven workflow)
  3   *
  4   * Verifies that agents invoke each other immediately instead of waiting for cron cycles.
  5   */
  6  
  7  import { test, describe, beforeEach, afterEach, mock } from 'node:test';
  8  import assert from 'node:assert/strict';
  9  import Database from 'better-sqlite3';
 10  import fs from 'fs';
 11  import { BaseAgent, resetDb as resetBaseAgentDb } from '../../src/agents/base-agent.js';
 12  import { DeveloperAgent } from '../../src/agents/developer.js';
 13  import { QAAgent } from '../../src/agents/qa.js';
 14  import {
 15    createAgentTask,
 16    getTaskById,
 17    resetDb as resetTaskManagerDb,
 18  } from '../../src/agents/utils/task-manager.js';
 19  
 20  // Use unique database per test to avoid SQLITE_READONLY_DBMOVED errors
 21  const getTestDbPath = () => `./test-immediate-invocation-${Date.now()}-${Math.random()}.db`;
 22  const SCHEMA_PATH = './db/schema.sql';
 23  
 24  /**
 25   * Simple test agent for testing immediate invocation
 26   */
 27  class TestAgent extends BaseAgent {
 28    constructor() {
 29      super('monitor', ['base.md']);
 30      // Skip context loading in tests
 31      this.context = 'Test context';
 32      this.contextMetadata = { context: 'Test context', sizeKB: 1, files: ['base.md'] };
 33      this.isInitialized = true;
 34      // Initialize agent_state row so acquireLock() can succeed
 35      this.updateAgentState('idle');
 36    }
 37  
 38    async processTask(task) {
 39      // Record that we processed the task
 40      this.updateTask(task.id, { result: { processed: true } });
 41      await this.completeTask(task.id, { success: true });
 42    }
 43  }
 44  
 45  describe('Immediate Agent Invocation', () => {
 46    let db;
 47    let testDbPath;
 48  
 49    beforeEach(() => {
 50      // Use unique database path per test
 51      testDbPath = getTestDbPath();
 52  
 53      // Clean up old test database
 54      if (fs.existsSync(testDbPath)) {
 55        fs.unlinkSync(testDbPath);
 56      }
 57  
 58      // Create test database
 59      db = new Database(testDbPath);
 60      db.pragma('foreign_keys = ON');
 61  
 62      // Load schema
 63      const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
 64      db.exec(schema);
 65  
 66      // Set test database path
 67      process.env.DATABASE_PATH = testDbPath;
 68  
 69      // Enable immediate invocation
 70      process.env.AGENT_IMMEDIATE_INVOCATION = 'true';
 71      process.env.AGENT_MAX_CHAIN_DEPTH = '10';
 72  
 73      // Disable real-time notifications (we test in-process invocation, not spawning)
 74      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
 75  
 76      // Reset database connections
 77      resetBaseAgentDb();
 78      resetTaskManagerDb();
 79    });
 80  
 81    afterEach(() => {
 82      // Clean up
 83      if (db) {
 84        try {
 85          db.close();
 86        } catch (e) {
 87          // Ignore close errors
 88        }
 89      }
 90  
 91      if (testDbPath && fs.existsSync(testDbPath)) {
 92        try {
 93          fs.unlinkSync(testDbPath);
 94        } catch (e) {
 95          // Ignore unlink errors
 96        }
 97      }
 98  
 99      delete process.env.DATABASE_PATH;
100      delete process.env.AGENT_IMMEDIATE_INVOCATION;
101      delete process.env.AGENT_MAX_CHAIN_DEPTH;
102      delete process.env.AGENT_REALTIME_NOTIFICATIONS;
103      delete process.env.AGENT_INVOCATION_DEPTH;
104    });
105  
106    test('handoff() immediately invokes receiving agent', async () => {
107      const agent1 = new TestAgent();
108      // Skip initialize() - already done in constructor
109  
110      // Create a task and hand it off (assign to 'triage' so the mocked TestAgent can pick it up)
111      const taskId = await agent1.createTask({
112        task_type: 'test_task',
113        assigned_to: 'triage',
114        priority: 5,
115        context: { step: 1 },
116      });
117  
118      // Mock getAgentClass to return TestAgent (a 'monitor' named agent)
119      // Override so TestAgent can process tasks assigned to any agent
120      agent1.getAgentClass = async () => {
121        // Return a TestAgent subclass with matching name
122        class TempAgent extends BaseAgent {
123          constructor() {
124            super('triage', ['base.md']);
125            this.context = 'Test context';
126            this.contextMetadata = { context: 'Test context', sizeKB: 1, files: ['base.md'] };
127            this.isInitialized = true;
128          }
129          async processTask(task) {
130            this.updateTask(task.id, { result: { processed: true } });
131            await this.completeTask(task.id, { success: true });
132          }
133        }
134        return TempAgent;
135      };
136  
137      // Handoff to another agent
138      const startTime = Date.now();
139      await agent1.handoff(taskId, 'triage', 'Passing control');
140      const handoffTime = Date.now() - startTime;
141  
142      // Verify handoff was fast (< 1 second)
143      assert.ok(handoffTime < 1000, `Handoff took ${handoffTime}ms, expected < 1000ms`);
144  
145      // Verify task was processed
146      const task = getTaskById(taskId);
147      assert.equal(task.status, 'completed', 'Task should be completed immediately');
148    });
149  
150    test('createTask() immediately invokes assigned agent', async () => {
151      const agent = new TestAgent();
152      // Skip initialize() - already done in constructor
153  
154      // Mock getAgentClass to return TestAgent
155      agent.getAgentClass = async () => TestAgent;
156  
157      // Create task (assign to monitor so TestAgent - which has name 'monitor' - can pick it up)
158      const startTime = Date.now();
159      const taskId = await agent.createTask({
160        task_type: 'test_task',
161        assigned_to: 'monitor',
162        priority: 5,
163        context: { data: 'test' },
164      });
165      const createTime = Date.now() - startTime;
166  
167      // Verify creation was fast (< 1 second)
168      assert.ok(createTime < 1000, `Task creation took ${createTime}ms, expected < 1000ms`);
169  
170      // Verify task was processed
171      const task = getTaskById(taskId);
172      assert.equal(task.status, 'completed', 'Task should be completed immediately');
173    });
174  
175    test('immediate invocation respects max chain depth', async () => {
176      process.env.AGENT_MAX_CHAIN_DEPTH = '3';
177  
178      const agent = new TestAgent();
179      // Skip initialize() - already done in constructor
180  
181      // Mock getAgentClass to return TestAgent
182      agent.getAgentClass = async () => TestAgent;
183  
184      // Create tasks in a chain
185      const task1 = await agent.createTask({
186        task_type: 'test_task',
187        assigned_to: 'developer',
188        priority: 5,
189      });
190  
191      // Simulate deep chain
192      process.env.AGENT_INVOCATION_DEPTH = '3';
193  
194      // This should not invoke immediately (depth limit reached)
195      const task2 = await agent.createTask({
196        task_type: 'test_task',
197        assigned_to: 'developer',
198        priority: 5,
199      });
200  
201      const task = getTaskById(task2);
202  
203      // Task should be pending (not processed due to depth limit)
204      assert.equal(task.status, 'pending', 'Task should remain pending when depth limit reached');
205    });
206  
207    test('immediate invocation can be disabled', async () => {
208      process.env.AGENT_IMMEDIATE_INVOCATION = 'false';
209  
210      const agent = new TestAgent();
211      // Skip initialize() - already done in constructor
212  
213      // Mock getAgentClass
214      agent.getAgentClass = async () => TestAgent;
215  
216      // Create task
217      const taskId = await agent.createTask({
218        task_type: 'test_task',
219        assigned_to: 'developer',
220        priority: 5,
221      });
222  
223      // Task should remain pending (immediate invocation disabled)
224      const task = getTaskById(taskId);
225      assert.equal(
226        task.status,
227        'pending',
228        'Task should remain pending when immediate invocation is disabled'
229      );
230    });
231  
232    test('immediate invocation handles errors gracefully', async () => {
233      const agent = new TestAgent();
234      // Skip initialize() - already done in constructor
235  
236      // Mock getAgentClass to throw error
237      agent.getAgentClass = async () => {
238        throw new Error('Agent not found');
239      };
240  
241      // Create task (should not throw, should fall back to cron)
242      const taskId = await agent.createTask({
243        task_type: 'test_task',
244        assigned_to: 'monitor',
245        priority: 5,
246      });
247  
248      // Task should remain pending (error handled gracefully)
249      const task = getTaskById(taskId);
250      assert.equal(
251        task.status,
252        'pending',
253        'Task should remain pending when immediate invocation fails'
254      );
255    });
256  
257    test('workflow chain completes in under 2 minutes', async () => {
258      // This test simulates the bug fix workflow:
259      // Monitor → Triage → Developer → QA
260  
261      // Create minimal test agents
262      class MonitorAgent extends BaseAgent {
263        constructor() {
264          super('monitor', ['base.md']);
265          // Skip context loading in tests
266          this.context = 'Test context';
267          this.contextMetadata = { context: 'Test context', sizeKB: 1, files: ['base.md'] };
268          this.isInitialized = true;
269        }
270        async processTask(task) {
271          // Create triage task
272          await this.createTask({
273            task_type: 'classify_error',
274            assigned_to: 'triage',
275            priority: 7,
276            context: { error: 'Test error' },
277          });
278          await this.completeTask(task.id);
279        }
280      }
281  
282      class TriageAgent extends BaseAgent {
283        constructor() {
284          super('triage', ['base.md']);
285          // Skip context loading in tests
286          this.context = 'Test context';
287          this.contextMetadata = { context: 'Test context', sizeKB: 1, files: ['base.md'] };
288          this.isInitialized = true;
289        }
290        async processTask(task) {
291          // Hand off to developer
292          await this.handoff(task.id, 'developer', 'Bug classified');
293          await this.completeTask(task.id);
294        }
295      }
296  
297      // Set up agents
298      const monitor = new MonitorAgent();
299      // Skip initialize() - already done in constructor
300  
301      // Mock getAgentClass to return appropriate agent
302      const getAgentClass = async agentName => {
303        if (agentName === 'triage') return TriageAgent;
304        if (agentName === 'developer') return TestAgent;
305        if (agentName === 'qa') return TestAgent;
306        return TestAgent;
307      };
308  
309      monitor.getAgentClass = getAgentClass;
310  
311      // Create initial monitor task
312      const startTime = Date.now();
313  
314      const monitorTaskId = await monitor.createTask({
315        task_type: 'analyze_logs',
316        assigned_to: 'monitor',
317        priority: 7,
318      });
319  
320      // Process the workflow chain
321      await monitor.pollTasks(1);
322  
323      const duration = Date.now() - startTime;
324  
325      // Workflow should complete in < 2 minutes (120,000ms)
326      // With immediate invocation, should be < 5 seconds
327      assert.ok(duration < 120000, `Workflow took ${duration}ms, expected < 120000ms`);
328      assert.ok(
329        duration < 5000,
330        `Workflow took ${duration}ms, expected < 5000ms with immediate invocation`
331      );
332    });
333  
334    test('invocation depth increments and resets correctly', async () => {
335      const agent = new TestAgent();
336      // Skip initialize() - already done in constructor
337  
338      // Mock getAgentClass
339      agent.getAgentClass = async () => TestAgent;
340  
341      // Verify initial depth is 0
342      assert.equal(
343        process.env.AGENT_INVOCATION_DEPTH,
344        undefined,
345        'Initial depth should be undefined'
346      );
347  
348      // Create task (triggers immediate invocation)
349      await agent.createTask({
350        task_type: 'test_task',
351        assigned_to: 'developer',
352        priority: 5,
353      });
354  
355      // After invocation completes, depth should be reset
356      assert.equal(
357        process.env.AGENT_INVOCATION_DEPTH,
358        undefined,
359        'Depth should be reset after invocation'
360      );
361    });
362  });