proposal-generator-v2.test.js
1 /** 2 * Integration Tests for Proposal Generator V2 3 * 4 * Note: Most functions make LLM API calls which are expensive to test. 5 * These tests focus on database operations and module structure. 6 */ 7 8 import { describe, it, beforeEach, after, mock } from 'node:test'; 9 import assert from 'node:assert'; 10 import Database from 'better-sqlite3'; 11 import { readFileSync } from 'fs'; 12 import { join } from 'path'; 13 import { createPgMock } from '../helpers/pg-mock.js'; 14 15 // Create in-memory SQLite with full schema 16 const testDb = new Database(':memory:'); 17 const schema = readFileSync(join(import.meta.dirname, '../../db/schema.sql'), 'utf-8'); 18 testDb.exec(schema); 19 20 // Mock db.js BEFORE importing the module under test 21 mock.module('../../src/utils/db.js', { namedExports: createPgMock(testDb) }); 22 23 // Import after setting up mock 24 const { getPendingOutreaches, approveOutreach, reworkOutreach } = 25 await import('../../src/proposal-generator-v2.js'); 26 27 describe('Proposal Generator V2 - Database Operations', () => { 28 beforeEach(() => { 29 testDb.prepare('DELETE FROM messages').run(); 30 testDb.prepare('DELETE FROM sites').run(); 31 }); 32 33 after(() => { 34 testDb.close(); 35 }); 36 37 describe('getPendingOutreaches', () => { 38 it('should return empty array when no pending outreaches exist', async () => { 39 const result = await getPendingOutreaches(); 40 41 assert.ok(Array.isArray(result)); 42 assert.strictEqual(result.length, 0); 43 }); 44 45 it('should return pending outreaches', async () => { 46 testDb 47 .prepare( 48 `INSERT INTO sites (domain, landing_page_url, keyword, status) VALUES (?, ?, ?, ?)` 49 ) 50 .run('test.com', 'https://test.com', 'test keyword', 'enriched'); 51 52 const siteId = testDb.prepare('SELECT id FROM sites').get().id; 53 54 testDb 55 .prepare( 56 `INSERT INTO messages (site_id, contact_uri, contact_method, message_body, approval_status) 57 VALUES (?, ?, ?, ?, ?)` 58 ) 59 .run(siteId, 'contact@test.com', 'email', 'Test proposal', 'pending'); 60 61 testDb 62 .prepare( 63 `INSERT INTO messages (site_id, contact_uri, contact_method, message_body, approval_status, delivery_status) 64 VALUES (?, ?, ?, ?, ?, ?)` 65 ) 66 .run(siteId, 'contact2@test.com', 'email', 'Test proposal 2', 'approved', 'sent'); 67 68 const result = await getPendingOutreaches(); 69 70 assert.ok(Array.isArray(result)); 71 assert.strictEqual(result.length, 1); 72 assert.strictEqual(result[0].approval_status, 'pending'); 73 }); 74 75 it('should respect limit parameter', async () => { 76 testDb 77 .prepare( 78 `INSERT INTO sites (domain, landing_page_url, keyword, status) VALUES (?, ?, ?, ?)` 79 ) 80 .run('test.com', 'https://test.com', 'test keyword', 'enriched'); 81 82 const siteId = testDb.prepare('SELECT id FROM sites').get().id; 83 84 for (let i = 1; i <= 5; i++) { 85 testDb 86 .prepare( 87 `INSERT INTO messages (site_id, contact_uri, contact_method, message_body, approval_status) 88 VALUES (?, ?, ?, ?, ?)` 89 ) 90 .run(siteId, `contact${i}@test.com`, 'email', `Proposal ${i}`, 'pending'); 91 } 92 93 const result = await getPendingOutreaches(2); 94 95 assert.strictEqual(result.length, 2); 96 }); 97 }); 98 99 describe('approveOutreach', () => { 100 it('should successfully approve an outreach', async () => { 101 testDb 102 .prepare( 103 `INSERT INTO sites (domain, landing_page_url, keyword, status) VALUES (?, ?, ?, ?)` 104 ) 105 .run('test.com', 'https://test.com', 'test keyword', 'enriched'); 106 107 const siteId = testDb.prepare('SELECT id FROM sites').get().id; 108 109 testDb 110 .prepare( 111 `INSERT INTO messages (site_id, contact_uri, contact_method, message_body, approval_status) 112 VALUES (?, ?, ?, ?, ?)` 113 ) 114 .run(siteId, 'contact@test.com', 'email', 'Test proposal', 'pending'); 115 116 const outreachId = testDb.prepare('SELECT id FROM messages').get().id; 117 118 await approveOutreach(outreachId); 119 120 const outreach = testDb 121 .prepare('SELECT approval_status FROM messages WHERE id = ?') 122 .get(outreachId); 123 124 assert.strictEqual(outreach.approval_status, 'approved'); 125 }); 126 }); 127 128 describe('reworkOutreach', () => { 129 it('should mark outreach for rework with instructions', async () => { 130 testDb 131 .prepare( 132 `INSERT INTO sites (domain, landing_page_url, keyword, status) VALUES (?, ?, ?, ?)` 133 ) 134 .run('test.com', 'https://test.com', 'test keyword', 'enriched'); 135 136 const siteId = testDb.prepare('SELECT id FROM sites').get().id; 137 138 testDb 139 .prepare( 140 `INSERT INTO messages (site_id, contact_uri, contact_method, message_body, approval_status) 141 VALUES (?, ?, ?, ?, ?)` 142 ) 143 .run(siteId, 'contact@test.com', 'email', 'Test proposal', 'pending'); 144 145 const outreachId = testDb.prepare('SELECT id FROM messages').get().id; 146 147 await reworkOutreach(outreachId, 'Make it more personalized'); 148 149 const outreach = testDb.prepare('SELECT * FROM messages WHERE id = ?').get(outreachId); 150 151 assert.strictEqual(outreach.approval_status, 'rework'); 152 assert.strictEqual(outreach.rework_instructions, 'Make it more personalized'); 153 }); 154 }); 155 156 describe('Module Exports', () => { 157 it('should export all required functions', async () => { 158 const module = await import('../../src/proposal-generator-v2.js'); 159 160 assert.strictEqual(typeof module.generateProposalVariants, 'function'); 161 assert.strictEqual(typeof module.getPendingOutreaches, 'function'); 162 assert.strictEqual(typeof module.approveOutreach, 'function'); 163 assert.strictEqual(typeof module.reworkOutreach, 'function'); 164 assert.strictEqual(typeof module.processReworkQueue, 'function'); 165 assert.strictEqual(typeof module.generateBulkProposals, 'function'); 166 }); 167 }); 168 }); 169 170 /* 171 * NOTE: The following functions are not fully tested because they make expensive LLM API calls: 172 * - generateProposalVariants(siteId, reworkInstructions) 173 * - processReworkQueue() 174 * - generateBulkProposals(limit) 175 * 176 * These functions are tested manually with real data and in E2E tests. 177 */