korekt-cli 0.2.0
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 +130 -0
- package/package.json +50 -0
- package/src/config.js +75 -0
- package/src/formatter.js +143 -0
- package/src/git-logic.js +440 -0
- package/src/git-logic.test.js +542 -0
- package/src/index.js +398 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
parseNameStatus,
|
|
4
|
+
findJiraTicketIds,
|
|
5
|
+
findAdoTicketIds,
|
|
6
|
+
extractTicketIds,
|
|
7
|
+
runLocalReview,
|
|
8
|
+
runUncommittedReview,
|
|
9
|
+
truncateContent,
|
|
10
|
+
normalizeRepoUrl,
|
|
11
|
+
shouldIgnoreFile,
|
|
12
|
+
} from './git-logic.js';
|
|
13
|
+
import { execa } from 'execa';
|
|
14
|
+
|
|
15
|
+
describe('parseNameStatus', () => {
|
|
16
|
+
it('should correctly parse M, A, and D statuses', () => {
|
|
17
|
+
const input = 'M\tsrc/main.js\nA\tsrc/utils.js\nD\tdocs/old.md';
|
|
18
|
+
const expected = [
|
|
19
|
+
{ status: 'M', path: 'src/main.js', oldPath: 'src/main.js' },
|
|
20
|
+
{ status: 'A', path: 'src/utils.js', oldPath: 'src/utils.js' },
|
|
21
|
+
{ status: 'D', path: 'docs/old.md', oldPath: 'docs/old.md' },
|
|
22
|
+
];
|
|
23
|
+
expect(parseNameStatus(input)).toEqual(expected);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should correctly parse R (renamed) status', () => {
|
|
27
|
+
const input = 'R100\tsrc/old.js\tsrc/new.js';
|
|
28
|
+
const expected = [{ status: 'R', path: 'src/new.js', oldPath: 'src/old.js' }];
|
|
29
|
+
expect(parseNameStatus(input)).toEqual(expected);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should correctly parse C (copied) status', () => {
|
|
33
|
+
const input = 'C095\tsrc/template.js\tsrc/copy.js';
|
|
34
|
+
const expected = [{ status: 'C', path: 'src/copy.js', oldPath: 'src/template.js' }];
|
|
35
|
+
expect(parseNameStatus(input)).toEqual(expected);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should handle multiple files with mixed statuses', () => {
|
|
39
|
+
const input = 'M\tsrc/index.js\nR100\told.js\tnew.js\nA\tREADME.md\nD\ttest.js';
|
|
40
|
+
const result = parseNameStatus(input);
|
|
41
|
+
expect(result).toHaveLength(4);
|
|
42
|
+
expect(result[0].status).toBe('M');
|
|
43
|
+
expect(result[1].status).toBe('R');
|
|
44
|
+
expect(result[2].status).toBe('A');
|
|
45
|
+
expect(result[3].status).toBe('D');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should handle empty input', () => {
|
|
49
|
+
expect(parseNameStatus('')).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('runUncommittedReview', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
vi.mock('execa');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should analyze staged changes only', async () => {
|
|
63
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
64
|
+
const command = [cmd, ...args].join(' ');
|
|
65
|
+
|
|
66
|
+
if (command.includes('remote get-url origin')) {
|
|
67
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
68
|
+
}
|
|
69
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
70
|
+
return { stdout: 'feature-branch' };
|
|
71
|
+
}
|
|
72
|
+
if (command.includes('diff --cached --name-status')) {
|
|
73
|
+
return { stdout: 'M\tfile.js' };
|
|
74
|
+
}
|
|
75
|
+
if (command.includes('diff --cached -U15 -- file.js')) {
|
|
76
|
+
return { stdout: 'diff --git a/file.js b/file.js\n+new line' };
|
|
77
|
+
}
|
|
78
|
+
if (command.includes('show HEAD:file.js')) {
|
|
79
|
+
return { stdout: 'old content' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = await runUncommittedReview('staged', null);
|
|
86
|
+
|
|
87
|
+
expect(result).toBeDefined();
|
|
88
|
+
expect(result.repo_url).toBe('https://github.com/user/repo'); // Normalized (no .git)
|
|
89
|
+
expect(result.source_branch).toBe('feature-branch');
|
|
90
|
+
expect(result.commit_messages).toEqual([]);
|
|
91
|
+
expect(result.changed_files).toHaveLength(1);
|
|
92
|
+
expect(result.changed_files[0].path).toBe('file.js');
|
|
93
|
+
expect(result.changed_files[0].status).toBe('M');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should analyze unstaged changes only', async () => {
|
|
97
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
98
|
+
const command = [cmd, ...args].join(' ');
|
|
99
|
+
|
|
100
|
+
if (command.includes('remote get-url origin')) {
|
|
101
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
102
|
+
}
|
|
103
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
104
|
+
return { stdout: 'feature-branch' };
|
|
105
|
+
}
|
|
106
|
+
if (command === 'git diff --name-status') {
|
|
107
|
+
return { stdout: 'M\tfile.js' };
|
|
108
|
+
}
|
|
109
|
+
if (command.includes('diff -U15 -- file.js')) {
|
|
110
|
+
return { stdout: 'diff --git a/file.js b/file.js\n+new line' };
|
|
111
|
+
}
|
|
112
|
+
if (command.includes('show HEAD:file.js')) {
|
|
113
|
+
return { stdout: 'old content' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = await runUncommittedReview('unstaged', null);
|
|
120
|
+
|
|
121
|
+
expect(result).toBeDefined();
|
|
122
|
+
expect(result.source_branch).toBe('feature-branch');
|
|
123
|
+
expect(result.changed_files).toHaveLength(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should analyze all uncommitted changes', async () => {
|
|
127
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
128
|
+
const command = [cmd, ...args].join(' ');
|
|
129
|
+
|
|
130
|
+
if (command.includes('remote get-url origin')) {
|
|
131
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
132
|
+
}
|
|
133
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
134
|
+
return { stdout: 'feature-branch' };
|
|
135
|
+
}
|
|
136
|
+
if (command.includes('diff --cached --name-status')) {
|
|
137
|
+
return { stdout: 'M\tstaged.js' };
|
|
138
|
+
}
|
|
139
|
+
if (command === 'git diff --name-status') {
|
|
140
|
+
return { stdout: 'M\tunstaged.js' };
|
|
141
|
+
}
|
|
142
|
+
if (command.includes('diff --cached -U15 -- staged.js')) {
|
|
143
|
+
return { stdout: 'diff staged' };
|
|
144
|
+
}
|
|
145
|
+
if (command.includes('diff -U15 -- unstaged.js')) {
|
|
146
|
+
return { stdout: 'diff unstaged' };
|
|
147
|
+
}
|
|
148
|
+
if (command.includes('show HEAD:staged.js')) {
|
|
149
|
+
return { stdout: 'staged old content' };
|
|
150
|
+
}
|
|
151
|
+
if (command.includes('show HEAD:unstaged.js')) {
|
|
152
|
+
return { stdout: 'unstaged old content' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = await runUncommittedReview('all', null);
|
|
159
|
+
|
|
160
|
+
expect(result).toBeDefined();
|
|
161
|
+
expect(result.changed_files).toHaveLength(2);
|
|
162
|
+
expect(result.changed_files[0].path).toBe('staged.js');
|
|
163
|
+
expect(result.changed_files[1].path).toBe('unstaged.js');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should return null when no changes found', async () => {
|
|
167
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
168
|
+
const command = [cmd, ...args].join(' ');
|
|
169
|
+
|
|
170
|
+
if (command.includes('remote get-url origin')) {
|
|
171
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
172
|
+
}
|
|
173
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
174
|
+
return { stdout: 'feature-branch' };
|
|
175
|
+
}
|
|
176
|
+
if (command.includes('diff --cached --name-status')) {
|
|
177
|
+
return { stdout: '' };
|
|
178
|
+
}
|
|
179
|
+
if (command === 'git diff --name-status') {
|
|
180
|
+
return { stdout: '' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const result = await runUncommittedReview('all', null);
|
|
187
|
+
|
|
188
|
+
expect(result).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('runLocalReview - branch fetching', () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
vi.mock('execa');
|
|
195
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
196
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
afterEach(() => {
|
|
200
|
+
vi.restoreAllMocks();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should fail if target branch does not exist locally', async () => {
|
|
204
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
205
|
+
const command = [cmd, ...args].join(' ');
|
|
206
|
+
if (command.includes('remote get-url origin')) {
|
|
207
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
208
|
+
}
|
|
209
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
210
|
+
return { stdout: 'current-branch' };
|
|
211
|
+
}
|
|
212
|
+
if (command.includes('rev-parse --show-toplevel')) {
|
|
213
|
+
return { stdout: '/path/to/repo' };
|
|
214
|
+
}
|
|
215
|
+
if (command.includes('rev-parse --verify non-existent-branch')) {
|
|
216
|
+
throw new Error('Branch not found');
|
|
217
|
+
}
|
|
218
|
+
// No other commands should be called
|
|
219
|
+
return { stdout: '' };
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const result = await runLocalReview('non-existent-branch');
|
|
223
|
+
expect(result).toBeNull();
|
|
224
|
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Branch 'non-existent-branch' does not exist locally."));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should fetch latest changes if target branch exists locally', async () => {
|
|
228
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
229
|
+
const command = [cmd, ...args].join(' ');
|
|
230
|
+
// Common setup
|
|
231
|
+
if (command.includes('remote get-url origin')) return { stdout: 'https://github.com/user/repo.git' };
|
|
232
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) return { stdout: 'current-branch' };
|
|
233
|
+
if (command.includes('rev-parse --show-toplevel')) return { stdout: '/path/to/repo' };
|
|
234
|
+
|
|
235
|
+
// Branch verification and fetch (successful)
|
|
236
|
+
if (command.includes('rev-parse --verify main')) return { stdout: 'commit-hash' };
|
|
237
|
+
if (command === 'git fetch origin main') return { stdout: 'fetch successful' };
|
|
238
|
+
|
|
239
|
+
// Rest of the review logic
|
|
240
|
+
if (command.includes('merge-base origin/main HEAD')) return { stdout: 'abc1234' };
|
|
241
|
+
if (command.includes('log')) return { stdout: 'feat: message---EOC---' };
|
|
242
|
+
if (command.includes('diff --name-status')) return { stdout: 'M\tfile.js' };
|
|
243
|
+
if (command.includes('diff -U15')) return { stdout: 'diff content' };
|
|
244
|
+
if (command.includes('show')) return { stdout: 'original content' };
|
|
245
|
+
|
|
246
|
+
return { stdout: '' };
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const result = await runLocalReview('main');
|
|
250
|
+
expect(result).not.toBeNull();
|
|
251
|
+
const execaCalls = vi.mocked(execa).mock.calls;
|
|
252
|
+
const fetchCall = execaCalls.find(call => call[0] === 'git' && call[1].includes('fetch'));
|
|
253
|
+
expect(fetchCall).toBeDefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should warn and continue if fetch fails', async () => {
|
|
257
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
258
|
+
const command = [cmd, ...args].join(' ');
|
|
259
|
+
if (command.includes('remote get-url origin')) return { stdout: 'https://github.com/user/repo.git' };
|
|
260
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) return { stdout: 'current-branch' };
|
|
261
|
+
if (command.includes('rev-parse --show-toplevel')) return { stdout: '/path/to/repo' };
|
|
262
|
+
if (command.includes('rev-parse --verify main')) return { stdout: 'commit-hash' };
|
|
263
|
+
|
|
264
|
+
// Simulate fetch failure
|
|
265
|
+
if (command === 'git fetch origin main') {
|
|
266
|
+
throw new Error('Fetch failed');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Rest of the logic should still run (uses local branch since fetch failed)
|
|
270
|
+
if (command.includes('merge-base main HEAD')) return { stdout: 'abc1234' };
|
|
271
|
+
if (command.includes('log')) return { stdout: 'feat: message---EOC---' };
|
|
272
|
+
if (command.includes('diff --name-status')) return { stdout: 'M\tfile.js' };
|
|
273
|
+
if (command.includes('diff -U15')) return { stdout: 'diff content' };
|
|
274
|
+
if (command.includes('show')) return { stdout: 'original content' };
|
|
275
|
+
|
|
276
|
+
return { stdout: '' };
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const result = await runLocalReview('main');
|
|
280
|
+
expect(result).not.toBeNull(); // Should still proceed
|
|
281
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Could not fetch remote branch 'origin/main'."));
|
|
282
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Proceeding with local branch 'main' for comparison."));
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('runLocalReview - fork point detection', () => {
|
|
287
|
+
beforeEach(() => {
|
|
288
|
+
vi.mock('execa');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
afterEach(() => {
|
|
292
|
+
vi.restoreAllMocks();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should detect fork point when no target branch specified', async () => {
|
|
296
|
+
// Mock git commands for reflog-based fork point detection
|
|
297
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
298
|
+
const command = [cmd, ...args].join(' ');
|
|
299
|
+
|
|
300
|
+
if (command.includes('remote get-url origin')) {
|
|
301
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
302
|
+
}
|
|
303
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
304
|
+
return { stdout: 'feature-branch' };
|
|
305
|
+
}
|
|
306
|
+
if (command.includes('rev-parse --show-toplevel')) {
|
|
307
|
+
return { stdout: '/path/to/repo' };
|
|
308
|
+
}
|
|
309
|
+
if (command.includes('reflog show --no-abbrev-commit feature-branch')) {
|
|
310
|
+
// Simulate reflog output - last line is where branch was created
|
|
311
|
+
return { stdout: 'abc123def456 feature-branch@{0}: commit: latest\nfedcba654321 feature-branch@{1}: commit: middle\n510572bc5197788770004d0d0585822adab0128f feature-branch@{2}: branch: Created from master' };
|
|
312
|
+
}
|
|
313
|
+
if (command.includes('log --pretty=%B---EOC---') && command.includes('510572bc')) {
|
|
314
|
+
return { stdout: 'feat: add feature---EOC---' };
|
|
315
|
+
}
|
|
316
|
+
if (command.includes('diff --name-status') && command.includes('510572bc')) {
|
|
317
|
+
return { stdout: 'M\tfile.js' };
|
|
318
|
+
}
|
|
319
|
+
if (command.includes('diff -U15') && command.includes('510572bc')) {
|
|
320
|
+
return { stdout: 'diff content' };
|
|
321
|
+
}
|
|
322
|
+
if (command.includes('show 510572bc5197788770004d0d0585822adab0128f:file.js')) {
|
|
323
|
+
return { stdout: 'original content' };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const result = await runLocalReview(null, 'jira');
|
|
330
|
+
|
|
331
|
+
expect(result).toBeDefined();
|
|
332
|
+
expect(result.source_branch).toBe('feature-branch');
|
|
333
|
+
|
|
334
|
+
// Should have used reflog to find fork point
|
|
335
|
+
const execaCalls = vi.mocked(execa).mock.calls;
|
|
336
|
+
const reflogCall = execaCalls.find(call =>
|
|
337
|
+
call[0] === 'git' && call[1].includes('reflog')
|
|
338
|
+
);
|
|
339
|
+
expect(reflogCall).toBeDefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should use specified target branch instead of auto-detecting', async () => {
|
|
343
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
344
|
+
const command = [cmd, ...args].join(' ');
|
|
345
|
+
|
|
346
|
+
if (command.includes('remote get-url origin')) {
|
|
347
|
+
return { stdout: 'https://github.com/user/repo.git' };
|
|
348
|
+
}
|
|
349
|
+
if (command.includes('rev-parse --abbrev-ref HEAD')) {
|
|
350
|
+
return { stdout: 'feature-branch' };
|
|
351
|
+
}
|
|
352
|
+
if (command.includes('rev-parse --show-toplevel')) {
|
|
353
|
+
return { stdout: '/path/to/repo' };
|
|
354
|
+
}
|
|
355
|
+
// Add mocks for the new branch verification and fetch logic
|
|
356
|
+
if (command.includes('rev-parse --verify main')) {
|
|
357
|
+
return { stdout: 'commit-hash' };
|
|
358
|
+
}
|
|
359
|
+
if (command === 'git fetch origin main') {
|
|
360
|
+
return { stdout: '' };
|
|
361
|
+
}
|
|
362
|
+
if (command.includes('merge-base origin/main HEAD')) {
|
|
363
|
+
return { stdout: 'abc123' };
|
|
364
|
+
}
|
|
365
|
+
if (command.includes('log --pretty=%B---EOC---')) {
|
|
366
|
+
return { stdout: 'feat: add feature---EOC---' };
|
|
367
|
+
}
|
|
368
|
+
if (command.includes('diff --name-status')) {
|
|
369
|
+
return { stdout: 'M\tfile.js' };
|
|
370
|
+
}
|
|
371
|
+
if (command.includes('diff -U15')) {
|
|
372
|
+
return { stdout: 'diff content' };
|
|
373
|
+
}
|
|
374
|
+
if (command.includes('show abc123:file.js')) {
|
|
375
|
+
return { stdout: 'original content' };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const result = await runLocalReview('main', 'jira');
|
|
382
|
+
|
|
383
|
+
expect(result).toBeDefined();
|
|
384
|
+
|
|
385
|
+
// Should have used merge-base with origin/main (since fetch succeeded)
|
|
386
|
+
const execaCalls = vi.mocked(execa).mock.calls;
|
|
387
|
+
const mergeBaseCall = execaCalls.find(call =>
|
|
388
|
+
call[0] === 'git' && call[1].includes('merge-base') && call[1].includes('origin/main')
|
|
389
|
+
);
|
|
390
|
+
expect(mergeBaseCall).toBeDefined();
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe('truncateContent', () => {
|
|
395
|
+
it('should not truncate content with fewer lines than maxLines', () => {
|
|
396
|
+
const content = Array.from({ length: 100 }, (_, i) => `line ${i}`).join('\n');
|
|
397
|
+
const result = truncateContent(content, 2000);
|
|
398
|
+
expect(result).toBe(content);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should truncate content with more lines than maxLines', () => {
|
|
402
|
+
const content = Array.from({ length: 3000 }, (_, i) => `line ${i}`).join('\n');
|
|
403
|
+
const result = truncateContent(content, 2000);
|
|
404
|
+
|
|
405
|
+
// Should contain head and tail
|
|
406
|
+
expect(result).toContain('line 0');
|
|
407
|
+
expect(result).toContain('line 999'); // Last line of head (first 1000 lines)
|
|
408
|
+
expect(result).toContain('... [truncated] ...');
|
|
409
|
+
expect(result).toContain('line 2000'); // First line of tail (last 1000 lines)
|
|
410
|
+
expect(result).toContain('line 2999');
|
|
411
|
+
|
|
412
|
+
// Should not contain middle lines
|
|
413
|
+
expect(result).not.toContain('line 1500');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('should respect custom maxLines parameter', () => {
|
|
417
|
+
const content = Array.from({ length: 200 }, (_, i) => `line ${i}`).join('\n');
|
|
418
|
+
const result = truncateContent(content, 100);
|
|
419
|
+
|
|
420
|
+
expect(result).toContain('line 0');
|
|
421
|
+
expect(result).toContain('line 49'); // Last of first 50
|
|
422
|
+
expect(result).toContain('... [truncated] ...');
|
|
423
|
+
expect(result).toContain('line 150'); // First of last 50
|
|
424
|
+
expect(result).toContain('line 199');
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('normalizeRepoUrl', () => {
|
|
429
|
+
it('should normalize Azure DevOps SSH URL to HTTPS', () => {
|
|
430
|
+
const sshUrl = 'git@ssh.dev.azure.com:v3/VanillaSoftCollection/VanillaLand/VanillaLand';
|
|
431
|
+
const expected = 'https://dev.azure.com/VanillaSoftCollection/VanillaLand/_git/VanillaLand';
|
|
432
|
+
expect(normalizeRepoUrl(sshUrl)).toBe(expected);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should normalize GitHub SSH URL to HTTPS', () => {
|
|
436
|
+
const sshUrl = 'git@github.com:user/repo.git';
|
|
437
|
+
const expected = 'https://github.com/user/repo';
|
|
438
|
+
expect(normalizeRepoUrl(sshUrl)).toBe(expected);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('should normalize GitHub SSH URL without .git suffix', () => {
|
|
442
|
+
const sshUrl = 'git@github.com:user/repo';
|
|
443
|
+
const expected = 'https://github.com/user/repo';
|
|
444
|
+
expect(normalizeRepoUrl(sshUrl)).toBe(expected);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should normalize GitLab SSH URL to HTTPS', () => {
|
|
448
|
+
const sshUrl = 'git@gitlab.com:user/repo.git';
|
|
449
|
+
const expected = 'https://gitlab.com/user/repo';
|
|
450
|
+
expect(normalizeRepoUrl(sshUrl)).toBe(expected);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should remove .git suffix from HTTPS URLs', () => {
|
|
454
|
+
const httpsUrl = 'https://github.com/user/repo.git';
|
|
455
|
+
const expected = 'https://github.com/user/repo';
|
|
456
|
+
expect(normalizeRepoUrl(httpsUrl)).toBe(expected);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should keep HTTPS URLs without .git suffix unchanged', () => {
|
|
460
|
+
const httpsUrl = 'https://github.com/user/repo';
|
|
461
|
+
expect(normalizeRepoUrl(httpsUrl)).toBe(httpsUrl);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should keep Azure DevOps HTTPS URLs unchanged', () => {
|
|
465
|
+
const adoUrl = 'https://dev.azure.com/VanillaSoftCollection/VanillaLand/_git/VanillaLand';
|
|
466
|
+
expect(normalizeRepoUrl(adoUrl)).toBe(adoUrl);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe('shouldIgnoreFile', () => {
|
|
471
|
+
it('should return false when no patterns provided', () => {
|
|
472
|
+
expect(shouldIgnoreFile('file.js', [])).toBe(false);
|
|
473
|
+
expect(shouldIgnoreFile('file.js', null)).toBe(false);
|
|
474
|
+
expect(shouldIgnoreFile('file.js', undefined)).toBe(false);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should match exact filename', () => {
|
|
478
|
+
expect(shouldIgnoreFile('package-lock.json', ['package-lock.json'])).toBe(true);
|
|
479
|
+
expect(shouldIgnoreFile('package.json', ['package-lock.json'])).toBe(false);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should match files with * wildcard', () => {
|
|
483
|
+
expect(shouldIgnoreFile('file.lock', ['*.lock'])).toBe(true);
|
|
484
|
+
expect(shouldIgnoreFile('package-lock.json', ['*.lock'])).toBe(false);
|
|
485
|
+
expect(shouldIgnoreFile('test.min.js', ['*.min.js'])).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should match files in directories with *', () => {
|
|
489
|
+
expect(shouldIgnoreFile('dist/bundle.js', ['dist/*'])).toBe(true);
|
|
490
|
+
expect(shouldIgnoreFile('dist/css/style.css', ['dist/*'])).toBe(false); // * doesn't match /
|
|
491
|
+
expect(shouldIgnoreFile('src/index.js', ['dist/*'])).toBe(false);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should match files recursively with **', () => {
|
|
495
|
+
expect(shouldIgnoreFile('dist/bundle.js', ['dist/**'])).toBe(true);
|
|
496
|
+
expect(shouldIgnoreFile('dist/css/style.css', ['dist/**'])).toBe(true);
|
|
497
|
+
expect(shouldIgnoreFile('dist/js/vendor/lib.js', ['dist/**'])).toBe(true);
|
|
498
|
+
expect(shouldIgnoreFile('src/index.js', ['dist/**'])).toBe(false);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should match with ? wildcard for single character', () => {
|
|
502
|
+
expect(shouldIgnoreFile('test1.js', ['test?.js'])).toBe(true);
|
|
503
|
+
expect(shouldIgnoreFile('test2.js', ['test?.js'])).toBe(true);
|
|
504
|
+
expect(shouldIgnoreFile('test12.js', ['test?.js'])).toBe(false);
|
|
505
|
+
expect(shouldIgnoreFile('test.js', ['test?.js'])).toBe(false);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should handle multiple patterns', () => {
|
|
509
|
+
const patterns = ['*.lock', '*.log', 'dist/*'];
|
|
510
|
+
expect(shouldIgnoreFile('yarn.lock', patterns)).toBe(true);
|
|
511
|
+
expect(shouldIgnoreFile('error.log', patterns)).toBe(true);
|
|
512
|
+
expect(shouldIgnoreFile('dist/bundle.js', patterns)).toBe(true);
|
|
513
|
+
expect(shouldIgnoreFile('src/index.js', patterns)).toBe(false);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should match files with dots in paths', () => {
|
|
517
|
+
expect(shouldIgnoreFile('file.test.js', ['*.test.js'])).toBe(true);
|
|
518
|
+
expect(shouldIgnoreFile('component.spec.ts', ['*.spec.ts'])).toBe(true);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should match nested paths correctly', () => {
|
|
522
|
+
expect(shouldIgnoreFile('src/components/Button.js', ['src/components/*'])).toBe(true);
|
|
523
|
+
expect(shouldIgnoreFile('src/components/forms/Input.js', ['src/components/*'])).toBe(false);
|
|
524
|
+
expect(shouldIgnoreFile('src/components/forms/Input.js', ['src/components/**'])).toBe(true);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should handle edge cases', () => {
|
|
528
|
+
expect(shouldIgnoreFile('', ['*'])).toBe(true);
|
|
529
|
+
expect(shouldIgnoreFile('file', ['*'])).toBe(true);
|
|
530
|
+
expect(shouldIgnoreFile('path/to/file', ['**'])).toBe(true);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should match SQL files with **/*.sql pattern', () => {
|
|
534
|
+
const pattern = ['**/*.sql'];
|
|
535
|
+
expect(shouldIgnoreFile('SqlScripts/02_CreateTables.sql', pattern)).toBe(true);
|
|
536
|
+
expect(shouldIgnoreFile('SqlScripts/03_CreateStoredProcedures.sql', pattern)).toBe(true);
|
|
537
|
+
expect(shouldIgnoreFile('SqlScripts/20251006000000_CreateDatabase.sql', pattern)).toBe(true);
|
|
538
|
+
expect(shouldIgnoreFile('db/migrations/001_init.sql', pattern)).toBe(true);
|
|
539
|
+
expect(shouldIgnoreFile('file.sql', pattern)).toBe(true);
|
|
540
|
+
expect(shouldIgnoreFile('file.js', pattern)).toBe(false);
|
|
541
|
+
});
|
|
542
|
+
});
|