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.
@@ -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
- '.cursor/rules/', // directory to scan for all .md and .mdc files
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
- // Mock .cursor/rules directory scanning
28
- mockRealpath.mockResolvedValue('/test/workspace/.cursor/rules');
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 skip node_modules and other ignored directories', async () => {
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.mockResolvedValue('/test/workspace/.cursor/rules');
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('project.mdc'))
184
- return '# Project Rules';
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 project.mdc but not traverse node_modules or .git
210
+ // Should include specific cursor rules files
211
+ expect(context).toContain('TSX Rules');
189
212
  expect(context).toContain('Project Rules');
190
- expect(mockReaddir).toHaveBeenCalledTimes(1); // Only the .cursor/rules directory
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 scan .cursor/rules directory for .md and .mdc files', async () => {
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.mockResolvedValue('/test/workspace/.cursor/rules');
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('project.mdc'))
272
- return '# Project';
273
- if (path.includes('readme.md'))
274
- return '# Readme';
275
- if (path.includes('config.json'))
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
- expect(context).toContain('Project');
281
- expect(context).toContain('Readme');
282
- // Verify .json files from directory scan are not included (checking for the file marker)
283
- expect(context).not.toMatch(/<!--.*\.cursor\/rules\/config\.json.*-->/);
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 () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-review-mcp",
3
- "version": "2.3.5",
3
+ "version": "2.3.7",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "build": "tsc",