codex-review-mcp 2.3.5 → 2.3.7
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/dist/mcp-server.test.js
CHANGED
@@ -171,6 +171,34 @@ describe('MCP Server Integration Tests', () => {
|
|
171
171
|
}), expect.any(Function) // onProgress callback
|
172
172
|
);
|
173
173
|
});
|
174
|
+
it('should pass workspaceDir parameter for context gathering', async () => {
|
175
|
+
// CRITICAL TEST: Verifies workspaceDir flows from MCP layer to performCodeReview
|
176
|
+
const testWorkspaceDir = '/Users/test/Projects/learn-webapp';
|
177
|
+
await client.callTool({
|
178
|
+
name: 'perform_code_review',
|
179
|
+
arguments: {
|
180
|
+
content: 'console.log("test");',
|
181
|
+
workspaceDir: testWorkspaceDir
|
182
|
+
}
|
183
|
+
});
|
184
|
+
// Verify workspaceDir is passed to performCodeReview
|
185
|
+
expect(performCodeReview).toHaveBeenCalledWith(expect.objectContaining({
|
186
|
+
workspaceDir: testWorkspaceDir
|
187
|
+
}), expect.any(Function));
|
188
|
+
});
|
189
|
+
it('should handle missing workspaceDir (falls back to cwd)', async () => {
|
190
|
+
await client.callTool({
|
191
|
+
name: 'perform_code_review',
|
192
|
+
arguments: {
|
193
|
+
content: 'console.log("test");'
|
194
|
+
// workspaceDir NOT provided
|
195
|
+
}
|
196
|
+
});
|
197
|
+
// workspaceDir should be undefined, causing gatherContext to use process.cwd()
|
198
|
+
expect(performCodeReview).toHaveBeenCalledWith(expect.objectContaining({
|
199
|
+
workspaceDir: undefined
|
200
|
+
}), expect.any(Function));
|
201
|
+
});
|
174
202
|
it('should return markdown content', async () => {
|
175
203
|
const result = await client.callTool({
|
176
204
|
name: 'perform_code_review',
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
2
2
|
import { join } from 'node:path';
|
3
3
|
const CANDIDATE_FILES = [
|
4
|
-
'.cursor/rules/tsx.mdc',
|
5
|
-
'.cursor/rules/project.mdc',
|
6
|
-
|
4
|
+
'.cursor/rules/tsx.mdc', // Primary: React/TypeScript rules for code review
|
5
|
+
'.cursor/rules/project.mdc', // Secondary: Project-specific rules
|
6
|
+
// Note: We only check specific files, not the entire .cursor/rules/ directory
|
7
|
+
// Other files in .cursor/rules/customrules/ are for other agents
|
7
8
|
'CODE_REVIEW.md',
|
8
9
|
'CONTRIBUTING.md',
|
9
10
|
'ARCHITECTURE.md',
|
@@ -24,12 +24,8 @@ describe('gatherContext', () => {
|
|
24
24
|
const mockReadFile = vi.mocked(fs.readFile);
|
25
25
|
const mockReaddir = vi.mocked(fs.readdir);
|
26
26
|
const mockRealpath = vi.mocked(fs.realpath);
|
27
|
-
//
|
28
|
-
|
29
|
-
mockReaddir.mockResolvedValue([
|
30
|
-
{ name: 'project.mdc', isFile: () => true, isDirectory: () => false },
|
31
|
-
{ name: 'tsx.mdc', isFile: () => true, isDirectory: () => false },
|
32
|
-
]);
|
27
|
+
mockRealpath.mockRejectedValue(new Error('ENOENT')); // No directory scanning
|
28
|
+
mockReaddir.mockResolvedValue([]);
|
33
29
|
mockReadFile.mockImplementation(async (path) => {
|
34
30
|
if (path.includes('.cursor/rules/project.mdc'))
|
35
31
|
return '# Project Rules\nStrict guidelines';
|
@@ -46,6 +42,34 @@ describe('gatherContext', () => {
|
|
46
42
|
// Verify files are marked with their paths
|
47
43
|
expect(context).toMatch(/<!--.*\.cursor\/rules.*-->/);
|
48
44
|
});
|
45
|
+
it('should gracefully handle repos without .cursor/rules files', async () => {
|
46
|
+
const mockReadFile = vi.mocked(fs.readFile);
|
47
|
+
const mockReaddir = vi.mocked(fs.readdir);
|
48
|
+
const mockRealpath = vi.mocked(fs.realpath);
|
49
|
+
mockRealpath.mockRejectedValue(new Error('ENOENT'));
|
50
|
+
mockReaddir.mockResolvedValue([]);
|
51
|
+
mockReadFile.mockImplementation(async (path) => {
|
52
|
+
// .cursor/rules files don't exist
|
53
|
+
if (path.includes('.cursor/rules'))
|
54
|
+
return Promise.reject(new Error('ENOENT'));
|
55
|
+
// But other files do
|
56
|
+
if (path.includes('CODE_REVIEW.md'))
|
57
|
+
return '# Code Review Standards\nBe thorough';
|
58
|
+
if (path.includes('package.json'))
|
59
|
+
return '{"name": "my-project", "scripts": {"test": "jest"}}';
|
60
|
+
if (path.includes('tsconfig.json'))
|
61
|
+
return '{"compilerOptions": {"strict": true}}';
|
62
|
+
return Promise.reject(new Error('ENOENT'));
|
63
|
+
});
|
64
|
+
const context = await gatherContext();
|
65
|
+
// Should NOT contain .cursor/rules (they don't exist)
|
66
|
+
expect(context).not.toContain('.cursor/rules');
|
67
|
+
// Should still gather other useful context
|
68
|
+
expect(context).toContain('Code Review Standards');
|
69
|
+
expect(context).toContain('package.json');
|
70
|
+
expect(context).toContain('tsconfig.json');
|
71
|
+
expect(context.length).toBeGreaterThan(0);
|
72
|
+
});
|
49
73
|
it('should include CODE_REVIEW.md when present', async () => {
|
50
74
|
const mockReadFile = vi.mocked(fs.readFile);
|
51
75
|
const mockReaddir = vi.mocked(fs.readdir);
|
@@ -169,25 +193,25 @@ describe('gatherContext', () => {
|
|
169
193
|
// Should stop before processing all files
|
170
194
|
expect(context.length).toBeLessThanOrEqual(60000); // Allow some overhead
|
171
195
|
});
|
172
|
-
it('should
|
196
|
+
it('should check specific cursor rules files without directory scanning', async () => {
|
173
197
|
const mockReadFile = vi.mocked(fs.readFile);
|
174
198
|
const mockReaddir = vi.mocked(fs.readdir);
|
175
199
|
const mockRealpath = vi.mocked(fs.realpath);
|
176
|
-
mockRealpath.
|
177
|
-
mockReaddir.mockResolvedValue([
|
178
|
-
{ name: 'node_modules', isDirectory: () => true, isFile: () => false },
|
179
|
-
{ name: '.git', isDirectory: () => true, isFile: () => false },
|
180
|
-
{ name: 'project.mdc', isFile: () => true, isDirectory: () => false },
|
181
|
-
]);
|
200
|
+
mockRealpath.mockRejectedValue(new Error('ENOENT')); // No directory scanning
|
201
|
+
mockReaddir.mockResolvedValue([]); // No directory scanning
|
182
202
|
mockReadFile.mockImplementation(async (path) => {
|
183
|
-
if (path.includes('
|
184
|
-
return '#
|
203
|
+
if (path.includes('.cursor/rules/tsx.mdc'))
|
204
|
+
return '# TSX Rules\nReact patterns';
|
205
|
+
if (path.includes('.cursor/rules/project.mdc'))
|
206
|
+
return '# Project Rules\nGuidelines';
|
185
207
|
return Promise.reject(new Error('ENOENT'));
|
186
208
|
});
|
187
209
|
const context = await gatherContext();
|
188
|
-
// Should include
|
210
|
+
// Should include specific cursor rules files
|
211
|
+
expect(context).toContain('TSX Rules');
|
189
212
|
expect(context).toContain('Project Rules');
|
190
|
-
|
213
|
+
// Should NOT scan directories (no readdir calls for .cursor/rules/)
|
214
|
+
expect(mockReaddir).toHaveBeenCalledTimes(0);
|
191
215
|
});
|
192
216
|
it('should handle circular symlinks gracefully', async () => {
|
193
217
|
const mockReadFile = vi.mocked(fs.readFile);
|
@@ -257,32 +281,29 @@ describe('gatherContext', () => {
|
|
257
281
|
});
|
258
282
|
});
|
259
283
|
describe('File Format Support', () => {
|
260
|
-
it('should
|
284
|
+
it('should check specific cursor rules files only (no directory scan)', async () => {
|
261
285
|
const mockReadFile = vi.mocked(fs.readFile);
|
262
286
|
const mockReaddir = vi.mocked(fs.readdir);
|
263
287
|
const mockRealpath = vi.mocked(fs.realpath);
|
264
|
-
mockRealpath.
|
265
|
-
mockReaddir.mockResolvedValue([
|
266
|
-
{ name: 'project.mdc', isFile: () => true, isDirectory: () => false },
|
267
|
-
{ name: 'readme.md', isFile: () => true, isDirectory: () => false },
|
268
|
-
{ name: 'config.json', isFile: () => true, isDirectory: () => false },
|
269
|
-
]);
|
288
|
+
mockRealpath.mockRejectedValue(new Error('ENOENT')); // No directory scanning
|
289
|
+
mockReaddir.mockResolvedValue([]); // No directory scanning
|
270
290
|
mockReadFile.mockImplementation(async (path) => {
|
271
|
-
if (path.includes('
|
272
|
-
return '#
|
273
|
-
if (path.includes('
|
274
|
-
return '#
|
275
|
-
if (path.includes('
|
276
|
-
return '{}';
|
291
|
+
if (path.includes('.cursor/rules/tsx.mdc'))
|
292
|
+
return '# TSX Rules';
|
293
|
+
if (path.includes('.cursor/rules/project.mdc'))
|
294
|
+
return '# Project Rules';
|
295
|
+
if (path.includes('tsconfig.json'))
|
296
|
+
return '{ "compilerOptions": {} }';
|
277
297
|
return Promise.reject(new Error('ENOENT'));
|
278
298
|
});
|
279
299
|
const context = await gatherContext();
|
280
|
-
|
281
|
-
expect(context).toContain('
|
282
|
-
|
283
|
-
|
284
|
-
// But ts config from the root candidate list should still be included
|
300
|
+
// Should include specific tsx.mdc and project.mdc files
|
301
|
+
expect(context).toContain('TSX Rules');
|
302
|
+
expect(context).toContain('Project Rules');
|
303
|
+
// Should include tsconfig from root candidate list
|
285
304
|
expect(context).toContain('tsconfig.json');
|
305
|
+
// Should NOT scan .cursor/rules/ directory (avoids picking up other agent rules)
|
306
|
+
expect(mockReaddir).toHaveBeenCalledTimes(0);
|
286
307
|
});
|
287
308
|
it('should support multiple ESLint config formats', async () => {
|
288
309
|
const mockReadFile = vi.mocked(fs.readFile);
|
@@ -37,6 +37,16 @@ export async function performCodeReview(input, onProgress) {
|
|
37
37
|
: shouldSkip
|
38
38
|
? ''
|
39
39
|
: await gatherContext(input.workspaceDir);
|
40
|
+
// Debug: Log what context was gathered (to stderr for MCP logs)
|
41
|
+
console.error(`[codex-review-mcp] Context gathered: ${context.length} chars`);
|
42
|
+
const hasCursorRules = context.includes('.cursor/rules');
|
43
|
+
console.error(`[codex-review-mcp] Contains .cursor/rules: ${hasCursorRules}`);
|
44
|
+
if (hasCursorRules) {
|
45
|
+
const cursorFiles = (context.match(/<!-- \.cursor\/rules\/[^>]+ -->/g) || []);
|
46
|
+
console.error(`[codex-review-mcp] Cursor rules files: ${cursorFiles.join(', ')}`);
|
47
|
+
}
|
48
|
+
// Also use debugLog for persistent logging
|
49
|
+
await debugLog(`Context gathered: ${context.length} chars, .cursor/rules: ${hasCursorRules}`);
|
40
50
|
// Build expert prompt
|
41
51
|
await onProgress?.('Building expert prompt…', 50, 100);
|
42
52
|
const isStaticReview = input.contentType === 'code';
|
@@ -112,6 +112,34 @@ describe('performCodeReview', () => {
|
|
112
112
|
});
|
113
113
|
expect(gatherContextModule.gatherContext).toHaveBeenCalledWith(workspaceDir);
|
114
114
|
});
|
115
|
+
it('should use process.cwd() when workspaceDir is not provided', async () => {
|
116
|
+
const gatherSpy = vi.spyOn(gatherContextModule, 'gatherContext').mockResolvedValue('');
|
117
|
+
vi.spyOn(invokeAgentModule, 'invokeAgent').mockResolvedValue('# Review');
|
118
|
+
await performCodeReview({
|
119
|
+
content: mockDiff
|
120
|
+
// workspaceDir NOT provided
|
121
|
+
});
|
122
|
+
// Should be called with undefined, which makes gatherContext use process.cwd()
|
123
|
+
expect(gatherSpy).toHaveBeenCalledWith(undefined);
|
124
|
+
});
|
125
|
+
it('should find .cursor/rules when workspaceDir points to correct directory', async () => {
|
126
|
+
// This tests the critical path: workspaceDir -> gatherContext -> .cursor/rules
|
127
|
+
const workspaceDir = '/project/with/cursor/rules';
|
128
|
+
const gatherSpy = vi.spyOn(gatherContextModule, 'gatherContext')
|
129
|
+
.mockResolvedValue('<!-- .cursor/rules/project.mdc -->\n# Cursor Rules\nUse strict mode');
|
130
|
+
vi.spyOn(invokeAgentModule, 'invokeAgent').mockResolvedValue('# Review');
|
131
|
+
const buildPromptSpy = vi.spyOn(buildPromptModule, 'buildPrompt').mockReturnValue('prompt');
|
132
|
+
await performCodeReview({
|
133
|
+
content: mockDiff,
|
134
|
+
workspaceDir
|
135
|
+
});
|
136
|
+
// Verify gatherContext was called with the workspace dir
|
137
|
+
expect(gatherSpy).toHaveBeenCalledWith(workspaceDir);
|
138
|
+
// Verify the gathered context (with .cursor/rules) is passed to buildPrompt
|
139
|
+
expect(buildPromptSpy).toHaveBeenCalledWith(expect.objectContaining({
|
140
|
+
context: expect.stringContaining('.cursor/rules/project.mdc')
|
141
|
+
}));
|
142
|
+
});
|
115
143
|
});
|
116
144
|
describe('Content Types', () => {
|
117
145
|
it('should handle diff content type', async () => {
|