/ tests / reports / daily-progress-html-generator-supplement.test.js
daily-progress-html-generator-supplement.test.js
  1  /**
  2   * Supplement tests for src/reports/daily-progress-html-generator.js
  3   *
  4   * Exercises the correct data shape (gitActivity, dbChanges, summary) to cover
  5   * internal helpers: generateShippedSection, generateDatabaseMetrics,
  6   * generateStatusTree, generateOutreachCrossTab, generateConversationsCrossTab,
  7   * renderTree, renderChildRows, generateSourceCodeMetrics, generateCodeQualityMetrics,
  8   * escapeHtml, fmtDelta, fmtCumul, statusColor.
  9   */
 10  
 11  import { test, describe, after } from 'node:test';
 12  import assert from 'node:assert/strict';
 13  import { existsSync, unlinkSync, readFileSync } from 'fs';
 14  import { join } from 'path';
 15  import { tmpdir } from 'os';
 16  
 17  import { generateDailyReport } from '../../src/reports/daily-progress-html-generator.js';
 18  
 19  const OUTPUT_DIR = tmpdir();
 20  const generatedFiles = [];
 21  
 22  after(() => {
 23    for (const f of generatedFiles) {
 24      if (existsSync(f)) {
 25        try {
 26          unlinkSync(f);
 27        } catch {
 28          /* ignore */
 29        }
 30      }
 31    }
 32  });
 33  
 34  /**
 35   * Full data shape matching what the report actually reads (gitActivity, dbChanges, etc.)
 36   */
 37  function makeFullReportData(overrides = {}) {
 38    return {
 39      date: '2026-03-08',
 40      summary: {
 41        shipped: [
 42          'Shipped 5 new features including SMS templates',
 43          'Fixed 3 bug reports in outreach pipeline',
 44          'Added new test coverage for prioritize module',
 45          'Updated documentation for API changes',
 46          'Refactored scoring module for performance',
 47        ],
 48      },
 49      gitActivity: {
 50        commits: [
 51          { hash: 'abc123', message: 'feat: add SMS template feature', date: '2026-03-08' },
 52          { hash: 'def456', message: 'fix: fix outreach bug', date: '2026-03-08' },
 53          { hash: 'ghi789', message: 'test: add coverage for prioritize', date: '2026-03-08' },
 54          { hash: 'jkl012', message: 'docs: update API documentation', date: '2026-03-08' },
 55          { hash: 'mno345', message: 'refactor: improve scoring performance', date: '2026-03-08' },
 56        ],
 57        filesChanged: 42,
 58        insertions: 1200,
 59        deletions: 300,
 60        branches: ['main', 'feature/sms'],
 61      },
 62      dbChanges: {
 63        totalSites: 50000,
 64        newSites: 120,
 65        ignore: 5000,
 66        failing: 200,
 67        statusTree: [
 68          {
 69            status: 'enriched',
 70            total: 8000,
 71            cumulative: 8000,
 72            delta_24h: 120,
 73            delta_1h: 5,
 74            children: {
 75              type: 'channels',
 76              rows: [
 77                { label: 'sms', total: 500, delta_24h: 10, delta_1h: 1 },
 78                { label: 'email', total: 300, delta_24h: 5, delta_1h: 0 },
 79              ],
 80            },
 81          },
 82          {
 83            status: 'outreach_sent',
 84            total: 3500,
 85            cumulative: 3500,
 86            delta_24h: 50,
 87            delta_1h: 2,
 88          },
 89          {
 90            status: 'failing',
 91            total: 200,
 92            cumulative: 200,
 93            delta_24h: 10,
 94            delta_1h: 1,
 95            children: {
 96              type: 'errors',
 97              retriable: [{ label: 'Connection timeout', total: 50, delta_24h: 5, delta_1h: 0 }],
 98              terminal: [{ label: 'Invalid URL', total: 30, delta_24h: 2, delta_1h: 0 }],
 99              unknown: [{ label: 'Unknown error', total: 20, delta_24h: 1, delta_1h: 0 }],
100            },
101          },
102          {
103            status: 'ignored',
104            total: 5000,
105            cumulative: 5000,
106            delta_24h: 0,
107            delta_1h: 0,
108          },
109        ],
110        outreachCrossRows: [
111          { contact_method: 'sms', status: 'pending', total: 40 },
112          { contact_method: 'sms', status: 'approved', total: 20 },
113          { contact_method: 'sms', status: 'sent', total: 100 },
114          { contact_method: 'sms', status: 'delivered', total: 50 },
115          { contact_method: 'sms', status: 'failed', total: 5 },
116          { contact_method: 'email', status: 'pending', total: 30 },
117          { contact_method: 'email', status: 'approved', total: 15 },
118          { contact_method: 'email', status: 'sent', total: 200 },
119          { contact_method: 'email', status: 'delivered', total: 180 },
120          { contact_method: 'email', status: 'gdpr_blocked', total: 2 },
121        ],
122        outreachSentToday: 45,
123        convCrossRows: [
124          { intent: 'interested', sentiment: 'positive', total: 5 },
125          { intent: 'interested', sentiment: 'neutral', total: 2 },
126          { intent: 'interested', sentiment: 'objection', total: 1 },
127          { intent: 'not-interested', sentiment: 'negative', total: 3 },
128          { intent: 'not-interested', sentiment: 'objection', total: 2 },
129        ],
130        convChannelRows: [
131          { channel: 'sms', total: 12, replied: 8 },
132          { channel: 'email', total: 4, replied: 2 },
133        ],
134      },
135      codeQuality: {
136        coverage: 87.5,
137        todosAdded: 3,
138        todosResolved: 5,
139        newDependencies: ['better-sqlite3@9.0.0'],
140      },
141      systemHealth: {
142        errors: ['Error: Connection refused to external API', 'Error: Rate limit hit for ZenRows'],
143        warnings: ['Warning: Low disk space', 'Warning: High memory usage'],
144        uptime: '48h',
145      },
146      ...overrides,
147    };
148  }
149  
150  describe('generateDailyReport (full data shape)', () => {
151    test('generates HTML with full data shape including statusTree', async () => {
152      const outputPath = join(OUTPUT_DIR, `test-html-full-${Date.now()}.html`);
153      generatedFiles.push(outputPath);
154  
155      const result = await generateDailyReport(makeFullReportData(), outputPath);
156  
157      assert.equal(result, outputPath);
158      assert.ok(existsSync(outputPath));
159  
160      const content = readFileSync(outputPath, 'utf-8');
161      assert.ok(content.includes('<!DOCTYPE html>'));
162      assert.ok(content.includes('Daily Progress Report'));
163    });
164  
165    test('shipped section renders feat/fix/test/docs/refactor commit types', async () => {
166      const outputPath = join(OUTPUT_DIR, `test-html-shipped-${Date.now()}.html`);
167      generatedFiles.push(outputPath);
168  
169      await generateDailyReport(makeFullReportData(), outputPath);
170  
171      const content = readFileSync(outputPath, 'utf-8');
172      assert.ok(content.includes('SMS template feature'), 'feat commit should appear');
173      assert.ok(content.includes('outreach bug'), 'fix commit should appear');
174    });
175  
176    test('status tree renders enriched/outreach_sent/failing/ignore statuses', async () => {
177      const outputPath = join(OUTPUT_DIR, `test-html-statustree-${Date.now()}.html`);
178      generatedFiles.push(outputPath);
179  
180      await generateDailyReport(makeFullReportData(), outputPath);
181  
182      const content = readFileSync(outputPath, 'utf-8');
183      assert.ok(content.includes('enriched'), 'should include enriched status');
184      assert.ok(content.includes('outreach_sent'), 'should include outreach_sent status');
185      assert.ok(content.includes('failing'), 'should include failing status');
186    });
187  
188    test('status tree renders children rows (channels and errors)', async () => {
189      const outputPath = join(OUTPUT_DIR, `test-html-children-${Date.now()}.html`);
190      generatedFiles.push(outputPath);
191  
192      await generateDailyReport(makeFullReportData(), outputPath);
193  
194      const content = readFileSync(outputPath, 'utf-8');
195      // Children with type=channels should show channel labels
196      assert.ok(content.includes('sms') || content.includes('email'), 'should show channel names');
197      // Children with type=errors should show error categories
198      assert.ok(
199        content.includes('Retriable') || content.includes('Terminal'),
200        'should show error categories'
201      );
202    });
203  
204    test('outreach cross-tab renders channel rows', async () => {
205      const outputPath = join(OUTPUT_DIR, `test-html-outreach-${Date.now()}.html`);
206      generatedFiles.push(outputPath);
207  
208      await generateDailyReport(makeFullReportData(), outputPath);
209  
210      const content = readFileSync(outputPath, 'utf-8');
211      assert.ok(
212        content.includes('pending') || content.includes('approved'),
213        'outreach cross-tab should have statuses'
214      );
215    });
216  
217    test('conversations cross-tab renders intent rows', async () => {
218      const outputPath = join(OUTPUT_DIR, `test-html-convs-${Date.now()}.html`);
219      generatedFiles.push(outputPath);
220  
221      await generateDailyReport(makeFullReportData(), outputPath);
222  
223      const content = readFileSync(outputPath, 'utf-8');
224      assert.ok(
225        content.includes('interested') || content.includes('not-interested'),
226        'should show conversation intents'
227      );
228    });
229  
230    test('escapes HTML special characters in content', async () => {
231      const outputPath = join(OUTPUT_DIR, `test-html-escape-${Date.now()}.html`);
232      generatedFiles.push(outputPath);
233  
234      const data = makeFullReportData({
235        summary: {
236          shipped: ['Added <script>alert("xss")</script> feature'],
237        },
238        gitActivity: {
239          commits: [{ hash: 'abc', message: 'feat: test <html> & "quotes"', date: '2026-03-08' }],
240          filesChanged: 1,
241          insertions: 10,
242          deletions: 0,
243          branches: ['main'],
244        },
245      });
246  
247      await generateDailyReport(data, outputPath);
248  
249      const content = readFileSync(outputPath, 'utf-8');
250      assert.ok(!content.includes('<script>alert'), 'should escape script tags');
251      assert.ok(
252        content.includes('&lt;script&gt;') || !content.includes('<script>'),
253        'XSS should be escaped'
254      );
255    });
256  
257    test('handles empty outreach cross rows', async () => {
258      const outputPath = join(OUTPUT_DIR, `test-html-nooutreach-${Date.now()}.html`);
259      generatedFiles.push(outputPath);
260  
261      const data = makeFullReportData({
262        dbChanges: {
263          ...makeFullReportData().dbChanges,
264          outreachCrossRows: [],
265          convCrossRows: [],
266        },
267      });
268  
269      await assert.doesNotReject(generateDailyReport(data, outputPath));
270      assert.ok(existsSync(outputPath));
271    });
272  
273    test('handles null dbChanges.statusTree', async () => {
274      const outputPath = join(OUTPUT_DIR, `test-html-nostatus-${Date.now()}.html`);
275      generatedFiles.push(outputPath);
276  
277      const data = makeFullReportData({
278        dbChanges: {
279          ...makeFullReportData().dbChanges,
280          statusTree: null,
281        },
282      });
283  
284      await assert.doesNotReject(generateDailyReport(data, outputPath));
285    });
286  
287    test('renders database metrics box when dbChanges present', async () => {
288      const outputPath = join(OUTPUT_DIR, `test-html-dbmetrics-${Date.now()}.html`);
289      generatedFiles.push(outputPath);
290  
291      await generateDailyReport(makeFullReportData(), outputPath);
292  
293      const content = readFileSync(outputPath, 'utf-8');
294      assert.ok(
295        content.includes('Database Overview') || content.includes('50,000'),
296        'should show database metrics'
297      );
298    });
299  
300    test('renders source code metrics from gitActivity', async () => {
301      const outputPath = join(OUTPUT_DIR, `test-html-gitmetrics-${Date.now()}.html`);
302      generatedFiles.push(outputPath);
303  
304      await generateDailyReport(makeFullReportData(), outputPath);
305  
306      const content = readFileSync(outputPath, 'utf-8');
307      // Should include git stats
308      assert.ok(content.includes('1,200') || content.includes('42'), 'should show git metrics');
309    });
310  
311    test('renders code quality metrics from codeQuality', async () => {
312      const outputPath = join(OUTPUT_DIR, `test-html-codemetrics-${Date.now()}.html`);
313      generatedFiles.push(outputPath);
314  
315      await generateDailyReport(makeFullReportData(), outputPath);
316  
317      const content = readFileSync(outputPath, 'utf-8');
318      assert.ok(content.includes('87.5') || content.includes('Coverage'), 'should show coverage');
319    });
320  
321    test('handles outreach cross rows with sentToday count', async () => {
322      const outputPath = join(OUTPUT_DIR, `test-html-senttoday-${Date.now()}.html`);
323      generatedFiles.push(outputPath);
324  
325      await generateDailyReport(makeFullReportData(), outputPath);
326  
327      const content = readFileSync(outputPath, 'utf-8');
328      assert.ok(
329        content.includes('45') || content.includes('Sent today'),
330        'should show sent today count'
331      );
332    });
333  
334    test('handles conversations cross-tab with channel rows', async () => {
335      const outputPath = join(OUTPUT_DIR, `test-html-convchannels-${Date.now()}.html`);
336      generatedFiles.push(outputPath);
337  
338      await generateDailyReport(makeFullReportData(), outputPath);
339  
340      const content = readFileSync(outputPath, 'utf-8');
341      assert.ok(
342        content.includes('Channels') || content.includes('sms'),
343        'should show conversation channels'
344      );
345    });
346  });