gitlab-mcp 1.1.0 → 1.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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/dist/config/env.d.ts +56 -0
  3. package/dist/config/env.js +163 -0
  4. package/dist/config/env.js.map +1 -0
  5. package/dist/http-app.d.ts +45 -0
  6. package/dist/http-app.js +550 -0
  7. package/dist/http-app.js.map +1 -0
  8. package/dist/http.d.ts +2 -0
  9. package/dist/http.js +65 -0
  10. package/dist/http.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +65 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/lib/auth-context.d.ts +9 -0
  15. package/dist/lib/auth-context.js +9 -0
  16. package/dist/lib/auth-context.js.map +1 -0
  17. package/dist/lib/gitlab-client.d.ts +331 -0
  18. package/dist/lib/gitlab-client.js +1025 -0
  19. package/dist/lib/gitlab-client.js.map +1 -0
  20. package/dist/lib/logger.d.ts +2 -0
  21. package/dist/lib/logger.js +13 -0
  22. package/dist/lib/logger.js.map +1 -0
  23. package/dist/lib/network.d.ts +3 -0
  24. package/dist/lib/network.js +38 -0
  25. package/dist/lib/network.js.map +1 -0
  26. package/dist/lib/oauth.d.ts +29 -0
  27. package/dist/lib/oauth.js +220 -0
  28. package/dist/lib/oauth.js.map +1 -0
  29. package/dist/lib/output.d.ts +14 -0
  30. package/dist/lib/output.js +38 -0
  31. package/dist/lib/output.js.map +1 -0
  32. package/dist/lib/policy.d.ts +25 -0
  33. package/dist/lib/policy.js +48 -0
  34. package/dist/lib/policy.js.map +1 -0
  35. package/dist/lib/request-runtime.d.ts +26 -0
  36. package/dist/lib/request-runtime.js +323 -0
  37. package/dist/lib/request-runtime.js.map +1 -0
  38. package/dist/lib/sanitize.d.ts +1 -0
  39. package/dist/lib/sanitize.js +21 -0
  40. package/dist/lib/sanitize.js.map +1 -0
  41. package/dist/lib/session-capacity.d.ts +8 -0
  42. package/dist/lib/session-capacity.js +7 -0
  43. package/dist/lib/session-capacity.js.map +1 -0
  44. package/dist/server/build-server.d.ts +3 -0
  45. package/dist/server/build-server.js +13 -0
  46. package/dist/server/build-server.js.map +1 -0
  47. package/dist/tools/gitlab.d.ts +9 -0
  48. package/dist/tools/gitlab.js +2576 -0
  49. package/dist/tools/gitlab.js.map +1 -0
  50. package/dist/tools/health.d.ts +2 -0
  51. package/dist/tools/health.js +21 -0
  52. package/dist/tools/health.js.map +1 -0
  53. package/dist/tools/mr-code-context.d.ts +38 -0
  54. package/dist/tools/mr-code-context.js +330 -0
  55. package/dist/tools/mr-code-context.js.map +1 -0
  56. package/{src/types/context.ts → dist/types/context.d.ts} +5 -6
  57. package/dist/types/context.js +2 -0
  58. package/dist/types/context.js.map +1 -0
  59. package/docs/configuration.md +6 -6
  60. package/docs/mcp-integration-testing-best-practices.md +981 -0
  61. package/package.json +13 -1
  62. package/.dockerignore +0 -7
  63. package/.editorconfig +0 -9
  64. package/.env.example +0 -75
  65. package/.github/workflows/nodejs.yml +0 -31
  66. package/.github/workflows/npm-publish.yml +0 -31
  67. package/.husky/pre-commit +0 -1
  68. package/.nvmrc +0 -1
  69. package/.prettierrc.json +0 -6
  70. package/Dockerfile +0 -20
  71. package/docker-compose.yml +0 -10
  72. package/eslint.config.js +0 -23
  73. package/scripts/get-oauth-token.example.sh +0 -15
  74. package/src/config/env.ts +0 -171
  75. package/src/http.ts +0 -620
  76. package/src/index.ts +0 -77
  77. package/src/lib/auth-context.ts +0 -19
  78. package/src/lib/gitlab-client.ts +0 -1810
  79. package/src/lib/logger.ts +0 -17
  80. package/src/lib/network.ts +0 -45
  81. package/src/lib/oauth.ts +0 -287
  82. package/src/lib/output.ts +0 -51
  83. package/src/lib/policy.ts +0 -78
  84. package/src/lib/request-runtime.ts +0 -376
  85. package/src/lib/sanitize.ts +0 -25
  86. package/src/lib/session-capacity.ts +0 -14
  87. package/src/server/build-server.ts +0 -17
  88. package/src/tools/gitlab.ts +0 -3135
  89. package/src/tools/health.ts +0 -27
  90. package/src/tools/mr-code-context.ts +0 -473
  91. package/tests/auth-context.test.ts +0 -102
  92. package/tests/gitlab-client.test.ts +0 -672
  93. package/tests/graphql-guard.test.ts +0 -121
  94. package/tests/integration/agent-loop.integration.test.ts +0 -558
  95. package/tests/integration/server.integration.test.ts +0 -543
  96. package/tests/mr-code-context.test.ts +0 -600
  97. package/tests/oauth.test.ts +0 -43
  98. package/tests/output.test.ts +0 -186
  99. package/tests/policy.test.ts +0 -324
  100. package/tests/request-runtime.test.ts +0 -252
  101. package/tests/sanitize.test.ts +0 -123
  102. package/tests/session-capacity.test.ts +0 -49
  103. package/tests/upload-reference.test.ts +0 -88
  104. package/tsconfig.build.json +0 -11
  105. package/tsconfig.json +0 -21
  106. package/vitest.config.ts +0 -12
@@ -1,600 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { getMergeRequestCodeContext } from "../src/tools/mr-code-context.js";
4
-
5
- function makeDiffFile(
6
- overrides: Partial<{
7
- old_path: string;
8
- new_path: string;
9
- new_file: boolean;
10
- renamed_file: boolean;
11
- deleted_file: boolean;
12
- diff: string;
13
- }> = {}
14
- ) {
15
- return {
16
- old_path: overrides.old_path ?? "src/file.ts",
17
- new_path: overrides.new_path ?? "src/file.ts",
18
- new_file: overrides.new_file ?? false,
19
- renamed_file: overrides.renamed_file ?? false,
20
- deleted_file: overrides.deleted_file ?? false,
21
- diff: overrides.diff ?? "@@ -1,1 +1,2 @@\n-const a = 1;\n+const a = 2;\n+const b = 3;"
22
- };
23
- }
24
-
25
- function createMockContext(
26
- files: ReturnType<typeof makeDiffFile>[],
27
- fileContents?: Record<string, string>
28
- ) {
29
- return {
30
- gitlab: {
31
- getMergeRequest: async () => ({ source_branch: "feature/a", target_branch: "main" }),
32
- getMergeRequestDiffs: async () => ({ changes: files }),
33
- getFileContents: async (_projectId: string, filePath: string) => {
34
- const content = fileContents?.[filePath] ?? "line1\nline2\nline3\nline4\nline5";
35
- return {
36
- file_path: filePath,
37
- encoding: "base64",
38
- content: Buffer.from(content).toString("base64")
39
- };
40
- }
41
- }
42
- } as never;
43
- }
44
-
45
- const defaultArgs = {
46
- projectId: "group/app",
47
- mergeRequestIid: "12",
48
- includePaths: undefined as string[] | undefined,
49
- excludePaths: undefined as string[] | undefined,
50
- extensions: undefined as string[] | undefined,
51
- languages: undefined as string[] | undefined,
52
- maxFiles: 30,
53
- maxTotalChars: 120_000,
54
- contextLines: 20,
55
- mode: "patch" as const,
56
- sort: "changed_lines" as const,
57
- listOnly: false
58
- };
59
-
60
- describe("getMergeRequestCodeContext", () => {
61
- describe("basic behavior", () => {
62
- it("filters files and respects budget", async () => {
63
- const context = createMockContext([
64
- makeDiffFile({ new_path: "src/a.ts" }),
65
- makeDiffFile({ new_path: "docs/readme.md", diff: "@@ -1,1 +1,1 @@\n-old\n+new" })
66
- ]);
67
-
68
- const result = await getMergeRequestCodeContext(
69
- {
70
- ...defaultArgs,
71
- includePaths: ["src/**"],
72
- extensions: ["ts"],
73
- maxTotalChars: 10,
74
- mode: "fullfile",
75
- contextLines: 1
76
- },
77
- context
78
- );
79
-
80
- expect(result.filtered_files).toBe(1);
81
- expect(result.returned_files).toBe(1);
82
-
83
- const files = result.files as Array<Record<string, unknown>>;
84
- expect(files.length).toBeGreaterThan(0);
85
- expect(String(files[0]?.new_path)).toBe("src/a.ts");
86
- expect(files[0]?.truncated).toBeTruthy();
87
- });
88
-
89
- it("returns file list only when list_only is true", async () => {
90
- const context = createMockContext([
91
- makeDiffFile({ new_path: "src/a.ts", diff: "@@ -1,1 +1,1 @@\n-a\n+b" })
92
- ]);
93
-
94
- const result = await getMergeRequestCodeContext(
95
- { ...defaultArgs, listOnly: true, sort: "path", mode: "patch" },
96
- context
97
- );
98
-
99
- expect(result.list_only).toBeTruthy();
100
- expect(result.selected_files).toBe(1);
101
- });
102
- });
103
-
104
- describe("extractDiffFiles", () => {
105
- it("handles response with changes array", async () => {
106
- const context = {
107
- gitlab: {
108
- getMergeRequest: async () => ({ source_branch: "f", target_branch: "main" }),
109
- getMergeRequestDiffs: async () => ({
110
- changes: [makeDiffFile({ new_path: "a.ts" })]
111
- }),
112
- getFileContents: async () => ({ content: "", encoding: "text" })
113
- }
114
- } as never;
115
-
116
- const result = await getMergeRequestCodeContext({ ...defaultArgs, listOnly: true }, context);
117
-
118
- expect(result.total_files).toBe(1);
119
- });
120
-
121
- it("handles response with diffs array", async () => {
122
- const context = {
123
- gitlab: {
124
- getMergeRequest: async () => ({ source_branch: "f", target_branch: "main" }),
125
- getMergeRequestDiffs: async () => ({
126
- diffs: [makeDiffFile({ new_path: "a.ts" })]
127
- }),
128
- getFileContents: async () => ({ content: "", encoding: "text" })
129
- }
130
- } as never;
131
-
132
- const result = await getMergeRequestCodeContext({ ...defaultArgs, listOnly: true }, context);
133
-
134
- expect(result.total_files).toBe(1);
135
- });
136
-
137
- it("handles direct array response", async () => {
138
- const context = {
139
- gitlab: {
140
- getMergeRequest: async () => ({ source_branch: "f", target_branch: "main" }),
141
- getMergeRequestDiffs: async () => [makeDiffFile({ new_path: "a.ts" })],
142
- getFileContents: async () => ({ content: "", encoding: "text" })
143
- }
144
- } as never;
145
-
146
- const result = await getMergeRequestCodeContext({ ...defaultArgs, listOnly: true }, context);
147
-
148
- expect(result.total_files).toBe(1);
149
- });
150
-
151
- it("returns empty when response has no recognizable format", async () => {
152
- const context = {
153
- gitlab: {
154
- getMergeRequest: async () => ({ source_branch: "f", target_branch: "main" }),
155
- getMergeRequestDiffs: async () => ({ unrelated: "data" }),
156
- getFileContents: async () => ({ content: "", encoding: "text" })
157
- }
158
- } as never;
159
-
160
- const result = await getMergeRequestCodeContext({ ...defaultArgs, listOnly: true }, context);
161
-
162
- expect(result.total_files).toBe(0);
163
- });
164
- });
165
-
166
- describe("filtering", () => {
167
- it("filters by includePaths glob pattern", async () => {
168
- const context = createMockContext([
169
- makeDiffFile({ new_path: "src/index.ts" }),
170
- makeDiffFile({ new_path: "tests/test.ts" }),
171
- makeDiffFile({ new_path: "src/utils/helper.ts" })
172
- ]);
173
-
174
- const result = await getMergeRequestCodeContext(
175
- { ...defaultArgs, includePaths: ["src/**"], listOnly: true },
176
- context
177
- );
178
-
179
- // listOnly mode returns selected_files (after filtering + maxFiles)
180
- expect(result.selected_files).toBe(2);
181
- });
182
-
183
- it("filters by excludePaths glob pattern", async () => {
184
- const context = createMockContext([
185
- makeDiffFile({ new_path: "src/index.ts" }),
186
- makeDiffFile({ new_path: "node_modules/pkg/index.js" }),
187
- makeDiffFile({ new_path: "src/utils.ts" })
188
- ]);
189
-
190
- const result = await getMergeRequestCodeContext(
191
- { ...defaultArgs, excludePaths: ["node_modules/**"], listOnly: true },
192
- context
193
- );
194
-
195
- expect(result.selected_files).toBe(2);
196
- });
197
-
198
- it("filters by extensions", async () => {
199
- const context = createMockContext([
200
- makeDiffFile({ new_path: "src/index.ts" }),
201
- makeDiffFile({ new_path: "src/style.css" }),
202
- makeDiffFile({ new_path: "src/app.tsx" })
203
- ]);
204
-
205
- const result = await getMergeRequestCodeContext(
206
- { ...defaultArgs, extensions: [".ts", ".tsx"], listOnly: true },
207
- context
208
- );
209
-
210
- expect(result.selected_files).toBe(2);
211
- });
212
-
213
- it("filters by extensions without dot prefix", async () => {
214
- const context = createMockContext([
215
- makeDiffFile({ new_path: "src/index.ts" }),
216
- makeDiffFile({ new_path: "src/style.css" })
217
- ]);
218
-
219
- const result = await getMergeRequestCodeContext(
220
- { ...defaultArgs, extensions: ["ts"], listOnly: true },
221
- context
222
- );
223
-
224
- expect(result.selected_files).toBe(1);
225
- });
226
-
227
- it("filters by language", async () => {
228
- const context = createMockContext([
229
- makeDiffFile({ new_path: "src/index.ts" }),
230
- makeDiffFile({ new_path: "src/app.tsx" }),
231
- makeDiffFile({ new_path: "src/main.py" }),
232
- makeDiffFile({ new_path: "src/style.css" })
233
- ]);
234
-
235
- const result = await getMergeRequestCodeContext(
236
- { ...defaultArgs, languages: ["typescript"], listOnly: true },
237
- context
238
- );
239
-
240
- expect(result.selected_files).toBe(2);
241
- });
242
-
243
- it("combines include, exclude, and extensions filters", async () => {
244
- const context = createMockContext([
245
- makeDiffFile({ new_path: "src/index.ts" }),
246
- makeDiffFile({ new_path: "src/test.spec.ts" }),
247
- makeDiffFile({ new_path: "docs/readme.md" })
248
- ]);
249
-
250
- const result = await getMergeRequestCodeContext(
251
- {
252
- ...defaultArgs,
253
- includePaths: ["src/**"],
254
- excludePaths: ["**/*.spec.ts"],
255
- extensions: [".ts"],
256
- listOnly: true
257
- },
258
- context
259
- );
260
-
261
- expect(result.selected_files).toBe(1);
262
- });
263
-
264
- it("handles no filters (returns all files)", async () => {
265
- const context = createMockContext([
266
- makeDiffFile({ new_path: "a.ts" }),
267
- makeDiffFile({ new_path: "b.css" }),
268
- makeDiffFile({ new_path: "c.md" })
269
- ]);
270
-
271
- const result = await getMergeRequestCodeContext({ ...defaultArgs, listOnly: true }, context);
272
-
273
- expect(result.total_files).toBe(3);
274
- expect(result.selected_files).toBe(3);
275
- });
276
- });
277
-
278
- describe("sorting", () => {
279
- it("sorts by changed_lines descending", async () => {
280
- const context = createMockContext([
281
- makeDiffFile({ new_path: "small.ts", diff: "@@ -1 +1 @@\n+a" }),
282
- makeDiffFile({ new_path: "big.ts", diff: "@@ -1 +1,4 @@\n+a\n+b\n+c\n+d" }),
283
- makeDiffFile({ new_path: "medium.ts", diff: "@@ -1 +1,2 @@\n+a\n+b" })
284
- ]);
285
-
286
- const result = await getMergeRequestCodeContext(
287
- { ...defaultArgs, sort: "changed_lines", listOnly: true },
288
- context
289
- );
290
-
291
- const files = result.files as Array<Record<string, unknown>>;
292
- expect(files[0]?.new_path).toBe("big.ts");
293
- expect(files[1]?.new_path).toBe("medium.ts");
294
- expect(files[2]?.new_path).toBe("small.ts");
295
- });
296
-
297
- it("sorts by path alphabetically", async () => {
298
- const context = createMockContext([
299
- makeDiffFile({ new_path: "z.ts" }),
300
- makeDiffFile({ new_path: "a.ts" }),
301
- makeDiffFile({ new_path: "m.ts" })
302
- ]);
303
-
304
- const result = await getMergeRequestCodeContext(
305
- { ...defaultArgs, sort: "path", listOnly: true },
306
- context
307
- );
308
-
309
- const files = result.files as Array<Record<string, unknown>>;
310
- expect(files[0]?.new_path).toBe("a.ts");
311
- expect(files[1]?.new_path).toBe("m.ts");
312
- expect(files[2]?.new_path).toBe("z.ts");
313
- });
314
-
315
- it("sorts by file_size (diff length) descending", async () => {
316
- const context = createMockContext([
317
- makeDiffFile({ new_path: "short.ts", diff: "short" }),
318
- makeDiffFile({ new_path: "long.ts", diff: "a".repeat(100) }),
319
- makeDiffFile({ new_path: "medium.ts", diff: "a".repeat(50) })
320
- ]);
321
-
322
- const result = await getMergeRequestCodeContext(
323
- { ...defaultArgs, sort: "file_size", listOnly: true },
324
- context
325
- );
326
-
327
- const files = result.files as Array<Record<string, unknown>>;
328
- expect(files[0]?.new_path).toBe("long.ts");
329
- expect(files[1]?.new_path).toBe("medium.ts");
330
- expect(files[2]?.new_path).toBe("short.ts");
331
- });
332
- });
333
-
334
- describe("modes", () => {
335
- it("returns patch content in patch mode", async () => {
336
- const diff = "@@ -1,1 +1,2 @@\n-old\n+new\n+added";
337
- const context = createMockContext([makeDiffFile({ new_path: "a.ts", diff })]);
338
-
339
- const result = await getMergeRequestCodeContext({ ...defaultArgs, mode: "patch" }, context);
340
-
341
- const files = result.files as Array<Record<string, unknown>>;
342
- expect(files[0]?.content_mode).toBe("patch");
343
- expect(String(files[0]?.content)).toContain("+new");
344
- });
345
-
346
- it("returns full file content in fullfile mode", async () => {
347
- const context = createMockContext([makeDiffFile({ new_path: "a.ts" })], {
348
- "a.ts": "const x = 1;\nconst y = 2;\nconst z = 3;"
349
- });
350
-
351
- const result = await getMergeRequestCodeContext(
352
- { ...defaultArgs, mode: "fullfile" },
353
- context
354
- );
355
-
356
- const files = result.files as Array<Record<string, unknown>>;
357
- expect(files[0]?.content_mode).toBe("fullfile");
358
- expect(String(files[0]?.content)).toContain("const x = 1;");
359
- });
360
-
361
- it("returns surrounding snippets in surrounding mode", async () => {
362
- const lines = Array.from({ length: 50 }, (_, i) => `line ${i + 1}`).join("\n");
363
- const diff = "@@ -10,1 +10,2 @@\n-old line 10\n+new line 10\n+extra";
364
-
365
- const context = createMockContext([makeDiffFile({ new_path: "a.ts", diff })], {
366
- "a.ts": lines
367
- });
368
-
369
- const result = await getMergeRequestCodeContext(
370
- { ...defaultArgs, mode: "surrounding", contextLines: 3 },
371
- context
372
- );
373
-
374
- const files = result.files as Array<Record<string, unknown>>;
375
- expect(files[0]?.content_mode).toBe("surrounding");
376
- expect(files[0]?.snippet_windows).toBeDefined();
377
- });
378
-
379
- it("uses patch mode for deleted files even in fullfile mode", async () => {
380
- const diff = "@@ -1,2 +0,0 @@\n-line1\n-line2";
381
- const context = createMockContext([
382
- makeDiffFile({ new_path: "deleted.ts", deleted_file: true, diff })
383
- ]);
384
-
385
- const result = await getMergeRequestCodeContext(
386
- { ...defaultArgs, mode: "fullfile" },
387
- context
388
- );
389
-
390
- const files = result.files as Array<Record<string, unknown>>;
391
- expect(files[0]?.content_mode).toBe("patch");
392
- });
393
- });
394
-
395
- describe("budget management", () => {
396
- it("reports budget usage in response", async () => {
397
- const context = createMockContext([makeDiffFile({ new_path: "a.ts" })]);
398
-
399
- const result = await getMergeRequestCodeContext(
400
- { ...defaultArgs, maxTotalChars: 5000 },
401
- context
402
- );
403
-
404
- const budget = result.budget as Record<string, number>;
405
- expect(budget.max_total_chars).toBe(5000);
406
- expect(budget.used_chars).toBeGreaterThan(0);
407
- expect(budget.remaining_chars).toBeDefined();
408
- });
409
-
410
- it("truncates content when budget is exceeded", async () => {
411
- const context = createMockContext([
412
- makeDiffFile({
413
- new_path: "a.ts",
414
- diff: "x".repeat(100)
415
- })
416
- ]);
417
-
418
- const result = await getMergeRequestCodeContext(
419
- { ...defaultArgs, maxTotalChars: 10 },
420
- context
421
- );
422
-
423
- const files = result.files as Array<Record<string, unknown>>;
424
- expect(files[0]?.truncated).toBe(true);
425
- });
426
-
427
- it("respects maxFiles limit", async () => {
428
- const context = createMockContext([
429
- makeDiffFile({ new_path: "a.ts" }),
430
- makeDiffFile({ new_path: "b.ts" }),
431
- makeDiffFile({ new_path: "c.ts" }),
432
- makeDiffFile({ new_path: "d.ts" }),
433
- makeDiffFile({ new_path: "e.ts" })
434
- ]);
435
-
436
- const result = await getMergeRequestCodeContext(
437
- { ...defaultArgs, maxFiles: 2, listOnly: true },
438
- context
439
- );
440
-
441
- expect(result.total_files).toBe(5);
442
- expect(result.selected_files).toBe(2);
443
- });
444
-
445
- it("stops processing files when budget is exhausted", async () => {
446
- const context = createMockContext([
447
- makeDiffFile({ new_path: "a.ts", diff: "a".repeat(100) }),
448
- makeDiffFile({ new_path: "b.ts", diff: "b".repeat(100) }),
449
- makeDiffFile({ new_path: "c.ts", diff: "c".repeat(100) })
450
- ]);
451
-
452
- const result = await getMergeRequestCodeContext(
453
- { ...defaultArgs, maxTotalChars: 50 },
454
- context
455
- );
456
-
457
- const files = result.files as Array<Record<string, unknown>>;
458
- // Should stop after budget is exhausted
459
- expect(files.length).toBeLessThanOrEqual(2);
460
- });
461
- });
462
-
463
- describe("changed lines counting", () => {
464
- it("counts added and removed lines", async () => {
465
- const diff = "@@ -1,3 +1,4 @@\n context\n-removed1\n-removed2\n+added1\n+added2\n+added3";
466
- const context = createMockContext([makeDiffFile({ new_path: "a.ts", diff })]);
467
-
468
- const result = await getMergeRequestCodeContext({ ...defaultArgs, listOnly: true }, context);
469
-
470
- const files = result.files as Array<Record<string, unknown>>;
471
- expect(files[0]?.changed_lines).toBe(5); // 2 removed + 3 added
472
- });
473
-
474
- it("ignores --- and +++ header lines", async () => {
475
- const diff = "--- a/file.ts\n+++ b/file.ts\n@@ -1 +1 @@\n-old\n+new";
476
- const context = createMockContext([makeDiffFile({ new_path: "a.ts", diff })]);
477
-
478
- const result = await getMergeRequestCodeContext({ ...defaultArgs, listOnly: true }, context);
479
-
480
- const files = result.files as Array<Record<string, unknown>>;
481
- expect(files[0]?.changed_lines).toBe(2); // Only -old and +new
482
- });
483
-
484
- it("counts zero for empty diff", async () => {
485
- const context = createMockContext([makeDiffFile({ new_path: "a.ts", diff: "" })]);
486
-
487
- const result = await getMergeRequestCodeContext({ ...defaultArgs, listOnly: true }, context);
488
-
489
- const files = result.files as Array<Record<string, unknown>>;
490
- expect(files[0]?.changed_lines).toBe(0);
491
- });
492
- });
493
-
494
- describe("response structure", () => {
495
- it("includes all expected fields in response", async () => {
496
- const context = createMockContext([makeDiffFile({ new_path: "a.ts" })]);
497
-
498
- const result = await getMergeRequestCodeContext({ ...defaultArgs }, context);
499
-
500
- expect(result).toHaveProperty("project_id");
501
- expect(result).toHaveProperty("merge_request_iid");
502
- expect(result).toHaveProperty("source_branch");
503
- expect(result).toHaveProperty("target_branch");
504
- expect(result).toHaveProperty("mode");
505
- expect(result).toHaveProperty("total_files");
506
- expect(result).toHaveProperty("filtered_files");
507
- expect(result).toHaveProperty("selected_files");
508
- expect(result).toHaveProperty("returned_files");
509
- expect(result).toHaveProperty("budget");
510
- expect(result).toHaveProperty("files");
511
- });
512
-
513
- it("includes file summary fields", async () => {
514
- const context = createMockContext([
515
- makeDiffFile({
516
- new_path: "new.ts",
517
- old_path: "old.ts",
518
- new_file: true,
519
- renamed_file: true
520
- })
521
- ]);
522
-
523
- const result = await getMergeRequestCodeContext({ ...defaultArgs, listOnly: true }, context);
524
-
525
- const files = result.files as Array<Record<string, unknown>>;
526
- expect(files[0]).toHaveProperty("old_path");
527
- expect(files[0]).toHaveProperty("new_path");
528
- expect(files[0]).toHaveProperty("new_file");
529
- expect(files[0]).toHaveProperty("renamed_file");
530
- expect(files[0]).toHaveProperty("deleted_file");
531
- expect(files[0]).toHaveProperty("changed_lines");
532
- });
533
-
534
- it("uses source_branch as ref for file content", async () => {
535
- const context = {
536
- gitlab: {
537
- getMergeRequest: async () => ({ source_branch: "my-feature", target_branch: "develop" }),
538
- getMergeRequestDiffs: async () => ({
539
- changes: [makeDiffFile({ new_path: "a.ts" })]
540
- }),
541
- getFileContents: async (_projId: string, _path: string, ref: string) => {
542
- expect(ref).toBe("my-feature");
543
- return { content: Buffer.from("content").toString("base64"), encoding: "base64" };
544
- }
545
- }
546
- } as never;
547
-
548
- await getMergeRequestCodeContext({ ...defaultArgs, mode: "fullfile" }, context);
549
- });
550
- });
551
-
552
- describe("language mappings", () => {
553
- it("maps python to .py extension", async () => {
554
- const context = createMockContext([
555
- makeDiffFile({ new_path: "main.py" }),
556
- makeDiffFile({ new_path: "main.ts" })
557
- ]);
558
-
559
- const result = await getMergeRequestCodeContext(
560
- { ...defaultArgs, languages: ["python"], listOnly: true },
561
- context
562
- );
563
-
564
- expect(result.selected_files).toBe(1);
565
- });
566
-
567
- it("maps javascript to multiple extensions", async () => {
568
- const context = createMockContext([
569
- makeDiffFile({ new_path: "a.js" }),
570
- makeDiffFile({ new_path: "b.jsx" }),
571
- makeDiffFile({ new_path: "c.mjs" }),
572
- makeDiffFile({ new_path: "d.ts" })
573
- ]);
574
-
575
- const result = await getMergeRequestCodeContext(
576
- { ...defaultArgs, languages: ["javascript"], listOnly: true },
577
- context
578
- );
579
-
580
- expect(result.selected_files).toBe(3);
581
- });
582
-
583
- it("handles unknown language gracefully (no filtering when language map returns empty)", async () => {
584
- const context = createMockContext([
585
- makeDiffFile({ new_path: "a.ts" }),
586
- makeDiffFile({ new_path: "b.py" })
587
- ]);
588
-
589
- const result = await getMergeRequestCodeContext(
590
- { ...defaultArgs, languages: ["nonexistent"], listOnly: true },
591
- context
592
- );
593
-
594
- // When language is unknown, languageToExtensions returns [],
595
- // languageExtSet is empty, so the filter is effectively a no-op
596
- // and all files pass through.
597
- expect(result.selected_files).toBe(2);
598
- });
599
- });
600
- });
@@ -1,43 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { deriveGitLabBaseUrl } from "../src/lib/oauth.js";
4
-
5
- describe("deriveGitLabBaseUrl", () => {
6
- it("extracts base URL from standard API URL", () => {
7
- expect(deriveGitLabBaseUrl("https://gitlab.example.com/api/v4")).toBe(
8
- "https://gitlab.example.com"
9
- );
10
- });
11
-
12
- it("extracts base URL from API URL with trailing slash", () => {
13
- expect(deriveGitLabBaseUrl("https://gitlab.example.com/api/v4/")).toBe(
14
- "https://gitlab.example.com"
15
- );
16
- });
17
-
18
- it("handles subpath GitLab installations", () => {
19
- expect(deriveGitLabBaseUrl("https://company.com/gitlab/api/v4")).toBe(
20
- "https://company.com/gitlab"
21
- );
22
- });
23
-
24
- it("handles gitlab.com", () => {
25
- expect(deriveGitLabBaseUrl("https://gitlab.com/api/v4")).toBe("https://gitlab.com");
26
- });
27
-
28
- it("handles URL without /api/v4 suffix", () => {
29
- expect(deriveGitLabBaseUrl("https://gitlab.example.com/custom")).toBe(
30
- "https://gitlab.example.com/custom"
31
- );
32
- });
33
-
34
- it("handles URL with port", () => {
35
- expect(deriveGitLabBaseUrl("https://gitlab.example.com:8443/api/v4")).toBe(
36
- "https://gitlab.example.com:8443"
37
- );
38
- });
39
-
40
- it("handles HTTP URL", () => {
41
- expect(deriveGitLabBaseUrl("http://localhost:8080/api/v4")).toBe("http://localhost:8080");
42
- });
43
- });