busy-cli 0.1.2
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/README.md +129 -0
- package/dist/builders/context.d.ts +50 -0
- package/dist/builders/context.d.ts.map +1 -0
- package/dist/builders/context.js +190 -0
- package/dist/cache/index.d.ts +100 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +270 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +463 -0
- package/dist/commands/package.d.ts +96 -0
- package/dist/commands/package.d.ts.map +1 -0
- package/dist/commands/package.js +285 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/loader.d.ts +6 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +361 -0
- package/dist/merge.d.ts +16 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +102 -0
- package/dist/package/manifest.d.ts +59 -0
- package/dist/package/manifest.d.ts.map +1 -0
- package/dist/package/manifest.js +265 -0
- package/dist/parser.d.ts +28 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +220 -0
- package/dist/parsers/frontmatter.d.ts +14 -0
- package/dist/parsers/frontmatter.d.ts.map +1 -0
- package/dist/parsers/frontmatter.js +110 -0
- package/dist/parsers/imports.d.ts +48 -0
- package/dist/parsers/imports.d.ts.map +1 -0
- package/dist/parsers/imports.js +147 -0
- package/dist/parsers/links.d.ts +12 -0
- package/dist/parsers/links.d.ts.map +1 -0
- package/dist/parsers/links.js +79 -0
- package/dist/parsers/localdefs.d.ts +6 -0
- package/dist/parsers/localdefs.d.ts.map +1 -0
- package/dist/parsers/localdefs.js +132 -0
- package/dist/parsers/operations.d.ts +32 -0
- package/dist/parsers/operations.d.ts.map +1 -0
- package/dist/parsers/operations.js +313 -0
- package/dist/parsers/sections.d.ts +15 -0
- package/dist/parsers/sections.d.ts.map +1 -0
- package/dist/parsers/sections.js +173 -0
- package/dist/parsers/tools.d.ts +30 -0
- package/dist/parsers/tools.d.ts.map +1 -0
- package/dist/parsers/tools.js +178 -0
- package/dist/parsers/triggers.d.ts +35 -0
- package/dist/parsers/triggers.d.ts.map +1 -0
- package/dist/parsers/triggers.js +219 -0
- package/dist/providers/base.d.ts +60 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +34 -0
- package/dist/providers/github.d.ts +18 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +109 -0
- package/dist/providers/gitlab.d.ts +18 -0
- package/dist/providers/gitlab.d.ts.map +1 -0
- package/dist/providers/gitlab.js +101 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +17 -0
- package/dist/providers/local.d.ts +31 -0
- package/dist/providers/local.d.ts.map +1 -0
- package/dist/providers/local.js +116 -0
- package/dist/providers/url.d.ts +16 -0
- package/dist/providers/url.d.ts.map +1 -0
- package/dist/providers/url.js +45 -0
- package/dist/registry/index.d.ts +99 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +320 -0
- package/dist/types/schema.d.ts +3259 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +258 -0
- package/dist/utils/logger.d.ts +19 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +23 -0
- package/dist/utils/slugify.d.ts +14 -0
- package/dist/utils/slugify.d.ts.map +1 -0
- package/dist/utils/slugify.js +28 -0
- package/package.json +61 -0
- package/src/__tests__/cache.test.ts +393 -0
- package/src/__tests__/cli-package.test.ts +667 -0
- package/src/__tests__/fixtures/automated-workflow.busy.md +84 -0
- package/src/__tests__/fixtures/concept.busy.md +30 -0
- package/src/__tests__/fixtures/document.busy.md +44 -0
- package/src/__tests__/fixtures/simple-operation.busy.md +45 -0
- package/src/__tests__/fixtures/tool-document.busy.md +71 -0
- package/src/__tests__/fixtures/tool.busy.md +54 -0
- package/src/__tests__/imports.test.ts +244 -0
- package/src/__tests__/integration.test.ts +432 -0
- package/src/__tests__/operations.test.ts +408 -0
- package/src/__tests__/package-manifest.test.ts +455 -0
- package/src/__tests__/providers.test.ts +672 -0
- package/src/__tests__/registry.test.ts +402 -0
- package/src/__tests__/schema.test.ts +467 -0
- package/src/__tests__/tools.test.ts +376 -0
- package/src/__tests__/triggers.test.ts +312 -0
- package/src/builders/context.ts +294 -0
- package/src/cache/index.ts +312 -0
- package/src/cli/index.ts +514 -0
- package/src/commands/package.ts +392 -0
- package/src/index.ts +46 -0
- package/src/loader.ts +474 -0
- package/src/merge.ts +126 -0
- package/src/package/manifest.ts +349 -0
- package/src/parser.ts +278 -0
- package/src/parsers/frontmatter.ts +135 -0
- package/src/parsers/imports.ts +196 -0
- package/src/parsers/links.ts +108 -0
- package/src/parsers/localdefs.ts +166 -0
- package/src/parsers/operations.ts +404 -0
- package/src/parsers/sections.ts +230 -0
- package/src/parsers/tools.ts +215 -0
- package/src/parsers/triggers.ts +252 -0
- package/src/providers/base.ts +77 -0
- package/src/providers/github.ts +129 -0
- package/src/providers/gitlab.ts +121 -0
- package/src/providers/index.ts +25 -0
- package/src/providers/local.ts +129 -0
- package/src/providers/url.ts +56 -0
- package/src/registry/index.ts +408 -0
- package/src/types/schema.ts +369 -0
- package/src/utils/logger.ts +25 -0
- package/src/utils/slugify.ts +31 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider System Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for URL providers (GitHub, GitLab, generic URL) and ProviderRegistry.
|
|
5
|
+
* Following TDD approach for package manager implementation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
Provider,
|
|
11
|
+
ParsedURL,
|
|
12
|
+
ProviderRegistry,
|
|
13
|
+
providerRegistry,
|
|
14
|
+
} from '../providers/base.js';
|
|
15
|
+
import { GitHubProvider, githubProvider } from '../providers/github.js';
|
|
16
|
+
import { GitLabProvider, gitlabProvider } from '../providers/gitlab.js';
|
|
17
|
+
import { URLProvider, urlProvider } from '../providers/url.js';
|
|
18
|
+
|
|
19
|
+
describe('ProviderRegistry', () => {
|
|
20
|
+
let registry: ProviderRegistry;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
registry = new ProviderRegistry();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should start with no providers', () => {
|
|
27
|
+
expect(registry.getProviders()).toHaveLength(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should register a provider', () => {
|
|
31
|
+
const mockProvider: Provider = {
|
|
32
|
+
name: 'test',
|
|
33
|
+
matches: () => true,
|
|
34
|
+
parse: () => ({ provider: 'test', path: '/test' }),
|
|
35
|
+
getRawUrl: () => 'https://example.com/raw',
|
|
36
|
+
fetch: async () => 'content',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
registry.register(mockProvider);
|
|
40
|
+
expect(registry.getProviders()).toHaveLength(1);
|
|
41
|
+
expect(registry.getProviders()[0].name).toBe('test');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should find provider that matches URL', () => {
|
|
45
|
+
const githubMock: Provider = {
|
|
46
|
+
name: 'github',
|
|
47
|
+
matches: (url) => url.includes('github.com'),
|
|
48
|
+
parse: () => ({ provider: 'github', path: '/test' }),
|
|
49
|
+
getRawUrl: () => 'https://raw.githubusercontent.com/test',
|
|
50
|
+
fetch: async () => 'content',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const gitlabMock: Provider = {
|
|
54
|
+
name: 'gitlab',
|
|
55
|
+
matches: (url) => url.includes('gitlab.com'),
|
|
56
|
+
parse: () => ({ provider: 'gitlab', path: '/test' }),
|
|
57
|
+
getRawUrl: () => 'https://gitlab.com/raw',
|
|
58
|
+
fetch: async () => 'content',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
registry.register(githubMock);
|
|
62
|
+
registry.register(gitlabMock);
|
|
63
|
+
|
|
64
|
+
const githubResult = registry.findProvider('https://github.com/org/repo');
|
|
65
|
+
expect(githubResult?.name).toBe('github');
|
|
66
|
+
|
|
67
|
+
const gitlabResult = registry.findProvider('https://gitlab.com/org/repo');
|
|
68
|
+
expect(gitlabResult?.name).toBe('gitlab');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return undefined when no provider matches', () => {
|
|
72
|
+
const result = registry.findProvider('https://bitbucket.org/org/repo');
|
|
73
|
+
expect(result).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return first matching provider (order matters)', () => {
|
|
77
|
+
const provider1: Provider = {
|
|
78
|
+
name: 'first',
|
|
79
|
+
matches: () => true,
|
|
80
|
+
parse: () => ({ provider: 'first', path: '/test' }),
|
|
81
|
+
getRawUrl: () => 'https://first.com',
|
|
82
|
+
fetch: async () => 'content',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const provider2: Provider = {
|
|
86
|
+
name: 'second',
|
|
87
|
+
matches: () => true,
|
|
88
|
+
parse: () => ({ provider: 'second', path: '/test' }),
|
|
89
|
+
getRawUrl: () => 'https://second.com',
|
|
90
|
+
fetch: async () => 'content',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
registry.register(provider1);
|
|
94
|
+
registry.register(provider2);
|
|
95
|
+
|
|
96
|
+
const result = registry.findProvider('https://any.url');
|
|
97
|
+
expect(result?.name).toBe('first');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('GitHubProvider', () => {
|
|
102
|
+
let provider: GitHubProvider;
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
provider = new GitHubProvider();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('matches', () => {
|
|
109
|
+
it('should match github.com URLs', () => {
|
|
110
|
+
expect(provider.matches('https://github.com/org/repo')).toBe(true);
|
|
111
|
+
expect(provider.matches('https://github.com/org/repo/blob/main/file.md')).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should match raw.githubusercontent.com URLs', () => {
|
|
115
|
+
expect(provider.matches('https://raw.githubusercontent.com/org/repo/main/file.md')).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should not match other URLs', () => {
|
|
119
|
+
expect(provider.matches('https://gitlab.com/org/repo')).toBe(false);
|
|
120
|
+
expect(provider.matches('https://example.com/github.com')).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('parse', () => {
|
|
125
|
+
it('should parse blob URLs', () => {
|
|
126
|
+
const url = 'https://github.com/Bravo-Tensor/busy-lang/blob/main/busy-v2/core/document.busy.md';
|
|
127
|
+
const result = provider.parse(url);
|
|
128
|
+
|
|
129
|
+
expect(result.provider).toBe('github');
|
|
130
|
+
expect(result.org).toBe('Bravo-Tensor');
|
|
131
|
+
expect(result.repo).toBe('busy-lang');
|
|
132
|
+
expect(result.ref).toBe('main');
|
|
133
|
+
expect(result.path).toBe('busy-v2/core/document.busy.md');
|
|
134
|
+
expect(result.anchor).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should parse blob URLs with version tags', () => {
|
|
138
|
+
const url = 'https://github.com/org/repo/blob/v1.0.0/path/file.md';
|
|
139
|
+
const result = provider.parse(url);
|
|
140
|
+
|
|
141
|
+
expect(result.ref).toBe('v1.0.0');
|
|
142
|
+
expect(result.path).toBe('path/file.md');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should parse tree URLs (directories)', () => {
|
|
146
|
+
const url = 'https://github.com/org/repo/tree/main/src/components';
|
|
147
|
+
const result = provider.parse(url);
|
|
148
|
+
|
|
149
|
+
expect(result.provider).toBe('github');
|
|
150
|
+
expect(result.org).toBe('org');
|
|
151
|
+
expect(result.repo).toBe('repo');
|
|
152
|
+
expect(result.ref).toBe('main');
|
|
153
|
+
expect(result.path).toBe('src/components');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should parse raw.githubusercontent.com URLs', () => {
|
|
157
|
+
const url = 'https://raw.githubusercontent.com/org/repo/main/README.md';
|
|
158
|
+
const result = provider.parse(url);
|
|
159
|
+
|
|
160
|
+
expect(result.provider).toBe('github');
|
|
161
|
+
expect(result.org).toBe('org');
|
|
162
|
+
expect(result.repo).toBe('repo');
|
|
163
|
+
expect(result.ref).toBe('main');
|
|
164
|
+
expect(result.path).toBe('README.md');
|
|
165
|
+
expect(result.rawUrl).toBe(url);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should extract anchor from URL', () => {
|
|
169
|
+
const url = 'https://github.com/org/repo/blob/main/file.md#section-heading';
|
|
170
|
+
const result = provider.parse(url);
|
|
171
|
+
|
|
172
|
+
expect(result.path).toBe('file.md');
|
|
173
|
+
expect(result.anchor).toBe('section-heading');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should throw for invalid GitHub URLs', () => {
|
|
177
|
+
expect(() => provider.parse('https://github.com/invalid')).toThrow();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('getRawUrl', () => {
|
|
182
|
+
it('should convert parsed URL to raw URL', () => {
|
|
183
|
+
const parsed: ParsedURL = {
|
|
184
|
+
provider: 'github',
|
|
185
|
+
org: 'org',
|
|
186
|
+
repo: 'repo',
|
|
187
|
+
ref: 'main',
|
|
188
|
+
path: 'src/file.md',
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const rawUrl = provider.getRawUrl(parsed);
|
|
192
|
+
expect(rawUrl).toBe('https://raw.githubusercontent.com/org/repo/main/src/file.md');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should return existing rawUrl if present', () => {
|
|
196
|
+
const parsed: ParsedURL = {
|
|
197
|
+
provider: 'github',
|
|
198
|
+
org: 'org',
|
|
199
|
+
repo: 'repo',
|
|
200
|
+
ref: 'main',
|
|
201
|
+
path: 'file.md',
|
|
202
|
+
rawUrl: 'https://raw.githubusercontent.com/org/repo/main/file.md',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const rawUrl = provider.getRawUrl(parsed);
|
|
206
|
+
expect(rawUrl).toBe(parsed.rawUrl);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('fetch', () => {
|
|
211
|
+
let originalFetch: typeof global.fetch;
|
|
212
|
+
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
originalFetch = global.fetch;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
afterEach(() => {
|
|
218
|
+
global.fetch = originalFetch;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should fetch content from raw URL', async () => {
|
|
222
|
+
const mockContent = '# Test Content\n\nThis is test content.';
|
|
223
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
224
|
+
ok: true,
|
|
225
|
+
text: () => Promise.resolve(mockContent),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const url = 'https://github.com/org/repo/blob/main/README.md';
|
|
229
|
+
const result = await provider.fetch(url);
|
|
230
|
+
|
|
231
|
+
expect(result).toBe(mockContent);
|
|
232
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
233
|
+
'https://raw.githubusercontent.com/org/repo/main/README.md'
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should throw on fetch failure', async () => {
|
|
238
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
239
|
+
ok: false,
|
|
240
|
+
status: 404,
|
|
241
|
+
statusText: 'Not Found',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const url = 'https://github.com/org/repo/blob/main/nonexistent.md';
|
|
245
|
+
await expect(provider.fetch(url)).rejects.toThrow('Failed to fetch');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('getLatestVersion', () => {
|
|
250
|
+
let originalFetch: typeof global.fetch;
|
|
251
|
+
|
|
252
|
+
beforeEach(() => {
|
|
253
|
+
originalFetch = global.fetch;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
afterEach(() => {
|
|
257
|
+
global.fetch = originalFetch;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should return latest tag from GitHub API', async () => {
|
|
261
|
+
const mockTags = [
|
|
262
|
+
{ name: 'v1.2.0' },
|
|
263
|
+
{ name: 'v1.1.0' },
|
|
264
|
+
{ name: 'v1.0.0' },
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
268
|
+
ok: true,
|
|
269
|
+
json: () => Promise.resolve(mockTags),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const parsed: ParsedURL = {
|
|
273
|
+
provider: 'github',
|
|
274
|
+
org: 'org',
|
|
275
|
+
repo: 'repo',
|
|
276
|
+
ref: 'main',
|
|
277
|
+
path: 'file.md',
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const version = await provider.getLatestVersion(parsed);
|
|
281
|
+
expect(version).toBe('v1.2.0');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should throw when no tags found', async () => {
|
|
285
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
286
|
+
ok: true,
|
|
287
|
+
json: () => Promise.resolve([]),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const parsed: ParsedURL = {
|
|
291
|
+
provider: 'github',
|
|
292
|
+
org: 'org',
|
|
293
|
+
repo: 'repo',
|
|
294
|
+
ref: 'main',
|
|
295
|
+
path: 'file.md',
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
await expect(provider.getLatestVersion(parsed)).rejects.toThrow('No tags found');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should throw on API failure', async () => {
|
|
302
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
303
|
+
ok: false,
|
|
304
|
+
status: 403,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const parsed: ParsedURL = {
|
|
308
|
+
provider: 'github',
|
|
309
|
+
org: 'org',
|
|
310
|
+
repo: 'repo',
|
|
311
|
+
ref: 'main',
|
|
312
|
+
path: 'file.md',
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
await expect(provider.getLatestVersion(parsed)).rejects.toThrow('Failed to fetch tags');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('GitLabProvider', () => {
|
|
321
|
+
let provider: GitLabProvider;
|
|
322
|
+
|
|
323
|
+
beforeEach(() => {
|
|
324
|
+
provider = new GitLabProvider();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe('matches', () => {
|
|
328
|
+
it('should match gitlab.com URLs', () => {
|
|
329
|
+
expect(provider.matches('https://gitlab.com/org/repo')).toBe(true);
|
|
330
|
+
expect(provider.matches('https://gitlab.com/org/repo/-/blob/main/file.md')).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should not match other URLs', () => {
|
|
334
|
+
expect(provider.matches('https://github.com/org/repo')).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('parse', () => {
|
|
339
|
+
it('should parse blob URLs', () => {
|
|
340
|
+
const url = 'https://gitlab.com/org/repo/-/blob/main/src/file.md';
|
|
341
|
+
const result = provider.parse(url);
|
|
342
|
+
|
|
343
|
+
expect(result.provider).toBe('gitlab');
|
|
344
|
+
expect(result.org).toBe('org');
|
|
345
|
+
expect(result.repo).toBe('repo');
|
|
346
|
+
expect(result.ref).toBe('main');
|
|
347
|
+
expect(result.path).toBe('src/file.md');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should parse tree URLs', () => {
|
|
351
|
+
const url = 'https://gitlab.com/org/repo/-/tree/main/src';
|
|
352
|
+
const result = provider.parse(url);
|
|
353
|
+
|
|
354
|
+
expect(result.provider).toBe('gitlab');
|
|
355
|
+
expect(result.path).toBe('src');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should parse raw URLs', () => {
|
|
359
|
+
const url = 'https://gitlab.com/org/repo/-/raw/main/file.md';
|
|
360
|
+
const result = provider.parse(url);
|
|
361
|
+
|
|
362
|
+
expect(result.provider).toBe('gitlab');
|
|
363
|
+
expect(result.rawUrl).toBe(url);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should extract anchor from URL', () => {
|
|
367
|
+
const url = 'https://gitlab.com/org/repo/-/blob/main/file.md#heading';
|
|
368
|
+
const result = provider.parse(url);
|
|
369
|
+
|
|
370
|
+
expect(result.anchor).toBe('heading');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should throw for invalid GitLab URLs', () => {
|
|
374
|
+
expect(() => provider.parse('https://gitlab.com/invalid')).toThrow();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('getRawUrl', () => {
|
|
379
|
+
it('should convert parsed URL to raw URL', () => {
|
|
380
|
+
const parsed: ParsedURL = {
|
|
381
|
+
provider: 'gitlab',
|
|
382
|
+
org: 'org',
|
|
383
|
+
repo: 'repo',
|
|
384
|
+
ref: 'main',
|
|
385
|
+
path: 'src/file.md',
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const rawUrl = provider.getRawUrl(parsed);
|
|
389
|
+
expect(rawUrl).toBe('https://gitlab.com/org/repo/-/raw/main/src/file.md');
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('fetch', () => {
|
|
394
|
+
let originalFetch: typeof global.fetch;
|
|
395
|
+
|
|
396
|
+
beforeEach(() => {
|
|
397
|
+
originalFetch = global.fetch;
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
afterEach(() => {
|
|
401
|
+
global.fetch = originalFetch;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should fetch content from raw URL', async () => {
|
|
405
|
+
const mockContent = '# GitLab Content';
|
|
406
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
407
|
+
ok: true,
|
|
408
|
+
text: () => Promise.resolve(mockContent),
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const url = 'https://gitlab.com/org/repo/-/blob/main/file.md';
|
|
412
|
+
const result = await provider.fetch(url);
|
|
413
|
+
|
|
414
|
+
expect(result).toBe(mockContent);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
describe('getLatestVersion', () => {
|
|
419
|
+
let originalFetch: typeof global.fetch;
|
|
420
|
+
|
|
421
|
+
beforeEach(() => {
|
|
422
|
+
originalFetch = global.fetch;
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
afterEach(() => {
|
|
426
|
+
global.fetch = originalFetch;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should return latest tag from GitLab API', async () => {
|
|
430
|
+
const mockTags = [{ name: 'v2.0.0' }, { name: 'v1.0.0' }];
|
|
431
|
+
|
|
432
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
433
|
+
ok: true,
|
|
434
|
+
json: () => Promise.resolve(mockTags),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const parsed: ParsedURL = {
|
|
438
|
+
provider: 'gitlab',
|
|
439
|
+
org: 'org',
|
|
440
|
+
repo: 'repo',
|
|
441
|
+
ref: 'main',
|
|
442
|
+
path: 'file.md',
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const version = await provider.getLatestVersion(parsed);
|
|
446
|
+
expect(version).toBe('v2.0.0');
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe('URLProvider', () => {
|
|
452
|
+
let provider: URLProvider;
|
|
453
|
+
|
|
454
|
+
beforeEach(() => {
|
|
455
|
+
provider = new URLProvider();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe('matches', () => {
|
|
459
|
+
it('should match HTTP URLs', () => {
|
|
460
|
+
expect(provider.matches('http://example.com/file.md')).toBe(true);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should match HTTPS URLs', () => {
|
|
464
|
+
expect(provider.matches('https://example.com/file.md')).toBe(true);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should not match non-HTTP URLs', () => {
|
|
468
|
+
expect(provider.matches('ftp://example.com/file.md')).toBe(false);
|
|
469
|
+
expect(provider.matches('/local/path/file.md')).toBe(false);
|
|
470
|
+
expect(provider.matches('./relative/path.md')).toBe(false);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe('parse', () => {
|
|
475
|
+
it('should parse basic URL', () => {
|
|
476
|
+
const url = 'https://example.com/path/to/file.md';
|
|
477
|
+
const result = provider.parse(url);
|
|
478
|
+
|
|
479
|
+
expect(result.provider).toBe('url');
|
|
480
|
+
expect(result.path).toBe('/path/to/file.md');
|
|
481
|
+
expect(result.rawUrl).toBe(url);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should extract anchor from URL', () => {
|
|
485
|
+
const url = 'https://example.com/file.md#section';
|
|
486
|
+
const result = provider.parse(url);
|
|
487
|
+
|
|
488
|
+
expect(result.anchor).toBe('section');
|
|
489
|
+
expect(result.rawUrl).toBe('https://example.com/file.md');
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
describe('getRawUrl', () => {
|
|
494
|
+
it('should return rawUrl if present', () => {
|
|
495
|
+
const parsed: ParsedURL = {
|
|
496
|
+
provider: 'url',
|
|
497
|
+
path: '/file.md',
|
|
498
|
+
rawUrl: 'https://example.com/file.md',
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
expect(provider.getRawUrl(parsed)).toBe('https://example.com/file.md');
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('should fallback to path if no rawUrl', () => {
|
|
505
|
+
const parsed: ParsedURL = {
|
|
506
|
+
provider: 'url',
|
|
507
|
+
path: '/file.md',
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
expect(provider.getRawUrl(parsed)).toBe('/file.md');
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe('fetch', () => {
|
|
515
|
+
let originalFetch: typeof global.fetch;
|
|
516
|
+
|
|
517
|
+
beforeEach(() => {
|
|
518
|
+
originalFetch = global.fetch;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
afterEach(() => {
|
|
522
|
+
global.fetch = originalFetch;
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should fetch content from URL', async () => {
|
|
526
|
+
const mockContent = 'Remote content';
|
|
527
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
528
|
+
ok: true,
|
|
529
|
+
text: () => Promise.resolve(mockContent),
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const url = 'https://example.com/file.md';
|
|
533
|
+
const result = await provider.fetch(url);
|
|
534
|
+
|
|
535
|
+
expect(result).toBe(mockContent);
|
|
536
|
+
expect(global.fetch).toHaveBeenCalledWith(url);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
describe('getLatestVersion', () => {
|
|
541
|
+
it('should throw as version resolution is not supported', async () => {
|
|
542
|
+
const parsed: ParsedURL = {
|
|
543
|
+
provider: 'url',
|
|
544
|
+
path: '/file.md',
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
await expect(provider.getLatestVersion(parsed)).rejects.toThrow(
|
|
548
|
+
'Generic URL provider does not support version resolution'
|
|
549
|
+
);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe('Global Provider Registry', () => {
|
|
555
|
+
it('should have GitHub provider registered', () => {
|
|
556
|
+
const provider = providerRegistry.findProvider('https://github.com/org/repo/blob/main/file.md');
|
|
557
|
+
expect(provider?.name).toBe('github');
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('should have GitLab provider registered', () => {
|
|
561
|
+
const provider = providerRegistry.findProvider('https://gitlab.com/org/repo/-/blob/main/file.md');
|
|
562
|
+
expect(provider?.name).toBe('gitlab');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('should have URL provider as fallback', () => {
|
|
566
|
+
const provider = providerRegistry.findProvider('https://example.com/file.md');
|
|
567
|
+
expect(provider?.name).toBe('url');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should prioritize specific providers over generic URL provider', () => {
|
|
571
|
+
// GitHub should match before URL provider
|
|
572
|
+
const githubProvider = providerRegistry.findProvider('https://github.com/org/repo/blob/main/file.md');
|
|
573
|
+
expect(githubProvider?.name).toBe('github');
|
|
574
|
+
|
|
575
|
+
// GitLab should match before URL provider
|
|
576
|
+
const gitlabProvider = providerRegistry.findProvider('https://gitlab.com/org/repo/-/blob/main/file.md');
|
|
577
|
+
expect(gitlabProvider?.name).toBe('gitlab');
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
describe('Provider Interface Compliance', () => {
|
|
582
|
+
const providers = [
|
|
583
|
+
{ name: 'GitHub', instance: new GitHubProvider() },
|
|
584
|
+
{ name: 'GitLab', instance: new GitLabProvider() },
|
|
585
|
+
{ name: 'URL', instance: new URLProvider() },
|
|
586
|
+
];
|
|
587
|
+
|
|
588
|
+
providers.forEach(({ name, instance }) => {
|
|
589
|
+
describe(`${name}Provider`, () => {
|
|
590
|
+
it('should have required name property', () => {
|
|
591
|
+
expect(typeof instance.name).toBe('string');
|
|
592
|
+
expect(instance.name.length).toBeGreaterThan(0);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('should implement matches method', () => {
|
|
596
|
+
expect(typeof instance.matches).toBe('function');
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should implement parse method', () => {
|
|
600
|
+
expect(typeof instance.parse).toBe('function');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('should implement getRawUrl method', () => {
|
|
604
|
+
expect(typeof instance.getRawUrl).toBe('function');
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('should implement fetch method', () => {
|
|
608
|
+
expect(typeof instance.fetch).toBe('function');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should implement getLatestVersion method', () => {
|
|
612
|
+
expect(typeof instance.getLatestVersion).toBe('function');
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
describe('Edge Cases', () => {
|
|
619
|
+
describe('GitHub URL edge cases', () => {
|
|
620
|
+
const provider = new GitHubProvider();
|
|
621
|
+
|
|
622
|
+
it('should handle URLs with special characters in path', () => {
|
|
623
|
+
const url = 'https://github.com/org/repo/blob/main/path%20with%20spaces/file.md';
|
|
624
|
+
// This may or may not work depending on implementation
|
|
625
|
+
// We're testing that it doesn't throw unexpectedly
|
|
626
|
+
expect(() => provider.parse(url)).not.toThrow();
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('should handle commit SHA as ref', () => {
|
|
630
|
+
const url = 'https://github.com/org/repo/blob/abc123def456/file.md';
|
|
631
|
+
const result = provider.parse(url);
|
|
632
|
+
expect(result.ref).toBe('abc123def456');
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('should handle branch names with slashes', () => {
|
|
636
|
+
const url = 'https://github.com/org/repo/blob/feature/my-feature/src/file.md';
|
|
637
|
+
const result = provider.parse(url);
|
|
638
|
+
// The ref might include the full path after blob/
|
|
639
|
+
// This tests current behavior
|
|
640
|
+
expect(result.ref).toBe('feature');
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
describe('GitLab URL edge cases', () => {
|
|
645
|
+
const provider = new GitLabProvider();
|
|
646
|
+
|
|
647
|
+
it('should handle nested groups', () => {
|
|
648
|
+
// GitLab supports nested groups like gitlab.com/group/subgroup/repo
|
|
649
|
+
// Current implementation may not fully support this
|
|
650
|
+
const url = 'https://gitlab.com/group/repo/-/blob/main/file.md';
|
|
651
|
+
const result = provider.parse(url);
|
|
652
|
+
expect(result.org).toBe('group');
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
describe('URL Provider edge cases', () => {
|
|
657
|
+
const provider = new URLProvider();
|
|
658
|
+
|
|
659
|
+
it('should handle URLs with query parameters', () => {
|
|
660
|
+
const url = 'https://example.com/file.md?version=1';
|
|
661
|
+
const result = provider.parse(url);
|
|
662
|
+
expect(result.provider).toBe('url');
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('should handle URLs with multiple anchors (only first used)', () => {
|
|
666
|
+
const url = 'https://example.com/file.md#section#subsection';
|
|
667
|
+
const result = provider.parse(url);
|
|
668
|
+
// Behavior depends on implementation
|
|
669
|
+
expect(result.anchor).toBeDefined();
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
});
|