/ tests / utils / summary-generator-supplement.test.js
summary-generator-supplement.test.js
  1  /**
  2   * Supplement tests for src/utils/summary-generator.js
  3   *
  4   * Covers uncovered functions:
  5   *   - generatePipelineCompletion: builds items from stageResults, calls generateSummary
  6   *   - generateGradeDistribution: filters zero-count grades, computes total
  7   *   - generateSummary: renders all color types (success/info/warn/error/none)
  8   *   - displayProgress: formats progress bar (edge cases: 0%, 100%)
  9   */
 10  
 11  import { test, describe } from 'node:test';
 12  import assert from 'node:assert/strict';
 13  import {
 14    generateSummary,
 15    generateStageCompletion,
 16    generatePipelineCompletion,
 17    generateGradeDistribution,
 18    displayProgress,
 19  } from '../../src/utils/summary-generator.js';
 20  
 21  // Suppress console.log output during tests
 22  const origLog = console.log;
 23  {
 24    // Use a separate noop to avoid polluting test output
 25  }
 26  
 27  describe('generateSummary', () => {
 28    test('does not throw for empty items array', () => {
 29      // Capture and suppress output
 30      const logs = [];
 31      const prev = console.log;
 32      console.log = (...args) => logs.push(args.join(''));
 33  
 34      assert.doesNotThrow(() => generateSummary('Test Title', []));
 35  
 36      console.log = prev;
 37      assert.ok(
 38        logs.some(l => l.includes('Test Title')),
 39        'Should print title'
 40      );
 41    });
 42  
 43    test('renders items with all color types without throwing', () => {
 44      const prev = console.log;
 45      console.log = () => {};
 46  
 47      assert.doesNotThrow(() =>
 48        generateSummary('Colors', [
 49          { label: 'Success item', value: 10, type: 'success' },
 50          { label: 'Info item', value: 20, type: 'info' },
 51          { label: 'Warning item', value: 5, type: 'warn' },
 52          { label: 'Error item', value: 1, type: 'error' },
 53          { label: 'No type item', value: 0 }, // no type → no color
 54        ])
 55      );
 56  
 57      console.log = prev;
 58    });
 59  
 60    test('respects custom width option', () => {
 61      const lines = [];
 62      const prev = console.log;
 63      console.log = (...args) => lines.push(args.join(''));
 64  
 65      generateSummary('Wide', [{ label: 'Test', value: 42 }], { width: 80 });
 66  
 67      console.log = prev;
 68      // Border should have 80 chars of ═
 69      const border = lines.find(l => l.includes('═'.repeat(80)));
 70      assert.ok(border, 'Should use custom width of 80');
 71    });
 72  });
 73  
 74  describe('generateStageCompletion', () => {
 75    test('includes duration when stats.duration is set', () => {
 76      const lines = [];
 77      const prev = console.log;
 78      console.log = (...args) => lines.push(args.join(''));
 79  
 80      generateStageCompletion('Assets', {
 81        processed: 10,
 82        succeeded: 8,
 83        failed: 2,
 84        skipped: 0,
 85        duration: 5000,
 86      });
 87  
 88      console.log = prev;
 89      assert.ok(
 90        lines.some(l => l.includes('5.00s')),
 91        'Should include duration'
 92      );
 93    });
 94  
 95    test('omits duration when stats.duration is falsy', () => {
 96      const lines = [];
 97      const prev = console.log;
 98      console.log = (...args) => lines.push(args.join(''));
 99  
100      generateStageCompletion('Scoring', { processed: 5, succeeded: 5, failed: 0, skipped: 0 });
101  
102      console.log = prev;
103      assert.ok(!lines.some(l => l.includes('Duration')), 'Should not include duration');
104    });
105  
106    test('marks failed items as error type when failed > 0', () => {
107      // Just verify it doesn't throw
108      const prev = console.log;
109      console.log = () => {};
110  
111      assert.doesNotThrow(() =>
112        generateStageCompletion('Enrich', {
113          processed: 10,
114          succeeded: 7,
115          failed: 3,
116          skipped: 0,
117          duration: 2000,
118        })
119      );
120  
121      console.log = prev;
122    });
123  });
124  
125  describe('generatePipelineCompletion', () => {
126    test('renders pipeline summary with multiple stages', () => {
127      const lines = [];
128      const prev = console.log;
129      console.log = (...args) => lines.push(args.join(''));
130  
131      generatePipelineCompletion(
132        [
133          { stage: 'Keywords', stats: { processed: 100, succeeded: 100, failed: 0 } },
134          { stage: 'Scoring', stats: { processed: 80, succeeded: 75, failed: 5 } },
135          { stage: 'Proposals', stats: { processed: 60, succeeded: 60, failed: 0 } },
136        ],
137        30000
138      );
139  
140      console.log = prev;
141      assert.ok(
142        lines.some(l => l.includes('Pipeline Complete')),
143        'Should print Pipeline Complete'
144      );
145      assert.ok(
146        lines.some(l => l.includes('30.00s')),
147        'Should include total duration'
148      );
149    });
150  
151    test('marks stages with failures as error type', () => {
152      const prev = console.log;
153      console.log = () => {};
154  
155      // Should not throw even with failed stages
156      assert.doesNotThrow(() =>
157        generatePipelineCompletion(
158          [{ stage: 'Enrich', stats: { processed: 10, succeeded: 5, failed: 5 } }],
159          10000
160        )
161      );
162  
163      console.log = prev;
164    });
165  
166    test('handles empty stage results', () => {
167      const prev = console.log;
168      console.log = () => {};
169  
170      assert.doesNotThrow(() => generatePipelineCompletion([], 0));
171  
172      console.log = prev;
173    });
174  });
175  
176  describe('generateGradeDistribution', () => {
177    test('only shows grades with count > 0', () => {
178      const lines = [];
179      const prev = console.log;
180      console.log = (...args) => lines.push(args.join(''));
181  
182      generateGradeDistribution({ 'A+': 5, A: 10, B: 20, F: 3 });
183  
184      console.log = prev;
185      assert.ok(
186        lines.some(l => l.includes('Grade A+')),
187        'A+ should appear'
188      );
189      assert.ok(
190        lines.some(l => l.includes('Grade A')),
191        'A should appear'
192      );
193      // A- has 0 count so should not appear
194      assert.ok(!lines.some(l => l.includes('Grade A-')), 'A- should not appear');
195    });
196  
197    test('shows correct total', () => {
198      const lines = [];
199      const prev = console.log;
200      console.log = (...args) => lines.push(args.join(''));
201  
202      generateGradeDistribution({ B: 15, C: 10, D: 5 });
203  
204      console.log = prev;
205      assert.ok(
206        lines.some(l => l.includes('30')),
207        'Total should be 30'
208      );
209    });
210  
211    test('handles empty distribution', () => {
212      const prev = console.log;
213      console.log = () => {};
214  
215      assert.doesNotThrow(() => generateGradeDistribution({}));
216  
217      console.log = prev;
218    });
219  
220    test('handles A+/A/A- as success type, B+/B/B- as info, C/D as warn, F as error', () => {
221      const prev = console.log;
222      console.log = () => {};
223  
224      // All grades — just verify no throw
225      assert.doesNotThrow(() =>
226        generateGradeDistribution({
227          'A+': 1,
228          A: 1,
229          'A-': 1,
230          'B+': 1,
231          B: 1,
232          'B-': 1,
233          'C+': 1,
234          C: 1,
235          'C-': 1,
236          'D+': 1,
237          D: 1,
238          'D-': 1,
239          F: 1,
240        })
241      );
242  
243      console.log = prev;
244    });
245  });
246  
247  describe('displayProgress', () => {
248    test('does not throw for 0% progress', () => {
249      const prev = console.log;
250      console.log = () => {};
251  
252      assert.doesNotThrow(() => displayProgress(0, 100, 'Starting'));
253  
254      console.log = prev;
255    });
256  
257    test('does not throw for 100% progress', () => {
258      const prev = console.log;
259      console.log = () => {};
260  
261      assert.doesNotThrow(() => displayProgress(100, 100, 'Done'));
262  
263      console.log = prev;
264    });
265  
266    test('does not throw for mid-progress', () => {
267      const prev = console.log;
268      console.log = () => {};
269  
270      assert.doesNotThrow(() => displayProgress(50, 100, 'Halfway'));
271  
272      console.log = prev;
273    });
274  });