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.
- package/LICENSE +21 -0
- package/dist/config/env.d.ts +56 -0
- package/dist/config/env.js +163 -0
- package/dist/config/env.js.map +1 -0
- package/dist/http-app.d.ts +45 -0
- package/dist/http-app.js +550 -0
- package/dist/http-app.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +65 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth-context.d.ts +9 -0
- package/dist/lib/auth-context.js +9 -0
- package/dist/lib/auth-context.js.map +1 -0
- package/dist/lib/gitlab-client.d.ts +331 -0
- package/dist/lib/gitlab-client.js +1025 -0
- package/dist/lib/gitlab-client.js.map +1 -0
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +13 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/network.d.ts +3 -0
- package/dist/lib/network.js +38 -0
- package/dist/lib/network.js.map +1 -0
- package/dist/lib/oauth.d.ts +29 -0
- package/dist/lib/oauth.js +220 -0
- package/dist/lib/oauth.js.map +1 -0
- package/dist/lib/output.d.ts +14 -0
- package/dist/lib/output.js +38 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/policy.d.ts +25 -0
- package/dist/lib/policy.js +48 -0
- package/dist/lib/policy.js.map +1 -0
- package/dist/lib/request-runtime.d.ts +26 -0
- package/dist/lib/request-runtime.js +323 -0
- package/dist/lib/request-runtime.js.map +1 -0
- package/dist/lib/sanitize.d.ts +1 -0
- package/dist/lib/sanitize.js +21 -0
- package/dist/lib/sanitize.js.map +1 -0
- package/dist/lib/session-capacity.d.ts +8 -0
- package/dist/lib/session-capacity.js +7 -0
- package/dist/lib/session-capacity.js.map +1 -0
- package/dist/server/build-server.d.ts +3 -0
- package/dist/server/build-server.js +13 -0
- package/dist/server/build-server.js.map +1 -0
- package/dist/tools/gitlab.d.ts +9 -0
- package/dist/tools/gitlab.js +2576 -0
- package/dist/tools/gitlab.js.map +1 -0
- package/dist/tools/health.d.ts +2 -0
- package/dist/tools/health.js +21 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/mr-code-context.d.ts +38 -0
- package/dist/tools/mr-code-context.js +330 -0
- package/dist/tools/mr-code-context.js.map +1 -0
- package/{src/types/context.ts → dist/types/context.d.ts} +5 -6
- package/dist/types/context.js +2 -0
- package/dist/types/context.js.map +1 -0
- package/docs/configuration.md +6 -6
- package/docs/mcp-integration-testing-best-practices.md +981 -0
- package/package.json +13 -1
- package/.dockerignore +0 -7
- package/.editorconfig +0 -9
- package/.env.example +0 -75
- package/.github/workflows/nodejs.yml +0 -31
- package/.github/workflows/npm-publish.yml +0 -31
- package/.husky/pre-commit +0 -1
- package/.nvmrc +0 -1
- package/.prettierrc.json +0 -6
- package/Dockerfile +0 -20
- package/docker-compose.yml +0 -10
- package/eslint.config.js +0 -23
- package/scripts/get-oauth-token.example.sh +0 -15
- package/src/config/env.ts +0 -171
- package/src/http.ts +0 -620
- package/src/index.ts +0 -77
- package/src/lib/auth-context.ts +0 -19
- package/src/lib/gitlab-client.ts +0 -1810
- package/src/lib/logger.ts +0 -17
- package/src/lib/network.ts +0 -45
- package/src/lib/oauth.ts +0 -287
- package/src/lib/output.ts +0 -51
- package/src/lib/policy.ts +0 -78
- package/src/lib/request-runtime.ts +0 -376
- package/src/lib/sanitize.ts +0 -25
- package/src/lib/session-capacity.ts +0 -14
- package/src/server/build-server.ts +0 -17
- package/src/tools/gitlab.ts +0 -3135
- package/src/tools/health.ts +0 -27
- package/src/tools/mr-code-context.ts +0 -473
- package/tests/auth-context.test.ts +0 -102
- package/tests/gitlab-client.test.ts +0 -672
- package/tests/graphql-guard.test.ts +0 -121
- package/tests/integration/agent-loop.integration.test.ts +0 -558
- package/tests/integration/server.integration.test.ts +0 -543
- package/tests/mr-code-context.test.ts +0 -600
- package/tests/oauth.test.ts +0 -43
- package/tests/output.test.ts +0 -186
- package/tests/policy.test.ts +0 -324
- package/tests/request-runtime.test.ts +0 -252
- package/tests/sanitize.test.ts +0 -123
- package/tests/session-capacity.test.ts +0 -49
- package/tests/upload-reference.test.ts +0 -88
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -21
- 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
|
-
});
|
package/tests/oauth.test.ts
DELETED
|
@@ -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
|
-
});
|