/ tests / proposals / proposal-generator-v2.test.js
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   */