@dependabit/detector 0.1.2 → 0.1.15

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.
@@ -1,55 +1,334 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { promisify } from 'node:util';
3
+ import { GitHubCopilotProvider } from '../../src/llm/copilot.js';
4
+ import type { DetectedDependency } from '../../src/llm/client.js';
5
+
6
+ // Mock child_process with proper execFile behavior
7
+ vi.mock('node:child_process', () => {
8
+ const mockCallback = vi.fn();
9
+
10
+ // The custom promisify implementation that mimics Node.js behavior
11
+ const customPromisify = vi.fn((...execArgs) => {
12
+ return new Promise((resolve, reject) => {
13
+ // Call the mock with a callback that converts to { stdout, stderr }
14
+ mockCallback(...execArgs, (err: Error | null, stdout: string, stderr: string) => {
15
+ if (err) {
16
+ const error = err as NodeJS.ErrnoException & { stdout?: string; stderr?: string };
17
+ error.stdout = stdout;
18
+ error.stderr = stderr;
19
+ reject(error);
20
+ } else {
21
+ resolve({ stdout, stderr });
22
+ }
23
+ });
24
+ });
25
+ });
26
+
27
+ mockCallback[promisify.custom] = customPromisify;
28
+
29
+ return {
30
+ execFile: mockCallback
31
+ };
32
+ });
33
+
34
+ import { execFile } from 'node:child_process';
35
+ const mockExecFileCallback = vi.mocked(execFile);
2
36
 
3
37
  describe('GitHubCopilotProvider', () => {
38
+ let provider: GitHubCopilotProvider;
39
+
4
40
  beforeEach(() => {
5
41
  vi.clearAllMocks();
42
+ provider = new GitHubCopilotProvider();
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.restoreAllMocks();
6
47
  });
7
48
 
8
- describe('constructor', () => {
9
- it('should initialize with API configuration', () => {
10
- expect(true).toBe(true);
49
+ describe('CLI invocation', () => {
50
+ it('should use execFile with correct command and flags', async () => {
51
+ const mockResponse = JSON.stringify({ dependencies: [] });
52
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
53
+ if (typeof callback === 'function') {
54
+ // Use correct callback signature: callback(error, stdout, stderr)
55
+ callback(null, mockResponse, '');
56
+ }
57
+ return {} as any;
58
+ });
59
+
60
+ await provider.analyze('test content', 'test prompt');
61
+
62
+ expect(mockExecFileCallback).toHaveBeenCalledWith(
63
+ 'gh',
64
+ expect.arrayContaining([
65
+ 'copilot',
66
+ '-p',
67
+ expect.any(String),
68
+ '--silent',
69
+ '--allow-all-tools'
70
+ ]),
71
+ expect.objectContaining({
72
+ maxBuffer: 10 * 1024 * 1024,
73
+ timeout: 60000
74
+ }),
75
+ expect.any(Function)
76
+ );
77
+ });
78
+
79
+ it('should pass prompt directly without shell escaping', async () => {
80
+ const mockResponse = JSON.stringify({ dependencies: [] });
81
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
82
+ if (typeof callback === 'function') {
83
+ callback(null, mockResponse, '');
84
+ }
85
+ return {} as any;
86
+ });
87
+
88
+ const promptWithSpecialChars = 'test "quotes" $dollar `backtick`';
89
+ await provider.analyze('content', promptWithSpecialChars);
90
+
91
+ const callArgs = mockExecFileCallback.mock.calls[0];
92
+ const args = callArgs?.[1] as string[];
93
+ const fullPrompt = args?.[2];
94
+
95
+ // Verify prompt contains special characters unescaped
96
+ expect(fullPrompt).toContain('test "quotes" $dollar `backtick`');
11
97
  });
12
98
 
13
- it('should use environment variable for API key', () => {
14
- expect(true).toBe(true);
99
+ it('should handle prompts with newlines', async () => {
100
+ const mockResponse = JSON.stringify({ dependencies: [] });
101
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
102
+ if (typeof callback === 'function') {
103
+ callback(null, mockResponse, '');
104
+ }
105
+ return {} as any;
106
+ });
107
+
108
+ const promptWithNewlines = 'line1\nline2\nline3';
109
+ await provider.analyze('content', promptWithNewlines);
110
+
111
+ const callArgs = mockExecFileCallback.mock.calls[0];
112
+ const args = callArgs?.[1] as string[];
113
+ const fullPrompt = args?.[2];
114
+
115
+ expect(fullPrompt).toContain('line1\nline2\nline3');
116
+ });
117
+
118
+ it('should handle prompts ending with backslash', async () => {
119
+ const mockResponse = JSON.stringify({ dependencies: [] });
120
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
121
+ if (typeof callback === 'function') {
122
+ callback(null, mockResponse, '');
123
+ }
124
+ return {} as any;
125
+ });
126
+
127
+ const promptWithBackslash = 'path\\to\\file\\';
128
+ await provider.analyze('content', promptWithBackslash);
129
+
130
+ const callArgs = mockExecFileCallback.mock.calls[0];
131
+ const args = callArgs?.[1] as string[];
132
+ const fullPrompt = args?.[2];
133
+
134
+ expect(fullPrompt).toContain('path\\to\\file\\');
15
135
  });
16
136
  });
17
137
 
18
- describe('analyze', () => {
19
- it('should call Azure OpenAI API with correct parameters', async () => {
20
- expect(true).toBe(true);
138
+ describe('response parsing', () => {
139
+ it('should parse JSON response correctly', async () => {
140
+ const mockDeps: DetectedDependency[] = [
141
+ {
142
+ name: 'test-package',
143
+ version: '1.0.0',
144
+ type: 'npm',
145
+ confidence: 0.9,
146
+ context: 'test context'
147
+ }
148
+ ];
149
+ const mockResponse = JSON.stringify({ dependencies: mockDeps });
150
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
151
+ if (typeof callback === 'function') {
152
+ callback(null, mockResponse, '');
153
+ }
154
+ return {} as any;
155
+ });
156
+
157
+ const result = await provider.analyze('content', 'prompt');
158
+
159
+ expect(result.dependencies).toEqual(mockDeps);
21
160
  });
22
161
 
23
- it('should parse LLM response into structured dependencies', async () => {
24
- expect(true).toBe(true);
162
+ it('should parse JSON wrapped in markdown code blocks', async () => {
163
+ const mockDeps: DetectedDependency[] = [
164
+ {
165
+ name: 'test-package',
166
+ version: '1.0.0',
167
+ type: 'npm',
168
+ confidence: 0.9,
169
+ context: 'test context'
170
+ }
171
+ ];
172
+ const mockResponse = '```json\n' + JSON.stringify({ dependencies: mockDeps }) + '\n```';
173
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
174
+ if (typeof callback === 'function') {
175
+ callback(null, mockResponse, '');
176
+ }
177
+ return {} as any;
178
+ });
179
+
180
+ const result = await provider.analyze('content', 'prompt');
181
+
182
+ expect(result.dependencies).toEqual(mockDeps);
25
183
  });
26
184
 
27
- it('should include confidence scores for each detection', async () => {
28
- expect(true).toBe(true);
185
+ it('should handle malformed JSON gracefully', async () => {
186
+ const mockResponse = 'not valid json';
187
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
188
+ if (typeof callback === 'function') {
189
+ callback(null, mockResponse, '');
190
+ }
191
+ return {} as any;
192
+ });
193
+
194
+ const result = await provider.analyze('content', 'prompt');
195
+
196
+ expect(result.dependencies).toEqual([]);
29
197
  });
30
198
 
31
- it('should handle API errors gracefully', async () => {
32
- expect(true).toBe(true);
199
+ it('should return empty dependencies on empty response', async () => {
200
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
201
+ if (typeof callback === 'function') {
202
+ callback(null, '', '');
203
+ }
204
+ return {} as any;
205
+ });
206
+
207
+ const result = await provider.analyze('content', 'prompt');
208
+
209
+ expect(result.dependencies).toEqual([]);
33
210
  });
211
+ });
212
+
213
+ describe('error handling', () => {
214
+ it('should handle CLI execution errors', async () => {
215
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
216
+ if (typeof callback === 'function') {
217
+ callback(new Error('CLI error'), '', 'Error message');
218
+ }
219
+ return {} as any;
220
+ });
221
+
222
+ const result = await provider.analyze('content', 'prompt');
34
223
 
35
- it('should respect rate limits', async () => {
36
- expect(true).toBe(true);
224
+ expect(result.dependencies).toEqual([]);
225
+ expect(result.rawResponse).toContain('CLI error');
37
226
  });
38
227
 
39
- it('should log request/response metadata', async () => {
40
- expect(true).toBe(true);
228
+ it('should handle stderr without stdout as error', async () => {
229
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
230
+ if (typeof callback === 'function') {
231
+ callback(null, '', 'Authentication failed');
232
+ }
233
+ return {} as any;
234
+ });
235
+
236
+ const result = await provider.analyze('content', 'prompt');
237
+
238
+ expect(result.dependencies).toEqual([]);
239
+ });
240
+
241
+ it('should handle timeout errors', async () => {
242
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
243
+ if (typeof callback === 'function') {
244
+ const error = new Error('Command timeout') as NodeJS.ErrnoException;
245
+ error.code = 'ETIMEDOUT';
246
+ callback(error, '', '');
247
+ }
248
+ return {} as any;
249
+ });
250
+
251
+ const result = await provider.analyze('content', 'prompt');
252
+
253
+ expect(result.dependencies).toEqual([]);
254
+ });
255
+
256
+ it('should handle buffer overflow errors', async () => {
257
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
258
+ if (typeof callback === 'function') {
259
+ const error = new Error('maxBuffer exceeded') as NodeJS.ErrnoException;
260
+ error.code = 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
261
+ callback(error, '', '');
262
+ }
263
+ return {} as any;
264
+ });
265
+
266
+ const result = await provider.analyze('content', 'prompt');
267
+
268
+ expect(result.dependencies).toEqual([]);
269
+ });
270
+ });
271
+
272
+ describe('usage metadata', () => {
273
+ it('should include usage metadata in response', async () => {
274
+ const mockResponse = JSON.stringify({ dependencies: [] });
275
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
276
+ if (typeof callback === 'function') {
277
+ callback(null, mockResponse, '');
278
+ }
279
+ return {} as any;
280
+ });
281
+
282
+ const result = await provider.analyze('content', 'prompt');
283
+
284
+ expect(result.usage).toBeDefined();
285
+ expect(result.usage.promptTokens).toBeGreaterThan(0);
286
+ expect(result.usage.completionTokens).toBeGreaterThan(0);
287
+ expect(result.usage.totalTokens).toBeGreaterThan(0);
288
+ expect(result.usage.latencyMs).toBeGreaterThanOrEqual(0);
289
+ });
290
+
291
+ it('should use configured model in usage metadata', async () => {
292
+ const mockResponse = JSON.stringify({ dependencies: [] });
293
+ mockExecFileCallback.mockImplementation((file, args, options, callback) => {
294
+ if (typeof callback === 'function') {
295
+ callback(null, mockResponse, '');
296
+ }
297
+ return {} as any;
298
+ });
299
+
300
+ const customProvider = new GitHubCopilotProvider({ model: 'gpt-4-turbo' });
301
+ const result = await customProvider.analyze('content', 'prompt');
302
+
303
+ expect(result.usage.model).toBe('gpt-4-turbo');
41
304
  });
42
305
  });
43
306
 
44
307
  describe('getSupportedModels', () => {
45
- it('should return available GPT models', () => {
46
- expect(true).toBe(true);
308
+ it('should return list of supported models', () => {
309
+ const models = provider.getSupportedModels();
310
+
311
+ expect(models).toBeInstanceOf(Array);
312
+ expect(models.length).toBeGreaterThan(0);
313
+ expect(models).toContain('github-copilot');
47
314
  });
48
315
  });
49
316
 
50
317
  describe('getRateLimit', () => {
51
- it('should return current rate limit status', () => {
52
- expect(true).toBe(true);
318
+ it('should return rate limit info', async () => {
319
+ const rateLimit = await provider.getRateLimit();
320
+
321
+ expect(rateLimit).toBeDefined();
322
+ expect(rateLimit.remaining).toBe(-1);
323
+ expect(rateLimit.limit).toBe(-1);
324
+ });
325
+ });
326
+
327
+ describe('validateConfig', () => {
328
+ it('should validate configuration', () => {
329
+ const isValid = provider.validateConfig();
330
+
331
+ expect(typeof isValid).toBe('boolean');
53
332
  });
54
333
  });
55
334
  });