deep-code-analysis-import.test.js
1 /** 2 * Import-based tests for scripts/deep-code-analysis.js 3 * 4 * These tests actually import and execute the module's main() function 5 * to get real V8 coverage, using the cache-bust technique. 6 * 7 * The script calls main().catch(...) at the top level, which means 8 * main() runs immediately when the module is imported. 9 * 10 * Strategy: 11 * - Set up mocks BEFORE importing 12 * - Use dynamic import with cache-bust to get fresh execution 13 * - Capture side effects (writeFileSync calls, addReviewItem calls) 14 * - Test different scenarios by varying mock responses 15 */ 16 17 import { describe, test, before, mock } from 'node:test'; 18 import assert from 'node:assert/strict'; 19 import path from 'path'; 20 import { fileURLToPath } from 'url'; 21 22 const __filename = fileURLToPath(import.meta.url); 23 const __dirname = path.dirname(__filename); 24 25 // ── Track state across imports ───────────────────────────────────────────── 26 let addReviewItemCalls = []; 27 let initializeQueueCalls = []; 28 let writeFileSyncCalls = []; 29 let mkdirSyncCalls = []; 30 let execSyncCalls = []; 31 let exitCalls = []; 32 let consoleOutputLines = []; 33 34 // ── Save originals ───────────────────────────────────────────────────────── 35 const originalExit = process.exit; 36 const originalConsoleLog = console.log; 37 const originalConsoleError = console.error; 38 39 // ── Mock process.exit ───────────────────────────────────────────────────── 40 // Must mock process.exit before any imports that might fail 41 process.exit = code => { 42 exitCalls.push(code); 43 // Do NOT throw - the script's catch() will also call process.exit 44 }; 45 46 // ── State control for mocks ─────────────────────────────────────────────── 47 let execSyncHandler = () => ''; 48 let existsSyncHandler = () => true; 49 let readFileSyncHandler = () => ''; 50 let statSyncHandler = () => ({ mtimeMs: Date.now() }); 51 52 // ── Mock human-review-queue ─────────────────────────────────────────────── 53 mock.module('../../src/utils/human-review-queue.js', { 54 namedExports: { 55 addReviewItem: item => { 56 addReviewItemCalls.push(item); 57 }, 58 initializeQueue: () => { 59 initializeQueueCalls.push(true); 60 }, 61 }, 62 }); 63 64 // ── Mock child_process ──────────────────────────────────────────────────── 65 mock.module('child_process', { 66 namedExports: { 67 execSync: (cmd, opts) => { 68 execSyncCalls.push({ cmd, opts }); 69 return execSyncHandler(cmd, opts); 70 }, 71 }, 72 }); 73 74 // ── Mock fs ─────────────────────────────────────────────────────────────── 75 mock.module('fs', { 76 defaultExport: { 77 existsSync: p => existsSyncHandler(p), 78 mkdirSync: (p, opts) => { 79 mkdirSyncCalls.push({ p, opts }); 80 }, 81 readFileSync: (p, enc) => readFileSyncHandler(p, enc), 82 writeFileSync: (p, content) => { 83 writeFileSyncCalls.push({ path: p, content }); 84 }, 85 statSync: p => statSyncHandler(p), 86 }, 87 namedExports: { 88 existsSync: p => existsSyncHandler(p), 89 mkdirSync: (p, opts) => { 90 mkdirSyncCalls.push({ p, opts }); 91 }, 92 readFileSync: (p, enc) => readFileSyncHandler(p, enc), 93 writeFileSync: (p, content) => { 94 writeFileSyncCalls.push({ path: p, content }); 95 }, 96 statSync: p => statSyncHandler(p), 97 }, 98 }); 99 100 // ── Helper to reset state ───────────────────────────────────────────────── 101 function resetState() { 102 addReviewItemCalls = []; 103 initializeQueueCalls = []; 104 writeFileSyncCalls = []; 105 mkdirSyncCalls = []; 106 execSyncCalls = []; 107 exitCalls = []; 108 consoleOutputLines = []; 109 110 execSyncHandler = () => ''; 111 existsSyncHandler = () => true; 112 readFileSyncHandler = () => ''; 113 statSyncHandler = () => ({ mtimeMs: Date.now() }); 114 115 // Suppress console output in tests 116 console.log = (...args) => { 117 consoleOutputLines.push(args.join(' ')); 118 }; 119 console.error = (...args) => { 120 consoleOutputLines.push(`[ERROR] ${args.join(' ')}`); 121 }; 122 } 123 124 // ── Helper: run the script ───────────────────────────────────────────────── 125 async function runScript() { 126 const cacheBust = `?t=${Date.now()}-${Math.random()}`; 127 await import(`../../scripts/deep-code-analysis.js${cacheBust}`); 128 // Wait for async main() to complete 129 await new Promise(resolve => setTimeout(resolve, 150)); 130 } 131 132 // ── Restore console on completion ───────────────────────────────────────── 133 process.on('exit', () => { 134 process.exit = originalExit; 135 console.log = originalConsoleLog; 136 console.error = originalConsoleError; 137 }); 138 139 // ── Test Suites ─────────────────────────────────────────────────────────── 140 141 describe('deep-code-analysis import - happy path (all green)', () => { 142 before(async () => { 143 resetState(); 144 145 // Happy path: all commands succeed, no stale docs, few completed tasks 146 execSyncHandler = cmd => { 147 if (cmd.includes('npm run lint')) return ''; // lint passes 148 if (cmd.includes('npm audit')) { 149 return JSON.stringify({ 150 vulnerabilities: { critical: 0, high: 0, moderate: 0, low: 0 }, 151 }); 152 } 153 if (cmd.includes('git status')) return ''; // clean working tree 154 return ''; 155 }; 156 157 existsSyncHandler = p => { 158 if (p.includes('coverage-summary.json')) return false; // no coverage report 159 return true; 160 }; 161 162 readFileSyncHandler = p => { 163 if (p.includes('TODO.md')) { 164 return '# TODO\n- ✅ Done 1\n- ✅ Done 2\n- [ ] Pending'; 165 } 166 return ''; 167 }; 168 169 statSyncHandler = () => ({ mtimeMs: Date.now() }); // all files modified today 170 171 await runScript(); 172 }); 173 174 test('main() completes and writes report', () => { 175 assert.ok(writeFileSyncCalls.length >= 1, 'Should write report file'); 176 }); 177 178 test('report path contains deep-analysis- prefix and .md extension', () => { 179 const reportPath = writeFileSyncCalls[0]?.path || ''; 180 assert.ok(reportPath.includes('deep-analysis-'), 'Report path should contain prefix'); 181 assert.ok(reportPath.endsWith('.md'), 'Report should be .md file'); 182 }); 183 184 test('report contains all section headers', () => { 185 const content = writeFileSyncCalls[0]?.content || ''; 186 assert.ok(content.includes('# Deep Code Analysis Report')); 187 assert.ok(content.includes('TODO.md Review')); 188 assert.ok(content.includes('Stale Documentation Check')); 189 assert.ok(content.includes('Unused Code Detection')); 190 assert.ok(content.includes('Technical Debt Assessment')); 191 assert.ok(content.includes('Security Vulnerability Scan')); 192 assert.ok(content.includes('Git Repository Status')); 193 }); 194 195 test('initializeQueue was called', () => { 196 assert.ok(initializeQueueCalls.length >= 1); 197 }); 198 199 test('no review items added for clean project', () => { 200 // No TODOs > 10, no stale docs > 60d, no coverage report found, no vulns 201 assert.equal(addReviewItemCalls.length, 0); 202 }); 203 204 test('report shows no lint errors', () => { 205 const content = writeFileSyncCalls[0]?.content || ''; 206 assert.ok(content.includes('No lint errors detected')); 207 }); 208 209 test('report shows coverage report not found', () => { 210 const content = writeFileSyncCalls[0]?.content || ''; 211 assert.ok(content.includes('Coverage report not found') || content.includes('npm test')); 212 }); 213 214 test('report shows clean working directory', () => { 215 const content = writeFileSyncCalls[0]?.content || ''; 216 assert.ok(content.includes('clean')); 217 }); 218 219 test('report includes quick actions section', () => { 220 const content = writeFileSyncCalls[0]?.content || ''; 221 assert.ok(content.includes('npm run lint:fix')); 222 assert.ok(content.includes('npm audit fix')); 223 assert.ok(content.includes('npm test')); 224 assert.ok(content.includes('npm run deps:update')); 225 }); 226 }); 227 228 describe('deep-code-analysis import - critical problems scenario', () => { 229 before(async () => { 230 resetState(); 231 232 // Critical scenario: many completed tasks, stale docs, low coverage, vulns, changes 233 execSyncHandler = cmd => { 234 if (cmd.includes('npm run lint')) { 235 const err = new Error('Lint failed'); 236 err.stdout = 'src/test.js:5:3: error no-unused-vars x defined but never used'; 237 throw err; 238 } 239 if (cmd.includes('npm audit')) { 240 return JSON.stringify({ 241 vulnerabilities: { critical: 3, high: 2, moderate: 5, low: 10 }, 242 }); 243 } 244 if (cmd.includes('git status')) return ' M src/index.js\n?? new-file.js'; 245 return ''; 246 }; 247 248 const hundredDaysAgo = Date.now() - 100 * 24 * 60 * 60 * 1000; 249 const recentDate = Date.now() - 2 * 24 * 60 * 60 * 1000; 250 251 existsSyncHandler = p => { 252 if (p.includes('coverage-summary.json')) return true; 253 return true; 254 }; 255 256 readFileSyncHandler = p => { 257 if (p.includes('TODO.md')) { 258 // 15 completed tasks (> 10) 259 const completed = Array(15) 260 .fill(null) 261 .map((_, i) => `- ✅ Task ${i + 1}`) 262 .join('\n'); 263 return `# TODO\n${completed}\n- [ ] Pending`; 264 } 265 if (p.includes('coverage-summary.json')) { 266 return JSON.stringify({ 267 total: { lines: { pct: 60 }, branches: { pct: 55 }, functions: { pct: 65 } }, 268 }); 269 } 270 return ''; 271 }; 272 273 statSyncHandler = p => { 274 if (p.includes('TODO.md')) return { mtimeMs: recentDate }; 275 return { mtimeMs: hundredDaysAgo }; // all doc files 100 days old 276 }; 277 278 await runScript(); 279 }); 280 281 test('report was generated', () => { 282 assert.ok(writeFileSyncCalls.length >= 1); 283 }); 284 285 test('TODO.md archiving review item added for 15 completed tasks', () => { 286 const todoItem = addReviewItemCalls.find( 287 item => item.file === 'docs/TODO.md' && item.type === 'maintenance' 288 ); 289 assert.ok(todoItem, 'Should add review item for TODO.md'); 290 assert.equal(todoItem.priority, 'low'); 291 assert.ok(todoItem.reason.includes('15')); 292 }); 293 294 test('stale docs flagged with high priority for 100+ day old files', () => { 295 const docItems = addReviewItemCalls.filter(item => item.type === 'documentation'); 296 assert.ok(docItems.length > 0, 'Should flag stale docs'); 297 const highPriority = docItems.filter(item => item.priority === 'high'); 298 assert.ok(highPriority.length > 0, 'Should have high priority for 100+ day docs'); 299 }); 300 301 test('critical coverage flagged for 60% coverage', () => { 302 const coverageItem = addReviewItemCalls.find( 303 item => item.file === 'Test Coverage' && item.type === 'test' 304 ); 305 assert.ok(coverageItem, 'Should flag coverage'); 306 assert.equal(coverageItem.priority, 'high'); 307 }); 308 309 test('critical vulnerabilities flagged', () => { 310 const securityItem = addReviewItemCalls.find(item => item.type === 'security'); 311 assert.ok(securityItem, 'Should flag security issues'); 312 assert.equal(securityItem.priority, 'critical'); 313 }); 314 315 test('report mentions lint warnings', () => { 316 const content = writeFileSyncCalls[0]?.content || ''; 317 // Should mention unused vars or lint recommendation 318 assert.ok( 319 content.includes('no-unused-vars') || 320 content.includes('unused') || 321 content.includes('lint:fix') 322 ); 323 }); 324 325 test('report mentions uncommitted changes', () => { 326 const content = writeFileSyncCalls[0]?.content || ''; 327 assert.ok(content.includes('uncommitted') || content.includes('pending changes')); 328 }); 329 330 test('report mentions critical coverage', () => { 331 const content = writeFileSyncCalls[0]?.content || ''; 332 assert.ok(content.includes('Critical') || content.includes('below 70%')); 333 }); 334 }); 335 336 describe('deep-code-analysis import - moderate scenario', () => { 337 before(async () => { 338 resetState(); 339 340 // Moderate scenario: coverage 70-80%, only moderate vulns, 35-day stale docs 341 execSyncHandler = cmd => { 342 if (cmd.includes('npm run lint')) return ''; // lint passes 343 if (cmd.includes('npm audit')) { 344 return JSON.stringify({ 345 vulnerabilities: { critical: 0, high: 0, moderate: 4, low: 3 }, 346 }); 347 } 348 if (cmd.includes('git status')) return ''; 349 return ''; 350 }; 351 352 const thirtyFiveDaysAgo = Date.now() - 35 * 24 * 60 * 60 * 1000; 353 const recentDate = Date.now(); 354 355 existsSyncHandler = p => { 356 if (p.includes('coverage-summary.json')) return true; 357 return true; 358 }; 359 360 readFileSyncHandler = p => { 361 if (p.includes('TODO.md')) return '# TODO\n- [ ] Pending\n'; 362 if (p.includes('coverage-summary.json')) { 363 return JSON.stringify({ 364 total: { lines: { pct: 75 }, branches: { pct: 68 }, functions: { pct: 80 } }, 365 }); 366 } 367 return ''; 368 }; 369 370 statSyncHandler = p => { 371 if (p.includes('TODO.md')) return { mtimeMs: recentDate }; 372 return { mtimeMs: thirtyFiveDaysAgo }; // 35 days old (stale but not critical) 373 }; 374 375 await runScript(); 376 }); 377 378 test('report generated for moderate scenario', () => { 379 assert.ok(writeFileSyncCalls.length >= 1); 380 }); 381 382 test('moderate vulns flagged with medium priority', () => { 383 const securityItem = addReviewItemCalls.find(item => item.type === 'security'); 384 assert.ok(securityItem, 'Should flag moderate vulnerabilities'); 385 assert.equal(securityItem.priority, 'medium'); 386 }); 387 388 test('medium coverage 70-80% flagged with medium priority', () => { 389 const coverageItem = addReviewItemCalls.find(item => item.file === 'Test Coverage'); 390 assert.ok(coverageItem, 'Should flag coverage between 70-80%'); 391 assert.equal(coverageItem.priority, 'medium'); 392 }); 393 394 test('stale docs 35 days old shown as stale but not flagged for review', () => { 395 // 35 days > 30 threshold → shows warning 396 // 35 days ≤ 60 threshold → no review item for documentation 397 const docItems = addReviewItemCalls.filter(item => item.type === 'documentation'); 398 assert.equal(docItems.length, 0); 399 // But stale warning should appear in report 400 const content = writeFileSyncCalls[0]?.content || ''; 401 assert.ok(content.includes('days ago') || content.includes('Last modified')); 402 }); 403 }); 404 405 describe('deep-code-analysis import - high coverage passes scenario', () => { 406 before(async () => { 407 resetState(); 408 409 // Good coverage >= 80%, no vulns, clean git 410 execSyncHandler = cmd => { 411 if (cmd.includes('npm run lint')) return ''; 412 if (cmd.includes('npm audit')) { 413 return JSON.stringify({ 414 vulnerabilities: { critical: 0, high: 0, moderate: 0, low: 0 }, 415 }); 416 } 417 if (cmd.includes('git status')) return ''; 418 return ''; 419 }; 420 421 existsSyncHandler = p => { 422 if (p.includes('coverage-summary.json')) return true; 423 return true; 424 }; 425 426 readFileSyncHandler = p => { 427 if (p.includes('TODO.md')) return '# TODO\n'; 428 if (p.includes('coverage-summary.json')) { 429 return JSON.stringify({ 430 total: { lines: { pct: 85 }, branches: { pct: 80 }, functions: { pct: 90 } }, 431 }); 432 } 433 return ''; 434 }; 435 436 statSyncHandler = () => ({ mtimeMs: Date.now() - 5 * 24 * 60 * 60 * 1000 }); // 5 days old 437 438 await runScript(); 439 }); 440 441 test('no coverage review items when coverage >= 80%', () => { 442 const coverageItems = addReviewItemCalls.filter(item => item.file === 'Test Coverage'); 443 assert.equal(coverageItems.length, 0); 444 }); 445 446 test('report shows coverage meets target', () => { 447 const content = writeFileSyncCalls[0]?.content || ''; 448 assert.ok( 449 content.includes('meets target') || content.includes('85.0%') || content.includes('Coverage') 450 ); 451 }); 452 453 test('report shows no vulnerabilities', () => { 454 const content = writeFileSyncCalls[0]?.content || ''; 455 assert.ok( 456 content.includes('No significant vulnerabilities') || 457 content.includes('No vulnerabilities') || 458 content.includes('✅') 459 ); 460 }); 461 }); 462 463 describe('deep-code-analysis import - missing TODO.md scenario', () => { 464 before(async () => { 465 resetState(); 466 467 execSyncHandler = cmd => { 468 if (cmd.includes('npm run lint')) return ''; 469 if (cmd.includes('npm audit')) return ''; // empty output 470 if (cmd.includes('git status')) return ''; 471 return ''; 472 }; 473 474 existsSyncHandler = p => { 475 if (p.includes('TODO.md')) return false; // TODO.md missing 476 if (p.includes('coverage-summary.json')) return false; 477 return true; 478 }; 479 480 readFileSyncHandler = () => ''; 481 statSyncHandler = () => ({ mtimeMs: Date.now() - 50 * 24 * 60 * 60 * 1000 }); // 50 days 482 483 await runScript(); 484 }); 485 486 test('handles missing TODO.md gracefully', () => { 487 const content = writeFileSyncCalls[0]?.content || ''; 488 assert.ok(content.includes('TODO.md') && content.includes('not found')); 489 }); 490 491 test('empty npm audit output shows audit failed message', () => { 492 const content = writeFileSyncCalls[0]?.content || ''; 493 assert.ok( 494 content.includes('Audit failed') || 495 content.includes('Could not parse') || 496 content.includes('audit') 497 ); 498 }); 499 500 test('report still generated without TODO.md', () => { 501 assert.ok(writeFileSyncCalls.length >= 1); 502 }); 503 }); 504 505 describe('deep-code-analysis import - only high vulnerabilities scenario', () => { 506 before(async () => { 507 resetState(); 508 509 execSyncHandler = cmd => { 510 if (cmd.includes('npm run lint')) return ''; 511 if (cmd.includes('npm audit')) { 512 return JSON.stringify({ 513 vulnerabilities: { critical: 0, high: 3, moderate: 0, low: 1 }, 514 }); 515 } 516 if (cmd.includes('git status')) return ''; 517 return ''; 518 }; 519 520 existsSyncHandler = p => { 521 if (p.includes('coverage-summary.json')) return false; 522 return true; 523 }; 524 525 readFileSyncHandler = p => { 526 if (p.includes('TODO.md')) return '# TODO\n'; 527 return ''; 528 }; 529 530 statSyncHandler = () => ({ mtimeMs: Date.now() }); 531 532 await runScript(); 533 }); 534 535 test('only-high vulnerabilities flagged with high (not critical) priority', () => { 536 const securityItem = addReviewItemCalls.find(item => item.type === 'security'); 537 assert.ok(securityItem, 'Should flag vulnerabilities'); 538 assert.equal(securityItem.priority, 'high'); // critical=0, high>0 → 'high' 539 }); 540 }); 541 542 describe('deep-code-analysis import - lint with unused vars scenario', () => { 543 before(async () => { 544 resetState(); 545 546 execSyncHandler = cmd => { 547 if (cmd.includes('npm run lint')) { 548 const err = new Error('Lint failed'); 549 err.stdout = 550 'src/a.js:3:1: error no-unused-vars a\nsrc/b.js:5:1: warning unused-imports b'; 551 throw err; 552 } 553 if (cmd.includes('npm audit')) { 554 return JSON.stringify({ vulnerabilities: null }); 555 } 556 if (cmd.includes('git status')) return ''; 557 return ''; 558 }; 559 560 existsSyncHandler = p => { 561 if (p.includes('coverage-summary.json')) return false; 562 return true; 563 }; 564 565 readFileSyncHandler = p => { 566 if (p.includes('TODO.md')) return '# TODO\n'; 567 return ''; 568 }; 569 570 statSyncHandler = () => ({ mtimeMs: Date.now() }); 571 572 await runScript(); 573 }); 574 575 test('unused vars detected from lint output', () => { 576 const content = writeFileSyncCalls[0]?.content || ''; 577 assert.ok( 578 content.includes('potential unused') || 579 content.includes('unused') || 580 content.includes('lint:fix') 581 ); 582 }); 583 584 test('no significant vulns path when vulnerabilities is null', () => { 585 const content = writeFileSyncCalls[0]?.content || ''; 586 assert.ok( 587 content.includes('No vulnerabilities') || content.includes('✅') || content.includes('audit') 588 ); 589 }); 590 }); 591 592 describe('deep-code-analysis import - report dir missing and audit parse fail', () => { 593 before(async () => { 594 resetState(); 595 596 execSyncHandler = cmd => { 597 if (cmd.includes('npm run lint')) { 598 // Lint fails but with NO unused-vars in output 599 const err = new Error('Lint failed'); 600 err.stdout = 'src/test.js: error semi Missing semicolon\n1 error found'; 601 throw err; 602 } 603 if (cmd.includes('npm audit')) { 604 // Return invalid JSON to trigger parse error (lines 262-263) 605 return 'not valid json { broken {{{'; 606 } 607 if (cmd.includes('git status')) return ''; 608 return ''; 609 }; 610 611 existsSyncHandler = p => { 612 // Report dir does NOT exist (lines 64-65 mkdirSync) 613 if (p.includes('.analysis-reports')) return false; 614 if (p.includes('coverage-summary.json')) return false; 615 return true; 616 }; 617 618 readFileSyncHandler = p => { 619 if (p.includes('TODO.md')) return '# TODO\n- [ ] Pending\n'; 620 return ''; 621 }; 622 623 statSyncHandler = () => ({ mtimeMs: Date.now() }); 624 625 await runScript(); 626 }); 627 628 test('report dir created when missing (mkdirSync called)', () => { 629 const dirCreated = mkdirSyncCalls.some(call => call.p.includes('.analysis-reports')); 630 assert.ok(dirCreated, 'Should create .analysis-reports directory'); 631 }); 632 633 test('report still generated despite missing dir', () => { 634 assert.ok(writeFileSyncCalls.length >= 1); 635 }); 636 637 test('lint fails with no unused vars shows no-obvious-code message', () => { 638 const content = writeFileSyncCalls[0]?.content || ''; 639 // Lint failed but no no-unused-vars lines → "No obvious unused code detected" 640 assert.ok( 641 content.includes('No obvious unused code') || 642 content.includes('lint') || 643 content.includes('unused') 644 ); 645 }); 646 647 test('invalid JSON audit output shows could not parse message', () => { 648 const content = writeFileSyncCalls[0]?.content || ''; 649 assert.ok( 650 content.includes('Could not parse audit output') || 651 content.includes('parse') || 652 content.includes('audit') 653 ); 654 }); 655 });