proposals.test.js
1 import { test, describe } from 'node:test'; 2 import assert from 'node:assert'; 3 import { mock } from 'node:test'; 4 import * as fs from 'fs'; 5 import * as path from 'path'; 6 7 // Mock the modules 8 const mockModules = { 9 fs, 10 path, 11 }; 12 13 describe('proposals.js - Uncovered Code Paths', () => { 14 describe('getValidTemplateDirectories - Line 29-39', () => { 15 test('filters directories and returns valid template directories with legacy flat path', () => { 16 const mockDirent = [ 17 { name: 'US', isDirectory: () => true }, 18 { name: 'UK', isDirectory: () => true }, 19 { name: 'file.txt', isDirectory: () => false }, 20 ]; 21 22 const readDirSync = mock.fn(() => mockDirent); 23 const existsSync = mock.fn(filePath => { 24 return filePath.includes('US/email.json'); 25 }); 26 27 // Simulate the filter logic 28 const result = mockDirent.filter(d => { 29 if (!d.isDirectory()) return false; 30 return existsSync(path.join('/templates', d.name, 'email.json')); 31 }); 32 33 assert.strictEqual(result.length, 1); 34 assert.strictEqual(result[0].name, 'US'); 35 }); 36 37 test('filters out non-directory entries', () => { 38 const mockDirent = [ 39 { name: 'US', isDirectory: () => true }, 40 { name: 'config.json', isDirectory: () => false }, 41 { name: 'README.md', isDirectory: () => false }, 42 ]; 43 44 const result = mockDirent.filter(d => d.isDirectory()); 45 46 assert.strictEqual(result.length, 1); 47 assert.strictEqual(result[0].name, 'US'); 48 }); 49 50 test('handles catch block when template validation fails', () => { 51 const mockDirent = [{ name: 'INVALID', isDirectory: () => true }]; 52 53 const existsSync = mock.fn(() => { 54 throw new Error('File system error'); 55 }); 56 57 let caught = false; 58 try { 59 mockDirent.filter(d => { 60 if (!d.isDirectory()) return false; 61 try { 62 return existsSync(path.join('/templates', d.name, 'email.json')); 63 } catch { 64 return false; 65 } 66 }); 67 } catch { 68 caught = true; 69 } 70 71 assert.strictEqual(caught, false); 72 }); 73 74 test('returns empty array when no valid templates exist', () => { 75 const mockDirent = [{ name: 'EMPTY', isDirectory: () => true }]; 76 77 const existsSync = mock.fn(() => false); 78 79 const result = mockDirent.filter(d => { 80 if (!d.isDirectory()) return false; 81 return existsSync(path.join('/templates', d.name, 'email.json')); 82 }); 83 84 assert.strictEqual(result.length, 0); 85 }); 86 87 test('handles multiple valid template directories', () => { 88 const mockDirent = [ 89 { name: 'US', isDirectory: () => true }, 90 { name: 'UK', isDirectory: () => true }, 91 { name: 'CA', isDirectory: () => true }, 92 { name: 'config.json', isDirectory: () => false }, 93 ]; 94 95 const existsSync = mock.fn(() => true); 96 97 const result = mockDirent.filter(d => { 98 if (!d.isDirectory()) return false; 99 return existsSync(path.join('/templates', d.name, 'email.json')); 100 }); 101 102 assert.strictEqual(result.length, 3); 103 }); 104 }); 105 106 describe('enrichProposals - Line 196', () => { 107 test('throws error when enrichment fails', () => { 108 const testError = new Error('Database connection failed'); 109 110 const enrichFunction = () => { 111 throw testError; 112 }; 113 114 assert.throws(() => enrichFunction(), /Database connection failed/); 115 }); 116 117 test('returns stats before throwing error', () => { 118 const stats = { processed: 5, failed: 1 }; 119 120 const enrichFunction = () => { 121 if (Math.random() > 0.5) { 122 throw new Error('Random failure'); 123 } 124 return stats; 125 }; 126 127 // Test the success path 128 let result; 129 try { 130 result = enrichFunction(); 131 if (result) { 132 assert.deepStrictEqual(result, stats); 133 } 134 } catch (err) { 135 assert.match(err.message, /Random failure/); 136 } 137 }); 138 139 test('handles error with message property', () => { 140 const error = new Error('Enrichment failed: timeout'); 141 142 assert.strictEqual(error.message, 'Enrichment failed: timeout'); 143 assert.match(error.message, /timeout/); 144 }); 145 146 test('re-throws error after logging', () => { 147 const originalError = new Error('Original error'); 148 149 const processWithLogging = () => { 150 try { 151 throw originalError; 152 } catch (err) { 153 // Simulate logging 154 const logMessage = `Proposals stage failed: ${err.message}`; 155 assert.match(logMessage, /Proposals stage failed/); 156 throw err; 157 } 158 }; 159 160 assert.throws(() => processWithLogging(), /Original error/); 161 }); 162 }); 163 164 describe('recordFailure - Line 234', () => { 165 test('records failure when error occurs during processing', () => { 166 const mockDb = { prepare: mock.fn() }; 167 const siteId = 'site-123'; 168 const stage = 'proposals'; 169 const error = new Error('Processing failed'); 170 const status = 'enriched'; 171 172 const recordFailure = (db, id, stg, err, st) => { 173 assert.strictEqual(db, mockDb); 174 assert.strictEqual(id, siteId); 175 assert.strictEqual(stg, stage); 176 assert.strictEqual(err.message, 'Processing failed'); 177 assert.strictEqual(st, status); 178 }; 179 180 recordFailure(mockDb, siteId, stage, error, status); 181 }); 182 183 test('handles null error gracefully', () => { 184 const mockDb = { prepare: mock.fn() }; 185 const siteId = 'site-456'; 186 187 const recordFailure = (db, id, stg, err, st) => { 188 if (err) { 189 assert.ok(err.message); 190 } 191 }; 192 193 recordFailure(mockDb, siteId, 'proposals', null, 'enriched'); 194 }); 195 196 test('records failure with different status values', () => { 197 const mockDb = { prepare: mock.fn() }; 198 const statuses = ['enriched', 'generated', 'failed']; 199 200 statuses.forEach(status => { 201 const recordFailure = (db, id, stg, err, st) => { 202 assert.ok(['enriched', 'generated', 'failed'].includes(st)); 203 }; 204 205 recordFailure(mockDb, 'site-id', 'proposals', new Error('Test'), status); 206 }); 207 }); 208 209 test('increments retry count on failure', () => { 210 let retryCount = 0; 211 const mockDb = { prepare: mock.fn() }; 212 213 const recordFailureWithRetry = (db, id, stg, err, st) => { 214 retryCount++; 215 }; 216 217 recordFailureWithRetry(mockDb, 'site-id', 'proposals', new Error('Fail'), 'enriched'); 218 recordFailureWithRetry(mockDb, 'site-id', 'proposals', new Error('Fail'), 'enriched'); 219 220 assert.strictEqual(retryCount, 2); 221 }); 222 }); 223 224 describe('generateProposalForSite - Line 321', () => { 225 test('increments succeeded counter on successful generation', async () => { 226 let succeeded = 0; 227 const site = { id: 'site-1', url: 'https://example.com', keyword: 'test' }; 228 const mockDb = {}; 229 230 const generateProposalForSite = mock.fn(async () => { 231 succeeded++; 232 return { success: true }; 233 }); 234 235 try { 236 await generateProposalForSite(site.id, site.url, site.keyword, mockDb); 237 assert.strictEqual(succeeded, 1); 238 } catch (err) { 239 assert.fail('Should not throw'); 240 } 241 }); 242 243 test('increments failed counter on error', async () => { 244 let failed = 0; 245 const site = { id: 'site-2', url: 'https://example.com', keyword: 'test' }; 246 const mockDb = {}; 247 248 const generateProposalForSite = mock.fn(async () => { 249 throw new Error('Failed to regenerate proposal for https://example.com: Network error'); 250 }); 251 252 try { 253 await generateProposalForSite(site.id, site.url, site.keyword, mockDb); 254 } catch (err) { 255 failed++; 256 assert.match(err.message, /Failed to regenerate proposal/); 257 } 258 259 assert.strictEqual(failed, 1); 260 }); 261 262 test('logs error message with site URL', async () => { 263 const site = { id: 'site-3', url: 'https://test.com', keyword: 'keyword' }; 264 const mockDb = {}; 265 let errorLogged = ''; 266 267 const generateProposalForSite = mock.fn(async () => { 268 throw new Error('Connection timeout'); 269 }); 270 271 try { 272 await generateProposalForSite(site.id, site.url, site.keyword, mockDb); 273 } catch (err) { 274 errorLogged = `Failed to regenerate proposal for ${site.url}: ${err.message}`; 275 } 276 277 assert.match(errorLogged, /Failed to regenerate proposal for https:\/\/test.com/); 278 }); 279 280 test('handles multiple site generation attempts', async () => { 281 let succeeded = 0; 282 let failed = 0; 283 284 const sites = [ 285 { id: 'site-1', url: 'https://example1.com', keyword: 'test1' }, 286 { id: 'site-2', url: 'https://example2.com', keyword: 'test2' }, 287 { id: 'site-3', url: 'https://example3.com', keyword: 'test3' }, 288 ]; 289 290 const generateProposalForSite = mock.fn(async (id, url, keyword, db) => { 291 if (id === 'site-2') { 292 throw new Error('Generation failed'); 293 } 294 return { success: true }; 295 }); 296 297 const mockDb = {}; 298 299 for (const site of sites) { 300 try { 301 await generateProposalForSite(site.id, site.url, site.keyword, mockDb); 302 succeeded++; 303 } catch (err) { 304 failed++; 305 } 306 } 307 308 assert.strictEqual(succeeded, 2); 309 assert.strictEqual(failed, 1); 310 }); 311 312 test('preserves error message details', async () => { 313 const site = { id: 'site-4', url: 'https://error.com', keyword: 'test' }; 314 const mockDb = {}; 315 const errorMessage = 'Database query failed: timeout after 30s'; 316 317 const generateProposalForSite = mock.fn(async () => { 318 throw new Error(errorMessage); 319 }); 320 321 let caughtError = ''; 322 try { 323 await generateProposalForSite(site.id, site.url, site.keyword, mockDb); 324 } catch (err) { 325 caughtError = `Failed to regenerate proposal for ${site.url}: ${err.message}`; 326 } 327 328 assert.match(caughtError, /timeout after 30s/); 329 }); 330 331 test('continues processing after individual site failure', async () => { 332 let succeeded = 0; 333 let failed = 0; 334 335 const generateProposalForSite = mock.fn(async id => { 336 if (id === 'site-fail') { 337 throw new Error('Failed'); 338 } 339 succeeded++; 340 }); 341 342 const mockDb = {}; 343 344 try { 345 await generateProposalForSite('site-success', 'url1', 'kw1', mockDb); 346 } catch { 347 failed++; 348 } 349 350 try { 351 await generateProposalForSite('site-fail', 'url2', 'kw2', mockDb); 352 } catch { 353 failed++; 354 } 355 356 try { 357 await generateProposalForSite('site-success2', 'url3', 'kw3', mockDb); 358 } catch { 359 failed++; 360 } 361 362 assert.strictEqual(succeeded, 2); 363 assert.strictEqual(failed, 1); 364 }); 365 }); 366 367 describe('Error handling integration', () => { 368 test('error message includes context information', () => { 369 const err = new Error('Enrichment failed'); 370 const context = `Proposals stage failed: ${err.message}`; 371 372 assert.match(context, /Proposals stage failed/); 373 assert.match(context, /Enrichment failed/); 374 }); 375 376 test('handles errors with undefined message', () => { 377 const err = new Error(); 378 const message = err.message || 'Unknown error'; 379 380 assert.strictEqual(typeof message, 'string'); 381 }); 382 383 test('error propagation maintains stack trace', () => { 384 const originalError = new Error('Original error'); 385 const originalStack = originalError.stack; 386 387 try { 388 throw originalError; 389 } catch (err) { 390 assert.strictEqual(err.message, 'Original error'); 391 assert.ok(err.stack.includes('Original error')); 392 } 393 }); 394 }); 395 });