claude-batch-code-review.test.js
1 /** 2 * Tests for fetchCodeReview in scripts/claude-batch.js 3 * 4 * The function uses the module-level `db` variable, so we extract the logic 5 * into a testable form and inject an in-memory DB. 6 * 7 * Covers: 8 * - queue file missing → returns [] 9 * - queue exhausted (next_index >= files.length) → returns [] 10 * - normal case: returns file content, advances pointer 11 * - file has been deleted from disk → returns [], skips it 12 * - skip files with pending fix tasks, advance to next valid file 13 * - skip files with running fix tasks (not just pending) 14 * - all remaining files have active tasks → returns [] 15 * - queue pointer written back correctly 16 */ 17 18 import { test, describe, beforeEach, afterEach } from 'node:test'; 19 import assert from 'node:assert/strict'; 20 import { mkdtempSync, writeFileSync, readFileSync, mkdirSync, rmSync, existsSync } from 'node:fs'; 21 import { join } from 'node:path'; 22 import { tmpdir } from 'node:os'; 23 import { createTestDb } from '../helpers/test-data-generator.js'; 24 25 // ── Inline the function for isolation ──────────────────────────────────────── 26 // fetchCodeReview() uses module-level `db` from claude-batch.js, which opens the 27 // production DB at load time. We replicate the pure logic here for testability. 28 // Any change to the production function's skip/pointer logic must be mirrored here. 29 30 function makeFetchCodeReview(db, cwd) { 31 // Return a fetchCodeReview bound to a given cwd (queue file location) and db 32 return function fetchCodeReview() { 33 const queuePath = join(cwd, 'logs/code-review-queue.json'); 34 if (!existsSync(queuePath)) return []; 35 36 const queue = JSON.parse(readFileSync(queuePath, 'utf8')); 37 const { files, next_index: nextIndex } = queue; 38 39 if (!Array.isArray(files) || nextIndex >= files.length) return []; 40 41 let idx = nextIndex; 42 let filePath = null; 43 while (idx < files.length) { 44 const candidate = files[idx]; 45 const existing = db 46 .prepare( 47 `SELECT id FROM agent_tasks 48 WHERE task_type = 'code_review_fix' 49 AND json_extract(context_json, '$.file_path') = ? 50 AND status IN ('pending', 'running') 51 LIMIT 1` 52 ) 53 .get(candidate); 54 if (!existing) { 55 filePath = candidate; 56 break; 57 } 58 idx++; 59 } 60 61 if (!filePath) return []; 62 63 queue.next_index = idx + 1; 64 queue.reviewed = (queue.reviewed || 0) + 1; 65 writeFileSync(queuePath, JSON.stringify(queue, null, 2)); 66 67 const fullPath = join(cwd, filePath); 68 if (!existsSync(fullPath)) return []; 69 70 const content = readFileSync(fullPath, 'utf8'); 71 const lineCount = content.split('\n').length; 72 return [{ file_path: filePath, line_count: lineCount, content }]; 73 }; 74 } 75 76 // ── Test helpers ───────────────────────────────────────────────────────────── 77 78 function makeWorkdir() { 79 const dir = mkdtempSync(join(tmpdir(), 'cr-test-')); 80 mkdirSync(join(dir, 'logs')); 81 return dir; 82 } 83 84 function writeQueue(dir, files, nextIndex = 0, reviewed = 0) { 85 writeFileSync( 86 join(dir, 'logs/code-review-queue.json'), 87 JSON.stringify( 88 { files, next_index: nextIndex, reviewed, created_at: new Date().toISOString() }, 89 null, 90 2 91 ) 92 ); 93 } 94 95 function readQueue(dir) { 96 return JSON.parse(readFileSync(join(dir, 'logs/code-review-queue.json'), 'utf8')); 97 } 98 99 function writeSrcFile(dir, relPath, content = '// test\n') { 100 const fullPath = join(dir, relPath); 101 mkdirSync(join(fullPath, '..'), { recursive: true }); 102 writeFileSync(fullPath, content); 103 } 104 105 function addFixTask(db, filePath, status = 'pending') { 106 db.prepare( 107 `INSERT INTO agent_tasks (task_type, assigned_to, created_by, priority, status, context_json) 108 VALUES ('code_review_fix', 'developer', 'code_reviewer', 7, ?, ?)` 109 ).run(status, JSON.stringify({ file_path: filePath, severity: 7 })); 110 } 111 112 // ── Tests ──────────────────────────────────────────────────────────────────── 113 114 describe('fetchCodeReview', () => { 115 let dir; 116 let db; 117 let fetch; 118 119 beforeEach(() => { 120 dir = makeWorkdir(); 121 db = createTestDb(); 122 fetch = makeFetchCodeReview(db, dir); 123 }); 124 125 afterEach(() => { 126 try { 127 rmSync(dir, { recursive: true, force: true }); 128 } catch { 129 /* ignore */ 130 } 131 }); 132 133 test('returns [] when queue file is missing', () => { 134 const result = fetch(); 135 assert.deepEqual(result, []); 136 }); 137 138 test('returns [] when queue is exhausted', () => { 139 writeQueue(dir, ['src/a.js', 'src/b.js'], 2); 140 const result = fetch(); 141 assert.deepEqual(result, []); 142 }); 143 144 test('returns file content and advances pointer', () => { 145 writeSrcFile(dir, 'src/payment/paypal.js', '// paypal code\n'); 146 writeQueue(dir, ['src/payment/paypal.js'], 0); 147 148 const result = fetch(); 149 assert.equal(result.length, 1); 150 assert.equal(result[0].file_path, 'src/payment/paypal.js'); 151 assert.equal(result[0].content, '// paypal code\n'); 152 assert.equal(result[0].line_count, 2); 153 154 // Pointer advanced 155 const q = readQueue(dir); 156 assert.equal(q.next_index, 1); 157 assert.equal(q.reviewed, 1); 158 }); 159 160 test('returns [] when target file has been deleted from disk', () => { 161 writeQueue(dir, ['src/deleted-file.js'], 0); 162 // Don't create the file 163 const result = fetch(); 164 assert.deepEqual(result, []); 165 }); 166 167 test('skips file with pending fix task, returns next file', () => { 168 writeSrcFile(dir, 'src/a.js', '// a\n'); 169 writeSrcFile(dir, 'src/b.js', '// b\n'); 170 writeQueue(dir, ['src/a.js', 'src/b.js'], 0); 171 addFixTask(db, 'src/a.js', 'pending'); 172 173 const result = fetch(); 174 assert.equal(result.length, 1); 175 assert.equal(result[0].file_path, 'src/b.js'); 176 177 // Pointer is at idx+1 = 2 (past b.js) 178 const q = readQueue(dir); 179 assert.equal(q.next_index, 2); 180 }); 181 182 test('skips file with running fix task, not just pending', () => { 183 writeSrcFile(dir, 'src/a.js', '// a\n'); 184 writeSrcFile(dir, 'src/b.js', '// b\n'); 185 writeQueue(dir, ['src/a.js', 'src/b.js'], 0); 186 addFixTask(db, 'src/a.js', 'running'); 187 188 const result = fetch(); 189 assert.equal(result[0].file_path, 'src/b.js'); 190 }); 191 192 test('completed fix tasks do not cause skip', () => { 193 writeSrcFile(dir, 'src/a.js', '// a\n'); 194 writeQueue(dir, ['src/a.js'], 0); 195 // Add a completed task — should NOT block 196 db.prepare( 197 `INSERT INTO agent_tasks (task_type, assigned_to, created_by, priority, status, context_json) 198 VALUES ('code_review_fix', 'developer', 'code_reviewer', 7, 'completed', ?)` 199 ).run(JSON.stringify({ file_path: 'src/a.js', severity: 7 })); 200 201 const result = fetch(); 202 assert.equal(result.length, 1); 203 assert.equal(result[0].file_path, 'src/a.js'); 204 }); 205 206 test('returns [] when all remaining files have active tasks', () => { 207 writeSrcFile(dir, 'src/a.js', '// a\n'); 208 writeSrcFile(dir, 'src/b.js', '// b\n'); 209 writeQueue(dir, ['src/a.js', 'src/b.js'], 0); 210 addFixTask(db, 'src/a.js', 'pending'); 211 addFixTask(db, 'src/b.js', 'running'); 212 213 const result = fetch(); 214 assert.deepEqual(result, []); 215 }); 216 217 test('reviewed counter increments correctly', () => { 218 writeSrcFile(dir, 'src/a.js', '// a\n'); 219 writeSrcFile(dir, 'src/b.js', '// b\n'); 220 writeQueue(dir, ['src/a.js', 'src/b.js'], 0, 5); // 5 already reviewed 221 222 fetch(); // reviews a.js 223 const q = readQueue(dir); 224 assert.equal(q.reviewed, 6); 225 }); 226 }); 227 228 // ── Source constant validation ──────────────────────────────────────────────── 229 230 describe('fetchCodeReview source constants', () => { 231 test("status filter includes both 'pending' and 'running'", async () => { 232 const { readFileSync: rf } = await import('node:fs'); 233 const src = rf('scripts/claude-batch.js', 'utf8'); 234 assert.match( 235 src, 236 /'pending'.*'running'|"pending".*"running"/, 237 "Must skip both 'pending' and 'running' fix tasks" 238 ); 239 }); 240 241 test('queue file path is logs/code-review-queue.json', async () => { 242 const { readFileSync: rf } = await import('node:fs'); 243 const src = rf('scripts/claude-batch.js', 'utf8'); 244 assert.match(src, /code-review-queue\.json/); 245 }); 246 });