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('<script>') || !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 });