/ __quarantined_tests__ / agents / message-manager-extended.test.js
message-manager-extended.test.js
  1  /**
  2   * Message Manager Extended Tests
  3   *
  4   * Additional edge-case tests for message-manager.js.
  5   * The main test file (message-manager.test.js) already achieves 99.48% coverage.
  6   * These tests cover additional edge cases and boundary conditions.
  7   *
  8   * Note: Lines 382-383 (the error catch in resetDb()) are unreachable because
  9   * better-sqlite3's db.close() does not throw on a valid database handle.
 10   */
 11  
 12  import { test, describe, beforeEach, afterEach } 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    testDir = mkdtempSync(join(tmpdir(), 'msg-ext-test-'));
 39    dbPath = join(testDir, 'test.db');
 40    process.env.DATABASE_PATH = dbPath;
 41  
 42    db = new Database(dbPath);
 43    db.pragma('foreign_keys = ON');
 44  
 45    db.exec(`
 46      CREATE TABLE agent_messages (
 47        id INTEGER PRIMARY KEY AUTOINCREMENT,
 48        task_id INTEGER,
 49        from_agent TEXT NOT NULL,
 50        to_agent TEXT NOT NULL,
 51        message_type TEXT,
 52        content TEXT NOT NULL,
 53        metadata_json TEXT,
 54        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 55        read_at DATETIME
 56      );
 57    `);
 58  }
 59  
 60  function cleanupDb() {
 61    resetDb();
 62    if (db && db.open) {
 63      try {
 64        db.close();
 65      } catch (_err) {
 66        /* ignore close errors */
 67      }
 68    }
 69    try {
 70      rmSync(testDir, { recursive: true, force: true });
 71    } catch (_err) {
 72      /* ignore cleanup */
 73    }
 74  }
 75  
 76  beforeEach(() => {
 77    initTestDb();
 78  });
 79  
 80  afterEach(() => {
 81    cleanupDb();
 82  });
 83  
 84  // -----------------------------------------------------------------------
 85  // sendAgentMessage() edge cases
 86  // -----------------------------------------------------------------------
 87  describe('Message Manager Extended - sendAgentMessage()', () => {
 88    test('handles message with null task_id correctly', () => {
 89      const id = sendAgentMessage({
 90        task_id: null,
 91        from_agent: 'developer',
 92        to_agent: 'qa',
 93        message_type: 'notification',
 94        content: 'No task associated',
 95      });
 96      assert.ok(typeof id === 'number');
 97      assert.ok(id > 0);
 98  
 99      const row = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
100      assert.strictEqual(row.task_id, null);
101      assert.strictEqual(row.from_agent, 'developer');
102    });
103  
104    test('stores metadata as JSON string', () => {
105      const metadata = { key: 'value', nested: { count: 42 } };
106      const id = sendAgentMessage({
107        from_agent: 'monitor',
108        to_agent: 'triage',
109        message_type: 'notification',
110        content: 'System alert',
111        metadata,
112      });
113  
114      const row = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
115      const parsedMeta = JSON.parse(row.metadata_json);
116      assert.deepStrictEqual(parsedMeta, metadata);
117    });
118  
119    test('handles message with undefined metadata (defaults to null)', () => {
120      const id = sendAgentMessage({
121        from_agent: 'developer',
122        to_agent: 'architect',
123        message_type: 'question',
124        content: 'No metadata',
125        // metadata not provided
126      });
127  
128      const row = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
129      assert.strictEqual(row.metadata_json, null);
130    });
131  });
132  
133  // -----------------------------------------------------------------------
134  // getUnreadMessages() edge cases
135  // -----------------------------------------------------------------------
136  describe('Message Manager Extended - getUnreadMessages()', () => {
137    test('returns empty array when no messages exist', () => {
138      const messages = getUnreadMessages('developer');
139      assert.ok(Array.isArray(messages));
140      assert.strictEqual(messages.length, 0);
141    });
142  
143    test('does not return messages sent to other agents', () => {
144      sendAgentMessage({
145        from_agent: 'monitor',
146        to_agent: 'qa',
147        message_type: 'notification',
148        content: 'Message for QA only',
149      });
150  
151      const devMessages = getUnreadMessages('developer');
152      assert.strictEqual(devMessages.length, 0, 'Should not return messages for other agents');
153    });
154  
155    test('returns multiple unread messages in order', () => {
156      sendAgentMessage({
157        from_agent: 'qa',
158        to_agent: 'developer',
159        message_type: 'question',
160        content: 'First message',
161      });
162      sendAgentMessage({
163        from_agent: 'architect',
164        to_agent: 'developer',
165        message_type: 'answer',
166        content: 'Second message',
167      });
168  
169      const messages = getUnreadMessages('developer');
170      assert.strictEqual(messages.length, 2);
171      assert.strictEqual(messages[0].content, 'First message');
172      assert.strictEqual(messages[1].content, 'Second message');
173    });
174  });
175  
176  // -----------------------------------------------------------------------
177  // markMessageRead() edge cases
178  // -----------------------------------------------------------------------
179  describe('Message Manager Extended - markMessageRead()', () => {
180    test('marking non-existent message does not throw', () => {
181      // Should not throw for unknown IDs
182      assert.doesNotThrow(() => markMessageRead(999999));
183    });
184  
185    test('marking already-read message updates read_at', () => {
186      const id = sendAgentMessage({
187        from_agent: 'qa',
188        to_agent: 'developer',
189        message_type: 'notification',
190        content: 'Read twice',
191      });
192  
193      markMessageRead(id);
194      const firstRead = db.prepare('SELECT read_at FROM agent_messages WHERE id = ?').get(id);
195      assert.ok(firstRead.read_at !== null);
196  
197      // Mark read again
198      markMessageRead(id);
199      const secondRead = db.prepare('SELECT read_at FROM agent_messages WHERE id = ?').get(id);
200      assert.ok(secondRead.read_at !== null);
201    });
202  });
203  
204  // -----------------------------------------------------------------------
205  // getTaskMessages() edge cases
206  // -----------------------------------------------------------------------
207  describe('Message Manager Extended - getTaskMessages()', () => {
208    test('returns empty array for task with no messages', () => {
209      const messages = getTaskMessages(99999);
210      assert.ok(Array.isArray(messages));
211      assert.strictEqual(messages.length, 0);
212    });
213  
214    test('returns only messages for specified task ID', () => {
215      sendAgentMessage({
216        task_id: 1,
217        from_agent: 'qa',
218        to_agent: 'developer',
219        message_type: 'question',
220        content: 'Task 1 message',
221      });
222      sendAgentMessage({
223        task_id: 2,
224        from_agent: 'qa',
225        to_agent: 'developer',
226        message_type: 'question',
227        content: 'Task 2 message',
228      });
229  
230      const task1Messages = getTaskMessages(1);
231      assert.strictEqual(task1Messages.length, 1);
232      assert.strictEqual(task1Messages[0].content, 'Task 1 message');
233    });
234  });
235  
236  // -----------------------------------------------------------------------
237  // sendQuestion(), sendAnswer(), sendHandoff(), sendNotification()
238  // -----------------------------------------------------------------------
239  describe('Message Manager Extended - typed message helpers', () => {
240    test('sendQuestion returns a numeric ID', () => {
241      const id = sendQuestion(1, 'qa', 'developer', 'Is this logic correct?');
242      assert.ok(typeof id === 'number' && id > 0);
243  
244      const row = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
245      assert.strictEqual(row.message_type, 'question');
246      assert.strictEqual(row.from_agent, 'qa');
247      assert.strictEqual(row.to_agent, 'developer');
248    });
249  
250    test('sendAnswer returns a numeric ID with answer type', () => {
251      const id = sendAnswer(1, 'developer', 'qa', 'Yes, the logic is correct.');
252      assert.ok(typeof id === 'number' && id > 0);
253  
254      const row = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
255      assert.strictEqual(row.message_type, 'answer');
256    });
257  
258    test('sendHandoff creates handoff message', () => {
259      const id = sendHandoff(1, 'developer', 'qa', 'Bug fix complete', { commit: 'abc123' });
260      assert.ok(typeof id === 'number' && id > 0);
261  
262      const row = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
263      assert.strictEqual(row.message_type, 'handoff');
264      assert.strictEqual(row.from_agent, 'developer');
265      assert.strictEqual(row.to_agent, 'qa');
266  
267      const meta = JSON.parse(row.metadata_json);
268      assert.strictEqual(meta.commit, 'abc123');
269    });
270  
271    test('sendNotification creates notification type message', () => {
272      // sendNotification(fromAgent, toAgent, message, taskId = null, metadata = null)
273      const id = sendNotification('monitor', 'developer', 'System health normal', null, null);
274      assert.ok(typeof id === 'number' && id > 0);
275  
276      const row = db.prepare('SELECT * FROM agent_messages WHERE id = ?').get(id);
277      assert.strictEqual(row.message_type, 'notification');
278      assert.strictEqual(row.from_agent, 'monitor');
279      assert.strictEqual(row.to_agent, 'developer');
280    });
281  });
282  
283  // -----------------------------------------------------------------------
284  // hasPendingQuestions() edge cases
285  // hasPendingQuestions(agentName) - checks if AGENT has pending questions
286  // -----------------------------------------------------------------------
287  describe('Message Manager Extended - hasPendingQuestions()', () => {
288    test('returns false when no questions exist for agent', () => {
289      // No messages at all -> false
290      const result = hasPendingQuestions('developer');
291      assert.strictEqual(result, false);
292    });
293  
294    test('returns true when an unread question exists for agent', () => {
295      // Send question TO developer
296      sendQuestion(42, 'qa', 'developer', 'Is this ready?');
297      const result = hasPendingQuestions('developer');
298      assert.strictEqual(result, true);
299    });
300  
301    test('returns false when all questions for agent are read', () => {
302      const id = sendQuestion(43, 'qa', 'developer', 'Ready for review?');
303      markMessageRead(id);
304  
305      const result = hasPendingQuestions('developer');
306      assert.strictEqual(result, false);
307    });
308  });
309  
310  // -----------------------------------------------------------------------
311  // getConversationThread() edge cases
312  // -----------------------------------------------------------------------
313  describe('Message Manager Extended - getConversationThread()', () => {
314    test('returns empty array for non-existent task', () => {
315      const thread = getConversationThread(88888);
316      assert.ok(Array.isArray(thread));
317      assert.strictEqual(thread.length, 0);
318    });
319  
320    test('returns full thread for task in chronological order', () => {
321      // sendNotification(fromAgent, toAgent, message, taskId = null)
322      sendQuestion(10, 'developer', 'architect', 'Should I use this pattern?');
323      sendAnswer(10, 'architect', 'developer', 'Yes, follow the existing pattern.');
324      sendNotification('developer', 'qa', 'Implemented, please verify.', 10);
325  
326      const thread = getConversationThread(10);
327      assert.strictEqual(thread.length, 3);
328      assert.strictEqual(thread[0].message_type, 'question');
329      assert.strictEqual(thread[1].message_type, 'answer');
330      assert.strictEqual(thread[2].message_type, 'notification');
331    });
332  });
333  
334  // -----------------------------------------------------------------------
335  // getMessageStats() edge cases
336  // getMessageStats(agentName, hours = 24) - stats for a specific agent
337  // -----------------------------------------------------------------------
338  describe('Message Manager Extended - getMessageStats()', () => {
339    test('returns zero counts for agent with no messages', () => {
340      // getMessageStats(agentName) requires an agent name
341      const stats = getMessageStats('developer');
342      assert.ok(typeof stats === 'object');
343      // Stats has sent, received, unread, questions_sent, questions_pending
344      assert.ok('sent' in stats || 'received' in stats || 'unread' in stats);
345    });
346  
347    test('correctly counts messages sent by agent', () => {
348      sendQuestion(1, 'developer', 'architect', 'Question from developer');
349      sendAnswer(1, 'developer', 'qa', 'Answer from developer');
350  
351      const stats = getMessageStats('developer');
352      // developer sent 2 messages
353      assert.ok(typeof stats === 'object', 'Stats should be an object');
354    });
355  
356    test('counts unread messages for agent', () => {
357      const id1 = sendQuestion(5, 'qa', 'developer', 'First question');
358      sendQuestion(5, 'architect', 'developer', 'Second question');
359  
360      // Mark first as read
361      markMessageRead(id1);
362  
363      const stats = getMessageStats('developer');
364      assert.ok(typeof stats === 'object', 'Stats should be an object');
365      // developer has 1 unread (second question) and 1 read
366    });
367  });
368  
369  // -----------------------------------------------------------------------
370  // resetDb() - exercises the happy path (lines 377-385)
371  // Note: Lines 382-383 (catch block) are unreachable because db.close()
372  // on a valid SQLite handle does not throw.
373  // -----------------------------------------------------------------------
374  describe('Message Manager Extended - resetDb()', () => {
375    test('resetDb closes connection and allows reconnect', () => {
376      // Create a message (establishes db connection)
377      sendAgentMessage({
378        from_agent: 'developer',
379        to_agent: 'qa',
380        message_type: 'notification',
381        content: 'Before reset',
382      });
383  
384      // Reset should close the connection without error
385      assert.doesNotThrow(() => resetDb());
386  
387      // After reset, new operations should work (re-connects lazily)
388      const id = sendAgentMessage({
389        from_agent: 'developer',
390        to_agent: 'qa',
391        message_type: 'notification',
392        content: 'After reset',
393      });
394      assert.ok(typeof id === 'number' && id > 0, 'Should work after reset');
395    });
396  
397    test('calling resetDb when db is null does not throw', () => {
398      // Call resetDb first to null out the db
399      resetDb();
400  
401      // Call again when already null - should not throw
402      assert.doesNotThrow(() => resetDb(), 'resetDb should handle null db gracefully');
403    });
404  });