codex-review-mcp 1.4.0 β†’ 2.0.1

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.
@@ -0,0 +1,490 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { collectDiff } from './collectDiff.js';
3
+ import { execFile } from 'node:child_process';
4
+ import { promises as fs } from 'node:fs';
5
+ vi.mock('node:child_process');
6
+ vi.mock('node:fs');
7
+ describe('collectDiff', () => {
8
+ const mockExec = vi.mocked(execFile);
9
+ const mockStat = vi.mocked(fs.stat);
10
+ beforeEach(() => {
11
+ vi.clearAllMocks();
12
+ });
13
+ afterEach(() => {
14
+ vi.restoreAllMocks();
15
+ });
16
+ describe('Auto Mode - Uncommitted Changes', () => {
17
+ it('should detect and review uncommitted changes', async () => {
18
+ // Mock git root detection
19
+ mockStat.mockResolvedValueOnce({});
20
+ // Mock git status showing changes
21
+ mockExec.mockImplementation((file, args, opts, callback) => {
22
+ const cb = callback || opts;
23
+ if (args?.includes('status')) {
24
+ cb(null, { stdout: 'M src/test.ts\n', stderr: '' });
25
+ }
26
+ else if (args?.includes('rev-parse') && args?.includes('HEAD')) {
27
+ cb(null, { stdout: 'abc123\n', stderr: '' });
28
+ }
29
+ else if (args?.includes('diff')) {
30
+ cb(null, {
31
+ stdout: `diff --git a/test.ts b/test.ts
32
+ +++ b/test.ts
33
+ +added line`,
34
+ stderr: ''
35
+ });
36
+ }
37
+ return {};
38
+ });
39
+ const input = { target: 'auto' };
40
+ const result = await collectDiff(input, '/test/repo');
41
+ expect(result).toContain('added line');
42
+ expect(result).toContain('diff --git');
43
+ });
44
+ it('should handle repositories with no HEAD commit yet', async () => {
45
+ mockStat.mockResolvedValueOnce({});
46
+ mockExec.mockImplementation((file, args, opts, callback) => {
47
+ const cb = callback || opts;
48
+ if (args?.includes('status')) {
49
+ cb(null, { stdout: 'A new-file.ts\n', stderr: '' });
50
+ }
51
+ else if (args?.includes('rev-parse') && args?.includes('HEAD')) {
52
+ cb(new Error('fatal: ambiguous argument'), null);
53
+ }
54
+ else if (args?.includes('--staged')) {
55
+ cb(null, {
56
+ stdout: `diff --git a/new-file.ts b/new-file.ts
57
+ +++ b/new-file.ts
58
+ +new file content`,
59
+ stderr: ''
60
+ });
61
+ }
62
+ return {};
63
+ });
64
+ const input = { target: 'auto' };
65
+ const result = await collectDiff(input, '/test/repo');
66
+ expect(result).toContain('new file content');
67
+ });
68
+ it('should include untracked files in auto mode', async () => {
69
+ mockStat.mockResolvedValueOnce({});
70
+ mockExec.mockImplementation((file, args, opts, callback) => {
71
+ const cb = callback || opts;
72
+ if (args?.includes('status')) {
73
+ cb(null, { stdout: 'M tracked.ts\n', stderr: '' });
74
+ }
75
+ else if (args?.includes('rev-parse') && args?.includes('HEAD')) {
76
+ cb(null, { stdout: 'abc123\n', stderr: '' });
77
+ }
78
+ else if (args?.includes('ls-files') && args?.includes('--others')) {
79
+ cb(null, { stdout: 'untracked.ts\n', stderr: '' });
80
+ }
81
+ else if (args?.includes('diff') && args?.includes('/dev/null')) {
82
+ // Untracked file diff
83
+ const error = new Error('');
84
+ error.stdout = `diff --git /dev/null b/untracked.ts
85
+ +++ b/untracked.ts
86
+ +untracked content`;
87
+ cb(error, null);
88
+ }
89
+ else if (args?.includes('diff') && !args?.includes('/dev/null')) {
90
+ cb(null, {
91
+ stdout: `diff --git a/tracked.ts b/tracked.ts
92
+ +++ b/tracked.ts
93
+ +tracked change`,
94
+ stderr: ''
95
+ });
96
+ }
97
+ return {};
98
+ });
99
+ const input = { target: 'auto' };
100
+ const result = await collectDiff(input, '/test/repo');
101
+ expect(result).toContain('tracked change');
102
+ expect(result).toContain('untracked content');
103
+ });
104
+ });
105
+ describe('Auto Mode - Branch vs Default', () => {
106
+ it('should compare current branch to default branch when no uncommitted changes', async () => {
107
+ mockStat.mockResolvedValueOnce({});
108
+ mockExec.mockImplementation((file, args, opts, callback) => {
109
+ const cb = callback || opts;
110
+ if (args?.includes('status')) {
111
+ cb(null, { stdout: '', stderr: '' }); // No uncommitted changes
112
+ }
113
+ else if (args?.includes('remote show origin')) {
114
+ cb(null, { stdout: ' HEAD branch: main\n', stderr: '' });
115
+ }
116
+ else if (args?.includes('rev-parse') && args?.includes('--abbrev-ref')) {
117
+ cb(null, { stdout: 'feature-branch\n', stderr: '' });
118
+ }
119
+ else if (args?.includes('diff') && args?.includes('main...HEAD')) {
120
+ cb(null, {
121
+ stdout: `diff --git a/feature.ts b/feature.ts
122
+ +++ b/feature.ts
123
+ +feature code`,
124
+ stderr: ''
125
+ });
126
+ }
127
+ return {};
128
+ });
129
+ const input = { target: 'auto' };
130
+ const result = await collectDiff(input, '/test/repo');
131
+ expect(result).toContain('feature code');
132
+ });
133
+ it('should return empty when on default branch with clean tree', async () => {
134
+ mockStat.mockResolvedValueOnce({});
135
+ mockExec.mockImplementation((file, args, opts, callback) => {
136
+ const cb = callback || opts;
137
+ if (args?.includes('status')) {
138
+ cb(null, { stdout: '', stderr: '' });
139
+ }
140
+ else if (args?.includes('remote show origin')) {
141
+ cb(null, { stdout: ' HEAD branch: main\n', stderr: '' });
142
+ }
143
+ else if (args?.includes('rev-parse') && args?.includes('--abbrev-ref')) {
144
+ cb(null, { stdout: 'main\n', stderr: '' });
145
+ }
146
+ return {};
147
+ });
148
+ const input = { target: 'auto' };
149
+ const result = await collectDiff(input, '/test/repo');
150
+ expect(result).toBe('');
151
+ });
152
+ it('should fallback to master if main does not exist', async () => {
153
+ mockStat.mockResolvedValueOnce({});
154
+ mockExec.mockImplementation((file, args, opts, callback) => {
155
+ const cb = callback || opts;
156
+ if (args?.includes('status')) {
157
+ cb(null, { stdout: '', stderr: '' });
158
+ }
159
+ else if (args?.includes('remote show origin')) {
160
+ cb(new Error('No remote'), null);
161
+ }
162
+ else if (args?.includes('--verify main')) {
163
+ cb(new Error('Not found'), null);
164
+ }
165
+ else if (args?.includes('--verify master')) {
166
+ cb(null, { stdout: 'def456\n', stderr: '' });
167
+ }
168
+ else if (args?.includes('rev-parse') && args?.includes('--abbrev-ref')) {
169
+ cb(null, { stdout: 'feature\n', stderr: '' });
170
+ }
171
+ else if (args?.includes('diff') && args?.includes('master...HEAD')) {
172
+ cb(null, {
173
+ stdout: `diff --git a/file.ts b/file.ts
174
+ +content`,
175
+ stderr: ''
176
+ });
177
+ }
178
+ return {};
179
+ });
180
+ const input = { target: 'auto' };
181
+ const result = await collectDiff(input, '/test/repo');
182
+ expect(result).toContain('content');
183
+ });
184
+ it('should use HEAD~1 as last resort baseline', async () => {
185
+ mockStat.mockResolvedValueOnce({});
186
+ mockExec.mockImplementation((file, args, opts, callback) => {
187
+ const cb = callback || opts;
188
+ if (args?.includes('status')) {
189
+ cb(null, { stdout: '', stderr: '' });
190
+ }
191
+ else if (args?.includes('remote show origin')) {
192
+ cb(new Error('No remote'), null);
193
+ }
194
+ else if (args?.includes('--verify main')) {
195
+ cb(new Error('Not found'), null);
196
+ }
197
+ else if (args?.includes('--verify master')) {
198
+ cb(new Error('Not found'), null);
199
+ }
200
+ else if (args?.includes('--verify HEAD~1')) {
201
+ cb(null, { stdout: 'ghi789\n', stderr: '' });
202
+ }
203
+ else if (args?.includes('rev-parse') && args?.includes('--abbrev-ref')) {
204
+ cb(null, { stdout: 'orphan-branch\n', stderr: '' });
205
+ }
206
+ else if (args?.includes('diff') && args?.includes('HEAD~1...HEAD')) {
207
+ cb(null, {
208
+ stdout: `diff --git a/file.ts b/file.ts
209
+ +last commit change`,
210
+ stderr: ''
211
+ });
212
+ }
213
+ return {};
214
+ });
215
+ const input = { target: 'auto' };
216
+ const result = await collectDiff(input, '/test/repo');
217
+ expect(result).toContain('last commit change');
218
+ });
219
+ });
220
+ describe('Explicit Target Modes', () => {
221
+ it('should review staged changes when target is staged', async () => {
222
+ mockStat.mockResolvedValueOnce({});
223
+ mockExec.mockImplementation((file, args, opts, callback) => {
224
+ const cb = callback || opts;
225
+ if (args?.includes('--staged')) {
226
+ cb(null, {
227
+ stdout: `diff --git a/staged.ts b/staged.ts
228
+ +staged content`,
229
+ stderr: ''
230
+ });
231
+ }
232
+ return {};
233
+ });
234
+ const input = { target: 'staged' };
235
+ const result = await collectDiff(input, '/test/repo');
236
+ expect(result).toContain('staged content');
237
+ });
238
+ it('should review HEAD changes when target is head', async () => {
239
+ mockStat.mockResolvedValueOnce({});
240
+ mockExec.mockImplementation((file, args, opts, callback) => {
241
+ const cb = callback || opts;
242
+ if (args?.includes('diff') && args?.includes('HEAD')) {
243
+ cb(null, {
244
+ stdout: `diff --git a/head.ts b/head.ts
245
+ +head changes`,
246
+ stderr: ''
247
+ });
248
+ }
249
+ return {};
250
+ });
251
+ const input = { target: 'head' };
252
+ const result = await collectDiff(input, '/test/repo');
253
+ expect(result).toContain('head changes');
254
+ });
255
+ it('should compare custom refs when target is range', async () => {
256
+ mockStat.mockResolvedValueOnce({});
257
+ mockExec.mockImplementation((file, args, opts, callback) => {
258
+ const cb = callback || opts;
259
+ if (args?.includes('develop...feature')) {
260
+ cb(null, {
261
+ stdout: `diff --git a/range.ts b/range.ts
262
+ +range diff`,
263
+ stderr: ''
264
+ });
265
+ }
266
+ return {};
267
+ });
268
+ const input = {
269
+ target: 'range',
270
+ baseRef: 'develop',
271
+ headRef: 'feature'
272
+ };
273
+ const result = await collectDiff(input, '/test/repo');
274
+ expect(result).toContain('range diff');
275
+ });
276
+ it('should throw error when range mode missing refs', async () => {
277
+ const input = { target: 'range' };
278
+ await expect(collectDiff(input, '/test/repo')).rejects.toThrow('range target requires baseRef and headRef');
279
+ });
280
+ });
281
+ describe('Path Filtering', () => {
282
+ it('should filter diff to specified paths', async () => {
283
+ mockStat.mockResolvedValueOnce({});
284
+ let capturedArgs = [];
285
+ mockExec.mockImplementation((file, args, opts, callback) => {
286
+ capturedArgs = args || [];
287
+ const cb = callback || opts;
288
+ if (args?.includes('status')) {
289
+ cb(null, { stdout: 'M src/test.ts\n', stderr: '' });
290
+ }
291
+ else if (args?.includes('rev-parse')) {
292
+ cb(null, { stdout: 'abc123\n', stderr: '' });
293
+ }
294
+ else if (args?.includes('diff')) {
295
+ cb(null, {
296
+ stdout: `diff --git a/src/test.ts b/src/test.ts
297
+ +filtered content`,
298
+ stderr: ''
299
+ });
300
+ }
301
+ return {};
302
+ });
303
+ const input = {
304
+ target: 'auto',
305
+ paths: ['src/test.ts', 'src/utils.ts']
306
+ };
307
+ const result = await collectDiff(input, '/test/repo');
308
+ expect(result).toContain('filtered content');
309
+ // Verify paths were passed to git diff
310
+ expect(capturedArgs.some(arg => arg === 'src/test.ts')).toBe(true);
311
+ });
312
+ it('should filter out ignored patterns (dist, build, lock files)', async () => {
313
+ mockStat.mockResolvedValueOnce({});
314
+ let capturedArgs = [];
315
+ mockExec.mockImplementation((file, args, opts, callback) => {
316
+ capturedArgs = args || [];
317
+ const cb = callback || opts;
318
+ if (args?.includes('status')) {
319
+ cb(null, { stdout: 'M src/test.ts\n', stderr: '' });
320
+ }
321
+ else if (args?.includes('rev-parse')) {
322
+ cb(null, { stdout: 'abc123\n', stderr: '' });
323
+ }
324
+ else if (args?.includes('diff')) {
325
+ cb(null, { stdout: '', stderr: '' });
326
+ }
327
+ return {};
328
+ });
329
+ const input = {
330
+ target: 'auto',
331
+ paths: ['dist/bundle.js', 'package-lock.json', 'src/test.ts']
332
+ };
333
+ await collectDiff(input, '/test/repo');
334
+ // Should only include src/test.ts, not dist or lock files
335
+ expect(capturedArgs.includes('dist/bundle.js')).toBe(false);
336
+ expect(capturedArgs.includes('package-lock.json')).toBe(false);
337
+ expect(capturedArgs.some(arg => arg === 'src/test.ts')).toBe(true);
338
+ });
339
+ });
340
+ describe('Binary Files and Size Limits', () => {
341
+ it('should filter out binary file diff lines', async () => {
342
+ mockStat.mockResolvedValueOnce({});
343
+ mockExec.mockImplementation((file, args, opts, callback) => {
344
+ const cb = callback || opts;
345
+ if (args?.includes('status')) {
346
+ cb(null, { stdout: 'M image.png\n', stderr: '' });
347
+ }
348
+ else if (args?.includes('rev-parse')) {
349
+ cb(null, { stdout: 'abc123\n', stderr: '' });
350
+ }
351
+ else if (args?.includes('diff')) {
352
+ cb(null, {
353
+ stdout: `Binary files a/image.png and b/image.png differ
354
+ diff --git a/code.ts b/code.ts
355
+ +code change`,
356
+ stderr: ''
357
+ });
358
+ }
359
+ return {};
360
+ });
361
+ const input = { target: 'auto' };
362
+ const result = await collectDiff(input, '/test/repo');
363
+ expect(result).not.toContain('Binary files');
364
+ expect(result).toContain('code change');
365
+ });
366
+ it('should cap diff at ~300k characters', async () => {
367
+ mockStat.mockResolvedValueOnce({});
368
+ const hugeDiff = 'x'.repeat(400000);
369
+ mockExec.mockImplementation((file, args, opts, callback) => {
370
+ const cb = callback || opts;
371
+ if (args?.includes('status')) {
372
+ cb(null, { stdout: 'M huge.ts\n', stderr: '' });
373
+ }
374
+ else if (args?.includes('rev-parse')) {
375
+ cb(null, { stdout: 'abc123\n', stderr: '' });
376
+ }
377
+ else if (args?.includes('diff')) {
378
+ cb(null, { stdout: hugeDiff, stderr: '' });
379
+ }
380
+ return {};
381
+ });
382
+ const input = { target: 'auto' };
383
+ const result = await collectDiff(input, '/test/repo');
384
+ expect(result.length).toBeLessThanOrEqual(300100); // Cap + truncation message
385
+ expect(result).toContain('<!-- diff truncated -->');
386
+ });
387
+ });
388
+ describe('Repository Detection', () => {
389
+ it('should find git repo by walking up directory tree', async () => {
390
+ // First call fails (not in /test/repo)
391
+ // Second call fails (not in /test)
392
+ // Third call succeeds (found .git in /)
393
+ mockStat
394
+ .mockRejectedValueOnce(new Error('ENOENT'))
395
+ .mockRejectedValueOnce(new Error('ENOENT'))
396
+ .mockResolvedValueOnce({});
397
+ mockExec.mockImplementation((file, args, opts, callback) => {
398
+ const cb = callback || opts;
399
+ if (args?.includes('status')) {
400
+ cb(null, { stdout: 'M test.ts\n', stderr: '' });
401
+ }
402
+ else if (args?.includes('rev-parse')) {
403
+ cb(null, { stdout: 'abc123\n', stderr: '' });
404
+ }
405
+ else if (args?.includes('diff')) {
406
+ cb(null, { stdout: 'diff content', stderr: '' });
407
+ }
408
+ return {};
409
+ });
410
+ const input = { target: 'auto' };
411
+ const result = await collectDiff(input, '/test/repo/deep/path');
412
+ expect(result).toContain('diff content');
413
+ });
414
+ it('should use workspaceDir when explicitly provided', async () => {
415
+ mockStat.mockResolvedValueOnce({});
416
+ mockExec.mockImplementation((file, args, opts, callback) => {
417
+ const cb = callback || opts;
418
+ // Verify cwd is set to workspaceDir
419
+ expect(opts && typeof opts === 'object' && 'cwd' in opts ? opts.cwd : undefined).toBe('/explicit/workspace');
420
+ if (args?.includes('status')) {
421
+ cb(null, { stdout: '', stderr: '' });
422
+ }
423
+ else if (args?.includes('remote show origin')) {
424
+ cb(null, { stdout: ' HEAD branch: main\n', stderr: '' });
425
+ }
426
+ else if (args?.includes('rev-parse')) {
427
+ cb(null, { stdout: 'main\n', stderr: '' });
428
+ }
429
+ return {};
430
+ });
431
+ const input = { target: 'auto' };
432
+ await collectDiff(input, '/explicit/workspace');
433
+ });
434
+ it('should throw error when workspaceDir provided but no git repo found', async () => {
435
+ mockStat.mockRejectedValue(new Error('ENOENT'));
436
+ const input = { target: 'auto' };
437
+ await expect(collectDiff(input, '/not/a/repo')).rejects.toThrow('Could not locate a Git repository');
438
+ });
439
+ it('should respect environment variables for repo root', async () => {
440
+ const originalEnv = { ...process.env };
441
+ process.env.CODEX_REPO_ROOT = '/env/workspace';
442
+ mockStat.mockResolvedValueOnce({});
443
+ mockExec.mockImplementation((file, args, opts, callback) => {
444
+ const cb = callback || opts;
445
+ expect(opts && typeof opts === 'object' && 'cwd' in opts ? opts.cwd : undefined).toBe('/env/workspace');
446
+ if (args?.includes('status')) {
447
+ cb(null, { stdout: '', stderr: '' });
448
+ }
449
+ else if (args?.includes('remote show origin')) {
450
+ cb(null, { stdout: ' HEAD branch: main\n', stderr: '' });
451
+ }
452
+ else if (args?.includes('rev-parse')) {
453
+ cb(null, { stdout: 'main\n', stderr: '' });
454
+ }
455
+ return {};
456
+ });
457
+ const input = { target: 'auto' };
458
+ await collectDiff(input);
459
+ process.env = originalEnv;
460
+ });
461
+ });
462
+ describe('Error Handling', () => {
463
+ it('should provide helpful error message when git command fails', async () => {
464
+ mockStat.mockResolvedValueOnce({});
465
+ mockExec.mockImplementation((file, args, opts, callback) => {
466
+ const cb = callback || opts;
467
+ const error = new Error('git error');
468
+ error.stderr = 'fatal: not a git repository';
469
+ cb(error, null);
470
+ return {};
471
+ });
472
+ const input = { target: 'auto' };
473
+ await expect(collectDiff(input, '/test/repo')).rejects.toThrow('Failed to collect git diff');
474
+ });
475
+ it('should handle git command timeout gracefully', async () => {
476
+ mockStat.mockResolvedValueOnce({});
477
+ mockExec.mockImplementation((file, args, opts, callback) => {
478
+ // Verify maxBuffer is set
479
+ expect(opts && typeof opts === 'object' && 'maxBuffer' in opts ? opts.maxBuffer : undefined).toBe(10 * 1024 * 1024); // 10MB
480
+ const cb = callback || opts;
481
+ const error = new Error('timeout');
482
+ error.code = 'ETIMEDOUT';
483
+ cb(error, null);
484
+ return {};
485
+ });
486
+ const input = { target: 'auto' };
487
+ await expect(collectDiff(input, '/test/repo')).rejects.toThrow();
488
+ });
489
+ });
490
+ });
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatOutput } from './formatOutput.js';
3
+ describe('formatOutput', () => {
4
+ describe('Valid Agent Output', () => {
5
+ it('should return agent markdown as-is when valid', () => {
6
+ const agentOutput = '# Code Review\n\n## Issues\n\n- Issue 1\n- Issue 2';
7
+ const result = formatOutput(agentOutput);
8
+ expect(result).toBe(agentOutput);
9
+ });
10
+ it('should preserve formatting and special characters', () => {
11
+ const agentOutput = `# Review
12
+
13
+ ## Summary
14
+ - βœ… Good: Type safety
15
+ - ⚠️ Warning: Performance issue
16
+ - ❌ Critical: Security vulnerability
17
+
18
+ \`\`\`typescript
19
+ function fix() {
20
+ return true;
21
+ }
22
+ \`\`\`
23
+
24
+ **Bold** and *italic* text.`;
25
+ const result = formatOutput(agentOutput);
26
+ expect(result).toBe(agentOutput);
27
+ expect(result).toContain('βœ…');
28
+ expect(result).toContain('```typescript');
29
+ expect(result).toContain('**Bold**');
30
+ });
31
+ it('should handle multi-line markdown with code blocks', () => {
32
+ const agentOutput = `# Code Review
33
+
34
+ ## Issues Found
35
+
36
+ | Severity | File | Issue |
37
+ |----------|------|-------|
38
+ | High | test.ts | Security issue |
39
+
40
+ \`\`\`diff
41
+ - const bad = eval(userInput);
42
+ + const good = JSON.parse(userInput);
43
+ \`\`\``;
44
+ const result = formatOutput(agentOutput);
45
+ expect(result).toBe(agentOutput);
46
+ expect(result).toContain('```diff');
47
+ expect(result).toContain('| Severity |');
48
+ });
49
+ it('should handle output with only whitespace around content', () => {
50
+ const agentOutput = '\n\n# Review\n\nContent here\n\n';
51
+ const result = formatOutput(agentOutput);
52
+ expect(result).toBe(agentOutput);
53
+ });
54
+ });
55
+ describe('Empty or Invalid Output', () => {
56
+ it('should return fallback message when output is empty string', () => {
57
+ const result = formatOutput('');
58
+ expect(result).toContain('# Code Review');
59
+ expect(result).toContain('No issues detected or empty output from agent');
60
+ });
61
+ it('should return fallback message when output is only whitespace', () => {
62
+ const result = formatOutput(' \n\n \t ');
63
+ expect(result).toContain('# Code Review');
64
+ expect(result).toContain('No issues detected or empty output from agent');
65
+ });
66
+ it('should return fallback message when output is null/undefined', () => {
67
+ const result = formatOutput('');
68
+ expect(result).toContain('# Code Review');
69
+ expect(result).toContain('No issues detected');
70
+ });
71
+ it('should return valid markdown format for fallback', () => {
72
+ const result = formatOutput('');
73
+ // Should be valid markdown
74
+ expect(result).toMatch(/^# Code Review\n\nNo issues detected/);
75
+ // Should not have trailing whitespace issues
76
+ const lines = result.split('\n');
77
+ expect(lines[0]).toBe('# Code Review');
78
+ expect(lines[1]).toBe('');
79
+ expect(lines[2]).toContain('No issues detected');
80
+ });
81
+ });
82
+ describe('Edge Cases', () => {
83
+ it('should handle extremely long output', () => {
84
+ const longOutput = '# Review\n\n' + 'x'.repeat(100000);
85
+ const result = formatOutput(longOutput);
86
+ expect(result).toBe(longOutput);
87
+ expect(result.length).toBeGreaterThan(100000);
88
+ });
89
+ it('should handle output with unicode characters', () => {
90
+ const unicode = '# Review πŸŽ‰\n\n## ζ—₯本θͺž\n\n- Emoji: πŸš€ ✨ 🎯\n- Chinese: δΈ­ζ–‡\n- Arabic: Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ©';
91
+ const result = formatOutput(unicode);
92
+ expect(result).toBe(unicode);
93
+ expect(result).toContain('πŸŽ‰');
94
+ expect(result).toContain('ζ—₯本θͺž');
95
+ });
96
+ it('should handle output with HTML-like content', () => {
97
+ const htmlContent = `# Review
98
+
99
+ <div>This looks like HTML</div>
100
+
101
+ <script>alert('test')</script>`;
102
+ const result = formatOutput(htmlContent);
103
+ // Should preserve as-is (agent output is trusted)
104
+ expect(result).toBe(htmlContent);
105
+ expect(result).toContain('<div>');
106
+ });
107
+ it('should handle output with markdown escapes', () => {
108
+ const escaped = '# Review\n\n\\# Not a heading\n\n\\*Not italic\\*';
109
+ const result = formatOutput(escaped);
110
+ expect(result).toBe(escaped);
111
+ expect(result).toContain('\\#');
112
+ });
113
+ });
114
+ describe('Real-World Agent Output Patterns', () => {
115
+ it('should handle typical GPT-5 Codex review format', () => {
116
+ const typicalOutput = `# Code Review: Feature Implementation
117
+
118
+ ## Quick Summary
119
+ - Added new user authentication flow
120
+ - Updated API endpoints
121
+ - Improved error handling
122
+
123
+ ## Issues
124
+
125
+ | Severity | Location | Category | Issue | Fix |
126
+ |----------|----------|----------|-------|-----|
127
+ | Medium | auth.ts:45 | Security | Plain text password | Use bcrypt |
128
+ | Low | api.ts:23 | Style | Missing JSDoc | Add documentation |
129
+
130
+ ## Detailed Fixes
131
+
132
+ ### 1. Password Hashing
133
+
134
+ \`\`\`typescript
135
+ // Before
136
+ const user = { password: req.body.password };
137
+
138
+ // After
139
+ const user = {
140
+ password: await bcrypt.hash(req.body.password, 10)
141
+ };
142
+ \`\`\`
143
+
144
+ ## Positive Notes
145
+ - Good use of TypeScript types
146
+ - Proper error boundaries
147
+
148
+ ## Next Steps
149
+ 1. Review security practices
150
+ 2. Add unit tests`;
151
+ const result = formatOutput(typicalOutput);
152
+ expect(result).toBe(typicalOutput);
153
+ expect(result).toContain('Quick Summary');
154
+ expect(result).toContain('| Severity |');
155
+ expect(result).toContain('```typescript');
156
+ expect(result).toContain('Positive Notes');
157
+ });
158
+ });
159
+ });
@@ -82,10 +82,10 @@ async function scanDirectory(dirPath, extensions, visited = new Set()) {
82
82
  }
83
83
  return files;
84
84
  }
85
- export async function gatherContext() {
85
+ export async function gatherContext(baseDir) {
86
86
  const chunks = [];
87
87
  const processedPaths = new Set();
88
- const cwd = process.cwd();
88
+ const cwd = baseDir || process.cwd();
89
89
  for (const pattern of CANDIDATE_FILES) {
90
90
  // Check global size guard before processing each pattern
91
91
  if (chunks.join('').length > 50_000)