/ tests / utils / deep-code-analysis.test.js
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  });