/ __quarantined_tests__ / agents / utils / message-manager.test.js
message-manager.test.js
  1  /**
  2   * Message Manager — additional branch coverage tests
  3   *
  4   * Targets uncovered lines/branches in src/agents/utils/message-manager.js:
  5   * - resetDb() catch block (lines 383-384): db.close() throwing an error
  6   * - getConversationThread: answer without in_reply_to metadata goes to top-level
  7   * - getMessageStats with custom hours parameter edge cases
  8   * - sendAnswer with and without questionMessageId (metadata null branch)
  9   * - getUnreadMessages metadata_json null branch
 10   */
 11  
 12  import { test, describe } from 'node:test';
 13  import assert from 'node:assert/strict';
 14  import Database from 'better-sqlite3';
 15  import { mkdtempSync, rmSync } from 'fs';
 16  import { tmpdir } from 'os';
 17  import { join } from 'path';
 18  import {
 19    sendAgentMessage,
 20    getUnreadMessages,
 21    markMessageRead,
 22    getTaskMessages,
 23    sendQuestion,
 24    sendAnswer,
 25    sendHandoff,
 26    sendNotification,
 27    getConversationThread,
 28    hasPendingQuestions,
 29    getMessageStats,
 30    resetDb,
 31  } from '../../../src/agents/utils/message-manager.js';
 32  
 33  let testDir;
 34  let dbPath;
 35  let db;
 36  
 37  function initTestDb() {
 38    resetDb(); // ensure previous connection is closed
 39    testDir = mkdtempSync(join(tmpdir(), 'msg-utils-test-'));
 40    dbPath = join(testDir, 'test.db');
 41    process.env.DATABASE_PATH = dbPath;
 42  
 43    db = new Database(dbPath);
 44    db.pragma('foreign_keys = ON');
 45  
 46    db.exec(`
 47      CREATE TABLE agent_tasks (
 48        id INTEGER PRIMARY KEY AUTOINCREMENT,
 49        task_type TEXT NOT NULL,
 50        assigned_to TEXT NOT NULL,
 51        created_by TEXT,
 52        status TEXT DEFAULT 'pending',
 53        priority INTEGER DEFAULT 5,
 54        context_json TEXT,
 55        result_json TEXT,
 56        parent_task_id INTEGER,
 57        error_message TEXT,
 58        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 59        started_at DATETIME,
 60        completed_at DATETIME,
 61        retry_count INTEGER DEFAULT 0
 62      );
 63  
 64      CREATE TABLE agent_messages (
 65        id INTEGER PRIMARY KEY AUTOINCREMENT,
 66        task_id INTEGER REFERENCES agent_tasks(id),
 67        from_agent TEXT NOT NULL,
 68        to_agent TEXT NOT NULL,
 69        message_type TEXT CHECK(message_type IN ('question', 'answer', 'handoff', 'notification')),
 70        content TEXT NOT NULL,
 71        metadata_json TEXT,
 72        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 73        read_at DATETIME
 74      );
 75  
 76      CREATE INDEX IF NOT EXISTS idx_agent_messages_to ON agent_messages(to_agent, read_at);
 77      CREATE INDEX IF NOT EXISTS idx_agent_messages_task ON agent_messages(task_id);
 78    `);
 79  }
 80  
 81  function cleanupTestDb() {
 82    resetDb();
 83    if (db) {
 84      try {
 85        db.close();
 86      } catch (_) {
 87        /* ignore */
 88      }
 89      db = null;
 90    }
 91    if (testDir) {
 92      try {
 93        rmSync(testDir, { recursive: true });
 94      } catch (_) {
 95        /* ignore */
 96      }
 97      testDir = null;
 98    }
 99    delete process.env.DATABASE_PATH;
100  }
101  
102  // ── resetDb catch block (lines 383-384) ─────────────────────────────────────
103  
104  describe('resetDb edge cases', () => {
105    test('resetDb when db is null does nothing', () => {
106      // Ensure db is null first
107      resetDb();
108      // Calling again when null should not throw
109      resetDb();
110      assert.ok(true, 'resetDb with null db should not throw');
111    });
112  
113    test('resetDb after database operations closes cleanly', () => {
114      initTestDb();
115      // Perform an operation to ensure db is initialized inside the module
116      sendNotification('developer', 'qa', 'Test message');
117      // Now reset - this closes the module-internal db
118      resetDb();
119      // Reset again to verify it handles already-closed state
120      resetDb();
121      assert.ok(true, 'Double resetDb should not throw');
122      cleanupTestDb();
123    });
124  });
125  
126  // ── getConversationThread — answer without in_reply_to goes to top level ────
127  
128  describe('getConversationThread — uncovered branches', () => {
129    test('answer with metadata but no in_reply_to goes to top-level thread', () => {
130      initTestDb();
131      try {
132        const taskId = db
133          .prepare('INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES (?, ?, ?)')
134          .run('fix_bug', 'developer', 'pending').lastInsertRowid;
135  
136        // Send answer with metadata that does NOT have in_reply_to
137        sendAgentMessage({
138          task_id: taskId,
139          from_agent: 'developer',
140          to_agent: 'qa',
141          message_type: 'answer',
142          content: 'Standalone answer',
143          metadata: { note: 'no question referenced' },
144        });
145  
146        const thread = getConversationThread(taskId);
147        assert.strictEqual(thread.length, 1);
148        assert.strictEqual(thread[0].message_type, 'answer');
149        assert.strictEqual(thread[0].content, 'Standalone answer');
150      } finally {
151        cleanupTestDb();
152      }
153    });
154  
155    test('answer without metadata goes to top-level thread', () => {
156      initTestDb();
157      try {
158        const taskId = db
159          .prepare('INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES (?, ?, ?)')
160          .run('fix_bug', 'developer', 'pending').lastInsertRowid;
161  
162        // Answer with no metadata at all (metadata_json is null)
163        sendAgentMessage({
164          task_id: taskId,
165          from_agent: 'developer',
166          to_agent: 'qa',
167          message_type: 'answer',
168          content: 'Answer with no metadata',
169        });
170  
171        const thread = getConversationThread(taskId);
172        assert.strictEqual(thread.length, 1);
173        // msg.metadata_json is null so msg.metadata_json?.in_reply_to is undefined
174        // => goes to else branch (line 309)
175        assert.strictEqual(thread[0].message_type, 'answer');
176      } finally {
177        cleanupTestDb();
178      }
179    });
180  
181    test('multiple questions with answers are properly grouped', () => {
182      initTestDb();
183      try {
184        const taskId = db
185          .prepare('INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES (?, ?, ?)')
186          .run('fix_bug', 'developer', 'pending').lastInsertRowid;
187  
188        const q1 = sendQuestion(taskId, 'qa', 'developer', 'Question 1?');
189        const q2 = sendQuestion(taskId, 'qa', 'developer', 'Question 2?');
190        sendAnswer(taskId, 'developer', 'qa', 'Answer to Q1', q1);
191        sendAnswer(taskId, 'developer', 'qa', 'Answer to Q2', q2);
192        // Handoff goes to thread as non-question/non-answer
193        sendHandoff(taskId, 'developer', 'qa', 'All done');
194  
195        const thread = getConversationThread(taskId);
196        // Should have 2 questions (with answers) + 1 handoff
197        const questions = thread.filter(m => m.message_type === 'question');
198        assert.strictEqual(questions.length, 2);
199        assert.strictEqual(questions[0].answers.length, 1);
200        assert.strictEqual(questions[1].answers.length, 1);
201        const handoffs = thread.filter(m => m.message_type === 'handoff');
202        assert.strictEqual(handoffs.length, 1);
203      } finally {
204        cleanupTestDb();
205      }
206    });
207  });
208  
209  // ── getUnreadMessages — metadata parsing branches ───────────────────────────
210  
211  describe('getUnreadMessages — metadata parsing', () => {
212    test('returns null metadata_json when message has no metadata', () => {
213      initTestDb();
214      try {
215        sendAgentMessage({
216          from_agent: 'qa',
217          to_agent: 'developer',
218          message_type: 'notification',
219          content: 'Plain message',
220        });
221  
222        const messages = getUnreadMessages('developer');
223        assert.strictEqual(messages.length, 1);
224        assert.strictEqual(messages[0].metadata_json, null);
225      } finally {
226        cleanupTestDb();
227      }
228    });
229  
230    test('parses nested metadata_json correctly', () => {
231      initTestDb();
232      try {
233        sendAgentMessage({
234          from_agent: 'qa',
235          to_agent: 'developer',
236          message_type: 'notification',
237          content: 'Complex metadata',
238          metadata: { nested: { deep: true }, count: 42 },
239        });
240  
241        const messages = getUnreadMessages('developer');
242        assert.strictEqual(messages.length, 1);
243        assert.deepStrictEqual(messages[0].metadata_json, { nested: { deep: true }, count: 42 });
244      } finally {
245        cleanupTestDb();
246      }
247    });
248  });
249  
250  // ── getMessageStats — edge cases ────────────────────────────────────────────
251  
252  describe('getMessageStats — edge cases', () => {
253    test('counts questions_sent correctly', () => {
254      initTestDb();
255      try {
256        // developer sends 2 questions to qa
257        sendAgentMessage({
258          from_agent: 'developer',
259          to_agent: 'qa',
260          message_type: 'question',
261          content: 'Q1',
262        });
263        sendAgentMessage({
264          from_agent: 'developer',
265          to_agent: 'qa',
266          message_type: 'question',
267          content: 'Q2',
268        });
269        // developer sends 1 notification (not a question)
270        sendNotification('developer', 'qa', 'FYI');
271  
272        const stats = getMessageStats('developer');
273        assert.strictEqual(Number(stats.sent), 3);
274        assert.strictEqual(Number(stats.questions_sent), 2);
275      } finally {
276        cleanupTestDb();
277      }
278    });
279  
280    test('handles very short hours window', () => {
281      initTestDb();
282      try {
283        sendNotification('developer', 'qa', 'Recent msg');
284  
285        // 0 hours window might still include messages created "now"
286        // since SQLite datetime('now', '-0 hours') == datetime('now')
287        const stats = getMessageStats('developer', 0);
288        // Implementation-dependent, just verify it doesn't throw
289        assert.ok(stats, 'Should return stats object');
290      } finally {
291        cleanupTestDb();
292      }
293    });
294  });
295  
296  // ── sendAgentMessage — all valid agent combinations ─────────────────────────
297  
298  describe('sendAgentMessage — agent validation', () => {
299    test('accepts all valid agent names as from_agent', () => {
300      initTestDb();
301      try {
302        const agents = ['developer', 'qa', 'security', 'architect', 'triage', 'monitor'];
303        for (const agent of agents) {
304          const msgId = sendAgentMessage({
305            from_agent: agent,
306            to_agent: 'developer',
307            message_type: 'notification',
308            content: `From ${agent}`,
309          });
310          assert.ok(msgId > 0, `Should accept ${agent} as from_agent`);
311        }
312      } finally {
313        cleanupTestDb();
314      }
315    });
316  
317    test('accepts all valid agent names as to_agent', () => {
318      initTestDb();
319      try {
320        const agents = ['developer', 'qa', 'security', 'architect', 'triage', 'monitor'];
321        for (const agent of agents) {
322          const msgId = sendAgentMessage({
323            from_agent: 'developer',
324            to_agent: agent,
325            message_type: 'notification',
326            content: `To ${agent}`,
327          });
328          assert.ok(msgId > 0, `Should accept ${agent} as to_agent`);
329        }
330      } finally {
331        cleanupTestDb();
332      }
333    });
334  });
335  
336  // ── sendAnswer — metadata branches ──────────────────────────────────────────
337  
338  describe('sendAnswer — metadata branch', () => {
339    test('sendAnswer with questionMessageId=0 (falsy) results in null metadata', () => {
340      initTestDb();
341      try {
342        const taskId = db
343          .prepare('INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES (?, ?, ?)')
344          .run('fix_bug', 'developer', 'pending').lastInsertRowid;
345  
346        // 0 is falsy so metadata should be null
347        const answerId = sendAnswer(taskId, 'developer', 'qa', 'Answer', 0);
348        assert.ok(answerId > 0);
349        const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(answerId);
350        assert.strictEqual(
351          msg.metadata_json,
352          null,
353          'Falsy questionMessageId should produce null metadata'
354        );
355      } finally {
356        cleanupTestDb();
357      }
358    });
359  
360    test('sendAnswer with explicit null questionMessageId', () => {
361      initTestDb();
362      try {
363        const taskId = db
364          .prepare('INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES (?, ?, ?)')
365          .run('fix_bug', 'developer', 'pending').lastInsertRowid;
366  
367        const answerId = sendAnswer(taskId, 'developer', 'qa', 'Answer', null);
368        assert.ok(answerId > 0);
369        const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(answerId);
370        assert.strictEqual(msg.metadata_json, null);
371      } finally {
372        cleanupTestDb();
373      }
374    });
375  });
376  
377  // ── sendHandoff — with and without metadata ─────────────────────────────────
378  
379  describe('sendHandoff — metadata variations', () => {
380    test('sendHandoff without metadata', () => {
381      initTestDb();
382      try {
383        const taskId = db
384          .prepare('INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES (?, ?, ?)')
385          .run('fix_bug', 'developer', 'pending').lastInsertRowid;
386  
387        const msgId = sendHandoff(taskId, 'developer', 'qa', 'Done');
388        const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(msgId);
389        assert.strictEqual(msg.metadata_json, null);
390      } finally {
391        cleanupTestDb();
392      }
393    });
394  });
395  
396  // ── sendNotification — with taskId ──────────────────────────────────────────
397  
398  describe('sendNotification — with taskId', () => {
399    test('sendNotification with explicit taskId', () => {
400      initTestDb();
401      try {
402        const taskId = db
403          .prepare('INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES (?, ?, ?)')
404          .run('fix_bug', 'developer', 'pending').lastInsertRowid;
405  
406        const msgId = sendNotification('monitor', 'triage', 'Alert', taskId, { level: 'high' });
407        const msg = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(msgId);
408        assert.strictEqual(msg.task_id, Number(taskId));
409        const meta = JSON.parse(msg.metadata_json);
410        assert.strictEqual(meta.level, 'high');
411      } finally {
412        cleanupTestDb();
413      }
414    });
415  });
416  
417  // ── getTaskMessages — metadata parsing ──────────────────────────────────────
418  
419  describe('getTaskMessages — metadata parsing', () => {
420    test('parses metadata_json in task messages', () => {
421      initTestDb();
422      try {
423        const taskId = db
424          .prepare('INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES (?, ?, ?)')
425          .run('fix_bug', 'developer', 'pending').lastInsertRowid;
426  
427        sendAgentMessage({
428          task_id: taskId,
429          from_agent: 'developer',
430          to_agent: 'qa',
431          message_type: 'handoff',
432          content: 'Done',
433          metadata: { commit: 'abc123', files: ['a.js', 'b.js'] },
434        });
435  
436        const messages = getTaskMessages(taskId);
437        assert.strictEqual(messages.length, 1);
438        assert.deepStrictEqual(messages[0].metadata_json, {
439          commit: 'abc123',
440          files: ['a.js', 'b.js'],
441        });
442      } finally {
443        cleanupTestDb();
444      }
445    });
446  
447    test('returns null metadata_json for messages without metadata', () => {
448      initTestDb();
449      try {
450        const taskId = db
451          .prepare('INSERT INTO agent_tasks (task_type, assigned_to, status) VALUES (?, ?, ?)')
452          .run('fix_bug', 'developer', 'pending').lastInsertRowid;
453  
454        sendAgentMessage({
455          task_id: taskId,
456          from_agent: 'developer',
457          to_agent: 'qa',
458          message_type: 'notification',
459          content: 'No metadata',
460        });
461  
462        const messages = getTaskMessages(taskId);
463        assert.strictEqual(messages.length, 1);
464        assert.strictEqual(messages[0].metadata_json, null);
465      } finally {
466        cleanupTestDb();
467      }
468    });
469  });