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,317 @@
1
+ import { runAnalysis, gatherUnifiedContextForPR } from './rag-analyzer.js';
2
+ import { reviewFile, reviewFiles, reviewPullRequest } from './rag-review.js';
3
+ import { shouldProcessFile } from './utils/file-validation.js';
4
+ import { getChangedLinesInfo, getFileContentFromGit } from './utils/git.js';
5
+ import { shouldChunkPR, chunkPRFiles, combineChunkResults } from './utils/pr-chunking.js';
6
+
7
+ vi.mock('./rag-analyzer.js', () => ({
8
+ runAnalysis: vi.fn(),
9
+ gatherUnifiedContextForPR: vi.fn().mockResolvedValue({
10
+ codeExamples: [],
11
+ guidelines: [],
12
+ prComments: [],
13
+ customDocChunks: [],
14
+ }),
15
+ }));
16
+
17
+ vi.mock('./utils/file-validation.js', () => ({
18
+ shouldProcessFile: vi.fn().mockReturnValue(true),
19
+ }));
20
+
21
+ vi.mock('./utils/git.js', () => ({
22
+ findBaseBranch: vi.fn().mockReturnValue('main'),
23
+ getChangedLinesInfo: vi.fn().mockReturnValue({
24
+ hasChanges: true,
25
+ addedLines: [1, 2, 3],
26
+ removedLines: [4],
27
+ fullDiff: '+ new code\n- old code',
28
+ }),
29
+ getFileContentFromGit: vi.fn().mockReturnValue('const x = 1;'),
30
+ }));
31
+
32
+ vi.mock('./utils/language-detection.js', () => ({
33
+ detectFileType: vi.fn().mockReturnValue({ isTest: false }),
34
+ detectLanguageFromExtension: vi.fn().mockReturnValue('javascript'),
35
+ }));
36
+
37
+ vi.mock('./utils/pr-chunking.js', () => ({
38
+ shouldChunkPR: vi.fn().mockReturnValue({ shouldChunk: false, estimatedTokens: 1000 }),
39
+ chunkPRFiles: vi.fn(),
40
+ combineChunkResults: vi.fn(),
41
+ }));
42
+
43
+ describe('rag-review', () => {
44
+ beforeEach(() => {
45
+ mockConsole();
46
+ });
47
+
48
+ afterEach(() => {
49
+ vi.restoreAllMocks();
50
+ });
51
+
52
+ describe('reviewFile', () => {
53
+ it('should review a file successfully', async () => {
54
+ runAnalysis.mockResolvedValue({
55
+ success: true,
56
+ filePath: '/test/file.js',
57
+ language: 'javascript',
58
+ results: { issues: [] },
59
+ });
60
+
61
+ const result = await reviewFile('/test/file.js');
62
+
63
+ expect(result.success).toBe(true);
64
+ expect(runAnalysis).toHaveBeenCalledWith('/test/file.js', {});
65
+ });
66
+
67
+ it('should convert object results to array format', async () => {
68
+ runAnalysis.mockResolvedValue({
69
+ success: true,
70
+ filePath: '/test/file.js',
71
+ language: 'javascript',
72
+ results: { issues: [{ message: 'test' }] },
73
+ });
74
+
75
+ const result = await reviewFile('/test/file.js');
76
+
77
+ expect(result.success).toBe(true);
78
+ expect(Array.isArray(result.results)).toBe(true);
79
+ });
80
+
81
+ it('should handle analysis errors', async () => {
82
+ runAnalysis.mockRejectedValue(new Error('Analysis failed'));
83
+
84
+ const result = await reviewFile('/test/file.js');
85
+
86
+ expect(result.success).toBe(false);
87
+ expect(result.error).toBe('Analysis failed');
88
+ });
89
+
90
+ it('should pass options to runAnalysis', async () => {
91
+ runAnalysis.mockResolvedValue({ success: true, results: [] });
92
+
93
+ await reviewFile('/test/file.js', { verbose: true, maxExamples: 10 });
94
+
95
+ expect(runAnalysis).toHaveBeenCalledWith('/test/file.js', { verbose: true, maxExamples: 10 });
96
+ });
97
+ });
98
+
99
+ describe('reviewFiles', () => {
100
+ it('should review multiple files', async () => {
101
+ runAnalysis.mockResolvedValue({ success: true, results: [] });
102
+
103
+ const result = await reviewFiles(['/test/file1.js', '/test/file2.js']);
104
+
105
+ expect(result.success).toBe(true);
106
+ expect(result.results.length).toBe(2);
107
+ });
108
+
109
+ it('should process files in batches based on concurrency', async () => {
110
+ runAnalysis.mockResolvedValue({ success: true, results: [] });
111
+
112
+ await reviewFiles(['/file1.js', '/file2.js', '/file3.js', '/file4.js', '/file5.js'], { concurrency: 2 });
113
+
114
+ expect(runAnalysis).toHaveBeenCalledTimes(5);
115
+ });
116
+
117
+ it('should count successes, skips, and errors', async () => {
118
+ runAnalysis
119
+ .mockResolvedValueOnce({ success: true, results: [] })
120
+ .mockResolvedValueOnce({ success: true, skipped: true, results: [] })
121
+ .mockResolvedValueOnce({ success: false, error: 'Error' });
122
+
123
+ const result = await reviewFiles(['/file1.js', '/file2.js', '/file3.js']);
124
+
125
+ expect(result.message).toContain('Success: 1');
126
+ expect(result.message).toContain('Skipped: 1');
127
+ expect(result.message).toContain('Errors: 1');
128
+ });
129
+
130
+ it('should handle overall errors gracefully', async () => {
131
+ runAnalysis.mockRejectedValue(new Error('Fatal error'));
132
+
133
+ const result = await reviewFiles(['/file.js']);
134
+
135
+ // Individual file errors are collected in results, not in a top-level error field
136
+ expect(result.success).toBe(false);
137
+ expect(result.results.length).toBe(1);
138
+ expect(result.results[0].success).toBe(false);
139
+ expect(result.results[0].error).toBe('Fatal error');
140
+ });
141
+ });
142
+
143
+ describe('reviewPullRequest', () => {
144
+ it('should review PR files', async () => {
145
+ runAnalysis.mockResolvedValue({
146
+ success: true,
147
+ results: { issues: [], crossFileIssues: [], summary: 'OK' },
148
+ });
149
+
150
+ const result = await reviewPullRequest(['/src/file.js']);
151
+
152
+ expect(result.success).toBe(true);
153
+ });
154
+
155
+ it('should return empty results when no processable files', async () => {
156
+ const result = await reviewPullRequest([]);
157
+
158
+ expect(result.success).toBe(true);
159
+ expect(result.results).toEqual([]);
160
+ });
161
+
162
+ it('should skip files based on exclusion rules', async () => {
163
+ shouldProcessFile.mockReturnValue(false);
164
+
165
+ const result = await reviewPullRequest(['/file.js'], { verbose: true });
166
+
167
+ expect(result.success).toBe(true);
168
+ });
169
+
170
+ it('should skip files with no changes', async () => {
171
+ getChangedLinesInfo.mockReturnValue({ hasChanges: false });
172
+
173
+ const result = await reviewPullRequest(['/file.js'], { verbose: true });
174
+
175
+ expect(result.success).toBe(true);
176
+ });
177
+
178
+ it('should use chunked processing for large PRs', async () => {
179
+ // Ensure file processing succeeds first (files must pass filters to reach chunking logic)
180
+ shouldProcessFile.mockReturnValue(true);
181
+ getChangedLinesInfo.mockReturnValue({
182
+ hasChanges: true,
183
+ addedLines: [1, 2, 3],
184
+ removedLines: [],
185
+ fullDiff: '+ new code',
186
+ });
187
+
188
+ // Setup: shouldChunkPR returns true to trigger chunked processing
189
+ shouldChunkPR.mockReturnValue({ shouldChunk: true, estimatedTokens: 100000, recommendedChunks: 2 });
190
+
191
+ // Setup chunks that will be processed
192
+ chunkPRFiles.mockReturnValue([
193
+ { files: [{ filePath: '/file1.js' }], totalTokens: 30000 },
194
+ { files: [{ filePath: '/file2.js' }], totalTokens: 30000 },
195
+ ]);
196
+
197
+ // Setup combined results
198
+ combineChunkResults.mockReturnValue({
199
+ success: true,
200
+ results: [
201
+ { filePath: '/file1.js', success: true },
202
+ { filePath: '/file2.js', success: true },
203
+ ],
204
+ prContext: { totalFiles: 2 },
205
+ });
206
+
207
+ // Setup analysis for the recursive chunk calls - needs to return valid holistic result
208
+ runAnalysis.mockResolvedValue({
209
+ success: true,
210
+ results: { fileSpecificIssues: {}, crossFileIssues: [], summary: 'OK' },
211
+ });
212
+
213
+ const result = await reviewPullRequest(['/file1.js', '/file2.js'], { verbose: true });
214
+
215
+ // Verify chunking flow was triggered
216
+ expect(shouldChunkPR).toHaveBeenCalled();
217
+ expect(chunkPRFiles).toHaveBeenCalled();
218
+ expect(combineChunkResults).toHaveBeenCalled();
219
+ expect(result.success).toBe(true);
220
+ });
221
+
222
+ it('should gather unified context for PR files', async () => {
223
+ // Setup analysis to return valid holistic result
224
+ runAnalysis.mockResolvedValue({
225
+ success: true,
226
+ results: { fileSpecificIssues: {}, crossFileIssues: [], summary: 'OK' },
227
+ });
228
+
229
+ // Ensure file processing succeeds (these are already mocked at top level)
230
+ shouldProcessFile.mockReturnValue(true);
231
+ getChangedLinesInfo.mockReturnValue({
232
+ hasChanges: true,
233
+ addedLines: [1, 2, 3],
234
+ removedLines: [],
235
+ fullDiff: '+ new code',
236
+ });
237
+
238
+ await reviewPullRequest(['/src/file.js'], { verbose: true });
239
+
240
+ // gatherUnifiedContextForPR is called for regular (non-chunked) PRs
241
+ expect(gatherUnifiedContextForPR).toHaveBeenCalled();
242
+ });
243
+
244
+ it('should handle errors in file processing gracefully', async () => {
245
+ getFileContentFromGit.mockImplementation(() => {
246
+ throw new Error('File not found');
247
+ });
248
+
249
+ const result = await reviewPullRequest(['/missing.js'], { verbose: true });
250
+
251
+ // When file processing fails, the file is skipped, not failing the whole PR review
252
+ expect(result.success).toBe(true);
253
+ });
254
+
255
+ it('should use holisticReview mode when enabled', async () => {
256
+ runAnalysis.mockResolvedValue({
257
+ success: true,
258
+ results: { fileSpecificIssues: {}, crossFileIssues: [], summary: 'Holistic review' },
259
+ });
260
+
261
+ shouldProcessFile.mockReturnValue(true);
262
+ getChangedLinesInfo.mockReturnValue({
263
+ hasChanges: true,
264
+ addedLines: [1],
265
+ removedLines: [],
266
+ fullDiff: '+ code',
267
+ });
268
+
269
+ const result = await reviewPullRequest(['/src/file.js'], { holisticReview: true });
270
+
271
+ expect(result.success).toBe(true);
272
+ });
273
+
274
+ it('should include PR summary in results', async () => {
275
+ runAnalysis.mockResolvedValue({
276
+ success: true,
277
+ results: { fileSpecificIssues: {}, crossFileIssues: [], summary: 'Summary text' },
278
+ });
279
+
280
+ shouldProcessFile.mockReturnValue(true);
281
+ getChangedLinesInfo.mockReturnValue({
282
+ hasChanges: true,
283
+ addedLines: [1],
284
+ removedLines: [],
285
+ fullDiff: '+ code',
286
+ });
287
+
288
+ const result = await reviewPullRequest(['/src/file.js']);
289
+
290
+ expect(result.success).toBe(true);
291
+ });
292
+
293
+ it('should handle multiple files with different statuses', async () => {
294
+ runAnalysis
295
+ .mockResolvedValueOnce({
296
+ success: true,
297
+ results: { issues: [], summary: 'OK' },
298
+ })
299
+ .mockResolvedValueOnce({
300
+ success: true,
301
+ skipped: true,
302
+ });
303
+
304
+ shouldProcessFile.mockReturnValue(true);
305
+ getChangedLinesInfo.mockReturnValue({
306
+ hasChanges: true,
307
+ addedLines: [1],
308
+ removedLines: [],
309
+ fullDiff: '+ code',
310
+ });
311
+
312
+ const result = await reviewPullRequest(['/file1.js', '/file2.js']);
313
+
314
+ expect(result.success).toBe(true);
315
+ });
316
+ });
317
+ });
@@ -0,0 +1,131 @@
1
+ /* eslint-disable vitest/no-commented-out-tests */
2
+ import { setupConsoleSpies, suppressConsole, restoreConsole, CONSOLE_METHODS } from './test-utils/console-suppression.js';
3
+
4
+ /**
5
+ * Global test setup for Vitest
6
+ *
7
+ * Console Suppression:
8
+ * By default, console output from code under test is suppressed to keep test output clean.
9
+ * However, you can enable console output for debugging in several ways:
10
+ *
11
+ * 1. Environment Variable (for all tests):
12
+ * DEBUG_CONSOLE=true npm test
13
+ * or
14
+ * SHOW_CONSOLE=true npm test
15
+ *
16
+ * 2. Per-test helper functions:
17
+ * enableConsole() - Enable console output for the current test
18
+ * disableConsole() - Disable console output again
19
+ *
20
+ * Example:
21
+ * it('debug test', () => {
22
+ * enableConsole();
23
+ * console.log('This will be visible');
24
+ * // ... test code ...
25
+ * });
26
+ *
27
+ * 3. Restore console in specific tests:
28
+ * vi.restoreAllMocks() - Restores all mocks including console
29
+ */
30
+
31
+ // ============================================================================
32
+ // Global Mocks - Available in all test files without explicit vi.mock() calls
33
+ // ============================================================================
34
+
35
+ // Mock chalk - commonly used across all test files for console output
36
+ // This mock makes all chalk methods pass-through (return input unchanged)
37
+ vi.mock('chalk', () => ({
38
+ default: {
39
+ blue: vi.fn((s) => s),
40
+ green: vi.fn((s) => s),
41
+ yellow: vi.fn((s) => s),
42
+ red: vi.fn((s) => s),
43
+ cyan: vi.fn((s) => s),
44
+ gray: vi.fn((s) => s),
45
+ magenta: vi.fn((s) => s),
46
+ white: vi.fn((s) => s),
47
+ bold: vi.fn((s) => s),
48
+ },
49
+ }));
50
+
51
+ // Mock dotenv - prevent loading .env files during tests
52
+ vi.mock('dotenv', () => ({
53
+ default: { config: vi.fn() },
54
+ }));
55
+
56
+ // ============================================================================
57
+ // Console Suppression - Mock console methods to prevent output from code under test
58
+ // Can be disabled via DEBUG_CONSOLE environment variable for debugging
59
+ // ============================================================================
60
+
61
+ // Setup console spies immediately (before any modules are imported)
62
+ // This ensures spies exist for tests that assert on console calls
63
+ // Output suppression depends on enableConsoleOutput flag
64
+ setupConsoleSpies();
65
+
66
+ // ============================================================================
67
+ // Test Lifecycle Hooks
68
+ // ============================================================================
69
+
70
+ // Ensure console spies exist before each test
71
+ // Respects DEBUG_CONSOLE/SHOW_CONSOLE environment variable for output suppression
72
+ beforeEach(() => {
73
+ // Always ensure spies exist (they may have been restored in a previous test)
74
+ setupConsoleSpies();
75
+ });
76
+
77
+ // Clean up after each test
78
+ afterEach(() => {
79
+ vi.clearAllMocks();
80
+ });
81
+
82
+ // ============================================================================
83
+ // Global Helper Functions
84
+ // ============================================================================
85
+
86
+ // Helper to create mock embedding vectors (384 dimensions for BGE-Small)
87
+ globalThis.createMockEmbedding = (dim = 384, value = 0.1) => new Array(dim).fill(value);
88
+
89
+ // Helper to create a mock file stats object
90
+ globalThis.createMockStats = (overrides = {}) => ({
91
+ size: 1000,
92
+ isFile: () => true,
93
+ isDirectory: () => false,
94
+ mtime: new Date(),
95
+ ...overrides,
96
+ });
97
+
98
+ // Helper to mock all console methods - suppresses output during tests
99
+ // Usage: Call in beforeEach() to suppress console output
100
+ // NOTE: Console is already suppressed by default. Use this only if you need
101
+ // to re-suppress after calling enableConsole()
102
+ globalThis.mockConsole = () => {
103
+ suppressConsole();
104
+ };
105
+
106
+ // Helper to restore console output for debugging
107
+ // Usage: enableConsole() in a test to see console.log statements
108
+ // Example:
109
+ // it('debug test', () => {
110
+ // enableConsole();
111
+ // console.log('This will be visible');
112
+ // });
113
+ globalThis.enableConsole = () => {
114
+ restoreConsole();
115
+ };
116
+
117
+ // Helper to disable console output again after enabling it
118
+ // Usage: disableConsole() to re-suppress console output
119
+ globalThis.disableConsole = () => {
120
+ suppressConsole();
121
+ };
122
+
123
+ // Helper to mock only specific console methods
124
+ // Usage: mockConsoleSelective('log', 'error')
125
+ globalThis.mockConsoleSelective = (...methods) => {
126
+ methods.forEach((method) => {
127
+ if (CONSOLE_METHODS.includes(method)) {
128
+ vi.spyOn(console, method).mockImplementation(() => {});
129
+ }
130
+ });
131
+ };