context-builder.test.js
1 /** 2 * Context Builder — additional branch coverage tests 3 * 4 * Targets uncovered branches in src/agents/utils/context-builder.js: 5 * - getDb() reuse path (line 15: db already initialised) 6 * - getCached() TTL-hit path for related tasks cache key 7 * - tryParseJSON with related-task JSON fields 8 * - extractFilePathFromContext null guard 9 * - normalizeErrorMessage null/undefined input 10 * - estimateTokens null/empty input 11 */ 12 13 import { test, describe } from 'node:test'; 14 import assert from 'node:assert/strict'; 15 import Database from 'better-sqlite3'; 16 import fs from 'fs/promises'; 17 import path from 'path'; 18 import { fileURLToPath } from 'url'; 19 import { 20 buildAgentContext, 21 clearCache, 22 resetDb, 23 } from '../../../src/agents/utils/context-builder.js'; 24 import { 25 createAgentTask, 26 completeTask, 27 failTask, 28 resetDb as resetTaskDb, 29 } from '../../../src/agents/utils/task-manager.js'; 30 31 const __filename = fileURLToPath(import.meta.url); 32 const __dirname = path.dirname(__filename); 33 34 const dbPath = path.join('/tmp', `test-ctx-builder-utils-${Date.now()}.db`); 35 36 async function initDb() { 37 try { 38 await fs.unlink(dbPath); 39 } catch (_) { 40 /* ignore */ 41 } 42 43 const db = new Database(dbPath); 44 db.pragma('foreign_keys = ON'); 45 46 const migrationsDir = path.join(__dirname, '..', '..', '..', 'db', 'migrations'); 47 const migrations = [ 48 '047-create-agent-system.sql', 49 '052-create-agent-llm-usage.sql', 50 '053-create-agent-outcomes.sql', 51 ]; 52 53 for (const f of migrations) { 54 try { 55 const sql = await fs.readFile(path.join(migrationsDir, f), 'utf8'); 56 db.exec(sql); 57 } catch (_) { 58 /* ignore missing files */ 59 } 60 } 61 62 db.close(); 63 } 64 65 async function cleanup() { 66 resetDb(); 67 resetTaskDb(); 68 clearCache(); 69 delete process.env.DATABASE_PATH; 70 delete process.env.AGENT_ENABLE_TASK_HISTORY; 71 delete process.env.AGENT_REALTIME_NOTIFICATIONS; 72 try { 73 await fs.unlink(dbPath); 74 } catch (_) { 75 /* ignore */ 76 } 77 } 78 79 describe('context-builder branch coverage', () => { 80 test('getDb() reuse — second call returns same connection (line 15 branch)', async () => { 81 await initDb(); 82 process.env.DATABASE_PATH = dbPath; 83 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 84 85 try { 86 // First buildAgentContext call initialises db 87 const ctx1 = await buildAgentContext('developer', ['base.md']); 88 // Second call should reuse the same db connection (line 14: if (!db) is false) 89 const ctx2 = await buildAgentContext('developer', ['base.md']); 90 assert.ok(ctx1.fullContext, 'First call should produce context'); 91 assert.ok(ctx2.fullContext, 'Second call should reuse db and produce context'); 92 } finally { 93 await cleanup(); 94 } 95 }); 96 97 test('getCached hit for related tasks cache key (line 215 branch)', async () => { 98 await initDb(); 99 process.env.DATABASE_PATH = dbPath; 100 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 101 102 try { 103 // Create a completed task so getRelatedTasks has something to find 104 const taskId = await createAgentTask({ 105 task_type: 'fix_bug', 106 assigned_to: 'developer', 107 context: { file_path: 'src/utils/logger.js', error_type: 'import_error' }, 108 }); 109 completeTask(taskId, { approach: 'Fixed import' }); 110 111 clearCache(); 112 113 const currentTask = { 114 context_json: { file_path: 'src/utils/logger.js', error_type: 'import_error' }, 115 }; 116 117 // First call populates the related-tasks cache 118 const ctx1 = await buildAgentContext('developer', ['base.md'], currentTask); 119 // Second call hits the cache for the related-tasks key 120 const ctx2 = await buildAgentContext('developer', ['base.md'], currentTask); 121 122 assert.strictEqual( 123 ctx1.metadata.historyStats.relatedTasks, 124 ctx2.metadata.historyStats.relatedTasks, 125 'Related task count should match from cache' 126 ); 127 } finally { 128 await cleanup(); 129 } 130 }); 131 132 test('extractFilePathFromContext returns null for null context', async () => { 133 await initDb(); 134 process.env.DATABASE_PATH = dbPath; 135 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 136 137 try { 138 clearCache(); 139 140 // currentTask.context_json is an object but with no recognisable path fields 141 // AND no error_message or stack_trace — forces extractFilePathFromContext to return null 142 const currentTask = { context_json: { random_key: 'random_value' } }; 143 const ctx = await buildAgentContext('developer', ['base.md'], currentTask); 144 assert.strictEqual(ctx.metadata.historyStats.relatedTasks, 0); 145 } finally { 146 await cleanup(); 147 } 148 }); 149 150 test('estimateTokens with null/empty string returns 0 (line 495 branch)', async () => { 151 await initDb(); 152 process.env.DATABASE_PATH = dbPath; 153 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 154 process.env.AGENT_ENABLE_TASK_HISTORY = 'false'; 155 156 try { 157 // When history is disabled, historyContext is null -> estimateTokens(null) -> 0 158 const ctx = await buildAgentContext('developer', ['base.md']); 159 assert.strictEqual(ctx.historyTokens, 0, 'Null historyContext should yield 0 tokens'); 160 assert.strictEqual(ctx.historyContext, null); 161 } finally { 162 await cleanup(); 163 } 164 }); 165 166 test('normalizeErrorMessage with null error returns "Unknown error" (line 475 branch)', async () => { 167 await initDb(); 168 process.env.DATABASE_PATH = dbPath; 169 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 170 171 try { 172 // Insert a failed task with empty string error_message 173 // normalizeErrorMessage('') → the function checks !error which is truthy for '' 174 // Actually '' is falsy so normalizeErrorMessage('') returns 'Unknown error' 175 const db = new Database(dbPath); 176 db.prepare( 177 `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, context_json, completed_at) 178 VALUES (?, ?, ?, ?, ?, datetime('now'))` 179 ).run('fix_bug', 'developer', 'failed', '', JSON.stringify({ error_type: 'test' })); 180 db.close(); 181 182 clearCache(); 183 184 const ctx = await buildAgentContext('developer', ['base.md']); 185 assert.ok(ctx.fullContext, 'Should handle empty error_message'); 186 } finally { 187 await cleanup(); 188 } 189 }); 190 191 test('normalizeErrorMessage strips file paths and home paths', async () => { 192 await initDb(); 193 process.env.DATABASE_PATH = dbPath; 194 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 195 196 try { 197 // Insert failed task with error containing file path + home path 198 const db = new Database(dbPath); 199 db.prepare( 200 `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, context_json, completed_at) 201 VALUES (?, ?, ?, ?, ?, datetime('now'))` 202 ).run( 203 'fix_bug', 204 'developer', 205 'failed', 206 'Error at /home/jason/code/333Method/src/test.js:42:10 something broke', 207 JSON.stringify({ error_type: 'runtime' }) 208 ); 209 db.close(); 210 211 clearCache(); 212 213 const ctx = await buildAgentContext('developer', ['base.md']); 214 assert.ok(ctx.fullContext); 215 // The history should contain normalised error, not raw paths 216 assert.ok( 217 ctx.historyContext.includes('Past Failures to Avoid') || 218 ctx.historyContext.includes('No historical task data'), 219 'Should have failure section or empty history' 220 ); 221 } finally { 222 await cleanup(); 223 } 224 }); 225 226 test('getRelatedTasks with only errorType (no filePath) builds single-condition query', async () => { 227 await initDb(); 228 process.env.DATABASE_PATH = dbPath; 229 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 230 231 try { 232 // Create a completed task with error_type but no file info 233 const taskId = await createAgentTask({ 234 task_type: 'fix_bug', 235 assigned_to: 'developer', 236 context: { error_type: 'timeout' }, 237 }); 238 completeTask(taskId, { approach: 'Added retry logic' }); 239 240 clearCache(); 241 242 // currentTask has only error_type, no file paths at all 243 const currentTask = { context_json: { error_type: 'timeout' } }; 244 const ctx = await buildAgentContext('developer', ['base.md'], currentTask); 245 assert.ok(typeof ctx.metadata.historyStats.relatedTasks === 'number'); 246 } finally { 247 await cleanup(); 248 } 249 }); 250 251 test('getRelatedTasks with only filePath (no errorType) builds single-condition query', async () => { 252 await initDb(); 253 process.env.DATABASE_PATH = dbPath; 254 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 255 256 try { 257 const taskId = await createAgentTask({ 258 task_type: 'fix_bug', 259 assigned_to: 'developer', 260 context: { file_path: 'src/pipeline.js' }, 261 }); 262 completeTask(taskId, { approach: 'Fixed pipeline' }); 263 264 clearCache(); 265 266 // currentTask has file_path but no error_type 267 const currentTask = { context_json: { file_path: 'src/pipeline.js' } }; 268 const ctx = await buildAgentContext('developer', ['base.md'], currentTask); 269 assert.ok(typeof ctx.metadata.historyStats.relatedTasks === 'number'); 270 } finally { 271 await cleanup(); 272 } 273 }); 274 275 test('formatSuccessfulTask returns null when result_json and outcome_result are both null (line 337)', async () => { 276 await initDb(); 277 process.env.DATABASE_PATH = dbPath; 278 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 279 280 try { 281 // Create a completed task with NO result_json 282 const db = new Database(dbPath); 283 db.prepare( 284 `INSERT INTO agent_tasks (task_type, assigned_to, status, result_json, completed_at) 285 VALUES (?, ?, ?, ?, datetime('now'))` 286 ).run('fix_bug', 'developer', 'completed', null); 287 db.close(); 288 289 clearCache(); 290 291 const ctx = await buildAgentContext('developer', ['base.md']); 292 // The task has no result data so formatSuccessfulTask returns null, it gets filtered 293 assert.ok(ctx.fullContext); 294 } finally { 295 await cleanup(); 296 } 297 }); 298 299 test('formatSuccessfulTask with duration_ms shows duration (line 353)', async () => { 300 await initDb(); 301 process.env.DATABASE_PATH = dbPath; 302 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 303 304 try { 305 const taskId = await createAgentTask({ 306 task_type: 'fix_bug', 307 assigned_to: 'developer', 308 }); 309 completeTask(taskId, { approach: 'Quick fix' }); 310 311 const db = new Database(dbPath); 312 db.prepare( 313 `INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, duration_ms, result_json) 314 VALUES (?, ?, ?, ?, ?, ?)` 315 ).run( 316 taskId, 317 'developer', 318 'fix_bug', 319 'success', 320 5000, 321 JSON.stringify({ approach: 'Quick fix' }) 322 ); 323 db.close(); 324 325 clearCache(); 326 327 const ctx = await buildAgentContext('developer', ['base.md']); 328 assert.ok( 329 ctx.historyContext.includes('Duration') || ctx.historyContext.includes('5s'), 330 'Should show duration from outcome' 331 ); 332 } finally { 333 await cleanup(); 334 } 335 }); 336 337 test('formatRelatedTask with no file info and no insight still produces output', async () => { 338 await initDb(); 339 process.env.DATABASE_PATH = dbPath; 340 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 341 342 try { 343 // Insert a task with error_type but no file in context and no approach in result 344 const db = new Database(dbPath); 345 db.prepare( 346 `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, result_json, completed_at) 347 VALUES (?, ?, ?, ?, ?, datetime('now'))` 348 ).run('fix_bug', 'developer', 'completed', JSON.stringify({ error_type: 'mem_leak' }), null); 349 db.close(); 350 351 clearCache(); 352 353 const currentTask = { context_json: { error_type: 'mem_leak' } }; 354 const ctx = await buildAgentContext('developer', ['base.md'], currentTask); 355 assert.ok(ctx.fullContext); 356 } finally { 357 await cleanup(); 358 } 359 }); 360 });