weekly-learning-report.test.js
1 /** 2 * Tests for weekly-learning-report cron job 3 * 4 * Mocks generatePromptRecommendations (DB-dependent) and Logger. 5 * Uses an in-memory SQLite DB via pg-mock to verify human_review_queue writes. 6 */ 7 8 import { test, describe, mock, beforeEach } from 'node:test'; 9 import assert from 'node:assert/strict'; 10 import Database from 'better-sqlite3'; 11 import { createPgMock } from '../helpers/pg-mock.js'; 12 13 // ─── Create in-memory test DB ───────────────────────────────────────────────── 14 15 const db = new Database(':memory:'); 16 17 db.exec(` 18 CREATE TABLE IF NOT EXISTS human_review_queue ( 19 id INTEGER PRIMARY KEY AUTOINCREMENT, 20 file TEXT, 21 reason TEXT, 22 type TEXT, 23 priority TEXT, 24 status TEXT DEFAULT 'pending', 25 created_at DATETIME DEFAULT CURRENT_TIMESTAMP 26 ); 27 28 CREATE TABLE IF NOT EXISTS cron_jobs ( 29 job_name TEXT PRIMARY KEY, 30 last_run DATETIME, 31 status TEXT, 32 details TEXT 33 ); 34 `); 35 36 // ─── Mutable mock controls ───────────────────────────────────────────────────── 37 38 let mockGenerateImpl; 39 40 // ─── Mock db.js BEFORE importing module under test ──────────────────────────── 41 42 mock.module('../../src/utils/db.js', { 43 namedExports: createPgMock(db), 44 }); 45 46 mock.module('../../src/utils/prompt-learning.js', { 47 namedExports: { 48 generatePromptRecommendations: promptFile => mockGenerateImpl(promptFile), 49 }, 50 }); 51 52 mock.module('../../src/utils/logger.js', { 53 defaultExport: class { 54 info() {} 55 warn() {} 56 error() {} 57 success() {} 58 debug() {} 59 }, 60 }); 61 62 // ─── Import AFTER mock.module ───────────────────────────────────────────────── 63 64 const runWeeklyReport = (await import('../../src/cron/weekly-learning-report.js')).default; 65 66 // ─── Tests ─────────────────────────────────────────────────────────────────── 67 68 describe('runWeeklyReport', () => { 69 beforeEach(() => { 70 // Default: all prompts return no data 71 mockGenerateImpl = () => ({ stats: { total: 0 }, recommendations: [] }); 72 73 // Clear relevant tables 74 db.prepare('DELETE FROM human_review_queue').run(); 75 db.prepare('DELETE FROM cron_jobs').run(); 76 }); 77 78 test('returns correct shape', async () => { 79 const result = await runWeeklyReport(); 80 assert.ok(typeof result.analyzed === 'number'); 81 assert.ok(typeof result.flagged === 'number'); 82 assert.ok(typeof result.skipped === 'number'); 83 assert.ok(Array.isArray(result.errors)); 84 }); 85 86 test('skips prompts with no feedback data (total=0)', async () => { 87 mockGenerateImpl = () => ({ stats: { total: 0 }, recommendations: [] }); 88 89 const result = await runWeeklyReport(); 90 assert.strictEqual(result.analyzed, 5); // 5 prompt files analyzed 91 assert.strictEqual(result.skipped, 5); // all skipped (no data) 92 assert.strictEqual(result.flagged, 0); 93 }); 94 95 test('flags prompt when approval rate < 70% with recommendations', async () => { 96 mockGenerateImpl = promptFile => { 97 if (promptFile === 'PROPOSAL.md') { 98 return { 99 stats: { total: 100, approved: 50, rework: 30, rejected: 20, approvalRate: 50 }, 100 recommendations: [{ priority: 'high', issue: 'Low quality', recommendation: 'Fix it' }], 101 }; 102 } 103 return { stats: { total: 0 }, recommendations: [] }; 104 }; 105 106 const result = await runWeeklyReport(); 107 assert.strictEqual(result.flagged, 1); 108 109 const row = db.prepare("SELECT * FROM human_review_queue WHERE type='prompt_quality'").get(); 110 assert.ok(row, 'Should have created a human review queue entry'); 111 assert.ok(row.file.includes('PROPOSAL.md')); 112 assert.strictEqual(row.priority, 'medium'); 113 assert.strictEqual(row.status, 'pending'); 114 }); 115 116 test('does not flag prompt when approval rate >= 70%', async () => { 117 mockGenerateImpl = () => ({ 118 stats: { total: 100, approved: 75, rework: 15, rejected: 10, approvalRate: 75 }, 119 recommendations: [{ priority: 'low', issue: 'Minor', recommendation: 'OK' }], 120 }); 121 122 const result = await runWeeklyReport(); 123 assert.strictEqual(result.flagged, 0); 124 }); 125 126 test('does not flag prompt when no recommendations despite low rate', async () => { 127 mockGenerateImpl = () => ({ 128 stats: { total: 100, approved: 40, rework: 30, rejected: 30, approvalRate: 40 }, 129 recommendations: [], // no recommendations 130 }); 131 132 const result = await runWeeklyReport(); 133 assert.strictEqual(result.flagged, 0); 134 }); 135 136 test('completes successfully and returns results on success', async () => { 137 mockGenerateImpl = () => ({ stats: { total: 0 }, recommendations: [] }); 138 139 const result = await runWeeklyReport(); 140 assert.strictEqual(result.analyzed, 5); 141 assert.strictEqual(result.errors.length, 0); 142 }); 143 144 test('records errors for failing prompts but continues', async () => { 145 let callCount = 0; 146 mockGenerateImpl = promptFile => { 147 callCount++; 148 if (callCount === 2) throw new Error('DB error on prompt 2'); 149 return { stats: { total: 0 }, recommendations: [] }; 150 }; 151 152 const result = await runWeeklyReport(); 153 assert.strictEqual(result.analyzed, 4); // 5 tried, 1 threw before increment 154 assert.strictEqual(result.errors.length, 1); 155 assert.ok(result.errors[0].includes('DB error on prompt 2')); 156 }); 157 });