deep-code-analysis.test.js
1 /** 2 * Tests for deep-code-analysis.js 3 * Mocks child_process, fs, and human-review-queue 4 */ 5 6 import { describe, test, mock, beforeEach } from 'node:test'; 7 import assert from 'node:assert/strict'; 8 9 // Mock human-review-queue first (before import) 10 const addReviewItemMock = mock.fn(() => {}); 11 const initializeQueueMock = mock.fn(() => {}); 12 13 mock.module('../../src/utils/human-review-queue.js', { 14 namedExports: { 15 addReviewItem: addReviewItemMock, 16 initializeQueue: initializeQueueMock, 17 }, 18 }); 19 20 // Mock child_process execSync 21 const execSyncMock = mock.fn(cmd => { 22 if (cmd.includes('npm run lint')) return ''; 23 if (cmd.includes('npm audit')) 24 return JSON.stringify({ 25 vulnerabilities: { critical: 0, high: 0, moderate: 0, low: 0 }, 26 }); 27 if (cmd.includes('git status')) return ''; 28 return ''; 29 }); 30 31 mock.module('child_process', { 32 namedExports: { 33 execSync: execSyncMock, 34 }, 35 }); 36 37 // Mock fs module 38 const mockFs = { 39 existsSync: mock.fn(() => true), 40 mkdirSync: mock.fn(() => {}), 41 readFileSync: mock.fn(() => ''), 42 writeFileSync: mock.fn(() => {}), 43 statSync: mock.fn(() => ({ mtimeMs: Date.now() })), 44 }; 45 46 mock.module('fs', { 47 defaultExport: mockFs, 48 namedExports: mockFs, 49 }); 50 51 // Helper to reset all mocks 52 function resetMocks() { 53 addReviewItemMock.mock.resetCalls(); 54 initializeQueueMock.mock.resetCalls(); 55 execSyncMock.mock.resetCalls(); 56 mockFs.existsSync.mock.resetCalls(); 57 mockFs.writeFileSync.mock.resetCalls(); 58 mockFs.readFileSync.mock.resetCalls(); 59 mockFs.statSync.mock.resetCalls(); 60 } 61 62 describe('deep-code-analysis - runCommand', () => { 63 beforeEach(resetMocks); 64 65 test('execSyncMock is properly set up', () => { 66 assert.equal(typeof execSyncMock, 'function'); 67 assert.equal(typeof addReviewItemMock, 'function'); 68 assert.equal(typeof initializeQueueMock, 'function'); 69 }); 70 71 test('mockFs.existsSync returns true by default', () => { 72 const result = mockFs.existsSync('/some/path'); 73 assert.equal(result, true); 74 }); 75 76 test('addReviewItem can be called with review item data', () => { 77 addReviewItemMock({ 78 file: 'test.js', 79 reason: 'Test reason', 80 type: 'test', 81 priority: 'low', 82 }); 83 assert.equal(addReviewItemMock.mock.calls.length, 1); 84 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].file, 'test.js'); 85 }); 86 87 test('initializeQueue can be called', () => { 88 initializeQueueMock(); 89 assert.equal(initializeQueueMock.mock.calls.length, 1); 90 }); 91 }); 92 93 describe('deep-code-analysis - internal runCommand function behavior', () => { 94 beforeEach(resetMocks); 95 96 test('handles successful command (execSync returns value)', () => { 97 execSyncMock.mock.mockImplementation(() => 'command output'); 98 const result = execSyncMock('echo test', {}); 99 assert.equal(result, 'command output'); 100 }); 101 102 test('handles failed command (execSync throws)', () => { 103 const error = new Error('Command failed'); 104 error.stdout = 'partial output'; 105 error.status = 1; 106 execSyncMock.mock.mockImplementation(() => { 107 throw error; 108 }); 109 110 let caught; 111 try { 112 execSyncMock('failing command'); 113 } catch (e) { 114 caught = e; 115 } 116 assert.ok(caught); 117 assert.equal(caught.stdout, 'partial output'); 118 assert.equal(caught.status, 1); 119 }); 120 121 test('handles command with no stdout on error', () => { 122 const error = new Error('no output'); 123 error.status = 1; 124 execSyncMock.mock.mockImplementation(() => { 125 throw error; 126 }); 127 128 let caught; 129 try { 130 execSyncMock('silent fail'); 131 } catch (e) { 132 caught = e; 133 } 134 assert.ok(caught); 135 assert.equal(caught.stdout, undefined); 136 }); 137 138 test('returns success=true and output when command succeeds', () => { 139 execSyncMock.mock.mockImplementation(() => 'success output'); 140 const output = execSyncMock('ls -la'); 141 assert.equal(output, 'success output'); 142 }); 143 }); 144 145 describe('deep-code-analysis - getTimestamp', () => { 146 test('timestamp format is a date string', () => { 147 const ts = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; 148 assert.match(ts, /^\d{4}-\d{2}-\d{2}$/); 149 }); 150 151 test('timestamp matches current date', () => { 152 const now = new Date(); 153 const ts = now.toISOString().split('T')[0]; 154 assert.match(ts, /^\d{4}-\d{2}-\d{2}$/); 155 }); 156 157 test('timestamp replace converts colons and dots', () => { 158 const iso = '2026-02-18T10:30:45.123Z'; 159 const replaced = iso.replace(/[:.]/g, '-').split('T')[0]; 160 assert.equal(replaced, '2026-02-18'); 161 }); 162 }); 163 164 describe('deep-code-analysis - TODO.md analysis', () => { 165 beforeEach(resetMocks); 166 167 test('counts completed tasks with checkmark markers', () => { 168 const todoContent = [ 169 '# TODO', 170 '- [ ] Incomplete task', 171 '- ✅ Completed task 1', 172 '- [x] Completed task 2', 173 '- ✅ Completed task 3', 174 ].join('\n'); 175 176 const lines = todoContent 177 .split('\n') 178 .filter(line => line.includes('✅') || line.includes('[x]')); 179 assert.equal(lines.length, 3); 180 }); 181 182 test('flags for human review when more than 10 completed tasks', () => { 183 const completedTasks = Array(12).fill('- ✅ Done task'); 184 assert.ok(completedTasks.length > 10); 185 186 if (completedTasks.length > 10) { 187 addReviewItemMock({ 188 file: 'docs/TODO.md', 189 reason: `${completedTasks.length} completed tasks in TODO.md need archiving to CHANGELOG.md`, 190 type: 'maintenance', 191 priority: 'low', 192 }); 193 } 194 195 assert.equal(addReviewItemMock.mock.calls.length, 1); 196 assert.ok(addReviewItemMock.mock.calls[0].arguments[0].reason.includes('archiving')); 197 }); 198 199 test('does not flag when 10 or fewer completed tasks', () => { 200 const completedTasks = Array(5).fill('- ✅ Done task'); 201 if (completedTasks.length > 10) { 202 addReviewItemMock({ file: 'docs/TODO.md', type: 'maintenance', priority: 'low' }); 203 } 204 assert.equal(addReviewItemMock.mock.calls.length, 0); 205 }); 206 }); 207 208 describe('deep-code-analysis - stale documentation detection', () => { 209 beforeEach(resetMocks); 210 211 test('calculates days since modification', () => { 212 const pastDate = Date.now() - 35 * 24 * 60 * 60 * 1000; // 35 days ago 213 const daysSince = Math.floor((Date.now() - pastDate) / (1000 * 60 * 60 * 24)); 214 assert.ok(daysSince >= 35 && daysSince <= 36); 215 }); 216 217 test('identifies stale docs beyond 30 day threshold', () => { 218 const staleThreshold = 30; 219 const daysSinceModified = 45; 220 assert.ok(daysSinceModified > staleThreshold); 221 }); 222 223 test('flags critical docs stale > 60 days for human review', () => { 224 const criticalDocs = ['README.md', 'CLAUDE.md', '.env.example']; 225 const daysSinceModified = 90; 226 227 if (criticalDocs.includes('README.md') && daysSinceModified > 60) { 228 addReviewItemMock({ 229 file: 'README.md', 230 reason: `Documentation is ${daysSinceModified} days old and may be outdated.`, 231 type: 'documentation', 232 priority: daysSinceModified > 90 ? 'high' : 'medium', 233 }); 234 } 235 236 assert.equal(addReviewItemMock.mock.calls.length, 1); 237 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].priority, 'medium'); 238 }); 239 240 test('uses high priority for docs stale > 90 days', () => { 241 const daysSinceModified = 95; 242 const priority = daysSinceModified > 90 ? 'high' : 'medium'; 243 assert.equal(priority, 'high'); 244 }); 245 246 test('marks recently updated docs as OK', () => { 247 const staleThreshold = 30; 248 const daysSinceModified = 5; 249 const isStale = daysSinceModified > staleThreshold; 250 assert.equal(isStale, false); 251 }); 252 }); 253 254 describe('deep-code-analysis - lint/unused code detection', () => { 255 test('identifies no-unused-vars in lint output', () => { 256 const lintOutput = [ 257 'src/test.js:5:3: error no-unused-vars x is defined but never used', 258 'src/test.js:8:1: warning unused-imports imported something unused', 259 ].join('\n'); 260 261 const unusedLines = lintOutput 262 .split('\n') 263 .filter(line => line.includes('no-unused-vars') || line.includes('unused-imports')); 264 265 assert.equal(unusedLines.length, 2); 266 }); 267 268 test('returns empty array when no unused vars found', () => { 269 const cleanOutput = 'All files pass linting!'; 270 const unusedLines = cleanOutput 271 .split('\n') 272 .filter(line => line.includes('no-unused-vars') || line.includes('unused-imports')); 273 assert.equal(unusedLines.length, 0); 274 }); 275 }); 276 277 describe('deep-code-analysis - coverage assessment', () => { 278 beforeEach(resetMocks); 279 280 test('identifies critically low coverage below 70%', () => { 281 const coverage = { total: { lines: { pct: 60 } } }; 282 const isLow = coverage.total.lines.pct < 70; 283 assert.equal(isLow, true); 284 }); 285 286 test('identifies coverage below 80% target', () => { 287 const coverage = { total: { lines: { pct: 75 } } }; 288 const isBelowTarget = coverage.total.lines.pct >= 70 && coverage.total.lines.pct < 80; 289 assert.equal(isBelowTarget, true); 290 }); 291 292 test('identifies coverage meeting target at 80%+', () => { 293 const coverage = { total: { lines: { pct: 85 } } }; 294 const meetsTarget = coverage.total.lines.pct >= 80; 295 assert.equal(meetsTarget, true); 296 }); 297 298 test('flags critical coverage for human review', () => { 299 const pct = 60; 300 if (pct < 70) { 301 addReviewItemMock({ 302 file: 'Test Coverage', 303 reason: `Test coverage is critically low at ${pct.toFixed(1)}% (target: 70%+).`, 304 type: 'test', 305 priority: 'high', 306 }); 307 } 308 assert.equal(addReviewItemMock.mock.calls.length, 1); 309 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].priority, 'high'); 310 }); 311 312 test('flags below-target coverage with medium priority', () => { 313 const pct = 75; 314 if (pct >= 70 && pct < 80) { 315 addReviewItemMock({ 316 file: 'Test Coverage', 317 reason: `Test coverage is ${pct.toFixed(1)}% (target: 80%+).`, 318 type: 'test', 319 priority: 'medium', 320 }); 321 } 322 assert.equal(addReviewItemMock.mock.calls.length, 1); 323 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].priority, 'medium'); 324 }); 325 }); 326 327 describe('deep-code-analysis - security vulnerability scan', () => { 328 beforeEach(resetMocks); 329 330 test('parses npm audit JSON output correctly', () => { 331 const auditOutput = JSON.stringify({ 332 vulnerabilities: { critical: 2, high: 1, moderate: 3, low: 5 }, 333 }); 334 const auditData = JSON.parse(auditOutput); 335 assert.equal(auditData.vulnerabilities.critical, 2); 336 assert.equal(auditData.vulnerabilities.high, 1); 337 }); 338 339 test('identifies critical vulnerabilities requiring immediate action', () => { 340 const criticalCount = 2; 341 const highCount = 1; 342 343 if (criticalCount > 0 || highCount > 0) { 344 addReviewItemMock({ 345 file: 'npm dependencies', 346 reason: `${criticalCount} critical and ${highCount} high severity vulnerabilities detected.`, 347 type: 'security', 348 priority: criticalCount > 0 ? 'critical' : 'high', 349 }); 350 } 351 352 assert.equal(addReviewItemMock.mock.calls.length, 1); 353 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].priority, 'critical'); 354 }); 355 356 test('identifies moderate vulnerabilities with medium priority', () => { 357 const criticalCount = 0; 358 const highCount = 0; 359 const moderateCount = 3; 360 361 if (criticalCount > 0 || highCount > 0) { 362 addReviewItemMock({ priority: 'high' }); 363 } else if (moderateCount > 0) { 364 addReviewItemMock({ 365 file: 'npm dependencies', 366 reason: `${moderateCount} moderate severity vulnerabilities detected.`, 367 type: 'security', 368 priority: 'medium', 369 }); 370 } 371 372 assert.equal(addReviewItemMock.mock.calls.length, 1); 373 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].priority, 'medium'); 374 }); 375 376 test('no review item when no vulnerabilities', () => { 377 const criticalCount = 0; 378 const highCount = 0; 379 const moderateCount = 0; 380 381 if (criticalCount > 0 || highCount > 0) { 382 addReviewItemMock({ priority: 'critical' }); 383 } else if (moderateCount > 0) { 384 addReviewItemMock({ priority: 'medium' }); 385 } 386 387 assert.equal(addReviewItemMock.mock.calls.length, 0); 388 }); 389 390 test('handles unparseable audit output', () => { 391 const badOutput = 'not valid json {{{ broken'; 392 let caught; 393 try { 394 JSON.parse(badOutput); 395 } catch (e) { 396 caught = e; 397 } 398 assert.ok(caught); 399 assert.ok(caught instanceof SyntaxError); 400 }); 401 402 test('high priority for only-high vulnerabilities', () => { 403 const criticalCount = 0; 404 const highCount = 2; 405 406 if (criticalCount > 0 || highCount > 0) { 407 addReviewItemMock({ 408 file: 'npm dependencies', 409 type: 'security', 410 priority: criticalCount > 0 ? 'critical' : 'high', 411 }); 412 } 413 414 assert.equal(addReviewItemMock.mock.calls.length, 1); 415 assert.equal(addReviewItemMock.mock.calls[0].arguments[0].priority, 'high'); 416 }); 417 }); 418 419 describe('deep-code-analysis - git status check', () => { 420 test('detects uncommitted changes', () => { 421 const statusOutput = ' M src/test.js\n?? new-file.js'; 422 const modifiedFiles = statusOutput.trim().split('\n').length; 423 assert.equal(modifiedFiles, 2); 424 }); 425 426 test('detects clean working directory', () => { 427 const statusOutput = ''; 428 const isClean = statusOutput.trim() === ''; 429 assert.equal(isClean, true); 430 }); 431 432 test('counts modified files correctly', () => { 433 const files = [' M file1.js', ' M file2.js', '?? file3.js'].join('\n'); 434 const count = files.trim().split('\n').length; 435 assert.equal(count, 3); 436 }); 437 }); 438 439 describe('deep-code-analysis - report generation', () => { 440 test('constructs valid report sections array', () => { 441 const sections = []; 442 sections.push('# Deep Code Analysis Report'); 443 sections.push('\nGenerated: 2026-02-18T00:00:00.000Z\n'); 444 sections.push('## 1. TODO.md Review\n'); 445 446 const report = sections.join('\n'); 447 assert.ok(report.includes('# Deep Code Analysis Report')); 448 assert.ok(report.includes('TODO.md Review')); 449 }); 450 451 test('report includes quick actions section', () => { 452 const sections = []; 453 sections.push('### Quick Actions\n'); 454 sections.push('```bash'); 455 sections.push('npm run lint:fix'); 456 sections.push('```\n'); 457 458 const report = sections.join('\n'); 459 assert.ok(report.includes('npm run lint:fix')); 460 assert.ok(report.includes('```')); 461 }); 462 463 test('log object has correct methods', () => { 464 const log = { 465 info: msg => `[INFO] ${msg}`, 466 success: msg => `[SUCCESS] ${msg}`, 467 warn: msg => `[WARN] ${msg}`, 468 error: msg => `[ERROR] ${msg}`, 469 }; 470 471 assert.equal(log.info('test'), '[INFO] test'); 472 assert.equal(log.success('done'), '[SUCCESS] done'); 473 assert.equal(log.warn('caution'), '[WARN] caution'); 474 assert.equal(log.error('fail'), '[ERROR] fail'); 475 }); 476 477 test('report path includes timestamp and .md extension', () => { 478 const timestamp = '2026-02-18'; 479 const reportPath = `/path/to/.analysis-reports/deep-analysis-${timestamp}.md`; 480 assert.ok(reportPath.includes('deep-analysis-')); 481 assert.ok(reportPath.endsWith('.md')); 482 }); 483 484 test('report content joins sections with newlines', () => { 485 const sections = ['Section 1', 'Section 2', 'Section 3']; 486 const content = sections.join('\n'); 487 assert.equal(content, 'Section 1\nSection 2\nSection 3'); 488 }); 489 });