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.
- package/README.md +82 -114
- package/package.json +10 -9
- package/src/content-retrieval.test.js +775 -0
- package/src/custom-documents.test.js +440 -0
- package/src/feedback-loader.test.js +529 -0
- package/src/llm.test.js +256 -0
- package/src/project-analyzer.test.js +747 -0
- package/src/rag-analyzer.js +12 -0
- package/src/rag-analyzer.test.js +1109 -0
- package/src/rag-review.test.js +317 -0
- package/src/setupTests.js +131 -0
- package/src/zero-shot-classifier-open.test.js +278 -0
- package/src/embeddings/cache-manager.js +0 -364
- package/src/embeddings/constants.js +0 -40
- package/src/embeddings/database.js +0 -921
- package/src/embeddings/errors.js +0 -208
- package/src/embeddings/factory.js +0 -447
- package/src/embeddings/file-processor.js +0 -851
- package/src/embeddings/model-manager.js +0 -337
- package/src/embeddings/similarity-calculator.js +0 -97
- package/src/embeddings/types.js +0 -113
- package/src/pr-history/analyzer.js +0 -579
- package/src/pr-history/bot-detector.js +0 -123
- package/src/pr-history/cli-utils.js +0 -204
- package/src/pr-history/comment-processor.js +0 -549
- package/src/pr-history/database.js +0 -819
- package/src/pr-history/github-client.js +0 -629
- package/src/technology-keywords.json +0 -753
- package/src/utils/command.js +0 -48
- package/src/utils/constants.js +0 -263
- package/src/utils/context-inference.js +0 -364
- package/src/utils/document-detection.js +0 -105
- package/src/utils/file-validation.js +0 -271
- package/src/utils/git.js +0 -232
- package/src/utils/language-detection.js +0 -170
- package/src/utils/logging.js +0 -24
- package/src/utils/markdown.js +0 -132
- package/src/utils/mobilebert-tokenizer.js +0 -141
- package/src/utils/pr-chunking.js +0 -276
- 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
|
+
});
|