/ __quarantined_tests__ / agents / workflows-status.test.js
workflows-status.test.js
  1  /**
  2   * Tests for workflow status and creation functions
  3   *
  4   * getBugFixWorkflowStatus, getFeatureWorkflowStatus, getRefactorWorkflowStatus,
  5   * createBugFixWorkflow, createBulkBugFixWorkflows, createFeatureWorkflow,
  6   * createRefactorWorkflow — all use a mocked task-manager and a temp SQLite DB.
  7   */
  8  
  9  import { test, describe, before, after } from 'node:test';
 10  import assert from 'node:assert/strict';
 11  import { join } from 'path';
 12  import { tmpdir } from 'os';
 13  import { unlinkSync, existsSync } from 'fs';
 14  import Database from 'better-sqlite3';
 15  
 16  // ─── Setup temp DB ────────────────────────────────────────────────────────────
 17  
 18  const TEST_DB = join(tmpdir(), `test-workflows-status-${Date.now()}.db`);
 19  process.env.DATABASE_PATH = TEST_DB;
 20  
 21  // Create minimal schema: agent_tasks only
 22  const db = new Database(TEST_DB);
 23  db.exec(`
 24    CREATE TABLE IF NOT EXISTS agent_tasks (
 25      id INTEGER PRIMARY KEY AUTOINCREMENT,
 26      task_type TEXT NOT NULL,
 27      assigned_to TEXT NOT NULL,
 28      created_by TEXT DEFAULT 'system',
 29      parent_task_id INTEGER REFERENCES agent_tasks(id) ON DELETE CASCADE,
 30      priority INTEGER DEFAULT 5,
 31      status TEXT DEFAULT 'pending',
 32      context_json TEXT,
 33      result_json TEXT,
 34      error_message TEXT,
 35      retry_count INTEGER DEFAULT 0,
 36      reviewed_by TEXT,
 37      approval_json TEXT,
 38      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 39      started_at TIMESTAMP,
 40      completed_at TIMESTAMP
 41    );
 42  `);
 43  db.close();
 44  
 45  // Mock task-manager to avoid it querying additional tables on import
 46  import { mock } from 'node:test';
 47  
 48  let nextTaskId = 1000;
 49  const createAgentTaskMock = mock.fn(async _opts => nextTaskId++);
 50  
 51  mock.module('../../src/agents/utils/task-manager.js', {
 52    namedExports: {
 53      createAgentTask: createAgentTaskMock,
 54      getAgentTask: async _id => null,
 55    },
 56  });
 57  
 58  mock.module('dotenv', {
 59    defaultExport: { config: () => {} },
 60    namedExports: { config: () => {} },
 61  });
 62  
 63  const { getBugFixWorkflowStatus, createBugFixWorkflow, createBulkBugFixWorkflows } =
 64    await import('../../src/agents/workflows/bug-fix.js');
 65  const { getFeatureWorkflowStatus, createFeatureWorkflow } =
 66    await import('../../src/agents/workflows/feature.js');
 67  const { getRefactorWorkflowStatus, createRefactorWorkflow, createBulkRefactorWorkflows } =
 68    await import('../../src/agents/workflows/refactor.js');
 69  
 70  // ─── Helpers ─────────────────────────────────────────────────────────────────
 71  
 72  function openDb() {
 73    return new Database(TEST_DB);
 74  }
 75  
 76  function insertTask(
 77    db2,
 78    { taskType = 'classify_issue', assignedTo = 'triage', status = 'pending', parentId = null } = {}
 79  ) {
 80    const result = db2
 81      .prepare(
 82        `
 83      INSERT INTO agent_tasks (task_type, assigned_to, status, parent_task_id)
 84      VALUES (?, ?, ?, ?)
 85    `
 86      )
 87      .run(taskType, assignedTo, status, parentId);
 88    return result.lastInsertRowid;
 89  }
 90  
 91  // ─── Tests ───────────────────────────────────────────────────────────────────
 92  
 93  describe('getBugFixWorkflowStatus', () => {
 94    let db2;
 95    before(() => {
 96      db2 = openDb();
 97    });
 98    after(() => {
 99      db2.close();
100    });
101  
102    test('returns empty stages for nonexistent workflow ID', async () => {
103      const status = await getBugFixWorkflowStatus(999999);
104      assert.strictEqual(status.workflow_id, 999999);
105      assert.ok(Array.isArray(status.stages));
106      assert.strictEqual(status.stages.length, 0);
107      assert.strictEqual(status.is_complete, true); // every() on empty = true
108      assert.strictEqual(status.has_failures, false);
109      assert.strictEqual(status.is_blocked, false);
110    });
111  
112    test('returns correct status for single pending task', async () => {
113      const taskId = insertTask(db2, {
114        taskType: 'classify_issue',
115        assignedTo: 'triage',
116        status: 'pending',
117      });
118  
119      const status = await getBugFixWorkflowStatus(taskId);
120      assert.strictEqual(status.workflow_id, taskId);
121      assert.strictEqual(status.stages.length, 1);
122      assert.strictEqual(status.stages[0].status, 'pending');
123      assert.strictEqual(status.stages[0].agent, 'triage');
124      assert.strictEqual(status.is_complete, false);
125      assert.strictEqual(status.has_failures, false);
126      assert.ok(status.current_stage !== undefined);
127    });
128  
129    test('reports is_complete=true when all tasks are completed', async () => {
130      const parentId = insertTask(db2, {
131        taskType: 'classify_issue',
132        assignedTo: 'triage',
133        status: 'completed',
134      });
135      insertTask(db2, {
136        taskType: 'fix_bug',
137        assignedTo: 'developer',
138        status: 'completed',
139        parentId,
140      });
141  
142      const status = await getBugFixWorkflowStatus(parentId);
143      assert.strictEqual(status.is_complete, true);
144      assert.strictEqual(status.has_failures, false);
145    });
146  
147    test('reports has_failures=true when any task is failed', async () => {
148      const parentId = insertTask(db2, {
149        taskType: 'classify_issue',
150        assignedTo: 'triage',
151        status: 'failed',
152      });
153  
154      const status = await getBugFixWorkflowStatus(parentId);
155      assert.strictEqual(status.has_failures, true);
156      assert.strictEqual(status.is_complete, false);
157    });
158  
159    test('reports is_blocked=true when any task is blocked', async () => {
160      const parentId = insertTask(db2, {
161        taskType: 'classify_issue',
162        assignedTo: 'triage',
163        status: 'blocked',
164      });
165  
166      const status = await getBugFixWorkflowStatus(parentId);
167      assert.strictEqual(status.is_blocked, true);
168    });
169  });
170  
171  describe('getFeatureWorkflowStatus', () => {
172    test('returns status for feature workflow', async () => {
173      const db2 = openDb();
174      const taskId = insertTask(db2, {
175        taskType: 'review_design',
176        assignedTo: 'architect',
177        status: 'running',
178      });
179      db2.close();
180  
181      const status = await getFeatureWorkflowStatus(taskId);
182      assert.strictEqual(status.workflow_id, taskId);
183      assert.strictEqual(status.stages.length, 1);
184      assert.strictEqual(status.stages[0].task_type, 'review_design');
185      assert.ok(status.current_stage); // running task = current stage
186      assert.strictEqual(status.is_complete, false);
187    });
188  });
189  
190  describe('getRefactorWorkflowStatus', () => {
191    test('returns status for refactor workflow', async () => {
192      const db2 = openDb();
193      const taskId = insertTask(db2, {
194        taskType: 'review_refactor',
195        assignedTo: 'architect',
196        status: 'completed',
197      });
198      db2.close();
199  
200      const status = await getRefactorWorkflowStatus(taskId);
201      assert.strictEqual(status.workflow_id, taskId);
202      assert.strictEqual(status.stages.length, 1);
203      assert.strictEqual(status.is_complete, true);
204      assert.strictEqual(status.current_stage, undefined); // no pending/running
205    });
206  });
207  
208  // ─── Create workflow tests ─────────────────────────────────────────────────────
209  
210  describe('createBugFixWorkflow', () => {
211    test('returns a task ID (number) from createAgentTask', async () => {
212      createAgentTaskMock.mock.resetCalls();
213      const id = await createBugFixWorkflow('TypeError: cannot read x', 'stack trace', 'scoring', 3);
214      assert.strictEqual(typeof id, 'number');
215      assert.strictEqual(createAgentTaskMock.mock.calls.length, 1);
216      const callArgs = createAgentTaskMock.mock.calls[0].arguments[0];
217      assert.strictEqual(callArgs.task_type, 'classify_error');
218      assert.strictEqual(callArgs.assigned_to, 'triage');
219      assert.strictEqual(callArgs.context.stage, 'scoring');
220      assert.strictEqual(callArgs.context.frequency, 3);
221    });
222  
223    test('accepts options.priority and options.created_by', async () => {
224      createAgentTaskMock.mock.resetCalls();
225      await createBugFixWorkflow('Error', '', 'enrich', 1, { priority: 8, created_by: 'monitor' });
226      const callArgs = createAgentTaskMock.mock.calls[0].arguments[0];
227      assert.strictEqual(callArgs.priority, 8);
228      assert.strictEqual(callArgs.created_by, 'monitor');
229    });
230  });
231  
232  describe('createBulkBugFixWorkflows', () => {
233    test('returns array of workflow IDs for multiple errors', async () => {
234      createAgentTaskMock.mock.resetCalls();
235      const errors = [
236        { message: 'Error A', stage: 'scoring', frequency: 2 },
237        { message: 'Error B', stage: 'enrich', frequency: 1 },
238      ];
239      const ids = await createBulkBugFixWorkflows(errors);
240      assert.ok(Array.isArray(ids));
241      assert.strictEqual(ids.length, 2);
242      assert.strictEqual(createAgentTaskMock.mock.calls.length, 2);
243    });
244  
245    test('skips erroring items and continues', async () => {
246      createAgentTaskMock.mock.resetCalls();
247      createAgentTaskMock.mock.mockImplementationOnce(async () => {
248        throw new Error('task-manager down');
249      });
250      const errors = [{ message: 'Will fail' }, { message: 'Will succeed', stage: 'scoring' }];
251      const ids = await createBulkBugFixWorkflows(errors);
252      // First item failed, second succeeded — should have 1 ID
253      assert.strictEqual(ids.length, 1);
254    });
255  });
256  
257  describe('createFeatureWorkflow', () => {
258    test('creates architect review task by default', async () => {
259      createAgentTaskMock.mock.resetCalls();
260      const id = await createFeatureWorkflow('Add OAuth login', ['must support Google']);
261      assert.strictEqual(typeof id, 'number');
262      const callArgs = createAgentTaskMock.mock.calls[0].arguments[0];
263      assert.strictEqual(callArgs.task_type, 'review_design');
264      assert.strictEqual(callArgs.assigned_to, 'architect');
265    });
266  
267    test('skips architect review when skipArchitectReview=true', async () => {
268      createAgentTaskMock.mock.resetCalls();
269      const id = await createFeatureWorkflow('Quick fix', [], { skipArchitectReview: true });
270      assert.strictEqual(typeof id, 'number');
271      const callArgs = createAgentTaskMock.mock.calls[0].arguments[0];
272      assert.strictEqual(callArgs.task_type, 'implement_feature');
273      assert.strictEqual(callArgs.assigned_to, 'developer');
274    });
275  });
276  
277  describe('createRefactorWorkflow', () => {
278    test('creates refactor workflow and returns task ID', async () => {
279      createAgentTaskMock.mock.resetCalls();
280      const id = await createRefactorWorkflow('Extract utils into separate module', ['src/utils.js']);
281      assert.strictEqual(typeof id, 'number');
282      assert.strictEqual(createAgentTaskMock.mock.calls.length, 1);
283      const callArgs = createAgentTaskMock.mock.calls[0].arguments[0];
284      assert.strictEqual(callArgs.task_type, 'suggest_refactor');
285      assert.strictEqual(callArgs.assigned_to, 'architect');
286    });
287  });
288  
289  describe('createBulkRefactorWorkflows', () => {
290    test('creates workflow for files exceeding lines threshold', async () => {
291      createAgentTaskMock.mock.resetCalls();
292      const ids = await createBulkRefactorWorkflows([
293        { path: 'src/big.js', lines: 200, complexity: 5, depth: 2 },
294      ]);
295      assert.strictEqual(ids.length, 1);
296      assert.strictEqual(createAgentTaskMock.mock.calls.length, 1);
297    });
298  
299    test('creates workflow for files exceeding complexity threshold', async () => {
300      createAgentTaskMock.mock.resetCalls();
301      const ids = await createBulkRefactorWorkflows([
302        { path: 'src/complex.js', lines: 50, complexity: 20, depth: 2 },
303      ]);
304      assert.strictEqual(ids.length, 1);
305    });
306  
307    test('creates workflow for files exceeding depth threshold', async () => {
308      createAgentTaskMock.mock.resetCalls();
309      const ids = await createBulkRefactorWorkflows([
310        { path: 'src/deep.js', lines: 50, complexity: 5, depth: 6 },
311      ]);
312      assert.strictEqual(ids.length, 1);
313    });
314  
315    test('skips files within all thresholds', async () => {
316      createAgentTaskMock.mock.resetCalls();
317      const ids = await createBulkRefactorWorkflows([
318        { path: 'src/small.js', lines: 100, complexity: 10, depth: 3 },
319      ]);
320      assert.strictEqual(ids.length, 0);
321      assert.strictEqual(createAgentTaskMock.mock.calls.length, 0);
322    });
323  
324    test('handles createAgentTask errors gracefully', async () => {
325      createAgentTaskMock.mock.resetCalls();
326      createAgentTaskMock.mock.mockImplementationOnce(async () => {
327        throw new Error('DB connection lost');
328      });
329      const ids = await createBulkRefactorWorkflows([
330        { path: 'src/heavy.js', lines: 300, complexity: 25, depth: 8 },
331      ]);
332      // Should swallow error and return empty array
333      assert.strictEqual(ids.length, 0);
334    });
335  
336    after(() => {
337      if (existsSync(TEST_DB)) {
338        try {
339          unlinkSync(TEST_DB);
340        } catch {}
341      }
342    });
343  });