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 });