codecritique 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +82 -114
  2. package/package.json +10 -9
  3. package/src/content-retrieval.test.js +775 -0
  4. package/src/custom-documents.test.js +440 -0
  5. package/src/feedback-loader.test.js +529 -0
  6. package/src/llm.test.js +256 -0
  7. package/src/project-analyzer.test.js +747 -0
  8. package/src/rag-analyzer.js +12 -0
  9. package/src/rag-analyzer.test.js +1109 -0
  10. package/src/rag-review.test.js +317 -0
  11. package/src/setupTests.js +131 -0
  12. package/src/zero-shot-classifier-open.test.js +278 -0
  13. package/src/embeddings/cache-manager.js +0 -364
  14. package/src/embeddings/constants.js +0 -40
  15. package/src/embeddings/database.js +0 -921
  16. package/src/embeddings/errors.js +0 -208
  17. package/src/embeddings/factory.js +0 -447
  18. package/src/embeddings/file-processor.js +0 -851
  19. package/src/embeddings/model-manager.js +0 -337
  20. package/src/embeddings/similarity-calculator.js +0 -97
  21. package/src/embeddings/types.js +0 -113
  22. package/src/pr-history/analyzer.js +0 -579
  23. package/src/pr-history/bot-detector.js +0 -123
  24. package/src/pr-history/cli-utils.js +0 -204
  25. package/src/pr-history/comment-processor.js +0 -549
  26. package/src/pr-history/database.js +0 -819
  27. package/src/pr-history/github-client.js +0 -629
  28. package/src/technology-keywords.json +0 -753
  29. package/src/utils/command.js +0 -48
  30. package/src/utils/constants.js +0 -263
  31. package/src/utils/context-inference.js +0 -364
  32. package/src/utils/document-detection.js +0 -105
  33. package/src/utils/file-validation.js +0 -271
  34. package/src/utils/git.js +0 -232
  35. package/src/utils/language-detection.js +0 -170
  36. package/src/utils/logging.js +0 -24
  37. package/src/utils/markdown.js +0 -132
  38. package/src/utils/mobilebert-tokenizer.js +0 -141
  39. package/src/utils/pr-chunking.js +0 -276
  40. package/src/utils/string-utils.js +0 -28
@@ -0,0 +1,1109 @@
1
+ import fs from 'node:fs';
2
+ import * as llm from './llm.js';
3
+ import { findRelevantPRComments } from './pr-history/database.js';
4
+ import { runAnalysis, gatherUnifiedContextForPR } from './rag-analyzer.js';
5
+ import {
6
+ createMockReviewResponse,
7
+ createMockHolisticReviewResponse,
8
+ createMockPRFile,
9
+ createMockUnifiedContext,
10
+ createMockPRComment,
11
+ createMockLongCode,
12
+ } from './test-utils/fixtures.js';
13
+ import { shouldProcessFile, isTestFile } from './utils/file-validation.js';
14
+
15
+ // Create hoisted mock for embeddings system (inline since can't use imported functions)
16
+ const mockEmbeddingsSystem = vi.hoisted(() => ({
17
+ initialize: vi.fn().mockResolvedValue(undefined),
18
+ calculateEmbedding: vi.fn().mockResolvedValue(new Array(384).fill(0.1)),
19
+ calculateQueryEmbedding: vi.fn().mockResolvedValue(new Array(384).fill(0.1)),
20
+ getProjectSummary: vi.fn().mockResolvedValue(null),
21
+ findRelevantDocs: vi.fn().mockResolvedValue([]),
22
+ findSimilarCode: vi.fn().mockResolvedValue([]),
23
+ findRelevantCustomDocChunks: vi.fn().mockResolvedValue([]),
24
+ processCustomDocumentsInMemory: vi.fn().mockResolvedValue([]),
25
+ getExistingCustomDocumentChunks: vi.fn().mockResolvedValue([]),
26
+ contentRetriever: {
27
+ findSimilarCode: vi.fn().mockResolvedValue({ relevantFiles: [], relevantChunks: [] }),
28
+ findSimilarDocumentChunks: vi.fn().mockResolvedValue([]),
29
+ },
30
+ projectAnalyzer: {
31
+ analyzeProject: vi.fn().mockResolvedValue({ keyFiles: [], technologies: [] }),
32
+ },
33
+ customDocuments: {
34
+ queryCustomDocuments: vi.fn().mockResolvedValue([]),
35
+ },
36
+ getPRCommentsTable: vi.fn().mockResolvedValue(null),
37
+ updatePRCommentsIndex: vi.fn().mockResolvedValue(undefined),
38
+ }));
39
+
40
+ vi.mock('node:fs', () => ({
41
+ default: {
42
+ readFileSync: vi.fn(),
43
+ existsSync: vi.fn().mockReturnValue(true),
44
+ },
45
+ }));
46
+
47
+ vi.mock('./embeddings/factory.js', () => ({
48
+ getDefaultEmbeddingsSystem: vi.fn(() => mockEmbeddingsSystem),
49
+ }));
50
+
51
+ vi.mock('./feedback-loader.js', () => ({
52
+ loadFeedbackData: vi.fn().mockResolvedValue(null),
53
+ shouldSkipSimilarIssue: vi.fn().mockReturnValue(false),
54
+ extractDismissedPatterns: vi.fn().mockReturnValue([]),
55
+ generateFeedbackContext: vi.fn().mockReturnValue(''),
56
+ initializeSemanticSimilarity: vi.fn().mockResolvedValue(undefined),
57
+ isSemanticSimilarityAvailable: vi.fn().mockReturnValue(false),
58
+ }));
59
+
60
+ vi.mock('./llm.js', () => ({
61
+ sendPromptToClaude: vi.fn(),
62
+ }));
63
+
64
+ vi.mock('./pr-history/database.js', () => ({
65
+ findRelevantPRComments: vi.fn().mockResolvedValue([]),
66
+ }));
67
+
68
+ vi.mock('./utils/file-validation.js', () => ({
69
+ shouldProcessFile: vi.fn().mockReturnValue(true),
70
+ isTestFile: vi.fn().mockReturnValue(false),
71
+ }));
72
+
73
+ vi.mock('./utils/language-detection.js', () => ({
74
+ detectFileType: vi.fn().mockReturnValue({ isTest: false }),
75
+ detectLanguageFromExtension: vi.fn().mockReturnValue('javascript'),
76
+ }));
77
+
78
+ vi.mock('./utils/logging.js', () => ({
79
+ debug: vi.fn(),
80
+ }));
81
+
82
+ vi.mock('./utils/context-inference.js', () => ({
83
+ inferContextFromCodeContent: vi.fn().mockReturnValue({
84
+ area: 'Frontend',
85
+ dominantTech: ['JavaScript', 'React'],
86
+ frameworks: ['React'],
87
+ keywords: ['component', 'state', 'props'],
88
+ }),
89
+ inferContextFromDocumentContent: vi.fn().mockReturnValue({
90
+ area: 'Documentation',
91
+ dominantTech: ['Markdown'],
92
+ frameworks: [],
93
+ keywords: ['guide', 'reference'],
94
+ }),
95
+ }));
96
+
97
+ vi.mock('./utils/document-detection.js', () => ({
98
+ isGenericDocument: vi.fn().mockReturnValue(false),
99
+ getGenericDocumentContext: vi.fn().mockReturnValue({
100
+ area: 'General',
101
+ dominantTech: [],
102
+ frameworks: [],
103
+ keywords: [],
104
+ }),
105
+ }));
106
+
107
+ // ============================================================================
108
+ // Helper Functions
109
+ // ============================================================================
110
+
111
+ const setupSuccessfulLLMResponse = (response = createMockReviewResponse()) => {
112
+ llm.sendPromptToClaude.mockResolvedValue(response);
113
+ };
114
+
115
+ const setupHolisticReviewOptions = (overrides = {}) => ({
116
+ isHolisticPRReview: true,
117
+ prFiles: overrides.prFiles || [createMockPRFile()],
118
+ unifiedContext: overrides.unifiedContext || createMockUnifiedContext(),
119
+ prContext: overrides.prContext || { totalFiles: 1 },
120
+ ...overrides,
121
+ });
122
+
123
+ // ============================================================================
124
+ // Tests
125
+ // ============================================================================
126
+
127
+ describe('rag-analyzer', () => {
128
+ beforeEach(() => {
129
+ mockConsole();
130
+ llm.sendPromptToClaude.mockReset();
131
+ shouldProcessFile.mockReset().mockReturnValue(true);
132
+ isTestFile.mockReset().mockReturnValue(false);
133
+ findRelevantPRComments.mockReset().mockResolvedValue([]);
134
+ // Reset embeddings system (inline since can't use imported function with hoisted mocks)
135
+ mockEmbeddingsSystem.initialize.mockReset().mockResolvedValue(undefined);
136
+ mockEmbeddingsSystem.calculateEmbedding.mockReset().mockResolvedValue(new Array(384).fill(0.1));
137
+ mockEmbeddingsSystem.calculateQueryEmbedding.mockReset().mockResolvedValue(new Array(384).fill(0.1));
138
+ mockEmbeddingsSystem.getProjectSummary.mockReset().mockResolvedValue(null);
139
+ mockEmbeddingsSystem.findRelevantDocs.mockReset().mockResolvedValue([]);
140
+ mockEmbeddingsSystem.findSimilarCode.mockReset().mockResolvedValue([]);
141
+ mockEmbeddingsSystem.findRelevantCustomDocChunks.mockReset().mockResolvedValue([]);
142
+ mockEmbeddingsSystem.processCustomDocumentsInMemory.mockReset().mockResolvedValue([]);
143
+ mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockReset().mockResolvedValue([]);
144
+ mockEmbeddingsSystem.contentRetriever.findSimilarCode.mockReset().mockResolvedValue({ relevantFiles: [], relevantChunks: [] });
145
+ mockEmbeddingsSystem.contentRetriever.findSimilarDocumentChunks.mockReset().mockResolvedValue([]);
146
+ mockEmbeddingsSystem.projectAnalyzer.analyzeProject.mockReset().mockResolvedValue({ keyFiles: [], technologies: [] });
147
+ mockEmbeddingsSystem.customDocuments.queryCustomDocuments.mockReset().mockResolvedValue([]);
148
+ mockEmbeddingsSystem.getPRCommentsTable.mockReset().mockResolvedValue(null);
149
+ mockEmbeddingsSystem.updatePRCommentsIndex.mockReset().mockResolvedValue(undefined);
150
+ fs.readFileSync.mockReturnValue('const x = 1;\nconsole.log(x);');
151
+ fs.existsSync.mockReturnValue(true);
152
+ });
153
+
154
+ afterEach(() => {
155
+ vi.restoreAllMocks();
156
+ });
157
+
158
+ // ==========================================================================
159
+ // runAnalysis - Basic Scenarios
160
+ // ==========================================================================
161
+
162
+ describe('runAnalysis', () => {
163
+ it.each([
164
+ ['analyze a file successfully', { json: { summary: 'No issues', issues: [] } }, { success: true }],
165
+ ['handle LLM response without json property', { text: 'Raw text response' }, { success: true }],
166
+ ['handle empty issues array', { json: { summary: 'No issues', issues: [] } }, { success: true }],
167
+ ])('should %s', async (_, llmResponse, expected) => {
168
+ llm.sendPromptToClaude.mockResolvedValue(llmResponse);
169
+ const result = await runAnalysis('/test/file.js');
170
+ expect(result.success).toBe(expected.success);
171
+ });
172
+
173
+ it('should skip files that should not be processed', async () => {
174
+ shouldProcessFile.mockReturnValue(false);
175
+ const result = await runAnalysis('/test/excluded.js');
176
+ expect(result.skipped).toBe(true);
177
+ expect(llm.sendPromptToClaude).not.toHaveBeenCalled();
178
+ });
179
+
180
+ it('should handle LLM errors gracefully', async () => {
181
+ llm.sendPromptToClaude.mockRejectedValue(new Error('LLM unavailable'));
182
+ const result = await runAnalysis('/test/file.js');
183
+ expect(result.success).toBe(false);
184
+ expect(result.error).toContain('LLM unavailable');
185
+ });
186
+
187
+ it('should initialize embeddings system', async () => {
188
+ setupSuccessfulLLMResponse();
189
+ await runAnalysis('/test/file.js');
190
+ expect(mockEmbeddingsSystem.initialize).toHaveBeenCalled();
191
+ });
192
+
193
+ it('should return error when file does not exist', async () => {
194
+ fs.existsSync.mockReturnValue(false);
195
+ const result = await runAnalysis('/test/nonexistent.js');
196
+ expect(result.success).toBe(false);
197
+ expect(result.error).toContain('File not found');
198
+ });
199
+
200
+ it('should handle embeddings system initialization failure', async () => {
201
+ mockEmbeddingsSystem.initialize.mockRejectedValue(new Error('Init failed'));
202
+ const result = await runAnalysis('/test/file.js');
203
+ expect(result.success).toBe(false);
204
+ });
205
+ });
206
+
207
+ // ==========================================================================
208
+ // runAnalysis - Options Handling
209
+ // ==========================================================================
210
+
211
+ describe('runAnalysis options', () => {
212
+ beforeEach(() => setupSuccessfulLLMResponse());
213
+
214
+ it.each([
215
+ ['verbose option', { verbose: true }],
216
+ ['custom model option', { model: 'claude-3-opus' }],
217
+ ['custom directory option', { verbose: true, directory: '/custom/dir' }],
218
+ ['precomputed embedding', { precomputedEmbedding: createMockEmbedding() }],
219
+ ['project path', { projectPath: '/test' }],
220
+ ])('should handle %s', async (_, options) => {
221
+ const result = await runAnalysis('/test/file.js', options);
222
+ expect(result.success).toBe(true);
223
+ });
224
+
225
+ it('should handle diff-only mode', async () => {
226
+ const result = await runAnalysis('/test/file.js', {
227
+ diffOnly: true,
228
+ diffContent: '+ new line\n- old line',
229
+ fullFileContent: 'const x = 1;',
230
+ });
231
+ expect(result.success).toBe(true);
232
+ });
233
+
234
+ it('should handle PR context when provided', async () => {
235
+ const result = await runAnalysis('/test/file.js', {
236
+ prContext: {
237
+ totalFiles: 5,
238
+ testFiles: 1,
239
+ sourceFiles: 4,
240
+ allFiles: ['/file1.js', '/file2.js'],
241
+ },
242
+ });
243
+ expect(result.success).toBe(true);
244
+ });
245
+
246
+ it('should handle diff-only with branch info', async () => {
247
+ const result = await runAnalysis('/test/file.js', {
248
+ diffOnly: true,
249
+ diffContent: '+ added line\n- removed line',
250
+ baseBranch: 'main',
251
+ targetBranch: 'feature',
252
+ diffInfo: { addedLines: [1], removedLines: [2] },
253
+ });
254
+ expect(result.success).toBe(true);
255
+ });
256
+ });
257
+
258
+ // ==========================================================================
259
+ // runAnalysis - Test File Handling
260
+ // ==========================================================================
261
+
262
+ describe('runAnalysis test file handling', () => {
263
+ beforeEach(() => setupSuccessfulLLMResponse());
264
+
265
+ it('should handle test file analysis', async () => {
266
+ isTestFile.mockReturnValue(true);
267
+ const result = await runAnalysis('/test/file.test.js');
268
+ expect(result.success).toBe(true);
269
+ expect(isTestFile).toHaveBeenCalled();
270
+ });
271
+
272
+ it('should skip test file filtering for non-test files', async () => {
273
+ isTestFile.mockReturnValue(false);
274
+ const result = await runAnalysis('/src/component.js');
275
+ expect(result.success).toBe(true);
276
+ });
277
+
278
+ it('should use test-specific guideline queries for test files', async () => {
279
+ const { detectFileType } = await import('./utils/language-detection.js'); // eslint-disable-line no-restricted-syntax
280
+ detectFileType.mockReturnValue({ isTest: true });
281
+ const result = await runAnalysis('/test/component.test.js');
282
+ expect(result.success).toBe(true);
283
+ });
284
+ });
285
+
286
+ // ==========================================================================
287
+ // runAnalysis - Context Building
288
+ // ==========================================================================
289
+
290
+ describe('context building', () => {
291
+ beforeEach(() => setupSuccessfulLLMResponse());
292
+
293
+ it('should use project summary when available', async () => {
294
+ mockEmbeddingsSystem.getProjectSummary.mockResolvedValue({
295
+ name: 'Test Project',
296
+ technologies: ['JavaScript', 'Node.js'],
297
+ });
298
+ const result = await runAnalysis('/test/file.js');
299
+ expect(result.success).toBe(true);
300
+ expect(mockEmbeddingsSystem.getProjectSummary).toHaveBeenCalled();
301
+ });
302
+
303
+ it('should find similar code examples', async () => {
304
+ mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([{ path: '/similar.js', content: 'similar code', similarity: 0.9 }]);
305
+ const result = await runAnalysis('/test/file.js');
306
+ expect(result.success).toBe(true);
307
+ });
308
+
309
+ it('should find relevant documentation', async () => {
310
+ mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([{ path: '/docs/api.md', content: 'API docs', similarity: 0.8 }]);
311
+ const result = await runAnalysis('/test/file.js');
312
+ expect(result.success).toBe(true);
313
+ });
314
+
315
+ it('should include PR comments when available', async () => {
316
+ findRelevantPRComments.mockResolvedValue([createMockPRComment()]);
317
+ const result = await runAnalysis('/test/file.js');
318
+ expect(result.success).toBe(true);
319
+ });
320
+
321
+ it('should handle parallel context retrieval failure', async () => {
322
+ mockEmbeddingsSystem.findRelevantDocs.mockRejectedValue(new Error('Doc search failed'));
323
+ mockEmbeddingsSystem.findSimilarCode.mockRejectedValue(new Error('Code search failed'));
324
+ findRelevantPRComments.mockRejectedValue(new Error('PR comments failed'));
325
+ llm.sendPromptToClaude.mockResolvedValue({ json: { summary: 'Review', issues: [] } });
326
+ const result = await runAnalysis('/test/file.js');
327
+ expect(result.success).toBe(true);
328
+ });
329
+ });
330
+
331
+ // ==========================================================================
332
+ // runAnalysis - File Content Handling
333
+ // ==========================================================================
334
+
335
+ describe('file content handling', () => {
336
+ beforeEach(() => setupSuccessfulLLMResponse());
337
+
338
+ it.each([
339
+ ['empty file content', ''],
340
+ ['whitespace only', ' \n\n '],
341
+ ['normal content', 'const x = 1;\nfunction test() {}'],
342
+ ])('should handle %s', async (_, content) => {
343
+ fs.readFileSync.mockReturnValue(content);
344
+ const result = await runAnalysis('/test/file.js');
345
+ expect(result).toBeDefined();
346
+ });
347
+
348
+ it('should handle very long files', async () => {
349
+ fs.readFileSync.mockReturnValue(createMockLongCode(1000));
350
+ const result = await runAnalysis('/test/long.js');
351
+ expect(result.success).toBe(true);
352
+ });
353
+
354
+ it('should detect language from file extension', async () => {
355
+ const result = await runAnalysis('/test/file.ts');
356
+ expect(result.success).toBe(true);
357
+ expect(result.language).toBeDefined();
358
+ });
359
+ });
360
+
361
+ // ==========================================================================
362
+ // runAnalysis - LLM Response Parsing
363
+ // ==========================================================================
364
+
365
+ describe('LLM response parsing', () => {
366
+ it('should handle JSON response with issues array', async () => {
367
+ llm.sendPromptToClaude.mockResolvedValue({
368
+ json: {
369
+ summary: 'Found issues',
370
+ issues: [
371
+ { severity: 'medium', message: 'Issue 1', line: 10 },
372
+ { severity: 'high', message: 'Issue 2', line: 20 },
373
+ ],
374
+ },
375
+ });
376
+ const result = await runAnalysis('/test/file.js');
377
+ expect(result.success).toBe(true);
378
+ expect(result.results).toBeDefined();
379
+ });
380
+
381
+ it('should handle malformed LLM response with missing issues', async () => {
382
+ llm.sendPromptToClaude.mockResolvedValue({ json: { summary: 'Partial response' } });
383
+ const result = await runAnalysis('/test/file.js');
384
+ expect(result.success).toBe(true);
385
+ expect(result.results).toBeDefined();
386
+ });
387
+
388
+ it('should handle LLM response with null json', async () => {
389
+ llm.sendPromptToClaude.mockResolvedValue({ json: null });
390
+ const result = await runAnalysis('/test/file.js');
391
+ expect(result.success).toBe(true);
392
+ });
393
+ });
394
+
395
+ // ==========================================================================
396
+ // runAnalysis - Metadata and Results
397
+ // ==========================================================================
398
+
399
+ describe('metadata and results', () => {
400
+ beforeEach(() => setupSuccessfulLLMResponse());
401
+
402
+ it('should include metadata in results', async () => {
403
+ const result = await runAnalysis('/test/file.js');
404
+ expect(result.metadata).toBeDefined();
405
+ expect(result.metadata.analysisTimestamp).toBeDefined();
406
+ });
407
+
408
+ it('should include file metadata in results', async () => {
409
+ const result = await runAnalysis('/test/file.js');
410
+ expect(result.filePath).toBeDefined();
411
+ expect(result.language).toBeDefined();
412
+ });
413
+
414
+ it('should include context information', async () => {
415
+ const result = await runAnalysis('/test/file.js');
416
+ expect(result.context).toBeDefined();
417
+ expect(result.context.codeExamples).toBeDefined();
418
+ });
419
+
420
+ it('should include similar examples when found', async () => {
421
+ mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([{ path: '/similar.js', content: 'code', similarity: 0.9 }]);
422
+ const result = await runAnalysis('/test/file.js');
423
+ expect(result.success).toBe(true);
424
+ expect(result.similarExamples).toBeDefined();
425
+ });
426
+ });
427
+
428
+ // ==========================================================================
429
+ // runAnalysis - Low Severity Filtering
430
+ // ==========================================================================
431
+
432
+ describe('low severity filtering', () => {
433
+ it.each([
434
+ [
435
+ 'file issues',
436
+ { summary: 'Found issues', issues: [{ severity: 'high' }, { severity: 'low' }, { severity: 'medium' }] },
437
+ (r) => r.results.issues.length === 2 && r.results.issues.every((i) => i.severity !== 'low'),
438
+ ],
439
+ ])('should filter low severity %s', async (_, response, validator) => {
440
+ llm.sendPromptToClaude.mockResolvedValue({ json: response });
441
+ const result = await runAnalysis('/test/file.js');
442
+ expect(result.success).toBe(true);
443
+ expect(validator(result)).toBe(true);
444
+ });
445
+
446
+ it('should log filtered count when verbose and issues filtered', async () => {
447
+ llm.sendPromptToClaude.mockResolvedValue({
448
+ json: { summary: 'Found issues', issues: [{ severity: 'low' }, { severity: 'low' }] },
449
+ });
450
+ const result = await runAnalysis('/test/file.js', { verbose: true });
451
+ expect(result.success).toBe(true);
452
+ expect(result.results.issues).toHaveLength(0);
453
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Filtered'));
454
+ });
455
+ });
456
+
457
+ // ==========================================================================
458
+ // Holistic PR Review
459
+ // ==========================================================================
460
+
461
+ describe('holistic PR review', () => {
462
+ it('should handle holistic PR review mode', async () => {
463
+ llm.sendPromptToClaude.mockResolvedValue(createMockHolisticReviewResponse());
464
+ const result = await runAnalysis('PR_HOLISTIC_REVIEW', setupHolisticReviewOptions());
465
+ expect(result.success).toBe(true);
466
+ });
467
+
468
+ it.each([
469
+ ['cross-file issues', { crossFileIssues: [{ message: 'Cross-file issue', severity: 'medium', files: ['a.js', 'b.js'] }] }],
470
+ ['file-specific issues', { fileSpecificIssues: { 'file.js': [{ message: 'Issue', line: 5 }] } }],
471
+ ['recommendations', { recommendations: ['Add tests', 'Update docs', 'Refactor'] }],
472
+ ])('should handle holistic review with %s', async (_, responseOverrides) => {
473
+ llm.sendPromptToClaude.mockResolvedValue(createMockHolisticReviewResponse(responseOverrides));
474
+ const result = await runAnalysis('PR_HOLISTIC_REVIEW', setupHolisticReviewOptions());
475
+ expect(result.success).toBe(true);
476
+ });
477
+
478
+ it('should filter low severity cross-file issues', async () => {
479
+ llm.sendPromptToClaude.mockResolvedValue(
480
+ createMockHolisticReviewResponse({
481
+ crossFileIssues: [
482
+ { severity: 'low', message: 'Minor', files: ['a.js'] },
483
+ { severity: 'high', message: 'Critical', files: ['b.js'] },
484
+ ],
485
+ })
486
+ );
487
+ const result = await runAnalysis('PR_HOLISTIC_REVIEW', setupHolisticReviewOptions());
488
+ expect(result.success).toBe(true);
489
+ expect(result.results.crossFileIssues).toHaveLength(1);
490
+ expect(result.results.crossFileIssues[0].severity).toBe('high');
491
+ });
492
+
493
+ it('should filter low severity file-specific issues', async () => {
494
+ llm.sendPromptToClaude.mockResolvedValue(
495
+ createMockHolisticReviewResponse({
496
+ fileSpecificIssues: {
497
+ 'file.js': [
498
+ { severity: 'low', description: 'Minor issue' },
499
+ { severity: 'critical', description: 'Critical issue' },
500
+ ],
501
+ },
502
+ })
503
+ );
504
+ const result = await runAnalysis('PR_HOLISTIC_REVIEW', setupHolisticReviewOptions());
505
+ expect(result.success).toBe(true);
506
+ expect(result.results.fileSpecificIssues['file.js']).toHaveLength(1);
507
+ expect(result.results.fileSpecificIssues['file.js'][0].severity).toBe('critical');
508
+ });
509
+
510
+ it('should handle LLM error in holistic analysis', async () => {
511
+ llm.sendPromptToClaude.mockRejectedValue(new Error('LLM failed'));
512
+ const result = await runAnalysis('PR_HOLISTIC_REVIEW', setupHolisticReviewOptions());
513
+ expect(result.success).toBe(false);
514
+ expect(result.error).toContain('LLM failed');
515
+ });
516
+
517
+ it('should include all context types in holistic review', async () => {
518
+ mockEmbeddingsSystem.getProjectSummary.mockResolvedValue({ projectName: 'Test', technologies: ['React'] });
519
+ llm.sendPromptToClaude.mockResolvedValue(createMockHolisticReviewResponse());
520
+ const result = await runAnalysis(
521
+ 'PR_HOLISTIC_REVIEW',
522
+ setupHolisticReviewOptions({
523
+ prFiles: [createMockPRFile({ fullContent: 'const x = 1;', summary: 'Added code' })],
524
+ unifiedContext: createMockUnifiedContext({
525
+ codeExamples: [{ path: '/ex.js', content: 'example', similarity: 0.9, language: 'javascript' }],
526
+ guidelines: [{ path: '/docs/guide.md', content: 'Rules', similarity: 0.8, headingText: 'Rules' }],
527
+ prComments: [createMockPRComment({ relevanceScore: 0.7 })],
528
+ customDocChunks: [{ document_title: 'Custom', content: 'Content', chunk_index: 0, similarity: 0.75 }],
529
+ }),
530
+ prContext: { totalFiles: 1 },
531
+ })
532
+ );
533
+ expect(result.success).toBe(true);
534
+ });
535
+ });
536
+
537
+ // ==========================================================================
538
+ // Feedback Filtering
539
+ // ==========================================================================
540
+
541
+ describe('feedback filtering', () => {
542
+ it('should load feedback data when trackFeedback is enabled', async () => {
543
+ const { loadFeedbackData } = await import('./feedback-loader.js'); // eslint-disable-line no-restricted-syntax
544
+ loadFeedbackData.mockResolvedValue({ issues: [] });
545
+ setupSuccessfulLLMResponse();
546
+ const result = await runAnalysis('/test/file.js', {
547
+ trackFeedback: true,
548
+ feedbackPath: '/test/feedback.json',
549
+ });
550
+ expect(result.success).toBe(true);
551
+ expect(loadFeedbackData).toHaveBeenCalledWith('/test/feedback.json', expect.any(Object));
552
+ });
553
+
554
+ it('should filter issues based on feedback similarity', async () => {
555
+ const { loadFeedbackData, shouldSkipSimilarIssue } = await import('./feedback-loader.js'); // eslint-disable-line no-restricted-syntax
556
+ loadFeedbackData.mockResolvedValue({ issues: [{ description: 'Already fixed' }] });
557
+ shouldSkipSimilarIssue.mockReturnValue(true);
558
+ llm.sendPromptToClaude.mockResolvedValue({
559
+ json: { summary: 'Review', issues: [{ severity: 'high', description: 'Similar to dismissed' }] },
560
+ });
561
+ const result = await runAnalysis('/test/file.js', { trackFeedback: true, feedbackPath: '/test/feedback.json' });
562
+ expect(result.success).toBe(true);
563
+ expect(shouldSkipSimilarIssue).toHaveBeenCalled();
564
+ });
565
+
566
+ it('should include feedback filtering metadata in results', async () => {
567
+ const { loadFeedbackData, shouldSkipSimilarIssue } = await import('./feedback-loader.js'); // eslint-disable-line no-restricted-syntax
568
+ loadFeedbackData.mockResolvedValue({ issues: [{ description: 'Dismissed' }] });
569
+ shouldSkipSimilarIssue.mockImplementation((desc) => desc.includes('Skip'));
570
+ llm.sendPromptToClaude.mockResolvedValue({
571
+ json: {
572
+ summary: 'Review',
573
+ issues: [
574
+ { severity: 'high', description: 'Keep this' },
575
+ { severity: 'high', description: 'Skip this' },
576
+ ],
577
+ },
578
+ });
579
+ const result = await runAnalysis('/test/file.js', { trackFeedback: true, feedbackPath: '/test/feedback.json' });
580
+ expect(result.success).toBe(true);
581
+ expect(result.metadata.feedbackFiltering).toBeDefined();
582
+ });
583
+
584
+ it('should include dismissed patterns when feedback has patterns', async () => {
585
+ const { loadFeedbackData, extractDismissedPatterns } = await import('./feedback-loader.js'); // eslint-disable-line no-restricted-syntax
586
+ loadFeedbackData.mockResolvedValue({ issues: [{ description: 'Old issue', dismissed: true }] });
587
+ extractDismissedPatterns.mockReturnValue([
588
+ { issue: 'Import order', reason: 'false positive', sentiment: 'negative' },
589
+ { issue: 'Formatting', reason: 'handled by linter', sentiment: 'neutral' },
590
+ ]);
591
+ setupSuccessfulLLMResponse();
592
+ const result = await runAnalysis('/test/file.js', { trackFeedback: true, feedbackPath: '/test/feedback.json' });
593
+ expect(result.success).toBe(true);
594
+ });
595
+ });
596
+
597
+ // ==========================================================================
598
+ // Project Summary Formatting
599
+ // ==========================================================================
600
+
601
+ describe('project summary formatting', () => {
602
+ beforeEach(() => setupSuccessfulLLMResponse());
603
+
604
+ it('should format project summary with all fields', async () => {
605
+ mockEmbeddingsSystem.getProjectSummary.mockResolvedValue({
606
+ projectName: 'Test Project',
607
+ projectType: 'web-app',
608
+ technologies: ['JavaScript', 'React', 'Node.js', 'Express'],
609
+ mainFrameworks: ['React', 'Express'],
610
+ customImplementations: [
611
+ { name: 'CustomHook', description: 'A custom React hook', properties: ['useState', 'useEffect'] },
612
+ { name: 'ApiWrapper', description: 'API wrapper utility' },
613
+ ],
614
+ apiPatterns: [{ type: 'REST', description: 'RESTful API design' }],
615
+ stateManagement: { approach: 'Redux', patterns: ['Slice pattern', 'Thunks'] },
616
+ reviewGuidelines: ['Use TypeScript', 'Write tests', 'Follow ESLint rules'],
617
+ });
618
+ const result = await runAnalysis('/test/file.js');
619
+ expect(result.success).toBe(true);
620
+ expect(mockEmbeddingsSystem.getProjectSummary).toHaveBeenCalled();
621
+ });
622
+
623
+ it('should handle project summary with many technologies', async () => {
624
+ mockEmbeddingsSystem.getProjectSummary.mockResolvedValue({
625
+ projectName: 'Large Project',
626
+ technologies: ['JS', 'TS', 'React', 'Vue', 'Angular', 'Node', 'Express', 'Fastify', 'MongoDB', 'PostgreSQL'],
627
+ });
628
+ const result = await runAnalysis('/test/file.js');
629
+ expect(result.success).toBe(true);
630
+ });
631
+
632
+ it('should handle empty project summary gracefully', async () => {
633
+ mockEmbeddingsSystem.getProjectSummary.mockResolvedValue({});
634
+ const result = await runAnalysis('/test/file.js');
635
+ expect(result.success).toBe(true);
636
+ });
637
+
638
+ it('should handle getProjectSummary errors gracefully', async () => {
639
+ mockEmbeddingsSystem.getProjectSummary.mockRejectedValue(new Error('DB connection failed'));
640
+ const result = await runAnalysis('/test/file.js');
641
+ expect(result.success).toBe(true);
642
+ });
643
+ });
644
+
645
+ // ==========================================================================
646
+ // PR Comment Context
647
+ // ==========================================================================
648
+
649
+ describe('PR comment context', () => {
650
+ beforeEach(() => setupSuccessfulLLMResponse());
651
+
652
+ it('should format PR comments for context', async () => {
653
+ findRelevantPRComments.mockResolvedValue([
654
+ createMockPRComment({ author: 'reviewer1', body: 'This needs improvement', pr_title: 'Feature PR' }),
655
+ createMockPRComment({ author_login: 'reviewer2', comment_text: 'Consider refactoring', comment_type: 'inline' }),
656
+ ]);
657
+ const result = await runAnalysis('/test/file.js');
658
+ expect(result.success).toBe(true);
659
+ expect(result.prHistory).toBeDefined();
660
+ expect(result.prHistory.commentsFound).toBe(2);
661
+ });
662
+
663
+ it('should extract patterns from PR comments', async () => {
664
+ findRelevantPRComments.mockResolvedValue([
665
+ createMockPRComment({ body: 'This is a performance issue and could cause problems' }),
666
+ createMockPRComment({ body: 'Consider improving the security of this implementation' }),
667
+ ]);
668
+ const result = await runAnalysis('/test/file.js');
669
+ expect(result.success).toBe(true);
670
+ expect(result.prHistory.patterns).toBeDefined();
671
+ });
672
+
673
+ it('should handle PR comment search failure gracefully', async () => {
674
+ findRelevantPRComments.mockRejectedValue(new Error('Search failed'));
675
+ const result = await runAnalysis('/test/file.js');
676
+ expect(result.success).toBe(true);
677
+ });
678
+
679
+ it('should identify recent comments in summary', async () => {
680
+ const recentDate = new Date();
681
+ recentDate.setDate(recentDate.getDate() - 5);
682
+ findRelevantPRComments.mockResolvedValue([
683
+ createMockPRComment({ body: 'Recent comment about performance', created_at: recentDate.toISOString() }),
684
+ ]);
685
+ const result = await runAnalysis('/test/file.js');
686
+ expect(result.success).toBe(true);
687
+ expect(result.prHistory).toBeDefined();
688
+ });
689
+ });
690
+
691
+ // ==========================================================================
692
+ // Custom Documents
693
+ // ==========================================================================
694
+
695
+ describe('custom documents', () => {
696
+ beforeEach(() => setupSuccessfulLLMResponse());
697
+
698
+ it('should find relevant custom document chunks', async () => {
699
+ mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([
700
+ { content: 'Coding guidelines', document_title: 'Style Guide' },
701
+ { content: 'Testing best practices', document_title: 'Test Guide' },
702
+ ]);
703
+ const result = await runAnalysis('/test/file.js');
704
+ expect(result.success).toBe(true);
705
+ });
706
+
707
+ it('should process custom documents in memory', async () => {
708
+ mockEmbeddingsSystem.processCustomDocumentsInMemory.mockResolvedValue([{ content: 'In-memory doc', document_title: 'Temp Guide' }]);
709
+ const result = await runAnalysis('/test/file.js', { customDocuments: ['path/to/doc.md'] });
710
+ expect(result.success).toBe(true);
711
+ });
712
+
713
+ it('should process custom documents when not preprocessed', async () => {
714
+ mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([]);
715
+ mockEmbeddingsSystem.processCustomDocumentsInMemory.mockResolvedValue([
716
+ { id: 'c1', content: 'New processed chunk', document_title: 'New Doc', chunk_index: 0 },
717
+ ]);
718
+ mockEmbeddingsSystem.findRelevantCustomDocChunks.mockResolvedValue([
719
+ { id: 'c1', content: 'New processed chunk', document_title: 'New Doc', chunk_index: 0, similarity: 0.8 },
720
+ ]);
721
+ const result = await runAnalysis('/test/file.js', { customDocs: ['/docs/new-guide.md'] });
722
+ expect(result.success).toBe(true);
723
+ });
724
+
725
+ it('should log selected chunks when verbose', async () => {
726
+ mockEmbeddingsSystem.findRelevantCustomDocChunks.mockResolvedValue([
727
+ { id: 'chunk1', content: 'Guidelines', document_title: 'Guide', chunk_index: 0, similarity: 0.85 },
728
+ ]);
729
+ const result = await runAnalysis('/test/file.js', { customDocs: ['/docs/guide.md'], verbose: true });
730
+ expect(result.success).toBe(true);
731
+ });
732
+ });
733
+
734
+ // ==========================================================================
735
+ // Context Retrieval Edge Cases
736
+ // ==========================================================================
737
+
738
+ describe('context retrieval edge cases', () => {
739
+ beforeEach(() => setupSuccessfulLLMResponse());
740
+
741
+ it('should handle file with documentation chunks', async () => {
742
+ mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([
743
+ {
744
+ path: '/docs/api.md',
745
+ content: 'API Documentation',
746
+ similarity: 0.9,
747
+ type: 'documentation-chunk',
748
+ document_title: 'API Docs',
749
+ heading_text: 'Authentication',
750
+ },
751
+ ]);
752
+ const result = await runAnalysis('/test/file.js');
753
+ expect(result.success).toBe(true);
754
+ });
755
+
756
+ it('should deduplicate code examples by path', async () => {
757
+ mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([
758
+ { path: '/util.js', content: 'code1', similarity: 0.9 },
759
+ { path: '/util.js', content: 'code2', similarity: 0.85 },
760
+ { path: '/helper.js', content: 'code3', similarity: 0.8 },
761
+ ]);
762
+ const result = await runAnalysis('/test/file.js');
763
+ expect(result.success).toBe(true);
764
+ });
765
+
766
+ it('should include guidelines with heading text', async () => {
767
+ mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([
768
+ {
769
+ path: '/docs/api.md',
770
+ content: 'Authentication should use JWT tokens',
771
+ similarity: 0.9,
772
+ type: 'documentation-chunk',
773
+ document_title: 'API Reference',
774
+ heading_text: 'Security Best Practices',
775
+ chunk_index: 0,
776
+ },
777
+ ]);
778
+ const result = await runAnalysis('/test/auth.js');
779
+ expect(result.success).toBe(true);
780
+ expect(result.context.guidelines).toBeGreaterThanOrEqual(0);
781
+ });
782
+ });
783
+
784
+ // ==========================================================================
785
+ // Verbose Logging
786
+ // ==========================================================================
787
+
788
+ describe('verbose logging paths', () => {
789
+ it('should log context information when verbose', async () => {
790
+ mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([{ path: '/example.js', content: 'code', similarity: 0.9 }]);
791
+ mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([
792
+ { path: '/docs/api.md', content: 'docs', similarity: 0.8, type: 'documentation-chunk', document_title: 'API' },
793
+ ]);
794
+ findRelevantPRComments.mockResolvedValue([createMockPRComment()]);
795
+ setupSuccessfulLLMResponse();
796
+ const result = await runAnalysis('/test/file.js', { verbose: true });
797
+ expect(result.success).toBe(true);
798
+ expect(console.log).toHaveBeenCalled();
799
+ });
800
+ });
801
+
802
+ // ==========================================================================
803
+ // gatherUnifiedContextForPR
804
+ // ==========================================================================
805
+
806
+ describe('gatherUnifiedContextForPR', () => {
807
+ it('should gather context for PR files', async () => {
808
+ const prFiles = [
809
+ { filePath: '/src/file1.js', content: 'code1', language: 'javascript' },
810
+ { filePath: '/src/file2.js', content: 'code2', language: 'javascript' },
811
+ ];
812
+ const context = await gatherUnifiedContextForPR(prFiles);
813
+ expect(context).toHaveProperty('codeExamples');
814
+ expect(context).toHaveProperty('guidelines');
815
+ expect(context).toHaveProperty('prComments');
816
+ expect(context).toHaveProperty('customDocChunks');
817
+ });
818
+
819
+ it('should query for relevant PR comments', async () => {
820
+ const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
821
+ await gatherUnifiedContextForPR(prFiles);
822
+ expect(findRelevantPRComments).toHaveBeenCalled();
823
+ });
824
+
825
+ it('should handle empty PR files array', async () => {
826
+ const context = await gatherUnifiedContextForPR([]);
827
+ expect(context.codeExamples).toEqual([]);
828
+ expect(context.guidelines).toEqual([]);
829
+ });
830
+
831
+ it('should deduplicate context across files', async () => {
832
+ mockEmbeddingsSystem.contentRetriever.findSimilarCode.mockResolvedValue({
833
+ relevantFiles: [{ path: '/common/util.js', content: 'shared code' }],
834
+ relevantChunks: [],
835
+ });
836
+ const prFiles = [
837
+ { filePath: '/src/file1.js', content: 'code1', language: 'javascript' },
838
+ { filePath: '/src/file2.js', content: 'code2', language: 'javascript' },
839
+ ];
840
+ const context = await gatherUnifiedContextForPR(prFiles);
841
+ expect(Array.isArray(context.codeExamples)).toBe(true);
842
+ });
843
+
844
+ it('should handle options parameter', async () => {
845
+ const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
846
+ const context = await gatherUnifiedContextForPR(prFiles, { verbose: true, projectPath: '/project' });
847
+ expect(context).toHaveProperty('codeExamples');
848
+ });
849
+
850
+ it('should handle PR files with no content', async () => {
851
+ const prFiles = [{ filePath: '/src/empty.js', content: '', language: 'javascript' }];
852
+ const context = await gatherUnifiedContextForPR(prFiles);
853
+ expect(context).toHaveProperty('codeExamples');
854
+ });
855
+
856
+ it('should find custom document chunks', async () => {
857
+ mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([{ content: 'Custom doc', document_title: 'Guidelines' }]);
858
+ const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
859
+ const context = await gatherUnifiedContextForPR(prFiles);
860
+ expect(context).toHaveProperty('customDocChunks');
861
+ });
862
+
863
+ it('should use preprocessed custom doc chunks when available', async () => {
864
+ const preprocessedChunks = [{ id: 'chunk1', content: 'Style guidelines', document_title: 'Style', chunk_index: 0, similarity: 0.9 }];
865
+ mockEmbeddingsSystem.findRelevantCustomDocChunks.mockResolvedValue(preprocessedChunks);
866
+ const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
867
+ const context = await gatherUnifiedContextForPR(prFiles, { preprocessedCustomDocChunks: preprocessedChunks });
868
+ expect(context).toBeDefined();
869
+ });
870
+ });
871
+
872
+ // ==========================================================================
873
+ // gatherUnifiedContextForPR Error Handling
874
+ // ==========================================================================
875
+
876
+ describe('gatherUnifiedContextForPR error handling', () => {
877
+ it('should handle file context gathering errors', async () => {
878
+ fs.readFileSync.mockImplementation((path) => {
879
+ if (path.includes('error-file')) throw new Error('Read error');
880
+ return 'const x = 1;';
881
+ });
882
+ const prFiles = [
883
+ { filePath: '/src/good-file.js', content: 'code1', language: 'javascript' },
884
+ { filePath: '/src/error-file.js', content: 'code2', language: 'javascript' },
885
+ ];
886
+ const context = await gatherUnifiedContextForPR(prFiles);
887
+ expect(context).toBeDefined();
888
+ expect(context.codeExamples).toBeDefined();
889
+ });
890
+
891
+ it('should aggregate context from multiple files', async () => {
892
+ mockEmbeddingsSystem.findSimilarCode
893
+ .mockResolvedValueOnce([{ path: '/util1.js', content: 'code1', similarity: 0.9 }])
894
+ .mockResolvedValueOnce([{ path: '/util2.js', content: 'code2', similarity: 0.85 }]);
895
+ const prFiles = [
896
+ { filePath: '/src/file1.js', content: 'code1', language: 'javascript' },
897
+ { filePath: '/src/file2.js', content: 'code2', language: 'javascript' },
898
+ ];
899
+ const context = await gatherUnifiedContextForPR(prFiles);
900
+ expect(context.codeExamples.length).toBeGreaterThanOrEqual(0);
901
+ });
902
+
903
+ it('should limit aggregated results', async () => {
904
+ const manyExamples = Array.from({ length: 50 }, (_, i) => ({
905
+ path: `/util${i}.js`,
906
+ content: `code${i}`,
907
+ similarity: 0.9 - i * 0.01,
908
+ }));
909
+ mockEmbeddingsSystem.findSimilarCode.mockResolvedValue(manyExamples);
910
+ const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
911
+ const context = await gatherUnifiedContextForPR(prFiles, { maxExamples: 10 });
912
+ expect(context.codeExamples.length).toBeLessThanOrEqual(40);
913
+ });
914
+
915
+ it('should handle custom document processing errors', async () => {
916
+ mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockRejectedValue(new Error('DB error'));
917
+ const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
918
+ const context = await gatherUnifiedContextForPR(prFiles, { customDocs: ['/docs/style-guide.md'] });
919
+ expect(context).toBeDefined();
920
+ });
921
+
922
+ it('should reuse existing custom document chunks', async () => {
923
+ mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([
924
+ { id: 'existing1', content: 'Existing doc', document_title: 'Existing', chunk_index: 0 },
925
+ ]);
926
+ const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
927
+ const context = await gatherUnifiedContextForPR(prFiles, {
928
+ customDocs: ['/docs/style-guide.md'],
929
+ projectPath: '/project',
930
+ });
931
+ expect(context).toBeDefined();
932
+ expect(mockEmbeddingsSystem.processCustomDocumentsInMemory).not.toHaveBeenCalled();
933
+ });
934
+
935
+ it('should process custom documents for PR analysis', async () => {
936
+ mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([]);
937
+ mockEmbeddingsSystem.processCustomDocumentsInMemory.mockResolvedValue([
938
+ { id: 'chunk1', content: 'Coding standards', document_title: 'Style Guide', chunk_index: 0 },
939
+ ]);
940
+ const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
941
+ const context = await gatherUnifiedContextForPR(prFiles, {
942
+ customDocs: ['/docs/style-guide.md'],
943
+ projectPath: '/project',
944
+ });
945
+ expect(context.customDocChunks).toBeDefined();
946
+ });
947
+ });
948
+
949
+ // ==========================================================================
950
+ // Context Deduplication
951
+ // ==========================================================================
952
+
953
+ describe('context deduplication', () => {
954
+ it('should deduplicate guidelines by path and heading', async () => {
955
+ mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([
956
+ {
957
+ path: '/docs/api.md',
958
+ content: 'API docs',
959
+ similarity: 0.9,
960
+ type: 'documentation-chunk',
961
+ document_title: 'API',
962
+ heading_text: 'Overview',
963
+ },
964
+ ]);
965
+ const prFiles = [
966
+ { filePath: '/src/file1.js', content: 'code1' },
967
+ { filePath: '/src/file2.js', content: 'code2' },
968
+ ];
969
+ const context = await gatherUnifiedContextForPR(prFiles);
970
+ expect(context.guidelines).toBeDefined();
971
+ });
972
+
973
+ it('should keep higher similarity when deduplicating', async () => {
974
+ mockEmbeddingsSystem.findSimilarCode
975
+ .mockResolvedValueOnce([{ path: '/util.js', content: 'code', similarity: 0.7 }])
976
+ .mockResolvedValueOnce([{ path: '/util.js', content: 'code', similarity: 0.9 }]);
977
+ const prFiles = [
978
+ { filePath: '/src/file1.js', content: 'code1' },
979
+ { filePath: '/src/file2.js', content: 'code2' },
980
+ ];
981
+ const context = await gatherUnifiedContextForPR(prFiles);
982
+ if (context.codeExamples.length > 0) {
983
+ expect(context.codeExamples[0].similarity).toBeGreaterThanOrEqual(0.7);
984
+ }
985
+ });
986
+ });
987
+
988
+ // ==========================================================================
989
+ // Comprehensive Context Gathering
990
+ // ==========================================================================
991
+
992
+ describe('comprehensive context gathering', () => {
993
+ it('should gather all context types for comprehensive review', async () => {
994
+ mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([
995
+ { path: '/util.js', content: 'export function helper() {}', similarity: 0.92, language: 'javascript' },
996
+ { path: '/helper.js', content: 'export function format() {}', similarity: 0.88, language: 'javascript' },
997
+ ]);
998
+ mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([
999
+ {
1000
+ path: '/docs/conventions.md',
1001
+ content: 'Always use TypeScript',
1002
+ similarity: 0.85,
1003
+ type: 'documentation-chunk',
1004
+ document_title: 'Conventions',
1005
+ heading_text: 'Type Safety',
1006
+ },
1007
+ ]);
1008
+ findRelevantPRComments.mockResolvedValue([
1009
+ createMockPRComment({ author: 'tech-lead', body: 'Consider using the shared utility', pr_number: 100 }),
1010
+ ]);
1011
+ mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([
1012
+ { id: 'custom-1', content: 'Internal guidelines', document_title: 'Internal', chunk_index: 0 },
1013
+ ]);
1014
+ mockEmbeddingsSystem.findRelevantCustomDocChunks.mockResolvedValue([
1015
+ { id: 'custom-1', content: 'Internal guidelines', document_title: 'Internal', chunk_index: 0, similarity: 0.8 },
1016
+ ]);
1017
+ mockEmbeddingsSystem.getProjectSummary.mockResolvedValue({
1018
+ projectName: 'MyApp',
1019
+ technologies: ['React', 'TypeScript'],
1020
+ mainFrameworks: ['Next.js'],
1021
+ });
1022
+ llm.sendPromptToClaude.mockResolvedValue({
1023
+ json: {
1024
+ summary: 'Comprehensive review with all context',
1025
+ issues: [{ severity: 'medium', description: 'Consider using shared helper', lineNumbers: [10, 15] }],
1026
+ },
1027
+ });
1028
+ const result = await runAnalysis('/test/feature.js', { verbose: true, customDocs: ['/docs/internal.md'] });
1029
+ expect(result.success).toBe(true);
1030
+ expect(result.context.codeExamples).toBeGreaterThanOrEqual(0);
1031
+ expect(result.context.guidelines).toBeGreaterThanOrEqual(0);
1032
+ expect(result.similarExamples).toBeDefined();
1033
+ });
1034
+
1035
+ it('should gather unified context with all data types', async () => {
1036
+ mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([{ path: '/shared/util.js', content: 'shared', similarity: 0.95 }]);
1037
+ mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([
1038
+ { path: '/docs/style.md', content: 'Style guide', similarity: 0.85, type: 'documentation-chunk', document_title: 'Style' },
1039
+ ]);
1040
+ findRelevantPRComments.mockResolvedValue([createMockPRComment({ relevanceScore: 0.8 })]);
1041
+ mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([
1042
+ { id: 'cd1', content: 'Custom', document_title: 'Custom Doc', chunk_index: 0 },
1043
+ ]);
1044
+ mockEmbeddingsSystem.findRelevantCustomDocChunks.mockResolvedValue([
1045
+ { id: 'cd1', content: 'Custom', document_title: 'Custom Doc', chunk_index: 0, similarity: 0.75 },
1046
+ ]);
1047
+ const prFiles = [
1048
+ { filePath: '/src/new-feature.js', content: 'new code', diffContent: '+ new code', language: 'javascript' },
1049
+ { filePath: '/src/updated.js', content: 'updated', diffContent: '+ updated', language: 'javascript' },
1050
+ ];
1051
+ const context = await gatherUnifiedContextForPR(prFiles, { customDocs: ['/docs/custom.md'] });
1052
+ expect(context.codeExamples).toBeDefined();
1053
+ expect(context.guidelines).toBeDefined();
1054
+ expect(context.prComments).toBeDefined();
1055
+ expect(context.customDocChunks).toBeDefined();
1056
+ });
1057
+ });
1058
+
1059
+ // ==========================================================================
1060
+ // PR History with Metadata
1061
+ // ==========================================================================
1062
+
1063
+ describe('PR history with metadata', () => {
1064
+ it('should include PR history in results when comments found', async () => {
1065
+ findRelevantPRComments.mockResolvedValue([
1066
+ createMockPRComment({
1067
+ author: 'senior-dev',
1068
+ body: 'This pattern should use memoization for performance',
1069
+ pr_number: 456,
1070
+ pr_title: 'Performance improvements',
1071
+ similarity_score: 0.9,
1072
+ }),
1073
+ ]);
1074
+ setupSuccessfulLLMResponse();
1075
+ const result = await runAnalysis('/test/file.js');
1076
+ expect(result.success).toBe(true);
1077
+ expect(result.prHistory).not.toBeNull();
1078
+ expect(result.prHistory.commentsFound).toBe(1);
1079
+ expect(result.prHistory.patterns).toBeDefined();
1080
+ expect(result.prHistory.summary).toBeDefined();
1081
+ });
1082
+ });
1083
+
1084
+ // ==========================================================================
1085
+ // Handle Code with Rich Context
1086
+ // ==========================================================================
1087
+
1088
+ describe('rich code context', () => {
1089
+ it('should handle code with rich context', async () => {
1090
+ const richCode = `
1091
+ import React from 'react';
1092
+ import { useState, useEffect } from 'react';
1093
+ import axios from 'axios';
1094
+
1095
+ export function Dashboard() {
1096
+ const [data, setData] = useState(null);
1097
+ useEffect(() => {
1098
+ axios.get('/api/data').then(res => setData(res.data));
1099
+ }, []);
1100
+ return <div>{data}</div>;
1101
+ }
1102
+ `;
1103
+ fs.readFileSync.mockReturnValue(richCode);
1104
+ setupSuccessfulLLMResponse();
1105
+ const result = await runAnalysis('/test/Dashboard.jsx');
1106
+ expect(result.success).toBe(true);
1107
+ });
1108
+ });
1109
+ });