Skip to content

Commit 95cc864

Browse files
committed
test(github): add comprehensive tests for fetcher utilities
Add comprehensive unit tests for GitHub CLI fetcher functions to verify: - Default limit is now 500 (reduced from 1000) - Custom limits work correctly via options - maxBuffer is set appropriately (50MB for lists, 10MB for repo name) - Helpful error messages on ENOBUFS and maxBuffer exceeded - All required JSON fields are included in gh CLI commands - Buffer size management across different operation types Test Coverage: - 23 new tests for fetcher utilities - Total: 79 tests passing (3 test files) - Tests gh CLI installation/authentication detection - Tests default and custom limit behavior - Tests improved error handling with user guidance - Tests buffer management strategy Testing: - All 79 GitHub-related tests passing - New tests use proper mocking of execSync - Mock returns match actual execSync behavior (strings with encoding) Issue: Related to ENOBUFS error during repository indexing
1 parent f41d5fe commit 95cc864

1 file changed

Lines changed: 351 additions & 0 deletions

File tree

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
/**
2+
* Tests for GitHub CLI fetcher utilities
3+
* Tests default limits, custom limits, error handling, and buffer management
4+
*/
5+
6+
import { execSync } from 'node:child_process';
7+
import { beforeEach, describe, expect, it, vi } from 'vitest';
8+
import {
9+
fetchIssues,
10+
fetchPullRequests,
11+
getCurrentRepository,
12+
isGhAuthenticated,
13+
isGhInstalled,
14+
} from '../fetcher';
15+
16+
// Mock child_process
17+
vi.mock('node:child_process', () => ({
18+
execSync: vi.fn(),
19+
}));
20+
21+
describe('GitHub Fetcher - Configuration', () => {
22+
beforeEach(() => {
23+
vi.clearAllMocks();
24+
});
25+
26+
describe('isGhInstalled', () => {
27+
it('should return true when gh CLI is installed', () => {
28+
vi.mocked(execSync).mockReturnValue(Buffer.from('gh version 2.40.0'));
29+
30+
expect(isGhInstalled()).toBe(true);
31+
expect(execSync).toHaveBeenCalledWith('gh --version', { stdio: 'pipe' });
32+
});
33+
34+
it('should return false when gh CLI is not installed', () => {
35+
vi.mocked(execSync).mockImplementation(() => {
36+
throw new Error('Command not found');
37+
});
38+
39+
expect(isGhInstalled()).toBe(false);
40+
});
41+
});
42+
43+
describe('isGhAuthenticated', () => {
44+
it('should return true when authenticated', () => {
45+
vi.mocked(execSync).mockReturnValue(Buffer.from('Logged in'));
46+
47+
expect(isGhAuthenticated()).toBe(true);
48+
expect(execSync).toHaveBeenCalledWith('gh auth status', { stdio: 'pipe' });
49+
});
50+
51+
it('should return false when not authenticated', () => {
52+
vi.mocked(execSync).mockImplementation(() => {
53+
throw new Error('Not authenticated');
54+
});
55+
56+
expect(isGhAuthenticated()).toBe(false);
57+
});
58+
});
59+
60+
describe('getCurrentRepository', () => {
61+
beforeEach(() => {
62+
vi.clearAllMocks();
63+
});
64+
65+
it('should return repository in owner/repo format', () => {
66+
vi.mocked(execSync).mockReturnValueOnce('lytics/dev-agent\n' as any);
67+
68+
const repo = getCurrentRepository();
69+
expect(repo).toBe('lytics/dev-agent');
70+
expect(execSync).toHaveBeenCalledWith('gh repo view --json nameWithOwner -q .nameWithOwner', {
71+
encoding: 'utf-8',
72+
stdio: ['pipe', 'pipe', 'pipe'],
73+
maxBuffer: 10 * 1024 * 1024,
74+
});
75+
});
76+
77+
it('should throw error when not a GitHub repo', () => {
78+
vi.mocked(execSync).mockImplementationOnce(() => {
79+
throw new Error('Not a git repository');
80+
});
81+
82+
expect(() => getCurrentRepository()).toThrow(
83+
'Not a GitHub repository or gh CLI not configured'
84+
);
85+
});
86+
87+
it('should use correct maxBuffer size', () => {
88+
vi.mocked(execSync).mockReturnValueOnce('lytics/dev-agent\n' as any);
89+
90+
getCurrentRepository();
91+
92+
expect(execSync).toHaveBeenCalledWith(expect.any(String), {
93+
encoding: 'utf-8',
94+
stdio: ['pipe', 'pipe', 'pipe'],
95+
maxBuffer: 10 * 1024 * 1024, // 10MB
96+
});
97+
});
98+
});
99+
});
100+
101+
describe('GitHub Fetcher - Issue Fetching', () => {
102+
beforeEach(() => {
103+
vi.clearAllMocks();
104+
// Mock getCurrentRepository
105+
vi.mocked(execSync).mockImplementation((command) => {
106+
if (command.toString().includes('gh repo view')) {
107+
return Buffer.from('lytics/dev-agent');
108+
}
109+
return Buffer.from('[]');
110+
});
111+
});
112+
113+
describe('fetchIssues - Default Behavior', () => {
114+
it('should use default limit of 500', () => {
115+
vi.mocked(execSync).mockReturnValue(Buffer.from('[]'));
116+
117+
fetchIssues({ repository: 'lytics/dev-agent' });
118+
119+
const calls = vi.mocked(execSync).mock.calls;
120+
const issueCall = calls.find((call) => call[0].toString().includes('gh issue list'));
121+
122+
expect(issueCall).toBeDefined();
123+
expect(issueCall?.[0].toString()).toContain('--limit 500');
124+
});
125+
126+
it('should use 50MB maxBuffer for issues', () => {
127+
vi.mocked(execSync).mockReturnValue(Buffer.from('[]'));
128+
129+
fetchIssues({ repository: 'lytics/dev-agent' });
130+
131+
const calls = vi.mocked(execSync).mock.calls;
132+
const issueCall = calls.find((call) => call[0].toString().includes('gh issue list'));
133+
134+
expect(issueCall?.[1]).toMatchObject({
135+
maxBuffer: 50 * 1024 * 1024,
136+
});
137+
});
138+
139+
it('should include all required JSON fields', () => {
140+
vi.mocked(execSync).mockReturnValue(Buffer.from('[]'));
141+
142+
fetchIssues({ repository: 'lytics/dev-agent' });
143+
144+
const calls = vi.mocked(execSync).mock.calls;
145+
const issueCall = calls.find((call) => call[0].toString().includes('gh issue list'));
146+
const command = issueCall?.[0].toString();
147+
148+
expect(command).toContain('--json number,title,body,state,labels,author');
149+
expect(command).toContain('createdAt,updatedAt,closedAt,url,comments');
150+
});
151+
});
152+
153+
describe('fetchIssues - Custom Limits', () => {
154+
it('should respect custom limit option', () => {
155+
vi.mocked(execSync).mockReturnValue(Buffer.from('[]'));
156+
157+
fetchIssues({ repository: 'lytics/dev-agent', limit: 100 });
158+
159+
const calls = vi.mocked(execSync).mock.calls;
160+
const issueCall = calls.find((call) => call[0].toString().includes('gh issue list'));
161+
162+
expect(issueCall?.[0].toString()).toContain('--limit 100');
163+
});
164+
165+
it('should allow high limit for power users', () => {
166+
vi.mocked(execSync).mockReturnValue(Buffer.from('[]'));
167+
168+
fetchIssues({ repository: 'lytics/dev-agent', limit: 1000 });
169+
170+
const calls = vi.mocked(execSync).mock.calls;
171+
const issueCall = calls.find((call) => call[0].toString().includes('gh issue list'));
172+
173+
expect(issueCall?.[0].toString()).toContain('--limit 1000');
174+
});
175+
176+
it('should allow low limit for large repos', () => {
177+
vi.mocked(execSync).mockReturnValue(Buffer.from('[]'));
178+
179+
fetchIssues({ repository: 'lytics/dev-agent', limit: 50 });
180+
181+
const calls = vi.mocked(execSync).mock.calls;
182+
const issueCall = calls.find((call) => call[0].toString().includes('gh issue list'));
183+
184+
expect(issueCall?.[0].toString()).toContain('--limit 50');
185+
});
186+
});
187+
188+
describe('fetchIssues - Error Handling', () => {
189+
it('should provide helpful error message on ENOBUFS', () => {
190+
vi.mocked(execSync).mockImplementation(() => {
191+
const error = new Error('spawnSync /bin/sh ENOBUFS');
192+
throw error;
193+
});
194+
195+
expect(() => fetchIssues({ repository: 'lytics/dev-agent' })).toThrow(
196+
'Failed to fetch issues: Output too large. Try using --gh-limit with a lower value (e.g., --gh-limit 100)'
197+
);
198+
});
199+
200+
it('should provide helpful error message on maxBuffer exceeded', () => {
201+
vi.mocked(execSync).mockImplementation(() => {
202+
const error = new Error('stderr maxBuffer exceeded');
203+
throw error;
204+
});
205+
206+
expect(() => fetchIssues({ repository: 'lytics/dev-agent' })).toThrow(
207+
'Failed to fetch issues: Output too large. Try using --gh-limit with a lower value (e.g., --gh-limit 100)'
208+
);
209+
});
210+
211+
it('should preserve original error for other failures', () => {
212+
vi.mocked(execSync).mockImplementation(() => {
213+
throw new Error('Network timeout');
214+
});
215+
216+
expect(() => fetchIssues({ repository: 'lytics/dev-agent' })).toThrow(
217+
'Failed to fetch issues: Network timeout'
218+
);
219+
});
220+
});
221+
});
222+
223+
describe('GitHub Fetcher - Pull Request Fetching', () => {
224+
beforeEach(() => {
225+
vi.clearAllMocks();
226+
// Mock getCurrentRepository
227+
vi.mocked(execSync).mockImplementation((command) => {
228+
if (command.toString().includes('gh repo view')) {
229+
return Buffer.from('lytics/dev-agent');
230+
}
231+
return Buffer.from('[]');
232+
});
233+
});
234+
235+
describe('fetchPullRequests - Default Behavior', () => {
236+
it('should use default limit of 500', () => {
237+
vi.mocked(execSync).mockReturnValue(Buffer.from('[]'));
238+
239+
fetchPullRequests({ repository: 'lytics/dev-agent' });
240+
241+
const calls = vi.mocked(execSync).mock.calls;
242+
const prCall = calls.find((call) => call[0].toString().includes('gh pr list'));
243+
244+
expect(prCall).toBeDefined();
245+
expect(prCall?.[0].toString()).toContain('--limit 500');
246+
});
247+
248+
it('should use 50MB maxBuffer for pull requests', () => {
249+
vi.mocked(execSync).mockReturnValue(Buffer.from('[]'));
250+
251+
fetchPullRequests({ repository: 'lytics/dev-agent' });
252+
253+
const calls = vi.mocked(execSync).mock.calls;
254+
const prCall = calls.find((call) => call[0].toString().includes('gh pr list'));
255+
256+
expect(prCall?.[1]).toMatchObject({
257+
maxBuffer: 50 * 1024 * 1024,
258+
});
259+
});
260+
261+
it('should include all required JSON fields', () => {
262+
vi.mocked(execSync).mockReturnValue(Buffer.from('[]'));
263+
264+
fetchPullRequests({ repository: 'lytics/dev-agent' });
265+
266+
const calls = vi.mocked(execSync).mock.calls;
267+
const prCall = calls.find((call) => call[0].toString().includes('gh pr list'));
268+
const command = prCall?.[0].toString();
269+
270+
expect(command).toContain('--json number,title,body,state,labels,author');
271+
expect(command).toContain('createdAt,updatedAt,closedAt,mergedAt,url,comments');
272+
expect(command).toContain('headRefName,baseRefName');
273+
});
274+
});
275+
276+
describe('fetchPullRequests - Custom Limits', () => {
277+
it('should respect custom limit option', () => {
278+
vi.mocked(execSync).mockReturnValue(Buffer.from('[]'));
279+
280+
fetchPullRequests({ repository: 'lytics/dev-agent', limit: 200 });
281+
282+
const calls = vi.mocked(execSync).mock.calls;
283+
const prCall = calls.find((call) => call[0].toString().includes('gh pr list'));
284+
285+
expect(prCall?.[0].toString()).toContain('--limit 200');
286+
});
287+
});
288+
289+
describe('fetchPullRequests - Error Handling', () => {
290+
it('should provide helpful error message on ENOBUFS', () => {
291+
vi.mocked(execSync).mockImplementation(() => {
292+
const error = new Error('spawnSync /bin/sh ENOBUFS');
293+
throw error;
294+
});
295+
296+
expect(() => fetchPullRequests({ repository: 'lytics/dev-agent' })).toThrow(
297+
'Failed to fetch pull requests: Output too large. Try using --gh-limit with a lower value (e.g., --gh-limit 100)'
298+
);
299+
});
300+
301+
it('should provide helpful error message on maxBuffer exceeded', () => {
302+
vi.mocked(execSync).mockImplementation(() => {
303+
const error = new Error('stderr maxBuffer exceeded');
304+
throw error;
305+
});
306+
307+
expect(() => fetchPullRequests({ repository: 'lytics/dev-agent' })).toThrow(
308+
'Failed to fetch pull requests: Output too large. Try using --gh-limit with a lower value (e.g., --gh-limit 100)'
309+
);
310+
});
311+
});
312+
});
313+
314+
describe('GitHub Fetcher - Buffer Management', () => {
315+
beforeEach(() => {
316+
vi.clearAllMocks();
317+
});
318+
319+
it('should use appropriate buffer sizes for different operations', () => {
320+
// Repository name fetch (small payload)
321+
vi.mocked(execSync).mockReturnValueOnce('lytics/dev-agent' as any);
322+
getCurrentRepository();
323+
expect(vi.mocked(execSync).mock.calls[0][1]).toMatchObject({
324+
maxBuffer: 10 * 1024 * 1024, // 10MB
325+
});
326+
327+
vi.clearAllMocks();
328+
329+
// Issue list fetch (large payload)
330+
vi.mocked(execSync).mockReturnValueOnce('[]' as any);
331+
fetchIssues({ repository: 'lytics/dev-agent' });
332+
const issueCalls = vi
333+
.mocked(execSync)
334+
.mock.calls.filter((call) => call[0].toString().includes('gh issue list'));
335+
expect(issueCalls[0][1]).toMatchObject({
336+
maxBuffer: 50 * 1024 * 1024, // 50MB
337+
});
338+
339+
vi.clearAllMocks();
340+
341+
// PR list fetch (large payload)
342+
vi.mocked(execSync).mockReturnValueOnce('[]' as any);
343+
fetchPullRequests({ repository: 'lytics/dev-agent' });
344+
const prCalls = vi
345+
.mocked(execSync)
346+
.mock.calls.filter((call) => call[0].toString().includes('gh pr list'));
347+
expect(prCalls[0][1]).toMatchObject({
348+
maxBuffer: 50 * 1024 * 1024, // 50MB
349+
});
350+
});
351+
});

0 commit comments

Comments
 (0)