@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.
- package/CHANGELOG.md +107 -0
- package/dist/detector.d.ts +4 -0
- package/dist/detector.d.ts.map +1 -1
- package/dist/detector.js +76 -18
- package/dist/detector.js.map +1 -1
- package/dist/llm/copilot.d.ts.map +1 -1
- package/dist/llm/copilot.js +12 -8
- package/dist/llm/copilot.js.map +1 -1
- package/dist/llm/prompts.d.ts +1 -1
- package/dist/llm/prompts.d.ts.map +1 -1
- package/dist/llm/prompts.js +10 -1
- package/dist/llm/prompts.js.map +1 -1
- package/dist/parsers/readme.d.ts.map +1 -1
- package/dist/parsers/readme.js +8 -2
- package/dist/parsers/readme.js.map +1 -1
- package/package.json +9 -4
- package/src/detector.ts +107 -19
- package/src/llm/copilot.ts +13 -10
- package/src/llm/prompts.ts +10 -1
- package/src/parsers/readme.ts +8 -2
- package/test/detector.test.ts +67 -0
- package/test/llm/copilot.test.ts +302 -23
- package/tsconfig.tsbuildinfo +1 -1
package/test/llm/copilot.test.ts
CHANGED
|
@@ -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('
|
|
9
|
-
it('should
|
|
10
|
-
|
|
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
|
|
14
|
-
|
|
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('
|
|
19
|
-
it('should
|
|
20
|
-
|
|
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
|
|
24
|
-
|
|
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
|
|
28
|
-
|
|
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
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
expect(
|
|
224
|
+
expect(result.dependencies).toEqual([]);
|
|
225
|
+
expect(result.rawResponse).toContain('CLI error');
|
|
37
226
|
});
|
|
38
227
|
|
|
39
|
-
it('should
|
|
40
|
-
|
|
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
|
|
46
|
-
|
|
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
|
|
52
|
-
|
|
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
|
});
|