@dependabit/detector 0.1.2 → 0.1.14
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 +37 -9
- 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 +8 -3
- package/src/detector.ts +49 -10
- package/src/llm/copilot.ts +13 -10
- package/src/llm/prompts.ts +10 -1
- package/src/parsers/readme.ts +8 -2
- package/test/llm/copilot.test.ts +302 -23
- package/tsconfig.tsbuildinfo +1 -1
package/src/detector.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface DetectorOptions {
|
|
|
31
31
|
llmProvider: LLMProvider;
|
|
32
32
|
ignorePatterns?: string[];
|
|
33
33
|
useGitExcludes?: boolean;
|
|
34
|
+
repoOwner?: string;
|
|
35
|
+
repoName?: string;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export interface DetectionResult {
|
|
@@ -46,20 +48,13 @@ export interface DetectionResult {
|
|
|
46
48
|
|
|
47
49
|
const DEFAULT_IGNORE_PATTERNS = [
|
|
48
50
|
'node_modules',
|
|
49
|
-
'.git',
|
|
50
|
-
'.github',
|
|
51
|
-
'.claude',
|
|
52
|
-
'.codex',
|
|
53
51
|
'dist',
|
|
54
52
|
'build',
|
|
55
53
|
'target',
|
|
56
54
|
'vendor',
|
|
57
|
-
'.venv',
|
|
58
55
|
'venv',
|
|
59
56
|
'__pycache__',
|
|
60
|
-
'coverage'
|
|
61
|
-
'.next',
|
|
62
|
-
'.nuxt'
|
|
57
|
+
'coverage'
|
|
63
58
|
];
|
|
64
59
|
|
|
65
60
|
/**
|
|
@@ -69,13 +64,25 @@ export class Detector {
|
|
|
69
64
|
private options: Required<DetectorOptions>;
|
|
70
65
|
private ignoreMatcher: Ignore | null = null;
|
|
71
66
|
private ignoreMatcherLoaded = false;
|
|
67
|
+
private skipUrlPatterns: RegExp[];
|
|
72
68
|
|
|
73
69
|
constructor(options: DetectorOptions) {
|
|
74
70
|
this.options = {
|
|
75
71
|
...options,
|
|
76
72
|
ignorePatterns: options.ignorePatterns || DEFAULT_IGNORE_PATTERNS,
|
|
77
|
-
useGitExcludes: options.useGitExcludes ?? true
|
|
73
|
+
useGitExcludes: options.useGitExcludes ?? true,
|
|
74
|
+
repoOwner: options.repoOwner || '',
|
|
75
|
+
repoName: options.repoName || ''
|
|
78
76
|
};
|
|
77
|
+
|
|
78
|
+
this.skipUrlPatterns = [
|
|
79
|
+
/example\.com/,
|
|
80
|
+
/example\.org/,
|
|
81
|
+
/localhost/,
|
|
82
|
+
/127\.0\.0\.1/,
|
|
83
|
+
/\[.*\]/, // template placeholders like [NUMBER]
|
|
84
|
+
/github\.com\/user\/repo/ // common placeholder in docs
|
|
85
|
+
];
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
/**
|
|
@@ -232,6 +239,8 @@ Return as JSON with "dependencies" array.`;
|
|
|
232
239
|
const now = new Date().toISOString();
|
|
233
240
|
|
|
234
241
|
for (const [url, data] of allReferences) {
|
|
242
|
+
// Skip entries that will end up with low confidence
|
|
243
|
+
// (entries that can't be typed programmatically and LLM assigns low confidence)
|
|
235
244
|
// Prepare context for potential LLM use
|
|
236
245
|
const context = data.contexts
|
|
237
246
|
.map((c) => `${c.file}${c.line ? `:${c.line}` : ''}: ${c.text}`)
|
|
@@ -269,6 +278,11 @@ Return as JSON with "dependencies" array.`;
|
|
|
269
278
|
typeConfidence = 0.3;
|
|
270
279
|
}
|
|
271
280
|
|
|
281
|
+
// Skip low-confidence entries
|
|
282
|
+
if (typeConfidence < 0.5) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
272
286
|
// Step 5: Try programmatic access method determination
|
|
273
287
|
let accessMethod: AccessMethod | null = this.determineAccessMethod(url);
|
|
274
288
|
|
|
@@ -364,6 +378,27 @@ Return as JSON: {"accessMethod": "...", "confidence": 0.0-1.0}`;
|
|
|
364
378
|
context: { file: string; line?: number; text: string },
|
|
365
379
|
detectionMethod: DetectionMethod
|
|
366
380
|
): void {
|
|
381
|
+
// Skip URLs that don't start with http:// or https://
|
|
382
|
+
if (!/^https?:\/\//.test(url)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Skip URLs matching skip patterns (placeholders, localhost, etc.)
|
|
387
|
+
if (this.skipUrlPatterns.some((pattern) => pattern.test(url))) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Skip self-references (URLs pointing to the repo itself)
|
|
392
|
+
if (this.options.repoOwner && this.options.repoName) {
|
|
393
|
+
const selfPattern = new RegExp(
|
|
394
|
+
`github\\.com[/:]${this.escapeRegExp(this.options.repoOwner)}/${this.escapeRegExp(this.options.repoName)}(?:/|$|#|\\?)`,
|
|
395
|
+
'i'
|
|
396
|
+
);
|
|
397
|
+
if (selfPattern.test(url)) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
367
402
|
if (!map.has(url)) {
|
|
368
403
|
map.set(url, {
|
|
369
404
|
url,
|
|
@@ -374,6 +409,10 @@ Return as JSON: {"accessMethod": "...", "confidence": 0.0-1.0}`;
|
|
|
374
409
|
map.get(url)!.contexts.push(context);
|
|
375
410
|
}
|
|
376
411
|
|
|
412
|
+
private escapeRegExp(str: string): string {
|
|
413
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
414
|
+
}
|
|
415
|
+
|
|
377
416
|
/**
|
|
378
417
|
* Programmatically determine access method based on URL patterns
|
|
379
418
|
* Returns null if cannot be determined programmatically
|
|
@@ -526,7 +565,7 @@ Return as JSON: {"accessMethod": "...", "confidence": 0.0-1.0}`;
|
|
|
526
565
|
files.push(fullPath);
|
|
527
566
|
}
|
|
528
567
|
}
|
|
529
|
-
} catch
|
|
568
|
+
} catch {
|
|
530
569
|
// Ignore errors (permission denied, etc.)
|
|
531
570
|
}
|
|
532
571
|
|
package/src/llm/copilot.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Integrates with GitHub Copilot via CLI commands
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import type {
|
|
9
9
|
LLMProvider,
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
} from './client.js';
|
|
16
16
|
import { SYSTEM_PROMPT } from './prompts.js';
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
19
|
|
|
20
20
|
export class GitHubCopilotProvider implements LLMProvider {
|
|
21
21
|
private config: Required<LLMProviderConfig>;
|
|
@@ -44,14 +44,17 @@ export class GitHubCopilotProvider implements LLMProvider {
|
|
|
44
44
|
// Combine system prompt and user prompt for CLI
|
|
45
45
|
const fullPrompt = `${SYSTEM_PROMPT}\n\n${prompt}`;
|
|
46
46
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
// Use execFile to avoid shell escaping issues and command injection
|
|
48
|
+
// Pass prompt directly as an argument without manual escaping
|
|
49
|
+
const args = [
|
|
50
|
+
'copilot',
|
|
51
|
+
'-p',
|
|
52
|
+
fullPrompt,
|
|
53
|
+
'--silent',
|
|
54
|
+
'--allow-all-tools'
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const { stdout, stderr } = await execFileAsync('gh', args, {
|
|
55
58
|
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large responses
|
|
56
59
|
timeout: 60000 // 60 second timeout
|
|
57
60
|
});
|
package/src/llm/prompts.ts
CHANGED
|
@@ -17,11 +17,20 @@ INCLUDE these types of dependencies:
|
|
|
17
17
|
|
|
18
18
|
EXCLUDE these (handled by dependabot):
|
|
19
19
|
- NPM packages in package.json
|
|
20
|
-
- Python packages in requirements.txt
|
|
20
|
+
- Python packages in requirements.txt
|
|
21
21
|
- Rust crates in Cargo.toml
|
|
22
22
|
- Docker images in Dockerfile
|
|
23
23
|
- Any declared package manager dependencies
|
|
24
24
|
|
|
25
|
+
ALSO EXCLUDE (false positives):
|
|
26
|
+
- URLs that reference the repository itself (self-references)
|
|
27
|
+
- Relative file paths (e.g., CONTRIBUTING.md, docs/guide.md, ./src/utils)
|
|
28
|
+
- Placeholder URLs used in documentation examples (example.com, example.org, localhost)
|
|
29
|
+
- Internal documentation links within the same repository
|
|
30
|
+
- URLs with template variables or placeholders (e.g., issues/[NUMBER], user/repo)
|
|
31
|
+
|
|
32
|
+
Only return dependencies with confidence >= 0.7.
|
|
33
|
+
|
|
25
34
|
For each dependency found, provide:
|
|
26
35
|
1. url: The complete URL
|
|
27
36
|
2. name: A descriptive name
|
package/src/parsers/readme.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface ExtractedReference {
|
|
|
10
10
|
type: 'markdown-link' | 'bare-url' | 'reference-link';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
// Patterns to skip (package managers, CI badges, shields.io)
|
|
13
|
+
// Patterns to skip (package managers, CI badges, shields.io, placeholders)
|
|
14
14
|
const SKIP_PATTERNS = [
|
|
15
15
|
/npmjs\.com\/package/,
|
|
16
16
|
/pypi\.org\/project/,
|
|
@@ -21,7 +21,13 @@ const SKIP_PATTERNS = [
|
|
|
21
21
|
/badge(s)?\..*\.svg/,
|
|
22
22
|
/travis-ci\.(org|com)/,
|
|
23
23
|
/circleci\.com/,
|
|
24
|
-
/github\.com\/.*\/actions
|
|
24
|
+
/github\.com\/.*\/actions/, // GitHub Actions badges
|
|
25
|
+
/example\.com/, // placeholder domain
|
|
26
|
+
/example\.org/, // placeholder domain
|
|
27
|
+
/localhost/, // local dev URLs
|
|
28
|
+
/127\.0\.0\.1/, // loopback address
|
|
29
|
+
/\[.*\]/, // template placeholders (e.g., issues/[NUMBER])
|
|
30
|
+
/github\.com\/user\/repo/ // common placeholder in docs
|
|
25
31
|
];
|
|
26
32
|
|
|
27
33
|
/**
|
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
|
});
|