@dependabit/detector 0.1.16 → 0.1.18

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/detector.d.ts +179 -12
  3. package/dist/detector.d.ts.map +1 -1
  4. package/dist/detector.js +121 -12
  5. package/dist/detector.js.map +1 -1
  6. package/dist/diff-parser.js.map +1 -1
  7. package/dist/llm/client.d.ts +112 -3
  8. package/dist/llm/client.d.ts.map +1 -1
  9. package/dist/llm/client.js +34 -2
  10. package/dist/llm/client.js.map +1 -1
  11. package/dist/llm/copilot.d.ts.map +1 -1
  12. package/dist/llm/copilot.js +1 -7
  13. package/dist/llm/copilot.js.map +1 -1
  14. package/dist/llm/prompts.d.ts +42 -2
  15. package/dist/llm/prompts.d.ts.map +1 -1
  16. package/dist/llm/prompts.js +42 -2
  17. package/dist/llm/prompts.js.map +1 -1
  18. package/dist/parsers/code-comments.js.map +1 -1
  19. package/dist/parsers/package-files.js.map +1 -1
  20. package/dist/parsers/readme.d.ts +1 -1
  21. package/dist/parsers/readme.d.ts.map +1 -1
  22. package/dist/parsers/readme.js +1 -1
  23. package/dist/parsers/readme.js.map +1 -1
  24. package/package.json +28 -11
  25. package/src/detector.ts +0 -1043
  26. package/src/diff-parser.ts +0 -257
  27. package/src/index.ts +0 -43
  28. package/src/llm/client.ts +0 -85
  29. package/src/llm/copilot.ts +0 -150
  30. package/src/llm/prompts.ts +0 -111
  31. package/src/parsers/code-comments.ts +0 -178
  32. package/src/parsers/package-files.ts +0 -156
  33. package/src/parsers/readme.ts +0 -191
  34. package/test/detector.test.ts +0 -169
  35. package/test/diff-parser.test.ts +0 -187
  36. package/test/llm/client.test.ts +0 -31
  37. package/test/llm/copilot.test.ts +0 -334
  38. package/test/parsers/code-comments.test.ts +0 -98
  39. package/test/parsers/package-files.test.ts +0 -52
  40. package/test/parsers/readme.test.ts +0 -52
  41. package/tsconfig.json +0 -10
  42. package/tsconfig.tsbuildinfo +0 -1
@@ -1,178 +0,0 @@
1
- /**
2
- * Code Comment Parser
3
- * Extracts URLs and references from code comments
4
- */
5
-
6
- export interface CommentReference {
7
- url: string;
8
- context: string;
9
- file: string;
10
- line: number;
11
- commentType: 'single-line' | 'multi-line' | 'jsdoc';
12
- }
13
-
14
- /**
15
- * Parse code files and extract references from comments
16
- */
17
- export function parseCodeComments(content: string, filePath: string): CommentReference[] {
18
- const references: CommentReference[] = [];
19
- const extension = getFileExtension(filePath);
20
- const commentStyle = getCommentStyle(extension);
21
-
22
- if (!commentStyle) {
23
- return references; // Unsupported file type
24
- }
25
-
26
- const lines = content.split('\n');
27
- let inMultiLineComment = false;
28
-
29
- for (let i = 0; i < lines.length; i++) {
30
- const line = lines[i];
31
- if (!line) continue;
32
-
33
- const lineNumber = i + 1;
34
-
35
- // Check for multi-line comment start/end
36
- if (commentStyle.multiLine) {
37
- if (line.includes(commentStyle.multiLine.start)) {
38
- inMultiLineComment = true;
39
- }
40
- if (inMultiLineComment) {
41
- const urls = extractUrls(line);
42
- for (const url of urls) {
43
- references.push({
44
- url,
45
- context: line.trim(),
46
- file: filePath,
47
- line: lineNumber,
48
- commentType: line.includes('/**') ? 'jsdoc' : 'multi-line'
49
- });
50
- }
51
- }
52
- if (line.includes(commentStyle.multiLine.end)) {
53
- inMultiLineComment = false;
54
- }
55
- continue;
56
- }
57
-
58
- // Check for single-line comments
59
- if (commentStyle.singleLine) {
60
- const commentStart = line.indexOf(commentStyle.singleLine);
61
- if (commentStart !== -1) {
62
- const comment = line.substring(commentStart);
63
- const urls = extractUrls(comment);
64
- for (const url of urls) {
65
- references.push({
66
- url,
67
- context: comment.trim(),
68
- file: filePath,
69
- line: lineNumber,
70
- commentType: 'single-line'
71
- });
72
- }
73
- }
74
- }
75
- }
76
-
77
- return references;
78
- }
79
-
80
- function getFileExtension(filePath: string): string {
81
- const match = filePath.match(/\.([^.]+)$/);
82
- const ext = match?.[1];
83
- return ext ? ext.toLowerCase() : '';
84
- }
85
-
86
- interface CommentStyle {
87
- singleLine?: string;
88
- multiLine?: { start: string; end: string };
89
- }
90
-
91
- function getCommentStyle(extension: string): CommentStyle | null {
92
- const styles: Record<string, CommentStyle> = {
93
- // JavaScript/TypeScript
94
- js: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
95
- ts: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
96
- jsx: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
97
- tsx: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
98
-
99
- // Python
100
- py: { singleLine: '#' },
101
-
102
- // Ruby
103
- rb: { singleLine: '#', multiLine: { start: '=begin', end: '=end' } },
104
-
105
- // Go
106
- go: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
107
-
108
- // Rust
109
- rs: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
110
-
111
- // C/C++
112
- c: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
113
- cpp: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
114
- h: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
115
-
116
- // Java/Kotlin
117
- java: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
118
- kt: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
119
-
120
- // C#
121
- cs: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
122
-
123
- // PHP
124
- php: { singleLine: '//', multiLine: { start: '/*', end: '*/' } },
125
-
126
- // Shell
127
- sh: { singleLine: '#' },
128
- bash: { singleLine: '#' },
129
-
130
- // YAML
131
- yml: { singleLine: '#' },
132
- yaml: { singleLine: '#' }
133
- };
134
-
135
- return styles[extension] || null;
136
- }
137
-
138
- function extractUrls(text: string): string[] {
139
- const urls: string[] = [];
140
- const regex = /https?:\/\/[^\s<>()[\]'"]+/g;
141
- let match;
142
-
143
- while ((match = regex.exec(text)) !== null) {
144
- urls.push(match[0]);
145
- }
146
-
147
- return urls;
148
- }
149
-
150
- /**
151
- * Extract specification and RFC references from comments
152
- */
153
- export function extractSpecReferences(content: string): Array<{ spec: string; context: string }> {
154
- const references: Array<{ spec: string; context: string }> = [];
155
- const lines = content.split('\n');
156
-
157
- for (const line of lines) {
158
- // Match RFC references
159
- const rfcMatch = /RFC\s*(\d+)/i.exec(line);
160
- if (rfcMatch) {
161
- references.push({
162
- spec: `RFC ${rfcMatch[1]}`,
163
- context: line.trim()
164
- });
165
- }
166
-
167
- // Match standard references (ISO, IEEE, etc.)
168
- const standardMatch = /(ISO|IEEE|ECMA|W3C)[\s-]*(\d+(?:[-.]\d+)*)/i.exec(line);
169
- if (standardMatch) {
170
- references.push({
171
- spec: `${standardMatch[1]} ${standardMatch[2]}`,
172
- context: line.trim()
173
- });
174
- }
175
- }
176
-
177
- return references;
178
- }
@@ -1,156 +0,0 @@
1
- /**
2
- * Package File Parser
3
- * Extracts metadata and references from package manager files
4
- * EXCLUDES actual dependencies (handled by dependabot)
5
- */
6
-
7
- export interface PackageMetadata {
8
- repository?: string;
9
- homepage?: string;
10
- documentation?: string;
11
- urls: string[]; // URLs found in descriptions, etc.
12
- }
13
-
14
- /**
15
- * Parse package.json and extract metadata URLs (NOT dependencies)
16
- */
17
- export function parsePackageJson(content: string): PackageMetadata {
18
- try {
19
- const pkg = JSON.parse(content);
20
- const urls: string[] = [];
21
-
22
- // Extract repository URL
23
- let repository: string | undefined = undefined;
24
- if (typeof pkg.repository === 'string') {
25
- repository = pkg.repository;
26
- } else if (pkg.repository && pkg.repository.url) {
27
- repository = pkg.repository.url;
28
- }
29
-
30
- // Extract homepage
31
- const homepage: string | undefined = pkg.homepage;
32
-
33
- // Extract documentation (not standard but sometimes present)
34
- const documentation: string | undefined = pkg.documentation || pkg.docs;
35
-
36
- // Extract URLs from description
37
- if (pkg.description) {
38
- const descUrls = extractUrls(pkg.description);
39
- urls.push(...descUrls);
40
- }
41
-
42
- // Note: We DO NOT extract dependencies/devDependencies
43
- // Those are handled by dependabot
44
-
45
- return {
46
- ...(repository !== undefined && { repository }),
47
- ...(homepage !== undefined && { homepage }),
48
- ...(documentation !== undefined && { documentation }),
49
- urls
50
- };
51
- } catch {
52
- return { urls: [] };
53
- }
54
- }
55
-
56
- /**
57
- * Parse requirements.txt and extract URLs from comments
58
- * EXCLUDES actual packages (handled by dependabot)
59
- */
60
- export function parseRequirementsTxt(content: string): PackageMetadata {
61
- const urls: string[] = [];
62
- const lines = content.split('\n');
63
-
64
- for (const line of lines) {
65
- // Only extract URLs from comments
66
- if (line.trim().startsWith('#')) {
67
- const commentUrls = extractUrls(line);
68
- urls.push(...commentUrls);
69
- }
70
- // Skip actual package lines - dependabot handles those
71
- }
72
-
73
- return { urls };
74
- }
75
-
76
- /**
77
- * Parse Cargo.toml and extract metadata URLs
78
- * EXCLUDES actual dependencies (handled by dependabot)
79
- */
80
- export function parseCargoToml(content: string): PackageMetadata {
81
- const urls: string[] = [];
82
- let repository: string | undefined = undefined;
83
- let homepage: string | undefined = undefined;
84
- let documentation: string | undefined = undefined;
85
-
86
- const lines = content.split('\n');
87
- let inPackageSection = false;
88
-
89
- for (const line of lines) {
90
- if (line.trim() === '[package]') {
91
- inPackageSection = true;
92
- continue;
93
- }
94
-
95
- if (line.trim().startsWith('[') && line.trim() !== '[package]') {
96
- inPackageSection = false;
97
- continue;
98
- }
99
-
100
- if (inPackageSection) {
101
- const repoMatch = /repository\s*=\s*"([^"]+)"/.exec(line);
102
- if (repoMatch && repoMatch[1]) repository = repoMatch[1];
103
-
104
- const homepageMatch = /homepage\s*=\s*"([^"]+)"/.exec(line);
105
- if (homepageMatch && homepageMatch[1]) homepage = homepageMatch[1];
106
-
107
- const docMatch = /documentation\s*=\s*"([^"]+)"/.exec(line);
108
- if (docMatch && docMatch[1]) documentation = docMatch[1];
109
- }
110
-
111
- // Extract URLs from comments
112
- if (line.trim().startsWith('#')) {
113
- const commentUrls = extractUrls(line);
114
- urls.push(...commentUrls);
115
- }
116
- }
117
-
118
- return {
119
- ...(repository !== undefined && { repository }),
120
- ...(homepage !== undefined && { homepage }),
121
- ...(documentation !== undefined && { documentation }),
122
- urls
123
- };
124
- }
125
-
126
- /**
127
- * Parse go.mod and extract URLs from comments
128
- * EXCLUDES actual dependencies (handled by dependabot)
129
- */
130
- export function parseGoMod(content: string): PackageMetadata {
131
- const urls: string[] = [];
132
- const lines = content.split('\n');
133
-
134
- for (const line of lines) {
135
- // Only extract URLs from comments
136
- if (line.trim().startsWith('//')) {
137
- const commentUrls = extractUrls(line);
138
- urls.push(...commentUrls);
139
- }
140
- // Skip actual require lines - dependabot handles those
141
- }
142
-
143
- return { urls };
144
- }
145
-
146
- function extractUrls(text: string): string[] {
147
- const urls: string[] = [];
148
- const regex = /https?:\/\/[^\s<>()[\]'"]+/g;
149
- let match;
150
-
151
- while ((match = regex.exec(text)) !== null) {
152
- urls.push(match[0]);
153
- }
154
-
155
- return urls;
156
- }
@@ -1,191 +0,0 @@
1
- /**
2
- * README Parser
3
- * Extracts URLs and references from README and markdown files
4
- */
5
-
6
- export interface ExtractedReference {
7
- url: string;
8
- context: string; // Surrounding text
9
- line?: number;
10
- type: 'markdown-link' | 'bare-url' | 'reference-link';
11
- }
12
-
13
- // Patterns to skip (package managers, CI badges, shields.io, placeholders)
14
- const SKIP_PATTERNS = [
15
- /npmjs\.com\/package/,
16
- /pypi\.org\/project/,
17
- /crates\.io\/crates/,
18
- /rubygems\.org\/gems/,
19
- /packagist\.org\/packages/,
20
- /shields\.io/,
21
- /badge(s)?\..*\.svg/,
22
- /travis-ci\.(org|com)/,
23
- /circleci\.com/,
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
31
- ];
32
-
33
- /**
34
- * Parse README content and extract external references
35
- */
36
- export function parseReadme(content: string, filePath = 'README.md'): ExtractedReference[] {
37
- const references: ExtractedReference[] = [];
38
- const lines = content.split('\n');
39
-
40
- for (let i = 0; i < lines.length; i++) {
41
- const line = lines[i];
42
- if (!line) continue; // Skip undefined or empty lines
43
-
44
- const lineNumber = i + 1;
45
-
46
- // Extract markdown links [text](url)
47
- const markdownLinks = extractMarkdownLinks(line);
48
- for (const { url, text } of markdownLinks) {
49
- if (!shouldSkipUrl(url)) {
50
- references.push({
51
- url,
52
- context: text || line.trim(),
53
- line: lineNumber,
54
- type: 'markdown-link'
55
- });
56
- }
57
- }
58
-
59
- // Extract reference-style links [text]: url
60
- const referenceLinks = extractReferenceLinks(line);
61
- for (const { url, text } of referenceLinks) {
62
- if (!shouldSkipUrl(url)) {
63
- references.push({
64
- url,
65
- context: text || line.trim(),
66
- line: lineNumber,
67
- type: 'reference-link'
68
- });
69
- }
70
- }
71
-
72
- // Extract bare URLs
73
- const bareUrls = extractBareUrls(line);
74
- for (const url of bareUrls) {
75
- if (!shouldSkipUrl(url)) {
76
- references.push({
77
- url,
78
- context: line.trim(),
79
- line: lineNumber,
80
- type: 'bare-url'
81
- });
82
- }
83
- }
84
- }
85
-
86
- // Deduplicate by URL
87
- return deduplicateReferences(references);
88
- }
89
-
90
- function extractMarkdownLinks(line: string): Array<{ url: string; text: string }> {
91
- const links: Array<{ url: string; text: string }> = [];
92
- const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
93
- let match;
94
-
95
- while ((match = regex.exec(line)) !== null) {
96
- const text = match[1];
97
- const url = match[2];
98
- if (text !== undefined && url !== undefined) {
99
- links.push({ text, url });
100
- }
101
- }
102
-
103
- return links;
104
- }
105
-
106
- function extractReferenceLinks(line: string): Array<{ url: string; text: string }> {
107
- const links: Array<{ url: string; text: string }> = [];
108
- const regex = /^\[([^\]]+)\]:\s+(.+)$/;
109
- const match = regex.exec(line);
110
-
111
- if (match) {
112
- const text = match[1];
113
- const url = match[2];
114
- if (text !== undefined && url !== undefined) {
115
- links.push({ text, url });
116
- }
117
- }
118
-
119
- return links;
120
- }
121
-
122
- function extractBareUrls(line: string): string[] {
123
- const urls: string[] = [];
124
- const regex = /https?:\/\/[^\s<>()[\]]+/g;
125
- let match;
126
-
127
- while ((match = regex.exec(line)) !== null) {
128
- urls.push(match[0]);
129
- }
130
-
131
- return urls;
132
- }
133
-
134
- function shouldSkipUrl(url: string): boolean {
135
- return SKIP_PATTERNS.some((pattern) => pattern.test(url));
136
- }
137
-
138
- function deduplicateReferences(references: ExtractedReference[]): ExtractedReference[] {
139
- const seen = new Set<string>();
140
- return references.filter((ref) => {
141
- if (seen.has(ref.url)) {
142
- return false;
143
- }
144
- seen.add(ref.url);
145
- return true;
146
- });
147
- }
148
-
149
- /**
150
- * Extract GitHub repository mentions (owner/repo format)
151
- */
152
- export function extractGitHubReferences(
153
- content: string
154
- ): Array<{ owner: string; repo: string; context: string }> {
155
- const references: Array<{ owner: string; repo: string; context: string }> = [];
156
- const lines = content.split('\n');
157
-
158
- for (const line of lines) {
159
- // Match owner/repo pattern not in URLs, with basic length and context constraints
160
- const regex =
161
- /(?<!https?:\/\/github\.com\/)(?:^|[\s(])([a-zA-Z0-9_-]{2,}\/[a-zA-Z0-9_.-]{2,})(?=$|[\s),.;])/g;
162
- let match;
163
-
164
- while ((match = regex.exec(line)) !== null) {
165
- const ownerRepo = match[1];
166
- if (ownerRepo) {
167
- const parts = ownerRepo.split('/');
168
- const owner = parts[0];
169
- const repo = parts[1];
170
-
171
- if (
172
- owner &&
173
- repo &&
174
- owner.length >= 2 &&
175
- repo.length >= 2 &&
176
- owner !== 'owner' &&
177
- repo !== 'repo' &&
178
- !(/^\d+$/.test(owner) && /^\d+$/.test(repo))
179
- ) {
180
- references.push({
181
- owner,
182
- repo,
183
- context: line.trim()
184
- });
185
- }
186
- }
187
- }
188
- }
189
-
190
- return references;
191
- }
@@ -1,169 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { Detector } from '../src/detector.js';
6
-
7
- const createDetector = (): Detector =>
8
- new Detector({
9
- repoPath: '.',
10
- llmProvider: {
11
- analyze: vi.fn(async () => ({
12
- dependencies: [],
13
- usage: { totalTokens: 0, latencyMs: 0 },
14
- rawResponse: '{}'
15
- }))
16
- }
17
- });
18
-
19
- describe('Detector', () => {
20
- beforeEach(() => {
21
- vi.clearAllMocks();
22
- });
23
-
24
- describe('detectDependencies', () => {
25
- it('should orchestrate all parsers', async () => {
26
- // Expected: Call README parser, code comment parser, package file parser
27
- expect(true).toBe(true);
28
- });
29
-
30
- it('should aggregate results from all parsers', async () => {
31
- // Expected: Combine results from multiple parsers
32
- expect(true).toBe(true);
33
- });
34
-
35
- it('should send aggregated content to LLM for analysis', async () => {
36
- // Expected: Pass extracted content to LLM provider
37
- expect(true).toBe(true);
38
- });
39
-
40
- it('should deduplicate dependencies by URL', async () => {
41
- // Expected: Same URL from multiple sources = one dependency
42
- expect(true).toBe(true);
43
- });
44
-
45
- it('should calculate confidence scores', async () => {
46
- // Expected: LLM confidence * detection method weight
47
- expect(true).toBe(true);
48
- });
49
-
50
- it('should generate UUIDs for each dependency', async () => {
51
- expect(true).toBe(true);
52
- });
53
-
54
- it('should include detection metadata', async () => {
55
- // Expected: detectionMethod, detectedAt, referencedIn
56
- expect(true).toBe(true);
57
- });
58
-
59
- it('should handle empty repository', async () => {
60
- // Expected: Return empty dependencies array
61
- expect(true).toBe(true);
62
- });
63
-
64
- it('should handle LLM failures gracefully', async () => {
65
- // Expected: Continue with parser results, log error
66
- expect(true).toBe(true);
67
- });
68
-
69
- it('should keep GitHub repo links when LLM parser misses them', async () => {
70
- const repoPath = await mkdtemp(join(tmpdir(), 'dependabit-detector-'));
71
-
72
- try {
73
- await mkdir(join(repoPath, 'docs'), { recursive: true });
74
- await writeFile(
75
- join(repoPath, 'docs', 'tooling.md'),
76
- '- [spec-kit](https://github.com/github/spec-kit)\n'
77
- );
78
-
79
- const detector = new Detector({
80
- repoPath,
81
- llmProvider: {
82
- analyze: vi.fn(async () => ({
83
- dependencies: [],
84
- usage: { totalTokens: 0, latencyMs: 0 },
85
- rawResponse: '{}'
86
- })),
87
- getSupportedModels: vi.fn(() => ['test-model']),
88
- getRateLimit: vi.fn(async () => ({
89
- remaining: 100,
90
- limit: 100,
91
- resetAt: new Date(Date.now() + 60_000)
92
- })),
93
- validateConfig: vi.fn(() => true)
94
- },
95
- repoOwner: 'pradeepmouli',
96
- repoName: 'dependabit'
97
- });
98
-
99
- const result = await detector.detectDependencies();
100
- const dep = result.dependencies.find((d) => d.url === 'https://github.com/github/spec-kit');
101
-
102
- expect(dep).toBeDefined();
103
- expect(dep?.type).toBe('reference-implementation');
104
- expect(dep?.accessMethod).toBe('github-api');
105
- } finally {
106
- await rm(repoPath, { recursive: true, force: true });
107
- }
108
- });
109
- });
110
-
111
- describe('classifyDependency', () => {
112
- it('should classify GitHub URLs as repository', () => {
113
- const url = 'https://github.com/owner/repo';
114
- expect(url).toContain('github.com');
115
- });
116
-
117
- it('should classify GitHub repository links as reference-implementation', () => {
118
- const detector = createDetector();
119
- const type = (detector as any).determineDependencyType(
120
- 'https://github.com/github/spec-kit',
121
- 'spec-kit'
122
- );
123
-
124
- expect(type).toBe('reference-implementation');
125
- });
126
-
127
- it('should classify arXiv URLs as research-paper', () => {
128
- const url = 'https://arxiv.org/abs/1706.03762';
129
- expect(url).toContain('arxiv.org');
130
- });
131
-
132
- it('should classify OpenAPI specs as schema', () => {
133
- const url = 'https://api.example.com/openapi.yaml';
134
- expect(url).toContain('openapi');
135
- });
136
-
137
- it('should classify documentation sites as documentation', () => {
138
- const url = 'https://docs.example.com/guide';
139
- expect(url).toContain('docs.');
140
- });
141
-
142
- it('should use LLM for ambiguous URLs', () => {
143
- const url = 'https://example.com/some-resource';
144
- expect(typeof url).toBe('string');
145
- });
146
- });
147
-
148
- describe('determineAccessMethod', () => {
149
- it('should use github-api for GitHub URLs', () => {
150
- const url = 'https://github.com/owner/repo';
151
- expect(url).toContain('github.com');
152
- });
153
-
154
- it('should use arxiv for arXiv URLs', () => {
155
- const url = 'https://arxiv.org/abs/1234.5678';
156
- expect(url).toContain('arxiv.org');
157
- });
158
-
159
- it('should use openapi for OpenAPI spec URLs', () => {
160
- const url = 'https://api.example.com/openapi.json';
161
- expect(url).toContain('openapi');
162
- });
163
-
164
- it('should use http as fallback', () => {
165
- const url = 'https://example.com/docs';
166
- expect(url).toContain('http');
167
- });
168
- });
169
- });