/ tests / utils / human-review-queue.test.js
human-review-queue.test.js
  1  /**
  2   * Tests for src/utils/human-review-queue.js
  3   */
  4  import { test, describe, before, mock } from 'node:test';
  5  import assert from 'node:assert/strict';
  6  import Database from 'better-sqlite3';
  7  import { createPgMock } from '../helpers/pg-mock.js';
  8  
  9  const db = new Database(':memory:');
 10  db.exec(`
 11    CREATE TABLE IF NOT EXISTS human_review_queue (
 12      id INTEGER PRIMARY KEY AUTOINCREMENT,
 13      file TEXT,
 14      reason TEXT,
 15      type TEXT,
 16      priority TEXT DEFAULT 'medium',
 17      status TEXT DEFAULT 'pending',
 18      created_at TEXT DEFAULT (datetime('now')),
 19      reviewed_at TEXT,
 20      reviewed_by TEXT,
 21      notes TEXT
 22    );
 23    CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_pending_reviews
 24      ON human_review_queue(file, type)
 25      WHERE status = 'pending';
 26  `);
 27  
 28  mock.module('../../src/utils/db.js', { namedExports: createPgMock(db) });
 29  
 30  const {
 31    initializeQueue,
 32    addReviewItem,
 33    addReviewItems,
 34    getPendingReviews,
 35    markReviewed,
 36    dismissReview,
 37    getQueueStats,
 38    cleanupReviewedItems,
 39  } = await import('../../src/utils/human-review-queue.js');
 40  
 41  before(() => {
 42    initializeQueue();
 43  });
 44  
 45  function clearTable() {
 46    db.exec('DELETE FROM human_review_queue');
 47  }
 48  
 49  describe('initializeQueue', () => {
 50    test('creates the table without throwing', async () => {
 51      await assert.doesNotReject(() => initializeQueue());
 52    });
 53  
 54    test('is idempotent (can be called multiple times)', async () => {
 55      await assert.doesNotReject(async () => {
 56        await initializeQueue();
 57        await initializeQueue();
 58      });
 59    });
 60  });
 61  
 62  describe('addReviewItem', () => {
 63    before(() => clearTable());
 64  
 65    test('inserts a new item', async () => {
 66      await addReviewItem({ file: 'src/foo.js', reason: 'needs review', type: 'architecture' });
 67      const row = db.prepare("SELECT * FROM human_review_queue WHERE file = 'src/foo.js'").get();
 68      assert.equal(row.reason, 'needs review');
 69      assert.equal(row.type, 'architecture');
 70      assert.equal(row.priority, 'medium');
 71      assert.equal(row.status, 'pending');
 72    });
 73  
 74    test('sets custom priority', async () => {
 75      await addReviewItem({
 76        file: 'src/bar.js',
 77        reason: 'critical issue',
 78        type: 'security',
 79        priority: 'critical',
 80      });
 81      const row = db.prepare("SELECT * FROM human_review_queue WHERE file = 'src/bar.js'").get();
 82      assert.equal(row.priority, 'critical');
 83    });
 84  
 85    test('updates existing pending item with same file+type instead of inserting duplicate', async () => {
 86      clearTable();
 87      await addReviewItem({ file: 'src/dup.js', reason: 'first reason', type: 'test' });
 88      await addReviewItem({ file: 'src/dup.js', reason: 'updated reason', type: 'test' });
 89  
 90      const rows = db.prepare("SELECT * FROM human_review_queue WHERE file = 'src/dup.js'").all();
 91      assert.equal(rows.length, 1, 'Should not create duplicate');
 92      assert.equal(rows[0].reason, 'updated reason');
 93    });
 94  
 95    test('does not deduplicate across different types', async () => {
 96      clearTable();
 97      await addReviewItem({ file: 'src/multi.js', reason: 'arch issue', type: 'architecture' });
 98      await addReviewItem({ file: 'src/multi.js', reason: 'security issue', type: 'security' });
 99  
100      const rows = db.prepare("SELECT * FROM human_review_queue WHERE file = 'src/multi.js'").all();
101      assert.equal(rows.length, 2);
102    });
103  });
104  
105  describe('addReviewItems', () => {
106    before(() => clearTable());
107  
108    test('inserts multiple items', async () => {
109      await addReviewItems([
110        { file: 'src/a.js', reason: 'reason a', type: 'architecture' },
111        { file: 'src/b.js', reason: 'reason b', type: 'security' },
112      ]);
113  
114      const count = db.prepare('SELECT COUNT(*) as n FROM human_review_queue').get();
115      assert.equal(count.n, 2);
116    });
117  
118    test('deduplicates within batch for same file+type', async () => {
119      clearTable();
120      await addReviewItems([{ file: 'src/dup.js', reason: 'first', type: 'test' }]);
121      await addReviewItems([{ file: 'src/dup.js', reason: 'updated', type: 'test' }]);
122  
123      const rows = db.prepare("SELECT * FROM human_review_queue WHERE file = 'src/dup.js'").all();
124      assert.equal(rows.length, 1);
125      assert.equal(rows[0].reason, 'updated');
126    });
127  
128    test('handles empty array', async () => {
129      await assert.doesNotReject(() => addReviewItems([]));
130    });
131  
132    test('defaults priority to medium', async () => {
133      clearTable();
134      await addReviewItems([{ file: 'src/c.js', reason: 'reason', type: 'documentation' }]);
135  
136      const row = db.prepare("SELECT priority FROM human_review_queue WHERE file = 'src/c.js'").get();
137      assert.equal(row.priority, 'medium');
138    });
139  });
140  
141  describe('getPendingReviews', () => {
142    before(() => {
143      clearTable();
144      return addReviewItems([
145        { file: 'src/a.js', reason: 'arch', type: 'architecture', priority: 'high' },
146        { file: 'src/b.js', reason: 'sec', type: 'security', priority: 'critical' },
147        { file: 'src/c.js', reason: 'test', type: 'test', priority: 'low' },
148        { file: 'src/d.js', reason: 'doc', type: 'documentation', priority: 'medium' },
149      ]);
150    });
151  
152    test('returns all pending items', async () => {
153      const items = await getPendingReviews();
154      assert.equal(items.length, 4);
155    });
156  
157    test('filters by type', async () => {
158      const items = await getPendingReviews({ type: 'security' });
159      assert.equal(items.length, 1);
160      assert.equal(items[0].file, 'src/b.js');
161    });
162  
163    test('filters by priority', async () => {
164      const items = await getPendingReviews({ priority: 'high' });
165      assert.equal(items.length, 1);
166      assert.equal(items[0].file, 'src/a.js');
167    });
168  
169    test('limits results', async () => {
170      const items = await getPendingReviews({ limit: 2 });
171      assert.equal(items.length, 2);
172    });
173  
174    test('sorts by priority (critical first)', async () => {
175      const items = await getPendingReviews();
176      assert.equal(items[0].priority, 'critical');
177    });
178  
179    test('excludes reviewed items', async () => {
180      const items = await getPendingReviews();
181      const { id } = items[0];
182      await markReviewed(id, 'tester', 'looks good');
183  
184      const pending = await getPendingReviews();
185      assert.equal(pending.length, 3);
186      assert.ok(!pending.find(i => i.id === id));
187    });
188  });
189  
190  describe('markReviewed', () => {
191    before(() => {
192      clearTable();
193      return addReviewItem({ file: 'src/mark.js', reason: 'review me', type: 'architecture' });
194    });
195  
196    test('sets status to reviewed', async () => {
197      const row = db.prepare("SELECT id FROM human_review_queue WHERE file = 'src/mark.js'").get();
198      await markReviewed(row.id, 'Jason', 'approved');
199  
200      const updated = db.prepare('SELECT * FROM human_review_queue WHERE id = ?').get(row.id);
201      assert.equal(updated.status, 'reviewed');
202      assert.equal(updated.reviewed_by, 'Jason');
203      assert.equal(updated.notes, 'approved');
204      assert.ok(updated.reviewed_at);
205    });
206  
207    test('works without notes (null)', async () => {
208      clearTable();
209      await addReviewItem({ file: 'src/nonotes.js', reason: 'test', type: 'test' });
210      const row = db.prepare("SELECT id FROM human_review_queue WHERE file = 'src/nonotes.js'").get();
211      await assert.doesNotReject(() => markReviewed(row.id, 'reviewer'));
212    });
213  });
214  
215  describe('dismissReview', () => {
216    before(() => {
217      clearTable();
218      return addReviewItem({ file: 'src/dismiss.js', reason: 'maybe', type: 'documentation' });
219    });
220  
221    test('sets status to dismissed', async () => {
222      const row = db.prepare("SELECT id FROM human_review_queue WHERE file = 'src/dismiss.js'").get();
223      await dismissReview(row.id, 'not needed');
224  
225      const updated = db.prepare('SELECT * FROM human_review_queue WHERE id = ?').get(row.id);
226      assert.equal(updated.status, 'dismissed');
227      assert.equal(updated.notes, 'not needed');
228    });
229  
230    test('works without reason', async () => {
231      clearTable();
232      await addReviewItem({ file: 'src/dismiss2.js', reason: 'test', type: 'test' });
233      const row = db
234        .prepare("SELECT id FROM human_review_queue WHERE file = 'src/dismiss2.js'")
235        .get();
236      await assert.doesNotReject(() => dismissReview(row.id));
237    });
238  });
239  
240  describe('getQueueStats', () => {
241    before(async () => {
242      clearTable();
243      await addReviewItems([
244        { file: 'src/s1.js', reason: 'r', type: 'architecture', priority: 'critical' },
245        { file: 'src/s2.js', reason: 'r', type: 'security', priority: 'high' },
246        { file: 'src/s3.js', reason: 'r', type: 'test', priority: 'medium' },
247      ]);
248      const row = db.prepare("SELECT id FROM human_review_queue WHERE file = 'src/s3.js'").get();
249      await markReviewed(row.id, 'tester');
250    });
251  
252    test('returns correct counts', async () => {
253      const stats = await getQueueStats();
254      assert.equal(stats.total, 3);
255      assert.equal(stats.pending, 2);
256      assert.equal(stats.reviewed, 1);
257      assert.equal(stats.critical, 1);
258      assert.equal(stats.high, 1);
259    });
260  });
261  
262  describe('cleanupReviewedItems', () => {
263    test('returns number of deleted rows (0 when nothing old)', async () => {
264      clearTable();
265      await addReviewItem({ file: 'src/clean.js', reason: 'test', type: 'test' });
266      const row = db.prepare("SELECT id FROM human_review_queue WHERE file = 'src/clean.js'").get();
267      await markReviewed(row.id, 'tester');
268  
269      const deleted = await cleanupReviewedItems();
270      assert.equal(deleted, 0);
271    });
272  
273    test('deletes old reviewed/dismissed items', async () => {
274      clearTable();
275      db.prepare(`
276        INSERT INTO human_review_queue (file, reason, type, status, reviewed_at)
277        VALUES ('src/old.js', 'old', 'test', 'reviewed', datetime('now', '-31 days'))
278      `).run();
279  
280      const deleted = await cleanupReviewedItems();
281      assert.equal(deleted, 1);
282    });
283  });