/ tests / cron / weekly-learning-report.test.js
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  });