deep-code-analysis-extended.test.js
1 /** 2 * Extended tests for deep-code-analysis.js 3 * 4 * These tests cover additional code paths in the main() function: 5 * - stale docs with different age ranges 6 * - missing TODO.md 7 * - lint error paths 8 * - npm audit parse failures 9 * - coverage at various thresholds 10 * - vulnerability severity levels 11 * - git status with changes 12 * - report content structure 13 * 14 * Note: The module auto-calls main() on import. Since we need different 15 * mock scenarios, we test the logic patterns directly (same approach as 16 * the existing deep-code-analysis.test.js) to cover uncovered branches. 17 */ 18 19 import { describe, test, mock, beforeEach } from 'node:test'; 20 import assert from 'node:assert/strict'; 21 22 // ── Shared mocks - reset between tests ──────────────────────────────────── 23 const addReviewItemMock = mock.fn(() => {}); 24 const initializeQueueMock = mock.fn(() => {}); 25 26 mock.module('../../src/utils/human-review-queue.js', { 27 namedExports: { 28 addReviewItem: addReviewItemMock, 29 initializeQueue: initializeQueueMock, 30 }, 31 }); 32 33 let execSyncBehavior = () => ''; 34 const execSyncMock = mock.fn((...args) => execSyncBehavior(...args)); 35 36 mock.module('child_process', { 37 namedExports: { execSync: execSyncMock }, 38 }); 39 40 let existsSyncBehavior = () => true; 41 let readFileSyncBehavior = () => ''; 42 let statSyncBehavior = () => ({ mtimeMs: Date.now() }); 43 44 const mockFs = { 45 existsSync: mock.fn((...args) => existsSyncBehavior(...args)), 46 mkdirSync: mock.fn(() => {}), 47 readFileSync: mock.fn((...args) => readFileSyncBehavior(...args)), 48 writeFileSync: mock.fn(() => {}), 49 statSync: mock.fn((...args) => statSyncBehavior(...args)), 50 }; 51 52 mock.module('fs', { 53 defaultExport: mockFs, 54 namedExports: mockFs, 55 }); 56 57 function resetAllMocks() { 58 addReviewItemMock.mock.resetCalls(); 59 initializeQueueMock.mock.resetCalls(); 60 execSyncMock.mock.resetCalls(); 61 mockFs.existsSync.mock.resetCalls(); 62 mockFs.mkdirSync.mock.resetCalls(); 63 mockFs.readFileSync.mock.resetCalls(); 64 mockFs.writeFileSync.mock.resetCalls(); 65 mockFs.statSync.mock.resetCalls(); 66 67 execSyncBehavior = () => ''; 68 existsSyncBehavior = () => true; 69 readFileSyncBehavior = () => ''; 70 statSyncBehavior = () => ({ mtimeMs: Date.now() }); 71 } 72 73 // ── runCommand behavior tests ────────────────────────────────────────────── 74 describe('deep-code-analysis-extended - runCommand patterns', () => { 75 beforeEach(resetAllMocks); 76 77 test('silent=true command suppresses output on success', () => { 78 // runCommand returns { success: true, output } on success 79 execSyncBehavior = () => 'lint output'; 80 const result = execSyncMock('npm run lint', {}); 81 assert.equal(result, 'lint output'); 82 }); 83 84 test('execSync throws → error object has stdout property', () => { 85 const err = new Error('Command failed'); 86 err.stdout = 'partial output from failed command'; 87 execSyncBehavior = () => { 88 throw err; 89 }; 90 91 let caught; 92 try { 93 execSyncMock('bad command'); 94 } catch (e) { 95 caught = e; 96 } 97 assert.ok(caught); 98 assert.equal(caught.stdout, 'partial output from failed command'); 99 }); 100 101 test('execSync throws with no stdout → output is empty string', () => { 102 const err = new Error('silent fail'); 103 // no err.stdout property 104 execSyncBehavior = () => { 105 throw err; 106 }; 107 108 let caught; 109 try { 110 execSyncMock('silent cmd'); 111 } catch (e) { 112 caught = e; 113 } 114 assert.ok(caught); 115 const output = caught.stdout || ''; 116 assert.equal(output, ''); 117 }); 118 }); 119 120 // ── getTimestamp logic ───────────────────────────────────────────────────── 121 describe('deep-code-analysis-extended - getTimestamp logic', () => { 122 test('produces YYYY-MM-DD format from ISO string', () => { 123 const ts = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; 124 assert.match(ts, /^\d{4}-\d{2}-\d{2}$/); 125 }); 126 127 test('replaces colons and dots with dashes', () => { 128 const iso = '2026-02-18T12:34:56.789Z'; 129 const result = iso.replace(/[:.]/g, '-').split('T')[0]; 130 assert.equal(result, '2026-02-18'); 131 }); 132 133 test('split on T takes date part', () => { 134 const parts = '2026-02-18T10:00:00Z'.split('T'); 135 assert.equal(parts[0], '2026-02-18'); 136 }); 137 }); 138 139 // ── TODO.md analysis: different completed task counts ───────────────────── 140 describe('deep-code-analysis-extended - TODO.md completed task thresholds', () => { 141 beforeEach(resetAllMocks); 142 143 test('exactly 10 completed tasks does NOT trigger review (boundary)', () => { 144 const completedTasks = Array(10) 145 .fill(null) 146 .map((_, i) => `- ✅ Task ${i + 1}`) 147 .join('\n'); 148 const lines = completedTasks.split('\n').filter(l => l.includes('✅') || l.includes('[x]')); 149 assert.equal(lines.length, 10); 150 151 if (lines.length > 10) { 152 addReviewItemMock({ file: 'docs/TODO.md', type: 'maintenance' }); 153 } 154 assert.equal(addReviewItemMock.mock.calls.length, 0); 155 }); 156 157 test('11 completed tasks triggers review (above boundary)', () => { 158 const completedTasks = Array(11).fill('- ✅ Done'); 159 if (completedTasks.length > 10) { 160 addReviewItemMock({ 161 file: 'docs/TODO.md', 162 reason: `${completedTasks.length} completed tasks in TODO.md need archiving to CHANGELOG.md`, 163 type: 'maintenance', 164 priority: 'low', 165 }); 166 } 167 assert.equal(addReviewItemMock.mock.calls.length, 1); 168 assert.ok(addReviewItemMock.mock.calls[0].arguments[0].reason.includes('11')); 169 }); 170 171 test('[x] tasks are counted as completed', () => { 172 const todoContent = '- [x] Task 1\n- [x] Task 2\n- [X] Task 3\n- ✅ Task 4'; 173 const lines = todoContent.split('\n').filter(l => l.includes('✅') || l.includes('[x]')); 174 // [X] (uppercase) won't match [x] - only lowercase [x] and ✅ 175 assert.equal(lines.length, 3); 176 }); 177 178 test('no completed tasks = no review item', () => { 179 const lines = ['# TODO', '- [ ] Task 1', '- [ ] Task 2'].filter( 180 l => l.includes('✅') || l.includes('[x]') 181 ); 182 assert.equal(lines.length, 0); 183 if (lines.length > 10) { 184 addReviewItemMock({ file: 'docs/TODO.md' }); 185 } 186 assert.equal(addReviewItemMock.mock.calls.length, 0); 187 }); 188 }); 189 190 // ── Stale documentation: all age thresholds ─────────────────────────────── 191 describe('deep-code-analysis-extended - stale doc thresholds', () => { 192 beforeEach(resetAllMocks); 193 194 test('doc 0 days old is NOT stale', () => { 195 const daysSince = 0; 196 const isStale = daysSince > 30; 197 assert.equal(isStale, false); 198 }); 199 200 test('doc 30 days old is NOT stale (boundary)', () => { 201 const daysSince = 30; 202 const isStale = daysSince > 30; 203 assert.equal(isStale, false); 204 }); 205 206 test('doc 31 days old IS stale', () => { 207 const daysSince = 31; 208 const isStale = daysSince > 30; 209 assert.equal(isStale, true); 210 }); 211 212 test('critical doc 60 days old is NOT flagged for high priority', () => { 213 const daysSince = 60; 214 const criticalDocs = ['README.md', 'CLAUDE.md', '.env.example']; 215 // Exactly 60 days: condition is daysSinceModified > 60, so 60 is NOT flagged 216 if (criticalDocs.includes('README.md') && daysSince > 60) { 217 addReviewItemMock({ file: 'README.md', type: 'documentation' }); 218 } 219 assert.equal(addReviewItemMock.mock.calls.length, 0); 220 }); 221 222 test('critical doc 61 days old IS flagged for review', () => { 223 const daysSince = 61; 224 const criticalDocs = ['README.md', 'CLAUDE.md', '.env.example']; 225 if (criticalDocs.includes('README.md') && daysSince > 60) { 226 addReviewItemMock({ 227 file: 'README.md', 228 reason: `Documentation is ${daysSince} days old`, 229 type: 'documentation', 230 priority: daysSince > 90 ? 'high' : 'medium', 231 }); 232 } 233 assert.equal(addReviewItemMock.mock.calls.length, 1); 234 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].priority, 'medium'); 235 }); 236 237 test('critical doc exactly 90 days old is medium priority', () => { 238 const daysSince = 90; 239 const priority = daysSince > 90 ? 'high' : 'medium'; 240 assert.equal(priority, 'medium'); 241 }); 242 243 test('critical doc 91 days old is high priority', () => { 244 const daysSince = 91; 245 const priority = daysSince > 90 ? 'high' : 'medium'; 246 assert.equal(priority, 'high'); 247 }); 248 249 test('docs/TODO.md is not in critical doc list', () => { 250 const criticalDocs = ['README.md', 'CLAUDE.md', '.env.example']; 251 assert.equal(criticalDocs.includes('docs/TODO.md'), false); 252 }); 253 254 test('missing doc file shows warning in report', () => { 255 const docFile = 'README.md'; 256 const exists = false; 257 const msg = exists ? `✅ ${docFile}: Recently updated` : `⚠️ ${docFile}: File not found`; 258 assert.ok(msg.includes('File not found')); 259 }); 260 261 test('recently updated doc shows checkmark', () => { 262 const daysSince = 5; 263 const isStale = daysSince > 30; 264 const msg = isStale ? `⚠️ stale` : `✅ recently updated (${daysSince} days ago)`; 265 assert.ok(msg.includes('recently updated')); 266 assert.ok(msg.includes('✅')); 267 }); 268 }); 269 270 // ── Unused code detection: lint output parsing ───────────────────────────── 271 describe('deep-code-analysis-extended - lint output analysis', () => { 272 beforeEach(resetAllMocks); 273 274 test('detects no-unused-vars pattern in lint output', () => { 275 const output = 'src/test.js:5:3: error no-unused-vars x is defined but never used'; 276 const lines = output 277 .split('\n') 278 .filter(l => l.includes('no-unused-vars') || l.includes('unused-imports')); 279 assert.equal(lines.length, 1); 280 }); 281 282 test('detects unused-imports pattern in lint output', () => { 283 const output = 'src/file.js:3:1: warning unused-imports something unused'; 284 const lines = output 285 .split('\n') 286 .filter(l => l.includes('no-unused-vars') || l.includes('unused-imports')); 287 assert.equal(lines.length, 1); 288 }); 289 290 test('handles multi-line lint output correctly', () => { 291 const output = [ 292 'src/a.js:1:1: error no-unused-vars a unused', 293 'src/b.js:2:1: error no-unused-vars b unused', 294 'src/c.js:3:1: warning other-rule something else', 295 ].join('\n'); 296 const lines = output.split('\n').filter(l => l.includes('no-unused-vars')); 297 assert.equal(lines.length, 2); 298 }); 299 300 test('lint success (empty output) has no unused vars', () => { 301 const output = ''; 302 const lines = output 303 .split('\n') 304 .filter(l => l.includes('no-unused-vars') || l.includes('unused-imports')); 305 assert.equal(lines.length, 0); 306 }); 307 308 test('lint output with other errors but no unused vars', () => { 309 const output = 'src/test.js:5:3: error semi Missing semicolon'; 310 const lines = output 311 .split('\n') 312 .filter(l => l.includes('no-unused-vars') || l.includes('unused-imports')); 313 assert.equal(lines.length, 0); 314 }); 315 316 test('lint success path shows checkmark in report', () => { 317 // When lint succeeds (lintResult.success = true) 318 const lintResult = { success: true }; 319 const msg = lintResult.success ? '✅ No lint errors detected' : '⚠️ Lint issues found'; 320 assert.ok(msg.includes('✅')); 321 assert.ok(msg.includes('No lint errors')); 322 }); 323 324 test('lint failure with no unused vars shows no-issues message', () => { 325 const output = ''; 326 const unusedLines = output 327 .split('\n') 328 .filter(l => l.includes('no-unused-vars') || l.includes('unused-imports')); 329 const msg = 330 unusedLines.length > 0 331 ? `⚠️ Found ${unusedLines.length} potential unused` 332 : '✅ No obvious unused code detected'; 333 assert.ok(msg.includes('No obvious unused code')); 334 }); 335 }); 336 337 // ── Coverage threshold analysis ──────────────────────────────────────────── 338 describe('deep-code-analysis-extended - coverage threshold logic', () => { 339 beforeEach(resetAllMocks); 340 341 test('coverage 0% is below 70% critical threshold', () => { 342 const pct = 0; 343 assert.ok(pct < 70); 344 addReviewItemMock({ file: 'Test Coverage', type: 'test', priority: 'high' }); 345 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].priority, 'high'); 346 }); 347 348 test('coverage 69.9% is below 70% critical threshold', () => { 349 const pct = 69.9; 350 assert.ok(pct < 70); 351 }); 352 353 test('coverage exactly 70% is NOT below 70% threshold', () => { 354 const pct = 70; 355 assert.equal(pct < 70, false); 356 // But 70 < 80, so it's medium priority 357 const isMedium = pct >= 70 && pct < 80; 358 assert.equal(isMedium, true); 359 }); 360 361 test('coverage 79.9% is below 80% target (medium priority)', () => { 362 const pct = 79.9; 363 const isBelowTarget = pct >= 70 && pct < 80; 364 assert.equal(isBelowTarget, true); 365 addReviewItemMock({ file: 'Test Coverage', priority: 'medium' }); 366 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].priority, 'medium'); 367 }); 368 369 test('coverage exactly 80% meets target', () => { 370 const pct = 80; 371 assert.equal(pct >= 80, true); 372 }); 373 374 test('coverage 100% meets target', () => { 375 const pct = 100; 376 assert.equal(pct >= 80, true); 377 // No review item needed 378 assert.equal(addReviewItemMock.mock.calls.length, 0); 379 }); 380 381 test('coverage report formatting: toFixed(1) works correctly', () => { 382 const pct = 68.512345; 383 assert.equal(pct.toFixed(1), '68.5'); 384 }); 385 386 test('coverage report shows line/branch/function percentages', () => { 387 const totalCoverage = { lines: { pct: 75 }, branches: { pct: 70 }, functions: { pct: 80 } }; 388 const sections = []; 389 sections.push(`- Line coverage: ${totalCoverage.lines.pct.toFixed(1)}%`); 390 sections.push(`- Branch coverage: ${totalCoverage.branches.pct.toFixed(1)}%`); 391 sections.push(`- Function coverage: ${totalCoverage.functions.pct.toFixed(1)}%`); 392 393 const report = sections.join('\n'); 394 assert.ok(report.includes('75.0%')); 395 assert.ok(report.includes('70.0%')); 396 assert.ok(report.includes('80.0%')); 397 }); 398 }); 399 400 // ── Vulnerability parsing edge cases ────────────────────────────────────── 401 describe('deep-code-analysis-extended - vulnerability parsing', () => { 402 beforeEach(resetAllMocks); 403 404 test('audit output with vulnerabilities=null is treated as no vulns', () => { 405 const auditData = JSON.parse(JSON.stringify({ vulnerabilities: null })); 406 const hasVulns = auditData.vulnerabilities !== null && auditData.vulnerabilities !== undefined; 407 assert.equal(hasVulns, false); 408 }); 409 410 test('audit output missing vulnerabilities key is treated as no vulns', () => { 411 const auditData = { metadata: {} }; 412 const hasVulns = Boolean(auditData.vulnerabilities); 413 assert.equal(hasVulns, false); 414 }); 415 416 test('critical=0, high=0, moderate=5 → moderate review item', () => { 417 const vulns = { critical: 0, high: 0, moderate: 5, low: 2 }; 418 if (vulns.critical > 0 || vulns.high > 0) { 419 addReviewItemMock({ priority: 'critical' }); 420 } else if (vulns.moderate > 0) { 421 addReviewItemMock({ priority: 'medium' }); 422 } 423 assert.equal(addReviewItemMock.mock.calls.length, 1); 424 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].priority, 'medium'); 425 }); 426 427 test('critical=0, high=0, moderate=0 → no review item', () => { 428 const vulns = { critical: 0, high: 0, moderate: 0, low: 5 }; 429 if (vulns.critical > 0 || vulns.high > 0) { 430 addReviewItemMock({ priority: 'critical' }); 431 } else if (vulns.moderate > 0) { 432 addReviewItemMock({ priority: 'medium' }); 433 } 434 // Only low vulns - no review item 435 assert.equal(addReviewItemMock.mock.calls.length, 0); 436 }); 437 438 test('no vulnerabilities at all → no review items', () => { 439 const vulns = { critical: 0, high: 0, moderate: 0, low: 0 }; 440 if (vulns.critical > 0 || vulns.high > 0) { 441 addReviewItemMock({ priority: 'critical' }); 442 } else if (vulns.moderate > 0) { 443 addReviewItemMock({ priority: 'medium' }); 444 } 445 // else: no review item 446 assert.equal(addReviewItemMock.mock.calls.length, 0); 447 }); 448 449 test('audit JSON parse failure → catch block skips review', () => { 450 const badJson = 'not json {{{'; 451 let parsed; 452 let parseError; 453 try { 454 parsed = JSON.parse(badJson); 455 } catch (e) { 456 parseError = e; 457 } 458 assert.ok(parseError instanceof SyntaxError); 459 assert.equal(parsed, undefined); 460 // Review item would NOT be added in catch block 461 assert.equal(addReviewItemMock.mock.calls.length, 0); 462 }); 463 464 test('empty audit output → "audit failed to run" path', () => { 465 const auditOutput = ''; 466 const hasSomeOutput = Boolean(auditOutput); 467 assert.equal(hasSomeOutput, false); 468 // This triggers the "audit failed to run" branch 469 }); 470 471 test('vulnerability count string for review item', () => { 472 const criticalCount = 3; 473 const highCount = 2; 474 const reason = `${criticalCount} critical and ${highCount} high severity vulnerabilities detected in npm dependencies.`; 475 assert.ok(reason.includes('3 critical')); 476 assert.ok(reason.includes('2 high')); 477 }); 478 479 test('moderate vulnerability reason string', () => { 480 const moderateCount = 7; 481 const reason = `${moderateCount} moderate severity vulnerabilities detected.`; 482 assert.ok(reason.includes('7 moderate')); 483 }); 484 }); 485 486 // ── Git status analysis ──────────────────────────────────────────────────── 487 describe('deep-code-analysis-extended - git status analysis', () => { 488 beforeEach(resetAllMocks); 489 490 test('empty git status output = clean working directory', () => { 491 const output = ''; 492 const isClean = output.trim() === ''; 493 assert.equal(isClean, true); 494 }); 495 496 test('whitespace-only git status = clean working directory', () => { 497 const output = ' \n '; 498 const isClean = output.trim() === ''; 499 assert.equal(isClean, true); 500 }); 501 502 test('single changed file detected', () => { 503 const output = ' M src/index.js'; 504 const count = output.trim().split('\n').length; 505 assert.equal(count, 1); 506 }); 507 508 test('multiple changes counted correctly', () => { 509 const output = [' M src/a.js', ' M src/b.js', '?? new.js'].join('\n'); 510 const count = output.trim().split('\n').length; 511 assert.equal(count, 3); 512 }); 513 514 test('git status failure still shows message', () => { 515 // When git status fails (success=false) with empty output 516 const statusResult = { success: false, output: '' }; 517 // The code checks success AND output.trim() !== '' 518 const hasChanges = statusResult.success && statusResult.output.trim() !== ''; 519 assert.equal(hasChanges, false); 520 // Clean message would show even on failure 521 }); 522 523 test('git status success with changes shows warning', () => { 524 const statusResult = { success: true, output: ' M src/test.js' }; 525 const hasChanges = statusResult.success && statusResult.output.trim() !== ''; 526 assert.equal(hasChanges, true); 527 }); 528 }); 529 530 // ── Report generation structure ──────────────────────────────────────────── 531 describe('deep-code-analysis-extended - report generation structure', () => { 532 beforeEach(resetAllMocks); 533 534 test('report header includes title and generation time', () => { 535 const sections = []; 536 const now = new Date().toISOString(); 537 sections.push('# Deep Code Analysis Report'); 538 sections.push(`\nGenerated: ${now}\n`); 539 540 const report = sections.join('\n'); 541 assert.ok(report.startsWith('# Deep Code Analysis Report')); 542 assert.ok(report.includes('Generated:')); 543 assert.ok(report.includes('2026')); 544 }); 545 546 test('report path uses timestamp as YYYY-MM-DD', () => { 547 const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; 548 const reportPath = `/path/.analysis-reports/deep-analysis-${timestamp}.md`; 549 assert.match(reportPath, /deep-analysis-\d{4}-\d{2}-\d{2}\.md$/); 550 }); 551 552 test('sections array joined with newline produces valid markdown', () => { 553 const sections = ['# Report', '## Section 1', '- Item 1', '- Item 2', '', '## Section 2']; 554 const content = sections.join('\n'); 555 assert.ok(content.includes('# Report')); 556 assert.ok(content.includes('## Section 1')); 557 assert.ok(content.includes('## Section 2')); 558 assert.ok(content.includes('- Item 1')); 559 }); 560 561 test('quick actions section contains all needed commands', () => { 562 const sections = [ 563 '### Quick Actions\n', 564 '```bash', 565 '# Fix linting issues', 566 'npm run lint:fix', 567 '', 568 '# Run security audit', 569 'npm audit fix', 570 '', 571 '# Update test coverage', 572 'npm test', 573 '', 574 '# Update dependencies', 575 'npm run deps:update', 576 '```\n', 577 ]; 578 const report = sections.join('\n'); 579 assert.ok(report.includes('npm run lint:fix')); 580 assert.ok(report.includes('npm audit fix')); 581 assert.ok(report.includes('npm test')); 582 assert.ok(report.includes('npm run deps:update')); 583 assert.ok(report.includes('```bash')); 584 }); 585 586 test('summary section has standard message', () => { 587 const summaryMsg = 'This automated analysis has identified potential areas for improvement.'; 588 assert.ok(summaryMsg.includes('areas for improvement')); 589 assert.ok(summaryMsg.includes('automated analysis')); 590 }); 591 592 test('log methods format messages correctly', () => { 593 const messages = []; 594 const log = { 595 info: msg => messages.push(`[INFO] ${msg}`), 596 success: msg => messages.push(`[SUCCESS] ${msg}`), 597 warn: msg => messages.push(`[WARN] ${msg}`), 598 error: msg => messages.push(`[ERROR] ${msg}`), 599 }; 600 601 log.info('Starting analysis'); 602 log.success('Report generated'); 603 log.warn('Something stale'); 604 log.error('Audit failed'); 605 606 assert.equal(messages[0], '[INFO] Starting analysis'); 607 assert.equal(messages[1], '[SUCCESS] Report generated'); 608 assert.equal(messages[2], '[WARN] Something stale'); 609 assert.equal(messages[3], '[ERROR] Audit failed'); 610 }); 611 612 test('analysis report directory is created if missing', () => { 613 // existsSync returns false → mkdirSync is called 614 mockFs.existsSync.mock.mockImplementation(() => false); 615 const exists = mockFs.existsSync('/some/path'); 616 if (!exists) { 617 mockFs.mkdirSync('/some/path', { recursive: true }); 618 } 619 assert.equal(mockFs.mkdirSync.mock.calls.length, 1); 620 const [dirPath, opts] = mockFs.mkdirSync.mock.calls[0].arguments; 621 assert.ok(dirPath.includes('some/path')); 622 assert.equal(opts.recursive, true); 623 }); 624 }); 625 626 // ── Integration patterns ─────────────────────────────────────────────────── 627 describe('deep-code-analysis-extended - integration scenario coverage', () => { 628 beforeEach(resetAllMocks); 629 630 test('all review item types are valid', () => { 631 const validTypes = ['maintenance', 'documentation', 'test', 'security']; 632 for (const type of validTypes) { 633 addReviewItemMock({ 634 file: 'test.js', 635 reason: 'Test reason', 636 type, 637 priority: 'low', 638 }); 639 } 640 assert.equal(addReviewItemMock.mock.calls.length, 4); 641 for (let i = 0; i < 4; i++) { 642 assert.equal(addReviewItemMock.mock.calls[i].arguments[0].type, validTypes[i]); 643 } 644 }); 645 646 test('all review priorities are valid', () => { 647 const priorities = ['low', 'medium', 'high', 'critical']; 648 for (const priority of priorities) { 649 addReviewItemMock({ 650 file: 'test.js', 651 reason: 'Test', 652 type: 'security', 653 priority, 654 }); 655 } 656 assert.equal(addReviewItemMock.mock.calls.length, 4); 657 for (let i = 0; i < 4; i++) { 658 assert.equal(addReviewItemMock.mock.calls[i].arguments[0].priority, priorities[i]); 659 } 660 }); 661 662 test('review item for docs missing file field handles path correctly', () => { 663 // Test that the file path for different doc types is correct 664 addReviewItemMock({ 665 file: 'npm dependencies', 666 reason: 'Critical vulns found', 667 type: 'security', 668 priority: 'critical', 669 }); 670 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].file, 'npm dependencies'); 671 }); 672 673 test('multiple review items accumulated in single analysis', () => { 674 // Simulate a full analysis that adds multiple review items 675 const scenarios = [ 676 { file: 'docs/TODO.md', type: 'maintenance', priority: 'low' }, 677 { file: 'README.md', type: 'documentation', priority: 'high' }, 678 { file: 'Test Coverage', type: 'test', priority: 'high' }, 679 { file: 'npm dependencies', type: 'security', priority: 'critical' }, 680 ]; 681 682 for (const s of scenarios) { 683 addReviewItemMock({ 684 file: s.file, 685 reason: `Review needed: ${s.file}`, 686 type: s.type, 687 priority: s.priority, 688 }); 689 } 690 691 assert.equal(addReviewItemMock.mock.calls.length, 4); 692 assert.equal(addReviewItemMock.mock.calls[2].arguments[0].type, 'test'); 693 assert.equal(addReviewItemMock.mock.calls[3].arguments[0].priority, 'critical'); 694 }); 695 696 test('doc file list contains expected files', () => { 697 const docFiles = ['README.md', 'CLAUDE.md', 'docs/TODO.md', '.env.example']; 698 assert.equal(docFiles.length, 4); 699 assert.ok(docFiles.includes('README.md')); 700 assert.ok(docFiles.includes('CLAUDE.md')); 701 assert.ok(docFiles.includes('.env.example')); 702 }); 703 704 test('stale threshold constant is 30 days', () => { 705 const staleThreshold = 30; 706 assert.equal(staleThreshold, 30); 707 }); 708 709 test('main() error handling: unexpected error calls process.exit(1)', () => { 710 // Test the pattern of the error handler 711 let exitCode; 712 const mockExit = code => { 713 exitCode = code; 714 }; 715 716 const err = new Error('Unexpected error'); 717 // Simulate catch block 718 try { 719 throw err; 720 } catch { 721 mockExit(1); 722 } 723 724 assert.equal(exitCode, 1); 725 }); 726 });