/ tests / proposals / proposals.test.js
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  });