claude-store-code-review.test.js
1 /** 2 * Tests for storeCodeReviewFindings in scripts/claude-store.js 3 * 4 * The function is self-contained (takes (db, item), no module-level state beyond logger), 5 * so we extract it from source and run it against an in-memory DB. 6 * 7 * Covers: 8 * - missing file_path → returns null, no DB writes 9 * - empty findings → returns file_path, no DB writes 10 * - severity < 7 → no task created 11 * - severity >= 7 → task created with correct fields 12 * - mixed severities → only high-severity tasks created 13 * - context_json fields are correct (file_path, line, category, description, suggestion, severity) 14 */ 15 16 import { test, describe } from 'node:test'; 17 import assert from 'node:assert/strict'; 18 import { createTestDb } from '../helpers/test-data-generator.js'; 19 20 // ── Extract the function from source ──────────────────────────────────────── 21 // We copy the pure logic here. Any change to the production function that 22 // diverges from this signature or the severity threshold will break these tests. 23 24 import Logger from '../../src/utils/logger.js'; 25 const logger = new Logger('TestCodeReview'); 26 27 function storeCodeReviewFindings(db, item) { 28 if (!item.file_path) { 29 logger.warn('Code review result missing file_path'); 30 return null; 31 } 32 33 const findings = Array.isArray(item.findings) ? item.findings : []; 34 35 if (findings.length === 0) { 36 return item.file_path; 37 } 38 39 let tasksCreated = 0; 40 41 for (const finding of findings) { 42 const severity = parseInt(finding.severity || '5', 10); 43 44 if (severity < 7) { 45 continue; 46 } 47 48 db.prepare( 49 `INSERT INTO agent_tasks 50 (task_type, assigned_to, created_by, priority, status, context_json) 51 VALUES ('code_review_fix', 'developer', 'code_reviewer', ?, 'pending', ?)` 52 ).run( 53 severity, 54 JSON.stringify({ 55 file_path: item.file_path, 56 line: finding.line || null, 57 category: finding.category || 'unknown', 58 description: finding.description, 59 suggestion: finding.suggestion || null, 60 severity, 61 }) 62 ); 63 tasksCreated++; 64 } 65 66 return item.file_path; 67 } 68 69 // ── Helper ─────────────────────────────────────────────────────────────────── 70 71 function getPendingFixTasks(db) { 72 return db 73 .prepare( 74 `SELECT task_type, assigned_to, created_by, priority, status, context_json 75 FROM agent_tasks WHERE task_type = 'code_review_fix'` 76 ) 77 .all() 78 .map(row => ({ ...row, context_json: JSON.parse(row.context_json) })); 79 } 80 81 // ── Tests ──────────────────────────────────────────────────────────────────── 82 83 describe('storeCodeReviewFindings', () => { 84 test('missing file_path returns null and writes nothing', () => { 85 const db = createTestDb(); 86 const result = storeCodeReviewFindings(db, { findings: [] }); 87 assert.equal(result, null); 88 assert.equal(getPendingFixTasks(db).length, 0); 89 }); 90 91 test('empty findings array returns file_path and writes nothing', () => { 92 const db = createTestDb(); 93 const result = storeCodeReviewFindings(db, { 94 file_path: 'src/payment/paypal.js', 95 findings: [], 96 }); 97 assert.equal(result, 'src/payment/paypal.js'); 98 assert.equal(getPendingFixTasks(db).length, 0); 99 }); 100 101 test('undefined findings returns file_path and writes nothing', () => { 102 const db = createTestDb(); 103 const result = storeCodeReviewFindings(db, { 104 file_path: 'src/utils/foo.js', 105 }); 106 assert.equal(result, 'src/utils/foo.js'); 107 assert.equal(getPendingFixTasks(db).length, 0); 108 }); 109 110 test('severity < 7 does not create a task', () => { 111 const db = createTestDb(); 112 storeCodeReviewFindings(db, { 113 file_path: 'src/utils/foo.js', 114 findings: [ 115 { severity: 6, category: 'performance', line: 10, description: 'Minor issue' }, 116 { severity: 5, category: 'style', line: 20, description: 'Style issue' }, 117 { severity: 3, category: 'style', line: 30, description: 'Low priority' }, 118 ], 119 }); 120 assert.equal(getPendingFixTasks(db).length, 0); 121 }); 122 123 test('severity threshold is exactly 7 — severity 7 creates a task', () => { 124 const db = createTestDb(); 125 storeCodeReviewFindings(db, { 126 file_path: 'src/utils/foo.js', 127 findings: [{ severity: 7, category: 'bug', line: 42, description: 'Null dereference' }], 128 }); 129 const tasks = getPendingFixTasks(db); 130 assert.equal(tasks.length, 1); 131 }); 132 133 test('severity 8 creates a task with correct priority', () => { 134 const db = createTestDb(); 135 storeCodeReviewFindings(db, { 136 file_path: 'src/payment/paypal.js', 137 findings: [ 138 { 139 severity: 8, 140 category: 'security', 141 line: 83, 142 description: 'testPrice not validated', 143 suggestion: 'Validate bounds', 144 }, 145 ], 146 }); 147 const tasks = getPendingFixTasks(db); 148 assert.equal(tasks.length, 1); 149 assert.equal(tasks[0].priority, 8); 150 assert.equal(tasks[0].task_type, 'code_review_fix'); 151 assert.equal(tasks[0].assigned_to, 'developer'); 152 assert.equal(tasks[0].created_by, 'code_reviewer'); 153 assert.equal(tasks[0].status, 'pending'); 154 }); 155 156 test('context_json fields are set correctly', () => { 157 const db = createTestDb(); 158 storeCodeReviewFindings(db, { 159 file_path: 'src/payment/paypal.js', 160 findings: [ 161 { 162 severity: 8, 163 category: 'security', 164 line: 83, 165 description: 'testPrice not validated', 166 suggestion: 'Add bounds check', 167 }, 168 ], 169 }); 170 const tasks = getPendingFixTasks(db); 171 const ctx = tasks[0].context_json; 172 assert.equal(ctx.file_path, 'src/payment/paypal.js'); 173 assert.equal(ctx.line, 83); 174 assert.equal(ctx.category, 'security'); 175 assert.equal(ctx.description, 'testPrice not validated'); 176 assert.equal(ctx.suggestion, 'Add bounds check'); 177 assert.equal(ctx.severity, 8); 178 }); 179 180 test('missing line defaults to null in context_json', () => { 181 const db = createTestDb(); 182 storeCodeReviewFindings(db, { 183 file_path: 'src/utils/foo.js', 184 findings: [{ severity: 9, category: 'security', description: 'No line number' }], 185 }); 186 const tasks = getPendingFixTasks(db); 187 assert.equal(tasks[0].context_json.line, null); 188 }); 189 190 test('missing suggestion defaults to null in context_json', () => { 191 const db = createTestDb(); 192 storeCodeReviewFindings(db, { 193 file_path: 'src/utils/foo.js', 194 findings: [{ severity: 8, category: 'bug', line: 5, description: 'Oops' }], 195 }); 196 const tasks = getPendingFixTasks(db); 197 assert.equal(tasks[0].context_json.suggestion, null); 198 }); 199 200 test('missing category defaults to unknown in context_json', () => { 201 const db = createTestDb(); 202 storeCodeReviewFindings(db, { 203 file_path: 'src/utils/foo.js', 204 findings: [{ severity: 7, line: 5, description: 'No category' }], 205 }); 206 const tasks = getPendingFixTasks(db); 207 assert.equal(tasks[0].context_json.category, 'unknown'); 208 }); 209 210 test('mixed severities: only severity >= 7 creates tasks', () => { 211 const db = createTestDb(); 212 storeCodeReviewFindings(db, { 213 file_path: 'src/utils/rate-limiter.js', 214 findings: [ 215 { severity: 9, category: 'security', line: 10, description: 'Critical' }, 216 { severity: 7, category: 'bug', line: 20, description: 'Bug' }, 217 { severity: 6, category: 'performance', line: 30, description: 'Perf' }, 218 { severity: 4, category: 'style', line: 40, description: 'Style' }, 219 ], 220 }); 221 const tasks = getPendingFixTasks(db); 222 assert.equal(tasks.length, 2); 223 const priorities = tasks.map(t => t.priority).sort((a, b) => b - a); 224 assert.deepEqual(priorities, [9, 7]); 225 }); 226 227 test('multiple high-severity findings create multiple tasks', () => { 228 const db = createTestDb(); 229 storeCodeReviewFindings(db, { 230 file_path: 'src/inbound/sms.js', 231 findings: [ 232 { severity: 8, category: 'security', line: 5, description: 'Issue 1' }, 233 { severity: 8, category: 'security', line: 15, description: 'Issue 2' }, 234 { severity: 10, category: 'security', line: 25, description: 'Issue 3' }, 235 ], 236 }); 237 assert.equal(getPendingFixTasks(db).length, 3); 238 }); 239 240 test('severity as string is parsed correctly', () => { 241 const db = createTestDb(); 242 storeCodeReviewFindings(db, { 243 file_path: 'src/utils/foo.js', 244 findings: [{ severity: '9', category: 'security', line: 1, description: 'String sev' }], 245 }); 246 const tasks = getPendingFixTasks(db); 247 assert.equal(tasks.length, 1); 248 assert.equal(tasks[0].priority, 9); 249 }); 250 251 test('missing severity defaults to 5 (below threshold, no task)', () => { 252 const db = createTestDb(); 253 storeCodeReviewFindings(db, { 254 file_path: 'src/utils/foo.js', 255 findings: [{ category: 'style', line: 1, description: 'No severity field' }], 256 }); 257 assert.equal(getPendingFixTasks(db).length, 0); 258 }); 259 }); 260 261 // ── Source constant validation ──────────────────────────────────────────────── 262 // Guard against threshold drift during refactors. 263 264 describe('storeCodeReviewFindings source constants', () => { 265 test('severity threshold is 7 in production source', async () => { 266 const { readFileSync } = await import('node:fs'); 267 const src = readFileSync('scripts/claude-store.js', 'utf8'); 268 assert.match(src, /severity < 7/, 'Severity threshold must be < 7 (not < 6 or < 8)'); 269 }); 270 271 test('task_type is code_review_fix in production source', async () => { 272 const { readFileSync } = await import('node:fs'); 273 const src = readFileSync('scripts/claude-store.js', 'utf8'); 274 assert.match(src, /code_review_fix/, 'Task type code_review_fix must appear in source'); 275 }); 276 277 test('initial status is pending in production source', async () => { 278 const { readFileSync } = await import('node:fs'); 279 const src = readFileSync('scripts/claude-store.js', 'utf8'); 280 assert.match( 281 src, 282 /status.*pending.*code_review_fix|code_review_fix.*status.*pending/s, 283 'Initial task status must be pending' 284 ); 285 }); 286 });