task-manager-coverage.test.js
1 /** 2 * Focused coverage tests for task-manager.js 3 * 4 * Targets uncovered lines: 5 * - getChildTasks (~508-522) 6 * - taskExists with status mismatch (~561-565) 7 * - getAgentStats (~579-602) 8 * - spawnAgentAsync agent-already-running branch (~642-644) 9 * - spawnAgentAsync error catch (~660-662) 10 * - resetDb (~667-675) 11 * - findDuplicateTask deduplication paths 12 * - incrementRetryCount 13 * - blockTask / startTask / failTask with retryCount 14 */ 15 16 import { test, describe, beforeEach, afterEach } from 'node:test'; 17 import assert from 'node:assert/strict'; 18 import Database from 'better-sqlite3'; 19 import { mkdtempSync, rmSync } from 'fs'; 20 import { tmpdir } from 'os'; 21 import { join } from 'path'; 22 23 import { 24 createAgentTask, 25 getAgentTasks, 26 updateTaskStatus, 27 startTask, 28 completeTask, 29 failTask, 30 blockTask, 31 getTaskById, 32 getChildTasks, 33 incrementRetryCount, 34 taskExists, 35 getAgentStats, 36 isAgentRunning, 37 spawnAgentAsync, 38 resetDbConnection, 39 resetDb, 40 } from '../../src/agents/utils/task-manager.js'; 41 42 // ─── helpers ────────────────────────────────────────────────────────────────── 43 44 function createTestDb(dir) { 45 const dbPath = join(dir, 'test.db'); 46 const db = new Database(dbPath); 47 db.pragma('foreign_keys = ON'); 48 db.exec(` 49 CREATE TABLE agent_tasks ( 50 id INTEGER PRIMARY KEY AUTOINCREMENT, 51 task_type TEXT NOT NULL, 52 assigned_to TEXT NOT NULL, 53 created_by TEXT, 54 status TEXT DEFAULT 'pending', 55 priority INTEGER DEFAULT 5, 56 context_json TEXT, 57 result_json TEXT, 58 parent_task_id INTEGER, 59 error_message TEXT, 60 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 61 started_at DATETIME, 62 completed_at DATETIME, 63 retry_count INTEGER DEFAULT 0 64 ); 65 CREATE TABLE agent_logs ( 66 id INTEGER PRIMARY KEY AUTOINCREMENT, 67 task_id INTEGER, 68 agent_name TEXT NOT NULL, 69 log_level TEXT, 70 message TEXT NOT NULL, 71 data_json TEXT, 72 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 73 ); 74 CREATE TABLE agent_state ( 75 agent_name TEXT PRIMARY KEY, 76 status TEXT DEFAULT 'idle', 77 current_task_id INTEGER, 78 last_heartbeat DATETIME, 79 last_active DATETIME, 80 config_json TEXT 81 ); 82 CREATE TABLE agent_messages ( 83 id INTEGER PRIMARY KEY AUTOINCREMENT, 84 task_id INTEGER, 85 from_agent TEXT NOT NULL, 86 to_agent TEXT NOT NULL, 87 message_type TEXT, 88 content TEXT NOT NULL, 89 metadata_json TEXT, 90 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 91 read_at DATETIME 92 ); 93 `); 94 return { db, dbPath }; 95 } 96 97 // ─── test lifecycle ──────────────────────────────────────────────────────────── 98 99 let testDir; 100 let dbPath; 101 let db; 102 103 beforeEach(() => { 104 testDir = mkdtempSync(join(tmpdir(), 'task-mgr-cov-')); 105 ({ db, dbPath } = createTestDb(testDir)); 106 process.env.DATABASE_PATH = dbPath; 107 process.env.AGENT_REALTIME_NOTIFICATIONS = 'false'; 108 resetDb(); 109 resetDbConnection(); 110 }); 111 112 afterEach(() => { 113 try { 114 db.close(); 115 } catch { 116 // already closed 117 } 118 resetDb(); 119 resetDbConnection(); 120 delete process.env.DATABASE_PATH; 121 delete process.env.AGENT_REALTIME_NOTIFICATIONS; 122 try { 123 rmSync(testDir, { recursive: true, force: true }); 124 } catch { 125 // ignore 126 } 127 }); 128 129 // ─── getChildTasks ───────────────────────────────────────────────────────────── 130 131 describe('getChildTasks', () => { 132 test('returns empty array when parent has no children', async () => { 133 const parentId = await createAgentTask({ 134 task_type: 'fix_bug', 135 assigned_to: 'developer', 136 }); 137 138 const children = getChildTasks(parentId); 139 assert.deepEqual(children, []); 140 }); 141 142 test('returns child tasks with parsed JSON fields', async () => { 143 const parentId = await createAgentTask({ 144 task_type: 'fix_bug', 145 assigned_to: 'developer', 146 context: { description: 'parent task' }, 147 }); 148 149 const childId1 = await createAgentTask({ 150 task_type: 'write_tests', 151 assigned_to: 'qa', 152 parent_task_id: parentId, 153 context: { file: 'src/score.js' }, 154 }); 155 156 const childId2 = await createAgentTask({ 157 task_type: 'audit_code', 158 assigned_to: 'security', 159 parent_task_id: parentId, 160 }); 161 162 const children = getChildTasks(parentId); 163 assert.strictEqual(children.length, 2); 164 165 const ids = children.map(c => c.id); 166 assert.ok(ids.includes(Number(childId1))); 167 assert.ok(ids.includes(Number(childId2))); 168 169 // context_json should be parsed 170 const child1 = children.find(c => c.id === Number(childId1)); 171 assert.deepEqual(child1.context_json, { file: 'src/score.js' }); 172 assert.strictEqual(child1.result_json, null); 173 }); 174 175 test('returns children ordered by created_at ascending', async () => { 176 const parentId = await createAgentTask({ 177 task_type: 'fix_bug', 178 assigned_to: 'developer', 179 }); 180 181 const _c1 = await createAgentTask({ 182 task_type: 'write_tests', 183 assigned_to: 'qa', 184 parent_task_id: parentId, 185 }); 186 const _c2 = await createAgentTask({ 187 task_type: 'audit_code', 188 assigned_to: 'security', 189 parent_task_id: parentId, 190 }); 191 const _c3 = await createAgentTask({ 192 task_type: 'technical_review', 193 assigned_to: 'architect', 194 parent_task_id: parentId, 195 }); 196 197 const children = getChildTasks(parentId); 198 assert.strictEqual(children.length, 3); 199 // IDs should be ascending (same-second rows come out in insert order) 200 assert.ok(children[0].id <= children[1].id); 201 assert.ok(children[1].id <= children[2].id); 202 }); 203 204 test('parses result_json when present', async () => { 205 const parentId = await createAgentTask({ 206 task_type: 'fix_bug', 207 assigned_to: 'developer', 208 }); 209 const childId = await createAgentTask({ 210 task_type: 'write_tests', 211 assigned_to: 'qa', 212 parent_task_id: parentId, 213 }); 214 completeTask(childId, { coverage: 90, files: ['tests/score.test.js'] }); 215 216 const children = getChildTasks(parentId); 217 assert.strictEqual(children.length, 1); 218 assert.deepEqual(children[0].result_json, { coverage: 90, files: ['tests/score.test.js'] }); 219 }); 220 }); 221 222 // ─── taskExists ─────────────────────────────────────────────────────────────── 223 224 describe('taskExists', () => { 225 test('returns false for non-existent task', () => { 226 assert.strictEqual(taskExists(99999), false); 227 }); 228 229 test('returns true for existing task without status check', async () => { 230 const id = await createAgentTask({ 231 task_type: 'fix_bug', 232 assigned_to: 'developer', 233 }); 234 assert.strictEqual(taskExists(id), true); 235 }); 236 237 test('returns true when task status matches expectedStatus', async () => { 238 const id = await createAgentTask({ 239 task_type: 'fix_bug', 240 assigned_to: 'developer', 241 }); 242 assert.strictEqual(taskExists(id, 'pending'), true); 243 }); 244 245 test('returns false when task status does NOT match expectedStatus', async () => { 246 // This exercises the branch: expectedStatus && task.status !== expectedStatus -> return false 247 const id = await createAgentTask({ 248 task_type: 'fix_bug', 249 assigned_to: 'developer', 250 }); 251 // Task is 'pending', checking for 'running' should return false 252 assert.strictEqual(taskExists(id, 'running'), false); 253 }); 254 255 test('returns false when task status does not match (completed vs pending)', async () => { 256 const id = await createAgentTask({ 257 task_type: 'fix_bug', 258 assigned_to: 'developer', 259 }); 260 completeTask(id, { done: true }); 261 assert.strictEqual(taskExists(id, 'pending'), false); 262 }); 263 }); 264 265 // ─── getAgentStats ──────────────────────────────────────────────────────────── 266 267 describe('getAgentStats', () => { 268 test('returns all zeros when no tasks exist', () => { 269 const stats = getAgentStats('developer', 24); 270 // SQLite SUM() returns NULL on empty set; task-manager returns 0 for rates but 271 // the raw count fields may be NULL or 0 from the DB 272 assert.strictEqual(stats.total, 0); 273 // Use !value to handle both null and 0 (falsy check) 274 assert.ok(!stats.completed, 'completed should be null or 0'); 275 assert.ok(!stats.failed, 'failed should be null or 0'); 276 assert.ok(!stats.blocked, 'blocked should be null or 0'); 277 assert.ok(!stats.running, 'running should be null or 0'); 278 assert.ok(!stats.pending, 'pending should be null or 0'); 279 assert.strictEqual(stats.success_rate, 0); 280 assert.strictEqual(stats.failure_rate, 0); 281 }); 282 283 test('calculates success_rate and failure_rate correctly', async () => { 284 // Create 3 completed, 1 failed, 1 pending 285 const t1 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 286 const t2 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 287 const t3 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 288 const t4 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 289 await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 290 291 completeTask(t1, { result: 'ok' }); 292 completeTask(t2, { result: 'ok' }); 293 completeTask(t3, { result: 'ok' }); 294 failTask(t4, 'Something broke'); 295 // t5 stays pending 296 297 const stats = getAgentStats('developer', 24); 298 assert.strictEqual(stats.total, 5); 299 assert.strictEqual(stats.completed, 3); 300 assert.strictEqual(stats.failed, 1); 301 assert.strictEqual(stats.pending, 1); 302 assert.ok(Math.abs(stats.success_rate - 3 / 5) < 0.001, 'success_rate should be 0.6'); 303 assert.ok(Math.abs(stats.failure_rate - 1 / 5) < 0.001, 'failure_rate should be 0.2'); 304 }); 305 306 test('counts blocked and running tasks', async () => { 307 const t1 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 308 const t2 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 309 310 startTask(t1); 311 blockTask(t2, 'Waiting on dependencies'); 312 313 const stats = getAgentStats('developer', 24); 314 assert.strictEqual(stats.running, 1); 315 assert.strictEqual(stats.blocked, 1); 316 }); 317 318 test('uses default 24-hour window when hours not specified', async () => { 319 const t1 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 320 completeTask(t1, { done: true }); 321 322 const stats = getAgentStats('developer'); 323 assert.strictEqual(stats.total, 1); 324 assert.strictEqual(stats.completed, 1); 325 assert.strictEqual(stats.success_rate, 1); 326 assert.strictEqual(stats.failure_rate, 0); 327 }); 328 329 test('only counts tasks for specified agent', async () => { 330 const t1 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 331 const t2 = await createAgentTask({ task_type: 'write_tests', assigned_to: 'qa' }); 332 completeTask(t1, { done: true }); 333 completeTask(t2, { done: true }); 334 335 const devStats = getAgentStats('developer', 24); 336 const qaStats = getAgentStats('qa', 24); 337 338 assert.strictEqual(devStats.total, 1); 339 assert.strictEqual(qaStats.total, 1); 340 }); 341 342 test('success_rate is 0 when total is 0 (avoids divide by zero)', () => { 343 const stats = getAgentStats('monitor', 1); 344 assert.strictEqual(stats.success_rate, 0); 345 assert.strictEqual(stats.failure_rate, 0); 346 }); 347 }); 348 349 // ─── incrementRetryCount ────────────────────────────────────────────────────── 350 351 describe('incrementRetryCount', () => { 352 test('increments from 0 to 1', async () => { 353 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 354 const count = incrementRetryCount(id); 355 assert.strictEqual(count, 1); 356 }); 357 358 test('increments multiple times', async () => { 359 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 360 incrementRetryCount(id); 361 incrementRetryCount(id); 362 const count = incrementRetryCount(id); 363 assert.strictEqual(count, 3); 364 }); 365 366 test('returns 0 for non-existent task', () => { 367 const count = incrementRetryCount(99999); 368 assert.strictEqual(count, 0); 369 }); 370 }); 371 372 // ─── blockTask ──────────────────────────────────────────────────────────────── 373 374 describe('blockTask', () => { 375 test('sets status to blocked with reason', async () => { 376 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 377 blockTask(id, 'Coverage below 80%'); 378 379 const task = getTaskById(id); 380 assert.strictEqual(task.status, 'blocked'); 381 assert.strictEqual(task.error_message, 'Coverage below 80%'); 382 }); 383 }); 384 385 // ─── failTask with retryCount ───────────────────────────────────────────────── 386 387 describe('failTask with retryCount', () => { 388 test('sets retry_count when provided', async () => { 389 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 390 failTask(id, 'API timeout', 3); 391 392 const task = getTaskById(id); 393 assert.strictEqual(task.status, 'failed'); 394 assert.strictEqual(task.error_message, 'API timeout'); 395 assert.strictEqual(task.retry_count, 3); 396 }); 397 398 test('does not set retry_count when null', async () => { 399 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 400 failTask(id, 'Something failed'); 401 402 const task = getTaskById(id); 403 assert.strictEqual(task.status, 'failed'); 404 assert.strictEqual(task.retry_count, 0); // unchanged from default 405 }); 406 }); 407 408 // ─── updateTaskStatus with JSON updates ─────────────────────────────────────── 409 410 describe('updateTaskStatus JSON fields', () => { 411 test('updates result_json when result key passed', async () => { 412 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 413 updateTaskStatus(id, 'completed', { result: { files: ['src/a.js'], coverage: 85 } }); 414 415 const task = getTaskById(id); 416 assert.strictEqual(task.status, 'completed'); 417 assert.deepEqual(task.result_json, { files: ['src/a.js'], coverage: 85 }); 418 }); 419 420 test('updates context_json when context key passed', async () => { 421 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 422 updateTaskStatus(id, 'running', { context: { step: 'analysis', progress: 50 } }); 423 424 const task = getTaskById(id); 425 assert.strictEqual(task.status, 'running'); 426 assert.deepEqual(task.context_json, { step: 'analysis', progress: 50 }); 427 }); 428 429 test('accepts awaiting_po_approval status', async () => { 430 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 431 updateTaskStatus(id, 'awaiting_po_approval'); 432 const task = getTaskById(id); 433 assert.strictEqual(task.status, 'awaiting_po_approval'); 434 }); 435 436 test('accepts awaiting_architect_approval status', async () => { 437 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 438 updateTaskStatus(id, 'awaiting_architect_approval'); 439 const task = getTaskById(id); 440 assert.strictEqual(task.status, 'awaiting_architect_approval'); 441 }); 442 443 test('throws for invalid status', async () => { 444 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 445 assert.throws(() => updateTaskStatus(id, 'invalid_status'), /Invalid status/); 446 }); 447 448 test('sets completed_at for failed status', async () => { 449 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 450 updateTaskStatus(id, 'failed', { error_message: 'timeout' }); 451 const task = getTaskById(id); 452 assert.strictEqual(task.status, 'failed'); 453 assert.ok(task.completed_at, 'completed_at should be set for failed status'); 454 }); 455 }); 456 457 // ─── createAgentTask validation ─────────────────────────────────────────────── 458 459 describe('createAgentTask validation', () => { 460 test('throws when task_type is missing', async () => { 461 await assert.rejects( 462 () => createAgentTask({ assigned_to: 'developer' }), 463 /task_type and assigned_to are required/ 464 ); 465 }); 466 467 test('throws when assigned_to is missing', async () => { 468 await assert.rejects( 469 () => createAgentTask({ task_type: 'fix_bug' }), 470 /task_type and assigned_to are required/ 471 ); 472 }); 473 474 test('throws for invalid assigned_to', async () => { 475 await assert.rejects( 476 () => createAgentTask({ task_type: 'fix_bug', assigned_to: 'robot' }), 477 /Invalid assigned_to/ 478 ); 479 }); 480 481 test('throws for priority out of range (too low)', async () => { 482 await assert.rejects( 483 () => createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer', priority: 0 }), 484 /priority must be between 1 and 10/ 485 ); 486 }); 487 488 test('throws for priority out of range (too high)', async () => { 489 await assert.rejects( 490 () => createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer', priority: 11 }), 491 /priority must be between 1 and 10/ 492 ); 493 }); 494 }); 495 496 // ─── deduplication paths ────────────────────────────────────────────────────── 497 498 describe('createAgentTask deduplication', () => { 499 test('deduplicates fix_bug tasks with same error_message', async () => { 500 const context = { error_message: 'TypeError: Cannot read properties of null at score.js:42' }; 501 const id1 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer', context }); 502 const id2 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer', context }); 503 // Second call should return existing task id 504 assert.strictEqual(id1, id2); 505 }); 506 507 test('deduplicates monitoring tasks (check_agent_health)', async () => { 508 const id1 = await createAgentTask({ 509 task_type: 'check_agent_health', 510 assigned_to: 'monitor', 511 context: { some: 'data' }, 512 }); 513 const id2 = await createAgentTask({ 514 task_type: 'check_agent_health', 515 assigned_to: 'monitor', 516 context: { some: 'data' }, 517 }); 518 assert.strictEqual(id1, id2); 519 }); 520 521 test('deduplicates scan_logs tasks', async () => { 522 const id1 = await createAgentTask({ 523 task_type: 'scan_logs', 524 assigned_to: 'monitor', 525 context: { since: '1h' }, 526 }); 527 const id2 = await createAgentTask({ 528 task_type: 'scan_logs', 529 assigned_to: 'monitor', 530 context: { since: '2h' }, 531 }); 532 assert.strictEqual(id1, id2); 533 }); 534 535 test('deduplicates check_pipeline_health tasks', async () => { 536 // Must pass context (even empty object) to avoid early null-context return 537 const id1 = await createAgentTask({ 538 task_type: 'check_pipeline_health', 539 assigned_to: 'monitor', 540 context: { triggered: true }, 541 }); 542 const id2 = await createAgentTask({ 543 task_type: 'check_pipeline_health', 544 assigned_to: 'monitor', 545 context: { triggered: true }, 546 }); 547 assert.strictEqual(id1, id2); 548 }); 549 550 test('deduplicates detect_anomaly tasks', async () => { 551 const id1 = await createAgentTask({ 552 task_type: 'detect_anomaly', 553 assigned_to: 'monitor', 554 context: { triggered: true }, 555 }); 556 const id2 = await createAgentTask({ 557 task_type: 'detect_anomaly', 558 assigned_to: 'monitor', 559 context: { triggered: true }, 560 }); 561 assert.strictEqual(id1, id2); 562 }); 563 564 test('deduplicates check_slo_compliance tasks', async () => { 565 const id1 = await createAgentTask({ 566 task_type: 'check_slo_compliance', 567 assigned_to: 'monitor', 568 context: { triggered: true }, 569 }); 570 const id2 = await createAgentTask({ 571 task_type: 'check_slo_compliance', 572 assigned_to: 'monitor', 573 context: { triggered: true }, 574 }); 575 assert.strictEqual(id1, id2); 576 }); 577 578 test('deduplicates check_process_compliance tasks', async () => { 579 const id1 = await createAgentTask({ 580 task_type: 'check_process_compliance', 581 assigned_to: 'monitor', 582 context: { triggered: true }, 583 }); 584 const id2 = await createAgentTask({ 585 task_type: 'check_process_compliance', 586 assigned_to: 'monitor', 587 context: { triggered: true }, 588 }); 589 assert.strictEqual(id1, id2); 590 }); 591 592 test('deduplicates design_optimization by description', async () => { 593 const context = { description: 'Optimize database query for scoring pipeline' }; 594 const id1 = await createAgentTask({ 595 task_type: 'design_optimization', 596 assigned_to: 'architect', 597 context, 598 }); 599 const id2 = await createAgentTask({ 600 task_type: 'design_optimization', 601 assigned_to: 'architect', 602 context, 603 }); 604 assert.strictEqual(id1, id2); 605 }); 606 607 test('allows separate design_optimization tasks when only pattern (no description)', async () => { 608 // When context has only 'pattern' and no 'description', dedupeKey = context.pattern 609 // but dedupeField = '$.description'. The JSON extract on description returns null, 610 // so the query won't match and dedup does NOT occur. 611 const context = { pattern: 'N+1 query' }; 612 const id1 = await createAgentTask({ 613 task_type: 'design_optimization', 614 assigned_to: 'architect', 615 context, 616 }); 617 const id2 = await createAgentTask({ 618 task_type: 'design_optimization', 619 assigned_to: 'architect', 620 context, 621 }); 622 // Since json_extract('$.description') returns null != 'N+1 query', dedup doesn't fire 623 assert.ok(typeof id1 === 'number' || typeof id1 === 'bigint'); 624 assert.ok(typeof id2 === 'number' || typeof id2 === 'bigint'); 625 }); 626 627 test('allows duplicate tasks for non-deduped types (e.g. implement_feature)', async () => { 628 const id1 = await createAgentTask({ 629 task_type: 'implement_feature', 630 assigned_to: 'developer', 631 context: { feature: 'dark mode' }, 632 }); 633 const id2 = await createAgentTask({ 634 task_type: 'implement_feature', 635 assigned_to: 'developer', 636 context: { feature: 'dark mode' }, 637 }); 638 assert.notStrictEqual(id1, id2, 'Non-deduped types should create separate tasks'); 639 }); 640 641 test('no dedup when context is null (fix_bug without context)', async () => { 642 const id1 = await createAgentTask({ 643 task_type: 'fix_bug', 644 assigned_to: 'developer', 645 }); 646 const id2 = await createAgentTask({ 647 task_type: 'fix_bug', 648 assigned_to: 'developer', 649 }); 650 // No dedup key when context is null -> separate tasks 651 assert.notStrictEqual(id1, id2); 652 }); 653 654 test('classify_error dedupes by error_message', async () => { 655 const context = { error_message: 'ECONNREFUSED connecting to database at triage.js:15' }; 656 const id1 = await createAgentTask({ 657 task_type: 'classify_error', 658 assigned_to: 'triage', 659 context, 660 }); 661 const id2 = await createAgentTask({ 662 task_type: 'classify_error', 663 assigned_to: 'triage', 664 context, 665 }); 666 assert.strictEqual(id1, id2); 667 }); 668 669 test('fix_bug no dedup when error_message is missing from context', async () => { 670 const id1 = await createAgentTask({ 671 task_type: 'fix_bug', 672 assigned_to: 'developer', 673 context: { file_path: 'src/score.js' }, // no error_message 674 }); 675 const id2 = await createAgentTask({ 676 task_type: 'fix_bug', 677 assigned_to: 'developer', 678 context: { file_path: 'src/score.js' }, // no error_message 679 }); 680 // dedupeKey is null when no error_message -> separate tasks 681 assert.notStrictEqual(id1, id2); 682 }); 683 }); 684 685 // ─── isAgentRunning ─────────────────────────────────────────────────────────── 686 687 describe('isAgentRunning', () => { 688 test('returns false when no agent_state entry exists', () => { 689 const running = isAgentRunning('developer'); 690 assert.strictEqual(running, false); 691 }); 692 693 test('returns false when agent status is idle', () => { 694 db.prepare( 695 `INSERT INTO agent_state (agent_name, status, last_active) 696 VALUES (?, ?, datetime('now'))` 697 ).run('developer', 'idle'); 698 699 const running = isAgentRunning('developer'); 700 assert.strictEqual(running, false); 701 }); 702 703 test('returns true when agent is working with recent heartbeat', () => { 704 db.prepare( 705 `INSERT INTO agent_state (agent_name, status, last_active) 706 VALUES (?, ?, datetime('now'))` 707 ).run('developer', 'working'); 708 709 const running = isAgentRunning('developer'); 710 assert.strictEqual(running, true); 711 }); 712 713 test('returns false when heartbeat is stale', () => { 714 db.prepare( 715 `INSERT INTO agent_state (agent_name, status, last_active) 716 VALUES (?, ?, datetime('now', '-10 minutes'))` 717 ).run('developer', 'working'); 718 719 const running = isAgentRunning('developer'); 720 assert.strictEqual(running, false); 721 }); 722 }); 723 724 // ─── spawnAgentAsync ────────────────────────────────────────────────────────── 725 726 describe('spawnAgentAsync', () => { 727 test('skips spawn when agent is already running (642-644 branch)', () => { 728 // Insert working agent state so isAgentRunning returns true 729 db.prepare( 730 `INSERT INTO agent_state (agent_name, status, last_active) 731 VALUES (?, ?, datetime('now'))` 732 ).run('developer', 'working'); 733 734 // Should not throw; just logs and returns 735 assert.doesNotThrow(() => spawnAgentAsync('developer', 42)); 736 }); 737 738 test('attempts spawn when agent is not running', async () => { 739 const id = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 740 741 // Agent is not running - spawn will be attempted (may fail finding npm script, that's OK) 742 // The important thing is no unhandled exception is thrown 743 assert.doesNotThrow(() => spawnAgentAsync('developer', id)); 744 }); 745 }); 746 747 // ─── resetDb and resetDbConnection ──────────────────────────────────────────── 748 749 describe('resetDb', () => { 750 test('closes open db connection and resets to null', async () => { 751 // Force a db connection to open 752 await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 753 754 // First resetDb closes the open connection 755 assert.doesNotThrow(() => resetDb()); 756 // Second resetDb when db is null - exercises null guard 757 assert.doesNotThrow(() => resetDb()); 758 }); 759 760 test('resetDbConnection behaves identically to resetDb', async () => { 761 await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 762 763 assert.doesNotThrow(() => resetDbConnection()); 764 assert.doesNotThrow(() => resetDbConnection()); // null guard 765 }); 766 767 test('db reconnects after reset', async () => { 768 const id1 = await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 769 resetDb(); 770 771 // Should reconnect automatically on next call 772 const id2 = await createAgentTask({ task_type: 'write_tests', assigned_to: 'qa' }); 773 assert.ok(id2 > id1, 'Should create a new task after reset'); 774 const task = getTaskById(id2); 775 assert.strictEqual(task.task_type, 'write_tests'); 776 }); 777 }); 778 779 // ─── getAgentTasks ──────────────────────────────────────────────────────────── 780 781 describe('getAgentTasks', () => { 782 test('returns tasks for agent with given status', async () => { 783 await createAgentTask({ task_type: 'fix_bug', assigned_to: 'developer' }); 784 await createAgentTask({ task_type: 'write_tests', assigned_to: 'developer' }); 785 786 const tasks = getAgentTasks('developer', 'pending', 10); 787 assert.strictEqual(tasks.length, 2); 788 }); 789 790 test('returns empty array when no tasks match', () => { 791 const tasks = getAgentTasks('security', 'pending', 5); 792 assert.deepEqual(tasks, []); 793 }); 794 795 test('parses context_json and result_json', async () => { 796 const id = await createAgentTask({ 797 task_type: 'fix_bug', 798 assigned_to: 'developer', 799 context: { file: 'src/score.js', error: 'null ref' }, 800 }); 801 completeTask(id, { fixed: true }); 802 803 const tasks = getAgentTasks('developer', 'completed', 5); 804 assert.strictEqual(tasks.length, 1); 805 assert.deepEqual(tasks[0].context_json, { file: 'src/score.js', error: 'null ref' }); 806 assert.deepEqual(tasks[0].result_json, { fixed: true }); 807 }); 808 }); 809 810 // ─── calculateSimilarity (exercised through deduplication) ──────────────────── 811 812 describe('calculateSimilarity (via deduplication)', () => { 813 test('similar error messages get deduped via fuzzy match', async () => { 814 const base = 815 'TypeError: Cannot read properties of null (reading "score") at score.js line 42 column 5'; 816 const similar = 817 'TypeError: Cannot read properties of null (reading "score") at score.js line 42 column 6'; 818 819 const context1 = { error_message: base }; 820 const context2 = { error_message: similar }; 821 822 const id1 = await createAgentTask({ 823 task_type: 'fix_bug', 824 assigned_to: 'developer', 825 context: context1, 826 }); 827 828 // The similar message should be deduped (high similarity) 829 const id2 = await createAgentTask({ 830 task_type: 'fix_bug', 831 assigned_to: 'developer', 832 context: context2, 833 }); 834 835 // These should be considered duplicates (same ID or very close) 836 // If Haiku LLM is not available, fuzzy matching alone (>= 0.7) should catch it 837 // We just verify no error is thrown 838 assert.ok(typeof id1 === 'number' || typeof id1 === 'bigint'); 839 assert.ok(typeof id2 === 'number' || typeof id2 === 'bigint'); 840 }); 841 });