context-builder-supplement.test.js
1 /** 2 * Context Builder Supplement Tests 3 * 4 * Covers additional untested paths in src/agents/utils/context-builder.js: 5 * - formatSuccessfulTask: null result (returns null), result with only approach 6 * - formatFailedTask: null error_message, context with error_type, file (not file_path) 7 * - formatRelatedTask: success outcome with 'completed' status, failed outcome icon 8 * - extractFilePathFromContext: all candidate fields, error_message regex, stack_trace regex 9 * - normalizeErrorMessage: null/undefined, long error, file paths, home paths 10 * - getRelatedTasks: only errorType (no filePath), only filePath (no errorType) 11 * - Cache: getCached TTL expiry path 12 * - buildAgentContext: history disabled, no current task, various combinations 13 */ 14 15 import { test, describe } from 'node:test'; 16 import assert from 'node:assert/strict'; 17 import Database from 'better-sqlite3'; 18 import fs from 'fs/promises'; 19 import path from 'path'; 20 import { fileURLToPath } from 'url'; 21 import { buildAgentContext, clearCache, resetDb } from '../../src/agents/utils/context-builder.js'; 22 import { 23 createAgentTask, 24 completeTask, 25 failTask, 26 resetDb as resetTaskDb, 27 } from '../../src/agents/utils/task-manager.js'; 28 29 const __filename = fileURLToPath(import.meta.url); 30 const __dirname = path.dirname(__filename); 31 32 // ── DB Helpers ───────────────────────────────────────────────────────────── 33 34 const dbPath = path.join(__dirname, '..', 'test-ctx-builder-supp.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 try { 67 await fs.unlink(dbPath); 68 } catch (_) { 69 /* ignore */ 70 } 71 } 72 73 function setTestDb() { 74 process.env.DATABASE_PATH = dbPath; 75 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 76 } 77 78 function clearTestEnv() { 79 delete process.env.DATABASE_PATH; 80 delete process.env.AGENT_REALTIME_NOTIFICATIONS; 81 delete process.env.AGENT_ENABLE_TASK_HISTORY; 82 } 83 84 function teardown() { 85 clearTestEnv(); 86 resetDb(); 87 resetTaskDb(); 88 clearCache(); 89 } 90 91 // ── buildAgentContext: history disabled ───────────────────────────────────── 92 93 describe('buildAgentContext - AGENT_ENABLE_TASK_HISTORY=false', () => { 94 test('returns null historyContext and 0 historyTokens when disabled', async () => { 95 await initDb(); 96 setTestDb(); 97 process.env.AGENT_ENABLE_TASK_HISTORY = 'false'; 98 99 try { 100 const ctx = await buildAgentContext('developer', ['base.md']); 101 assert.equal(ctx.historyContext, null); 102 assert.equal(ctx.historyTokens, 0); 103 assert.equal(ctx.fullContext, ctx.baseContext); 104 assert.ok(typeof ctx.totalTokens === 'number'); 105 assert.ok(ctx.totalTokens > 0); 106 } finally { 107 teardown(); 108 await cleanup(); 109 } 110 }); 111 }); 112 113 // ── buildAgentContext: no history data ──────────────────────────────────── 114 115 describe('buildAgentContext - empty database', () => { 116 test('shows "No historical task data" message when no tasks exist', async () => { 117 await initDb(); 118 setTestDb(); 119 120 try { 121 const ctx = await buildAgentContext('developer', ['base.md']); 122 assert.ok(ctx.historyContext.includes('No historical task data')); 123 assert.equal(ctx.metadata.historyStats.recentSuccesses, 0); 124 assert.equal(ctx.metadata.historyStats.recentFailures, 0); 125 assert.equal(ctx.metadata.historyStats.relatedTasks, 0); 126 } finally { 127 teardown(); 128 await cleanup(); 129 } 130 }); 131 132 test('totalTokens >= historyTokens', async () => { 133 await initDb(); 134 setTestDb(); 135 136 try { 137 const ctx = await buildAgentContext('developer', ['base.md']); 138 assert.ok(ctx.totalTokens >= ctx.historyTokens); 139 } finally { 140 teardown(); 141 await cleanup(); 142 } 143 }); 144 }); 145 146 // ── buildAgentContext: success path details ──────────────────────────────── 147 148 describe('buildAgentContext - successful tasks formatting', () => { 149 test('success with files_changed shows file in context', async () => { 150 await initDb(); 151 setTestDb(); 152 153 try { 154 const taskId = await createAgentTask({ 155 task_type: 'fix_bug', 156 assigned_to: 'developer', 157 context: { file_path: 'src/scoring.js' }, 158 }); 159 // completeTask stores result_json with files_changed and approach 160 completeTask(taskId, { 161 files_changed: ['src/scoring.js', 'tests/scoring.test.js'], 162 approach: 'Added null guard', 163 }); 164 165 clearCache(); 166 const ctx = await buildAgentContext('developer', ['base.md']); 167 // Should show the file from files_changed 168 assert.ok( 169 ctx.historyContext.includes('src/scoring.js'), 170 'Should include file from files_changed' 171 ); 172 assert.equal(ctx.metadata.historyStats.recentSuccesses, 1); 173 } finally { 174 teardown(); 175 await cleanup(); 176 } 177 }); 178 179 test('success with file_path (not files_changed) shows file in context', async () => { 180 await initDb(); 181 setTestDb(); 182 183 try { 184 const taskId = await createAgentTask({ 185 task_type: 'write_tests', 186 assigned_to: 'developer', 187 context: {}, 188 }); 189 completeTask(taskId, { file_path: 'tests/enrich.test.js', action_taken: 'Wrote unit tests' }); 190 191 const db = new Database(dbPath); 192 db.prepare( 193 'INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, result_json) VALUES (?, ?, ?, ?, ?)' 194 ).run( 195 taskId, 196 'developer', 197 'write_tests', 198 'success', 199 JSON.stringify({ file_path: 'tests/enrich.test.js', action_taken: 'Wrote unit tests' }) 200 ); 201 db.close(); 202 203 clearCache(); 204 const ctx = await buildAgentContext('developer', ['base.md']); 205 assert.ok(ctx.historyContext.includes('tests/enrich.test.js')); 206 assert.ok(ctx.historyContext.includes('Wrote unit tests')); 207 } finally { 208 teardown(); 209 await cleanup(); 210 } 211 }); 212 213 test('success task with duration shows duration in seconds', async () => { 214 await initDb(); 215 setTestDb(); 216 217 try { 218 const taskId = await createAgentTask({ 219 task_type: 'fix_bug', 220 assigned_to: 'developer', 221 context: {}, 222 }); 223 completeTask(taskId, { approach: 'Quick fix' }); 224 225 // Insert an outcome with duration_ms so the JOIN returns it 226 const db = new Database(dbPath); 227 db.prepare( 228 'INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, duration_ms, result_json) VALUES (?, ?, ?, ?, ?, ?)' 229 ).run( 230 taskId, 231 'developer', 232 'fix_bug', 233 'success', 234 5000, 235 JSON.stringify({ approach: 'Quick fix' }) 236 ); 237 db.close(); 238 239 clearCache(); 240 const ctx = await buildAgentContext('developer', ['base.md']); 241 // The duration comes from the JOIN with agent_outcomes (duration_ms=5000 -> 5s) 242 // The outcome_result has approach 'Quick fix' 243 // task.result_json has approach 'Quick fix' (from completeTask) 244 // Either way context should build successfully with 1 success 245 assert.equal(ctx.metadata.historyStats.recentSuccesses, 1); 246 assert.ok(ctx.historyContext.includes('fix_bug'), 'Should include task type'); 247 } finally { 248 teardown(); 249 await cleanup(); 250 } 251 }); 252 253 test('success task with null result returns null from formatSuccessfulTask (filtered out)', async () => { 254 await initDb(); 255 setTestDb(); 256 257 try { 258 // Insert task directly with null result_json 259 const db = new Database(dbPath); 260 db.prepare( 261 `INSERT INTO agent_tasks (task_type, assigned_to, status, result_json, completed_at) 262 VALUES (?, ?, ?, ?, datetime('now'))` 263 ).run('fix_bug', 'developer', 'completed', null); 264 db.close(); 265 266 clearCache(); 267 const ctx = await buildAgentContext('developer', ['base.md']); 268 // Task is counted as success but formatSuccessfulTask returns null (no result) -> filtered 269 assert.ok(ctx.fullContext, 'Context should still be built'); 270 } finally { 271 teardown(); 272 await cleanup(); 273 } 274 }); 275 }); 276 277 // ── buildAgentContext: failure path details ──────────────────────────────── 278 279 describe('buildAgentContext - failed tasks formatting', () => { 280 test('failed task with error_message shows in context', async () => { 281 await initDb(); 282 setTestDb(); 283 284 try { 285 const taskId = await createAgentTask({ 286 task_type: 'fix_bug', 287 assigned_to: 'developer', 288 context: {}, 289 }); 290 failTask(taskId, 'TypeError: Cannot read property of undefined'); 291 292 clearCache(); 293 const ctx = await buildAgentContext('developer', ['base.md']); 294 assert.ok(ctx.historyContext.includes('Past Failures')); 295 assert.equal(ctx.metadata.historyStats.recentFailures, 1); 296 } finally { 297 teardown(); 298 await cleanup(); 299 } 300 }); 301 302 test('failed task with null error_message is filtered out of display', async () => { 303 await initDb(); 304 setTestDb(); 305 306 try { 307 const db = new Database(dbPath); 308 db.prepare( 309 `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, completed_at) 310 VALUES (?, ?, ?, ?, datetime('now'))` 311 ).run('fix_bug', 'developer', 'failed', null); 312 db.close(); 313 314 clearCache(); 315 const ctx = await buildAgentContext('developer', ['base.md']); 316 // Failure exists in DB but formatFailedTask returns null -> not displayed 317 assert.ok(ctx.fullContext, 'Context should still build'); 318 assert.ok(typeof ctx.metadata.historyStats.recentFailures === 'number'); 319 } finally { 320 teardown(); 321 await cleanup(); 322 } 323 }); 324 325 test('failed task with context.error_type shows error type', async () => { 326 await initDb(); 327 setTestDb(); 328 329 try { 330 const taskId = await createAgentTask({ 331 task_type: 'fix_bug', 332 assigned_to: 'developer', 333 context: { error_type: 'auth_failure', file_path: 'src/auth.js' }, 334 }); 335 failTask(taskId, 'Authentication token expired'); 336 337 clearCache(); 338 const ctx = await buildAgentContext('developer', ['base.md']); 339 assert.ok(ctx.historyContext.includes('auth_failure')); 340 } finally { 341 teardown(); 342 await cleanup(); 343 } 344 }); 345 346 test('failed task with context.file (not file_path) shows file', async () => { 347 await initDb(); 348 setTestDb(); 349 350 try { 351 const db = new Database(dbPath); 352 db.prepare( 353 `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, context_json, completed_at) 354 VALUES (?, ?, ?, ?, ?, datetime('now'))` 355 ).run( 356 'fix_bug', 357 'developer', 358 'failed', 359 'Something broke', 360 JSON.stringify({ file: 'src/sms.js', error_type: 'send_failed' }) 361 ); 362 db.close(); 363 364 clearCache(); 365 const ctx = await buildAgentContext('developer', ['base.md']); 366 assert.ok(ctx.historyContext.includes('src/sms.js')); 367 } finally { 368 teardown(); 369 await cleanup(); 370 } 371 }); 372 373 test('error message with file path gets normalized', async () => { 374 await initDb(); 375 setTestDb(); 376 377 try { 378 const taskId = await createAgentTask({ 379 task_type: 'fix_bug', 380 assigned_to: 'developer', 381 context: {}, 382 }); 383 failTask(taskId, 'Error in /home/user/code/src/scrape.js:42:3 - undefined is not a function'); 384 385 clearCache(); 386 const ctx = await buildAgentContext('developer', ['base.md']); 387 // normalizeErrorMessage should replace /home paths and file paths 388 assert.ok( 389 ctx.historyContext.includes('[path]') || ctx.historyContext.includes('[file:line]') 390 ); 391 } finally { 392 teardown(); 393 await cleanup(); 394 } 395 }); 396 }); 397 398 // ── buildAgentContext: related tasks ─────────────────────────────────────── 399 400 describe('buildAgentContext - related tasks', () => { 401 test('no related tasks when currentTask has null context_json', async () => { 402 await initDb(); 403 setTestDb(); 404 405 try { 406 const ctx = await buildAgentContext('developer', ['base.md'], { context_json: null }); 407 assert.equal(ctx.metadata.historyStats.relatedTasks, 0); 408 } finally { 409 teardown(); 410 await cleanup(); 411 } 412 }); 413 414 test('no related tasks when context has no file_path or error_type', async () => { 415 await initDb(); 416 setTestDb(); 417 418 try { 419 const ctx = await buildAgentContext('developer', ['base.md'], { 420 context_json: { description: 'just some context without path or error_type' }, 421 }); 422 assert.equal(ctx.metadata.historyStats.relatedTasks, 0); 423 } finally { 424 teardown(); 425 await cleanup(); 426 } 427 }); 428 429 test('finds related tasks by error_type only (no file_path)', async () => { 430 await initDb(); 431 setTestDb(); 432 433 try { 434 const taskId = await createAgentTask({ 435 task_type: 'fix_bug', 436 assigned_to: 'developer', 437 context: { error_type: 'rate_limit' }, 438 }); 439 completeTask(taskId, { approach: 'Added retry logic' }); 440 441 clearCache(); 442 443 const ctx = await buildAgentContext('developer', ['base.md'], { 444 context_json: { error_type: 'rate_limit' }, 445 }); 446 assert.ok(ctx.metadata.historyStats.relatedTasks >= 1); 447 } finally { 448 teardown(); 449 await cleanup(); 450 } 451 }); 452 453 test('finds related tasks by file_path only (no error_type)', async () => { 454 await initDb(); 455 setTestDb(); 456 457 try { 458 const taskId = await createAgentTask({ 459 task_type: 'fix_bug', 460 assigned_to: 'developer', 461 context: { file_path: 'src/capture.js' }, 462 }); 463 completeTask(taskId, { approach: 'Fixed capture timeout' }); 464 465 clearCache(); 466 467 const ctx = await buildAgentContext('developer', ['base.md'], { 468 context_json: { file_path: 'src/capture.js' }, 469 }); 470 assert.ok(ctx.metadata.historyStats.relatedTasks >= 1); 471 } finally { 472 teardown(); 473 await cleanup(); 474 } 475 }); 476 477 test('related task with completed status shows checkmark icon', async () => { 478 await initDb(); 479 setTestDb(); 480 481 try { 482 const taskId = await createAgentTask({ 483 task_type: 'fix_bug', 484 assigned_to: 'developer', 485 context: { file_path: 'src/score.js' }, 486 }); 487 completeTask(taskId, { approach: 'Fixed score bug' }); 488 489 clearCache(); 490 491 const ctx = await buildAgentContext('developer', ['base.md'], { 492 context_json: { file_path: 'src/score.js' }, 493 }); 494 // Completed status should use checkmark icon 495 assert.ok(ctx.historyContext.includes('✓') || ctx.historyContext.includes('completed')); 496 } finally { 497 teardown(); 498 await cleanup(); 499 } 500 }); 501 502 test('related task with failed status shows X icon', async () => { 503 await initDb(); 504 setTestDb(); 505 506 try { 507 const db = new Database(dbPath); 508 db.prepare( 509 `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, context_json, completed_at) 510 VALUES (?, ?, ?, ?, ?, datetime('now'))` 511 ).run( 512 'fix_bug', 513 'developer', 514 'failed', 515 'Failed hard', 516 JSON.stringify({ file_path: 'src/enrich.js' }) 517 ); 518 db.close(); 519 520 clearCache(); 521 522 const ctx = await buildAgentContext('developer', ['base.md'], { 523 context_json: { file_path: 'src/enrich.js' }, 524 }); 525 assert.ok(ctx.historyContext.includes('✗') || ctx.historyContext.includes('failed')); 526 } finally { 527 teardown(); 528 await cleanup(); 529 } 530 }); 531 532 test('related task insight shows "what worked" for completed task with approach', async () => { 533 await initDb(); 534 setTestDb(); 535 536 try { 537 const taskId = await createAgentTask({ 538 task_type: 'fix_bug', 539 assigned_to: 'developer', 540 context: { file_path: 'src/proposals.js' }, 541 }); 542 completeTask(taskId, { approach: 'Refactored proposal generator' }); 543 544 const db = new Database(dbPath); 545 db.prepare( 546 'INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome, result_json) VALUES (?, ?, ?, ?, ?)' 547 ).run( 548 taskId, 549 'developer', 550 'fix_bug', 551 'success', 552 JSON.stringify({ approach: 'Refactored proposal generator' }) 553 ); 554 db.close(); 555 556 clearCache(); 557 558 const ctx = await buildAgentContext('developer', ['base.md'], { 559 context_json: { file_path: 'src/proposals.js' }, 560 }); 561 // extractRelatedInsight should produce "What worked:" text for completed tasks with approach 562 assert.ok( 563 ctx.historyContext.includes('What worked') || ctx.historyContext.includes('Refactored'), 564 'Should show what worked insight' 565 ); 566 } finally { 567 teardown(); 568 await cleanup(); 569 } 570 }); 571 572 test('related task insight shows "what failed" for failed task with error in context', async () => { 573 await initDb(); 574 setTestDb(); 575 576 try { 577 const db = new Database(dbPath); 578 db.prepare( 579 `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, context_json, completed_at) 580 VALUES (?, ?, ?, ?, ?, datetime('now'))` 581 ).run( 582 'fix_bug', 583 'developer', 584 'failed', 585 'Network error', 586 JSON.stringify({ file_path: 'src/outreach/email.js', error: 'SMTP connection refused' }) 587 ); 588 db.close(); 589 590 clearCache(); 591 592 const ctx = await buildAgentContext('developer', ['base.md'], { 593 context_json: { file_path: 'src/outreach/email.js' }, 594 }); 595 assert.ok( 596 ctx.historyContext.includes('What failed') || ctx.historyContext.includes('SMTP'), 597 'Should show what failed insight' 598 ); 599 } finally { 600 teardown(); 601 await cleanup(); 602 } 603 }); 604 }); 605 606 // ── Cache behavior ───────────────────────────────────────────────────────── 607 608 describe('buildAgentContext - cache behavior', () => { 609 test('second call returns cached result (same stats)', async () => { 610 await initDb(); 611 setTestDb(); 612 613 try { 614 const taskId = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 615 completeTask(taskId, { approach: 'Fixed' }); 616 617 clearCache(); 618 619 const ctx1 = await buildAgentContext('developer', ['base.md']); 620 const ctx2 = await buildAgentContext('developer', ['base.md']); 621 622 assert.equal( 623 ctx1.metadata.historyStats.recentSuccesses, 624 ctx2.metadata.historyStats.recentSuccesses 625 ); 626 } finally { 627 teardown(); 628 await cleanup(); 629 } 630 }); 631 632 test('clearCache() causes next call to re-query database', async () => { 633 await initDb(); 634 setTestDb(); 635 636 try { 637 clearCache(); 638 const ctx1 = await buildAgentContext('developer', ['base.md']); 639 640 // Add a new task after first call 641 const taskId = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 642 completeTask(taskId, { approach: 'New fix' }); 643 644 // Without clear - still uses cache (should return same count) 645 const ctx2 = await buildAgentContext('developer', ['base.md']); 646 assert.equal( 647 ctx1.metadata.historyStats.recentSuccesses, 648 ctx2.metadata.historyStats.recentSuccesses 649 ); 650 651 // After clear - fresh query includes new task 652 clearCache(); 653 const ctx3 = await buildAgentContext('developer', ['base.md']); 654 assert.ok( 655 ctx3.metadata.historyStats.recentSuccesses >= ctx1.metadata.historyStats.recentSuccesses 656 ); 657 } finally { 658 teardown(); 659 await cleanup(); 660 } 661 }); 662 663 test('cache TTL expiry causes re-query', async () => { 664 const { mock } = await import('node:test'); 665 666 await initDb(); 667 setTestDb(); 668 mock.timers.enable({ apis: ['Date'] }); 669 670 try { 671 clearCache(); 672 const ctx1 = await buildAgentContext('developer', ['base.md']); 673 674 // Advance time past 30-minute TTL 675 mock.timers.tick(31 * 60 * 1000); 676 677 const ctx2 = await buildAgentContext('developer', ['base.md']); 678 assert.ok(ctx2.fullContext, 'Should succeed after cache expiry'); 679 } finally { 680 mock.timers.reset(); 681 teardown(); 682 await cleanup(); 683 } 684 }); 685 }); 686 687 // ── resetDb ──────────────────────────────────────────────────────────────── 688 689 describe('resetDb', () => { 690 test('can be called multiple times without error', async () => { 691 await initDb(); 692 setTestDb(); 693 694 try { 695 await buildAgentContext('developer', ['base.md']); 696 resetDb(); 697 resetDb(); // Second call when db is null 698 assert.ok(true, 'resetDb twice should not throw'); 699 } finally { 700 teardown(); 701 await cleanup(); 702 } 703 }); 704 705 test('resetDb when db is null does nothing', () => { 706 resetDb(); // Should not throw even when not initialized 707 assert.ok(true, 'resetDb on uninitialized db should not throw'); 708 }); 709 }); 710 711 // ── Mixed success and failure ────────────────────────────────────────────── 712 713 describe('buildAgentContext - mixed history', () => { 714 test('shows both success and failure sections when both exist', async () => { 715 await initDb(); 716 setTestDb(); 717 718 try { 719 // 2 successes 720 for (let i = 0; i < 2; i++) { 721 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 722 completeTask(id, { approach: `Fix ${i}` }); 723 const db = new Database(dbPath); 724 db.prepare( 725 'INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome) VALUES (?, ?, ?, ?)' 726 ).run(id, 'developer', 'fix_bug', 'success'); 727 db.close(); 728 } 729 730 // 1 failure 731 const failId = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 732 failTask(failId, 'Something went wrong completely'); 733 const db = new Database(dbPath); 734 db.prepare( 735 'INSERT INTO agent_outcomes (task_id, agent_name, task_type, outcome) VALUES (?, ?, ?, ?)' 736 ).run(failId, 'developer', 'fix_bug', 'failure'); 737 db.close(); 738 739 clearCache(); 740 741 const ctx = await buildAgentContext('developer', ['base.md']); 742 assert.equal(ctx.metadata.historyStats.recentSuccesses, 2); 743 assert.equal(ctx.metadata.historyStats.recentFailures, 1); 744 assert.ok(ctx.historyContext.includes('Recent Successful Approaches')); 745 assert.ok(ctx.historyContext.includes('Past Failures to Avoid')); 746 } finally { 747 teardown(); 748 await cleanup(); 749 } 750 }); 751 752 test('buildAgentContext without currentTask argument has 0 related tasks', async () => { 753 await initDb(); 754 setTestDb(); 755 756 try { 757 clearCache(); 758 const ctx = await buildAgentContext('developer', ['base.md']); // no currentTask 759 assert.equal(ctx.metadata.historyStats.relatedTasks, 0); 760 } finally { 761 teardown(); 762 await cleanup(); 763 } 764 }); 765 766 test('buildAgentContext with currentTask=null has 0 related tasks', async () => { 767 await initDb(); 768 setTestDb(); 769 770 try { 771 clearCache(); 772 const ctx = await buildAgentContext('developer', ['base.md'], null); 773 assert.equal(ctx.metadata.historyStats.relatedTasks, 0); 774 } finally { 775 teardown(); 776 await cleanup(); 777 } 778 }); 779 }); 780 781 // ── extractFilePathFromContext edge cases ────────────────────────────────── 782 783 describe('buildAgentContext - extractFilePathFromContext variants', () => { 784 test('uses filePath (camelCase) field in current task context', async () => { 785 await initDb(); 786 setTestDb(); 787 788 try { 789 // Stored task that can be related 790 const id = await createAgentTask({ 791 task_type: 'fix_bug', 792 assigned_to: 'developer', 793 context: { filePath: 'src/capture.js' }, 794 }); 795 completeTask(id, { approach: 'Fixed it' }); 796 797 clearCache(); 798 799 // Current task uses camelCase filePath 800 const ctx = await buildAgentContext('developer', ['base.md'], { 801 context_json: { filePath: 'src/capture.js' }, 802 }); 803 assert.ok(typeof ctx.metadata.historyStats.relatedTasks === 'number'); 804 } finally { 805 teardown(); 806 await cleanup(); 807 } 808 }); 809 810 test('uses affected_file field in current task context', async () => { 811 await initDb(); 812 setTestDb(); 813 814 try { 815 const id = await createAgentTask({ 816 task_type: 'fix_bug', 817 assigned_to: 'developer', 818 context: { affected_file: 'src/proposals.js' }, 819 }); 820 completeTask(id, { approach: 'Fixed proposals' }); 821 822 clearCache(); 823 824 const ctx = await buildAgentContext('developer', ['base.md'], { 825 context_json: { affected_file: 'src/proposals.js' }, 826 }); 827 assert.ok(typeof ctx.metadata.historyStats.relatedTasks === 'number'); 828 } finally { 829 teardown(); 830 await cleanup(); 831 } 832 }); 833 834 test('uses files_changed[0] field in current task context', async () => { 835 await initDb(); 836 setTestDb(); 837 838 try { 839 const id = await createAgentTask({ 840 task_type: 'fix_bug', 841 assigned_to: 'developer', 842 context: { files_changed: ['src/enrich.js', 'src/score.js'] }, 843 }); 844 completeTask(id, { approach: 'Fixed enrichment' }); 845 846 clearCache(); 847 848 const ctx = await buildAgentContext('developer', ['base.md'], { 849 context_json: { files_changed: ['src/enrich.js'] }, 850 }); 851 assert.ok(typeof ctx.metadata.historyStats.relatedTasks === 'number'); 852 } finally { 853 teardown(); 854 await cleanup(); 855 } 856 }); 857 858 test('extracts path from error_message in current task context', async () => { 859 await initDb(); 860 setTestDb(); 861 862 try { 863 const id = await createAgentTask({ 864 task_type: 'fix_bug', 865 assigned_to: 'developer', 866 context: { error_message: 'Error at /home/user/src/assets.js:10' }, 867 }); 868 completeTask(id, { approach: 'Fixed assets' }); 869 870 clearCache(); 871 872 const ctx = await buildAgentContext('developer', ['base.md'], { 873 context_json: { error_message: 'Error at /home/user/src/assets.js:42:3' }, 874 }); 875 assert.ok(typeof ctx.metadata.historyStats.relatedTasks === 'number'); 876 } finally { 877 teardown(); 878 await cleanup(); 879 } 880 }); 881 882 test('extracts path from stack_trace in current task context', async () => { 883 await initDb(); 884 setTestDb(); 885 886 try { 887 const id = await createAgentTask({ 888 task_type: 'fix_bug', 889 assigned_to: 'developer', 890 context: { stack_trace: 'at fn (/home/user/src/outreach.js:5:3)' }, 891 }); 892 completeTask(id, { approach: 'Fixed outreach' }); 893 894 clearCache(); 895 896 const ctx = await buildAgentContext('developer', ['base.md'], { 897 context_json: { stack_trace: 'at handler (/home/user/src/outreach.js:20:7)' }, 898 }); 899 assert.ok(typeof ctx.metadata.historyStats.relatedTasks === 'number'); 900 } finally { 901 teardown(); 902 await cleanup(); 903 } 904 }); 905 }); 906 907 // ── Invalid JSON handling ────────────────────────────────────────────────── 908 909 describe('buildAgentContext - invalid JSON in task records', () => { 910 test('gracefully handles malformed context_json in completed tasks', async () => { 911 await initDb(); 912 setTestDb(); 913 914 try { 915 const db = new Database(dbPath); 916 db.prepare( 917 `INSERT INTO agent_tasks (task_type, assigned_to, status, context_json, completed_at) 918 VALUES (?, ?, ?, ?, datetime('now'))` 919 ).run('fix_bug', 'developer', 'completed', '{invalid json here}'); 920 db.close(); 921 922 clearCache(); 923 924 const ctx = await buildAgentContext('developer', ['base.md']); 925 assert.ok(ctx.fullContext, 'Should build context despite malformed JSON'); 926 } finally { 927 teardown(); 928 await cleanup(); 929 } 930 }); 931 932 test('gracefully handles malformed context_json in failed tasks', async () => { 933 await initDb(); 934 setTestDb(); 935 936 try { 937 const db = new Database(dbPath); 938 db.prepare( 939 `INSERT INTO agent_tasks (task_type, assigned_to, status, error_message, context_json, completed_at) 940 VALUES (?, ?, ?, ?, ?, datetime('now'))` 941 ).run('fix_bug', 'developer', 'failed', 'Error message', 'not-json'); 942 db.close(); 943 944 clearCache(); 945 946 const ctx = await buildAgentContext('developer', ['base.md']); 947 assert.ok(ctx.fullContext, 'Should build context despite malformed JSON in failed task'); 948 } finally { 949 teardown(); 950 await cleanup(); 951 } 952 }); 953 });