/ tests / scripts / claude-store-code-review.test.js
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  });