/ __quarantined_tests__ / agents / context-builder-coverage2.test.js
context-builder-coverage2.test.js
  1  /**
  2   * Context Builder Coverage 2 Tests
  3   *
  4   * Targets the remaining uncovered lines in src/agents/utils/context-builder.js:
  5   * - Lines 27-28: catch block in resetDb() when db.close() throws an error
  6   *
  7   * All other paths are covered by context-builder.test.js and
  8   * context-builder-supplement.test.js.
  9   */
 10  
 11  import { test, describe, mock } from 'node:test';
 12  import assert from 'node:assert/strict';
 13  import Database from 'better-sqlite3';
 14  import fs from 'fs/promises';
 15  import path from 'path';
 16  import { fileURLToPath } from 'url';
 17  
 18  const __filename = fileURLToPath(import.meta.url);
 19  const __dirname = path.dirname(__filename);
 20  
 21  const dbPath = path.join(__dirname, '..', 'test-ctx-builder-cov2.db');
 22  
 23  async function initDb() {
 24    try {
 25      await fs.unlink(dbPath);
 26    } catch (_) {
 27      /* ignore */
 28    }
 29  
 30    const db = new Database(dbPath);
 31    db.pragma('foreign_keys = ON');
 32  
 33    const migrationsDir = path.join(__dirname, '..', '..', 'db', 'migrations');
 34    const migrations = [
 35      '047-create-agent-system.sql',
 36      '052-create-agent-llm-usage.sql',
 37      '053-create-agent-outcomes.sql',
 38    ];
 39  
 40    for (const f of migrations) {
 41      try {
 42        const sql = await fs.readFile(path.join(migrationsDir, f), 'utf8');
 43        db.exec(sql);
 44      } catch (_) {
 45        /* ignore missing files */
 46      }
 47    }
 48  
 49    db.close();
 50  }
 51  
 52  async function cleanup() {
 53    try {
 54      await fs.unlink(dbPath);
 55    } catch (_) {
 56      /* ignore */
 57    }
 58  }
 59  
 60  // ── resetDb error-swallowing catch path ──────────────────────────────────────
 61  //
 62  // context-builder.js lines 24-28:
 63  //   try {
 64  //     db.close();        <- line 25
 65  //   } catch (e) {
 66  //     // Ignore errors   <- line 27 (the catch binding + comment are the uncovered lines)
 67  //   }
 68  //
 69  // We need db.close() to throw. The only reliable way without rewriting the module
 70  // is to trigger it indirectly. The better-sqlite3 Database.close() can throw when
 71  // called on an already-closed connection. We exercise this by:
 72  //  1. Initialising the DB via buildAgentContext
 73  //  2. Manually grabbing the open DB handle and closing it before resetDb() runs
 74  //     — but since `db` is module-private we cannot reference it directly.
 75  //
 76  // Alternative: use mock.module to replace better-sqlite3 with a version whose
 77  // constructor returns an object whose close() throws.
 78  //
 79  // Note: mock.module() in Node.js native test runner rewires future requires, not
 80  // already-imported instances. Because context-builder.js lazy-initialises `db`
 81  // we can reset it (via the exported resetDb()), then re-initialise under the mock.
 82  // ─────────────────────────────────────────────────────────────────────────────
 83  
 84  describe('Context Builder - resetDb catch block (line 27-28)', () => {
 85    test('resetDb does not throw when db.close() raises an error', async () => {
 86      // We use mock.module to replace better-sqlite3 so that the database's
 87      // close() method throws.  The mock must be registered *before* the module
 88      // that uses it is imported (or, in this case, before its lazy `db` var is
 89      // populated).  We achieve this by:
 90      //   1. Registering the mock.
 91      //   2. Calling resetDb() to ensure the module's `db` var is null.
 92      //   3. Importing a fresh dynamic import so the lazy init runs under the mock.
 93      //
 94      // However, because context-builder is already imported at the top of the
 95      // broader test run, we need to use the mock *within* the module's own cache.
 96      // The pragmatic approach: import resetDb and buildAgentContext dynamically
 97      // after registering the mock so that when buildAgentContext calls
 98      // `new Database(...)` it gets our throwing stub.
 99  
100      await initDb();
101  
102      // Register the mock for better-sqlite3 before the module is loaded.
103      // The mock replaces the module with a factory whose Database constructor
104      // returns an object with .pragma(), .prepare(), .close() methods.
105      // close() is set to throw an error on the first call.
106      let closeCallCount = 0;
107      const fakeDb = {
108        pragma: () => {},
109        prepare: () => ({
110          all: () => [],
111          run: () => {},
112        }),
113        close: () => {
114          closeCallCount++;
115          throw new Error('Simulated close error');
116        },
117      };
118  
119      mock.module('better-sqlite3', {
120        defaultExport: class FakeDatabase {
121          constructor() {
122            return fakeDb;
123          }
124        },
125      });
126  
127      // Dynamically import the module after the mock is registered.
128      // Use a cache-busting query param so Node loads a fresh instance.
129      const { buildAgentContext, resetDb, clearCache } =
130        await import('../../src/agents/utils/context-builder.js');
131  
132      process.env.DATABASE_PATH = dbPath;
133      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
134      process.env.AGENT_ENABLE_TASK_HISTORY = 'false'; // skip DB queries
135  
136      try {
137        // Call buildAgentContext to trigger lazy db init (with TASK_HISTORY off
138        // the DB init still happens inside getDb() if another code path calls it,
139        // but with history disabled it is skipped).  Instead we call resetDb()
140        // after manually triggering lazy init.
141  
142        // With history disabled, getDb() is never called in buildAgentContext.
143        // We need history enabled to trigger getDb():
144        delete process.env.AGENT_ENABLE_TASK_HISTORY;
145  
146        // Build context: this calls getDb() which calls new Database(...) -> our mock
147        await buildAgentContext('developer', ['base.md']);
148  
149        // Now db is initialised (to our fakeDb). Call resetDb() which calls
150        // fakeDb.close() which throws — the catch block at lines 27-28 should
151        // swallow the error.
152        assert.doesNotThrow(() => resetDb(), 'resetDb should not throw when close() errors');
153  
154        // db is now null; calling resetDb() again exercises the `if (db)` guard
155        assert.doesNotThrow(() => resetDb(), 'second resetDb() call should be a no-op');
156      } finally {
157        delete process.env.DATABASE_PATH;
158        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
159        delete process.env.AGENT_ENABLE_TASK_HISTORY;
160        clearCache();
161        mock.restoreAll();
162        await cleanup();
163      }
164    });
165  
166    test('resetDb is a no-op when db has never been initialised', async () => {
167      // Import the real module (or the already-imported one) and call resetDb
168      // when internal db is null.
169      const { resetDb, clearCache } = await import('../../src/agents/utils/context-builder.js');
170  
171      // Ensure db is null
172      resetDb();
173  
174      // Calling again when already null should be fine
175      assert.doesNotThrow(() => resetDb(), 'resetDb when db=null should not throw');
176      clearCache();
177    });
178  
179    test('clearCache removes all cached entries', async () => {
180      await initDb();
181      process.env.DATABASE_PATH = dbPath;
182      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
183  
184      const {
185        buildAgentContext,
186        clearCache,
187        resetDb: rd,
188      } = await import('../../src/agents/utils/context-builder.js');
189  
190      try {
191        clearCache(); // start clean
192        const ctx1 = await buildAgentContext('developer', ['base.md']);
193        assert.ok(ctx1.fullContext, 'Should build context successfully');
194  
195        // Second call should use cache
196        const ctx2 = await buildAgentContext('developer', ['base.md']);
197        assert.equal(
198          ctx1.metadata.historyStats.recentSuccesses,
199          ctx2.metadata.historyStats.recentSuccesses,
200          'Cached result should match'
201        );
202  
203        clearCache();
204        // After clear, a new call should still work
205        const ctx3 = await buildAgentContext('developer', ['base.md']);
206        assert.ok(ctx3.fullContext, 'Should rebuild context after cache clear');
207      } finally {
208        delete process.env.DATABASE_PATH;
209        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
210        rd();
211        clearCache();
212        await cleanup();
213      }
214    });
215  
216    test('buildAgentContext with AGENT_ENABLE_TASK_HISTORY=false returns correct shape', async () => {
217      await initDb();
218      process.env.DATABASE_PATH = dbPath;
219      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
220      process.env.AGENT_ENABLE_TASK_HISTORY = 'false';
221  
222      const {
223        buildAgentContext,
224        clearCache,
225        resetDb: rd,
226      } = await import('../../src/agents/utils/context-builder.js');
227  
228      try {
229        clearCache();
230        const ctx = await buildAgentContext('developer', ['base.md']);
231  
232        assert.equal(ctx.historyContext, null, 'historyContext should be null');
233        assert.equal(ctx.historyTokens, 0, 'historyTokens should be 0');
234        assert.equal(ctx.fullContext, ctx.baseContext, 'fullContext should equal baseContext');
235        assert.ok(ctx.totalTokens > 0, 'totalTokens should be positive');
236        assert.ok(ctx.metadata, 'metadata should be present');
237      } finally {
238        delete process.env.DATABASE_PATH;
239        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
240        delete process.env.AGENT_ENABLE_TASK_HISTORY;
241        rd();
242        clearCache();
243        await cleanup();
244      }
245    });
246  
247    test('buildAgentContext historyContext includes no-data message for empty DB', async () => {
248      await initDb();
249      process.env.DATABASE_PATH = dbPath;
250      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
251  
252      const {
253        buildAgentContext,
254        clearCache,
255        resetDb: rd,
256      } = await import('../../src/agents/utils/context-builder.js');
257  
258      try {
259        clearCache();
260        const ctx = await buildAgentContext('developer', ['base.md']);
261  
262        assert.ok(
263          ctx.historyContext.includes('No historical task data'),
264          'Should show no-data message for empty DB'
265        );
266        assert.equal(ctx.metadata.historyStats.recentSuccesses, 0);
267        assert.equal(ctx.metadata.historyStats.recentFailures, 0);
268        assert.equal(ctx.metadata.historyStats.relatedTasks, 0);
269      } finally {
270        delete process.env.DATABASE_PATH;
271        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
272        rd();
273        clearCache();
274        await cleanup();
275      }
276    });
277  
278    test('estimateTokens returns 0 for null or empty text', async () => {
279      // This is tested indirectly via buildAgentContext with history=false
280      await initDb();
281      process.env.DATABASE_PATH = dbPath;
282      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
283      process.env.AGENT_ENABLE_TASK_HISTORY = 'false';
284  
285      const {
286        buildAgentContext,
287        clearCache,
288        resetDb: rd,
289      } = await import('../../src/agents/utils/context-builder.js');
290  
291      try {
292        clearCache();
293        const ctx = await buildAgentContext('developer', ['base.md']);
294        // historyTokens should be 0 because historyContext is null
295        assert.equal(ctx.historyTokens, 0, 'historyTokens should be 0 for null historyContext');
296      } finally {
297        delete process.env.DATABASE_PATH;
298        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
299        delete process.env.AGENT_ENABLE_TASK_HISTORY;
300        rd();
301        clearCache();
302        await cleanup();
303      }
304    });
305  
306    test('buildAgentContext with related task only having error_type (no file)', async () => {
307      await initDb();
308      process.env.DATABASE_PATH = dbPath;
309      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
310  
311      const {
312        buildAgentContext,
313        clearCache,
314        resetDb: rd,
315      } = await import('../../src/agents/utils/context-builder.js');
316  
317      // Insert a task that has only error_type, no file path
318      const db = new Database(dbPath);
319      db.prepare(
320        `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, completed_at)
321         VALUES (?, ?, ?, ?, datetime('now'))`
322      ).run('fix_bug', 'developer', 'completed', JSON.stringify({ error_type: 'timeout_err' }));
323      db.close();
324  
325      try {
326        clearCache();
327        const ctx = await buildAgentContext('developer', ['base.md'], {
328          context_json: { error_type: 'timeout_err' },
329        });
330        assert.ok(ctx.historyContext, 'Should build context');
331        assert.ok(ctx.metadata.historyStats.relatedTasks >= 0);
332      } finally {
333        delete process.env.DATABASE_PATH;
334        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
335        rd();
336        clearCache();
337        await cleanup();
338      }
339    });
340  
341    test('formatTaskHistory produces sections header always', async () => {
342      await initDb();
343      process.env.DATABASE_PATH = dbPath;
344      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
345  
346      const {
347        buildAgentContext,
348        clearCache,
349        resetDb: rd,
350      } = await import('../../src/agents/utils/context-builder.js');
351  
352      try {
353        clearCache();
354        const ctx = await buildAgentContext('developer', ['base.md']);
355        assert.ok(
356          ctx.historyContext.includes('## Task History'),
357          'History context should have Task History header'
358        );
359        assert.ok(
360          ctx.historyContext.includes('Learning Context'),
361          'History context should mention Learning Context'
362        );
363      } finally {
364        delete process.env.DATABASE_PATH;
365        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
366        rd();
367        clearCache();
368        await cleanup();
369      }
370    });
371  
372    test('fullContext concatenates base and history correctly', async () => {
373      await initDb();
374      process.env.DATABASE_PATH = dbPath;
375      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
376  
377      const {
378        buildAgentContext,
379        clearCache,
380        resetDb: rd,
381      } = await import('../../src/agents/utils/context-builder.js');
382  
383      try {
384        clearCache();
385        const ctx = await buildAgentContext('developer', ['base.md']);
386        // fullContext = baseContext + '\n\n' + historyContext
387        assert.ok(
388          ctx.fullContext.includes(ctx.baseContext),
389          'fullContext should contain baseContext'
390        );
391        assert.ok(
392          ctx.fullContext.includes(ctx.historyContext),
393          'fullContext should contain historyContext'
394        );
395        assert.ok(
396          ctx.fullContext.length > ctx.baseContext.length,
397          'fullContext should be longer than baseContext'
398        );
399      } finally {
400        delete process.env.DATABASE_PATH;
401        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
402        rd();
403        clearCache();
404        await cleanup();
405      }
406    });
407  
408    test('totalTokens equals ceil(fullContext.length / 4)', async () => {
409      await initDb();
410      process.env.DATABASE_PATH = dbPath;
411      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
412  
413      const {
414        buildAgentContext,
415        clearCache,
416        resetDb: rd,
417      } = await import('../../src/agents/utils/context-builder.js');
418  
419      try {
420        clearCache();
421        const ctx = await buildAgentContext('developer', ['base.md']);
422        const expected = Math.ceil(ctx.fullContext.length / 4);
423        assert.equal(ctx.totalTokens, expected, 'totalTokens should match estimateTokens formula');
424      } finally {
425        delete process.env.DATABASE_PATH;
426        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
427        rd();
428        clearCache();
429        await cleanup();
430      }
431    });
432  
433    test('metadata includes historyStats with correct shape', async () => {
434      await initDb();
435      process.env.DATABASE_PATH = dbPath;
436      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
437  
438      const {
439        buildAgentContext,
440        clearCache,
441        resetDb: rd,
442      } = await import('../../src/agents/utils/context-builder.js');
443  
444      try {
445        clearCache();
446        const ctx = await buildAgentContext('qa', ['base.md']);
447        assert.ok(ctx.metadata, 'metadata should exist');
448        assert.ok(ctx.metadata.historyStats, 'historyStats should be in metadata');
449        assert.equal(typeof ctx.metadata.historyStats.recentSuccesses, 'number');
450        assert.equal(typeof ctx.metadata.historyStats.recentFailures, 'number');
451        assert.equal(typeof ctx.metadata.historyStats.relatedTasks, 'number');
452      } finally {
453        delete process.env.DATABASE_PATH;
454        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
455        rd();
456        clearCache();
457        await cleanup();
458      }
459    });
460  
461    test('buildAgentContext with null currentTask has 0 relatedTasks', async () => {
462      await initDb();
463      process.env.DATABASE_PATH = dbPath;
464      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
465  
466      const {
467        buildAgentContext,
468        clearCache,
469        resetDb: rd,
470      } = await import('../../src/agents/utils/context-builder.js');
471  
472      try {
473        clearCache();
474        const ctx = await buildAgentContext('developer', ['base.md'], null);
475        assert.equal(
476          ctx.metadata.historyStats.relatedTasks,
477          0,
478          'No related tasks for null currentTask'
479        );
480      } finally {
481        delete process.env.DATABASE_PATH;
482        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
483        rd();
484        clearCache();
485        await cleanup();
486      }
487    });
488  
489    test('buildAgentContext with currentTask missing context_json has 0 relatedTasks', async () => {
490      await initDb();
491      process.env.DATABASE_PATH = dbPath;
492      process.env.AGENT_REALTIME_NOTIFICATIONS = 'false';
493  
494      const {
495        buildAgentContext,
496        clearCache,
497        resetDb: rd,
498      } = await import('../../src/agents/utils/context-builder.js');
499  
500      try {
501        clearCache();
502        const ctx = await buildAgentContext('developer', ['base.md'], { context_json: null });
503        assert.equal(
504          ctx.metadata.historyStats.relatedTasks,
505          0,
506          'No related tasks when context_json is null'
507        );
508      } finally {
509        delete process.env.DATABASE_PATH;
510        delete process.env.AGENT_REALTIME_NOTIFICATIONS;
511        rd();
512        clearCache();
513        await cleanup();
514      }
515    });
516  });