@dpesch/mantisbt-mcp-server 1.10.2 → 1.10.6

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 (49) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.de.md +2 -1
  3. package/README.md +2 -1
  4. package/dist/client.js +0 -15
  5. package/dist/tools/files.js +14 -15
  6. package/docs/cookbook.de.md +1 -1
  7. package/docs/cookbook.md +1 -1
  8. package/package.json +1 -1
  9. package/server.json +2 -2
  10. package/.gitea/PULL_REQUEST_TEMPLATE.md +0 -17
  11. package/.gitea/workflows/ci.yml +0 -95
  12. package/.github/workflows/ci.yml +0 -32
  13. package/scripts/hooks/pre-push.mjs +0 -220
  14. package/scripts/init.mjs +0 -95
  15. package/scripts/record-fixtures.ts +0 -160
  16. package/tests/cache.test.ts +0 -265
  17. package/tests/client.test.ts +0 -372
  18. package/tests/config.test.ts +0 -263
  19. package/tests/fixtures/get_current_user.json +0 -40
  20. package/tests/fixtures/get_issue.json +0 -133
  21. package/tests/fixtures/get_issue_enums.json +0 -67
  22. package/tests/fixtures/get_issue_fields_sample.json +0 -76
  23. package/tests/fixtures/get_project_categories.json +0 -157
  24. package/tests/fixtures/get_project_versions.json +0 -3
  25. package/tests/fixtures/get_project_versions_with_data.json +0 -52
  26. package/tests/fixtures/list_issues.json +0 -95
  27. package/tests/fixtures/list_projects.json +0 -65
  28. package/tests/helpers/mock-server.ts +0 -166
  29. package/tests/helpers/search-mocks.ts +0 -84
  30. package/tests/prompts/prompts.test.ts +0 -242
  31. package/tests/resources/resources.test.ts +0 -309
  32. package/tests/search/embedder.test.ts +0 -81
  33. package/tests/search/highlight.test.ts +0 -129
  34. package/tests/search/store.test.ts +0 -193
  35. package/tests/search/sync.test.ts +0 -249
  36. package/tests/search/tools.test.ts +0 -661
  37. package/tests/tools/config.test.ts +0 -212
  38. package/tests/tools/files.test.ts +0 -344
  39. package/tests/tools/issues.test.ts +0 -1180
  40. package/tests/tools/metadata.test.ts +0 -509
  41. package/tests/tools/monitors.test.ts +0 -101
  42. package/tests/tools/projects.test.ts +0 -338
  43. package/tests/tools/relationships.test.ts +0 -177
  44. package/tests/tools/string-coercion.test.ts +0 -317
  45. package/tests/tools/users.test.ts +0 -62
  46. package/tests/utils/date-filter.test.ts +0 -169
  47. package/tests/version-hint.test.ts +0 -230
  48. package/tsconfig.build.json +0 -8
  49. package/vitest.config.ts +0 -8
@@ -1,372 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { MantisClient, MantisApiError, buildIssueViewUrl, buildNoteViewUrl } from '../src/client.js';
3
-
4
- // ---------------------------------------------------------------------------
5
- // Helpers
6
- // ---------------------------------------------------------------------------
7
-
8
- function makeResponse(
9
- status: number,
10
- body: string,
11
- headers: Record<string, string> = {},
12
- ): Response {
13
- return {
14
- ok: status >= 200 && status < 300,
15
- status,
16
- statusText: `Status ${status}`,
17
- text: () => Promise.resolve(body),
18
- headers: {
19
- get: (key: string) => headers[key] ?? null,
20
- },
21
- } as unknown as Response;
22
- }
23
-
24
- // ---------------------------------------------------------------------------
25
- // Setup
26
- // ---------------------------------------------------------------------------
27
-
28
- beforeEach(() => {
29
- vi.unstubAllGlobals();
30
- });
31
-
32
- // ---------------------------------------------------------------------------
33
- // URL building
34
- // ---------------------------------------------------------------------------
35
-
36
- describe('MantisClient – URL building', () => {
37
- it('appends /api/rest/<path> to the base URL', () => {
38
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
39
- vi.stubGlobal('fetch', fetchMock);
40
-
41
- const client = new MantisClient('https://mantis.example.com', 'token123');
42
- return client.get('issues/42').then(() => {
43
- const calledUrl: string = fetchMock.mock.calls[0][0] as string;
44
- expect(calledUrl).toBe('https://mantis.example.com/api/rest/issues/42');
45
- });
46
- });
47
-
48
- it('strips trailing slash from base URL', () => {
49
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
50
- vi.stubGlobal('fetch', fetchMock);
51
-
52
- const client = new MantisClient('https://mantis.example.com/', 'token123');
53
- return client.get('issues').then(() => {
54
- const calledUrl: string = fetchMock.mock.calls[0][0] as string;
55
- expect(calledUrl).toBe('https://mantis.example.com/api/rest/issues');
56
- });
57
- });
58
-
59
- it('strips /api/rest suffix when user includes it in the base URL', () => {
60
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
61
- vi.stubGlobal('fetch', fetchMock);
62
-
63
- const client = new MantisClient('https://mantis.example.com/api/rest', 'token123');
64
- return client.get('issues/42').then(() => {
65
- const calledUrl: string = fetchMock.mock.calls[0][0] as string;
66
- expect(calledUrl).toBe('https://mantis.example.com/api/rest/issues/42');
67
- });
68
- });
69
-
70
- it('strips /api/rest/ (with trailing slash) from base URL', () => {
71
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
72
- vi.stubGlobal('fetch', fetchMock);
73
-
74
- const client = new MantisClient('https://mantis.example.com/api/rest/', 'token123');
75
- return client.get('issues/42').then(() => {
76
- const calledUrl: string = fetchMock.mock.calls[0][0] as string;
77
- expect(calledUrl).toBe('https://mantis.example.com/api/rest/issues/42');
78
- });
79
- });
80
-
81
- it('appends defined query parameters', () => {
82
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
83
- vi.stubGlobal('fetch', fetchMock);
84
-
85
- const client = new MantisClient('https://mantis.example.com', 'token123');
86
- return client.get('issues', { page_size: 10, page: 1 }).then(() => {
87
- const calledUrl: string = fetchMock.mock.calls[0][0] as string;
88
- const url = new URL(calledUrl);
89
- expect(url.searchParams.get('page_size')).toBe('10');
90
- expect(url.searchParams.get('page')).toBe('1');
91
- });
92
- });
93
-
94
- it('omits undefined query parameters', () => {
95
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
96
- vi.stubGlobal('fetch', fetchMock);
97
-
98
- const client = new MantisClient('https://mantis.example.com', 'token123');
99
- return client.get('issues', { page_size: 10, filter_id: undefined }).then(() => {
100
- const calledUrl: string = fetchMock.mock.calls[0][0] as string;
101
- const url = new URL(calledUrl);
102
- expect(url.searchParams.has('filter_id')).toBe(false);
103
- expect(url.searchParams.get('page_size')).toBe('10');
104
- });
105
- });
106
- });
107
-
108
- // ---------------------------------------------------------------------------
109
- // Request headers
110
- // ---------------------------------------------------------------------------
111
-
112
- describe('MantisClient – request headers', () => {
113
- it('sends Authorization and Content-Type headers', () => {
114
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
115
- vi.stubGlobal('fetch', fetchMock);
116
-
117
- const client = new MantisClient('https://mantis.example.com', 'myApiKey');
118
- return client.get('issues').then(() => {
119
- const options = fetchMock.mock.calls[0][1] as RequestInit;
120
- const headers = options.headers as Record<string, string>;
121
- expect(headers['Authorization']).toBe('myApiKey');
122
- expect(headers['Content-Type']).toBe('application/json');
123
- });
124
- });
125
- });
126
-
127
- // ---------------------------------------------------------------------------
128
- // handleResponse
129
- // ---------------------------------------------------------------------------
130
-
131
- describe('MantisClient – handleResponse', () => {
132
- it('returns parsed JSON on 200', () => {
133
- const fetchMock = vi.fn().mockResolvedValue(
134
- makeResponse(200, JSON.stringify({ id: 42 })),
135
- );
136
- vi.stubGlobal('fetch', fetchMock);
137
-
138
- const client = new MantisClient('https://mantis.example.com', 'token');
139
- return client.get<{ id: number }>('issues/42').then((result) => {
140
- expect(result).toEqual({ id: 42 });
141
- });
142
- });
143
-
144
- it('returns undefined on 204 No Content', () => {
145
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(204, ''));
146
- vi.stubGlobal('fetch', fetchMock);
147
-
148
- const client = new MantisClient('https://mantis.example.com', 'token');
149
- return client.delete('issues/42').then((result) => {
150
- expect(result).toBeUndefined();
151
- });
152
- });
153
-
154
- it('throws MantisApiError with statusCode on 4xx', async () => {
155
- const fetchMock = vi.fn().mockResolvedValue(
156
- makeResponse(404, JSON.stringify({ message: 'Issue not found' })),
157
- );
158
- vi.stubGlobal('fetch', fetchMock);
159
-
160
- const client = new MantisClient('https://mantis.example.com', 'token');
161
- await expect(client.get('issues/9999')).rejects.toMatchObject({
162
- statusCode: 404,
163
- name: 'MantisApiError',
164
- });
165
- });
166
-
167
- it('uses raw body as message when JSON has no message field', async () => {
168
- const fetchMock = vi.fn().mockResolvedValue(
169
- makeResponse(403, JSON.stringify({ detail: 'forbidden' })),
170
- );
171
- vi.stubGlobal('fetch', fetchMock);
172
-
173
- const client = new MantisClient('https://mantis.example.com', 'token');
174
- // When body is JSON but has no `message` key, the raw body string is used
175
- await expect(client.get('issues')).rejects.toThrow('{"detail":"forbidden"}');
176
- });
177
-
178
- it('MantisApiError is instanceof Error', async () => {
179
- const fetchMock = vi.fn().mockResolvedValue(
180
- makeResponse(401, JSON.stringify({ message: 'Unauthorized' })),
181
- );
182
- vi.stubGlobal('fetch', fetchMock);
183
-
184
- const client = new MantisClient('https://mantis.example.com', 'token');
185
- await expect(client.get('issues')).rejects.toBeInstanceOf(MantisApiError);
186
- });
187
- });
188
-
189
- // ---------------------------------------------------------------------------
190
- // HTTP methods
191
- // ---------------------------------------------------------------------------
192
-
193
- describe('MantisClient – HTTP methods', () => {
194
- it('sends POST with JSON body', () => {
195
- const fetchMock = vi.fn().mockResolvedValue(
196
- makeResponse(201, JSON.stringify({ id: 1 })),
197
- );
198
- vi.stubGlobal('fetch', fetchMock);
199
-
200
- const client = new MantisClient('https://mantis.example.com', 'token');
201
- const payload = { summary: 'New issue', project: { id: 1 } };
202
- return client.post('issues', payload).then(() => {
203
- const options = fetchMock.mock.calls[0][1] as RequestInit;
204
- expect(options.method).toBe('POST');
205
- expect(options.body).toBe(JSON.stringify(payload));
206
- });
207
- });
208
-
209
- it('sends PATCH request', () => {
210
- const fetchMock = vi.fn().mockResolvedValue(
211
- makeResponse(200, JSON.stringify({ id: 1 })),
212
- );
213
- vi.stubGlobal('fetch', fetchMock);
214
-
215
- const client = new MantisClient('https://mantis.example.com', 'token');
216
- return client.patch('issues/1', { summary: 'Updated' }).then(() => {
217
- const options = fetchMock.mock.calls[0][1] as RequestInit;
218
- expect(options.method).toBe('PATCH');
219
- });
220
- });
221
-
222
- it('sends DELETE request', () => {
223
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(204, ''));
224
- vi.stubGlobal('fetch', fetchMock);
225
-
226
- const client = new MantisClient('https://mantis.example.com', 'token');
227
- return client.delete('issues/1').then(() => {
228
- const options = fetchMock.mock.calls[0][1] as RequestInit;
229
- expect(options.method).toBe('DELETE');
230
- });
231
- });
232
- });
233
-
234
- // ---------------------------------------------------------------------------
235
- // Credential factory (lazy constructor)
236
- // ---------------------------------------------------------------------------
237
-
238
- describe('MantisClient – credential factory', () => {
239
- it('does not call the factory until the first API method', async () => {
240
- const factory = vi.fn().mockResolvedValue({
241
- baseUrl: 'https://lazy.example.com',
242
- apiKey: 'lazy-key',
243
- });
244
-
245
- new MantisClient(factory);
246
-
247
- expect(factory).not.toHaveBeenCalled();
248
- });
249
-
250
- it('calls the factory on first API method and uses the returned credentials', async () => {
251
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
252
- vi.stubGlobal('fetch', fetchMock);
253
-
254
- const factory = vi.fn().mockResolvedValue({
255
- baseUrl: 'https://lazy.example.com',
256
- apiKey: 'lazy-key',
257
- });
258
-
259
- const client = new MantisClient(factory);
260
- await client.get('issues');
261
-
262
- expect(factory).toHaveBeenCalledOnce();
263
- const calledUrl: string = fetchMock.mock.calls[0][0] as string;
264
- expect(calledUrl).toBe('https://lazy.example.com/api/rest/issues');
265
- const options = fetchMock.mock.calls[0][1] as RequestInit;
266
- expect((options.headers as Record<string, string>)['Authorization']).toBe('lazy-key');
267
- });
268
-
269
- it('caches credentials after the first call and does not call the factory again', async () => {
270
- const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
271
- vi.stubGlobal('fetch', fetchMock);
272
-
273
- const factory = vi.fn().mockResolvedValue({
274
- baseUrl: 'https://lazy.example.com',
275
- apiKey: 'lazy-key',
276
- });
277
-
278
- const client = new MantisClient(factory);
279
- await client.get('issues');
280
- await client.get('projects');
281
-
282
- expect(factory).toHaveBeenCalledOnce();
283
- });
284
-
285
- it('forwards the responseObserver when using factory constructor', async () => {
286
- const observer = vi.fn();
287
- const fakeResponse = makeResponse(200, '{}');
288
- const fetchMock = vi.fn().mockResolvedValue(fakeResponse);
289
- vi.stubGlobal('fetch', fetchMock);
290
-
291
- const factory = vi.fn().mockResolvedValue({
292
- baseUrl: 'https://lazy.example.com',
293
- apiKey: 'lazy-key',
294
- });
295
-
296
- const client = new MantisClient(factory, observer);
297
- await client.get('issues');
298
-
299
- expect(observer).toHaveBeenCalledOnce();
300
- expect(observer).toHaveBeenCalledWith(fakeResponse);
301
- });
302
- });
303
-
304
- // ---------------------------------------------------------------------------
305
- // responseObserver
306
- // ---------------------------------------------------------------------------
307
-
308
- describe('MantisClient – responseObserver', () => {
309
- it('calls responseObserver on successful response', () => {
310
- const observer = vi.fn();
311
- const fakeResponse = makeResponse(200, '{}');
312
- const fetchMock = vi.fn().mockResolvedValue(fakeResponse);
313
- vi.stubGlobal('fetch', fetchMock);
314
-
315
- const client = new MantisClient('https://mantis.example.com', 'token', observer);
316
- return client.get('issues').then(() => {
317
- expect(observer).toHaveBeenCalledOnce();
318
- expect(observer).toHaveBeenCalledWith(fakeResponse);
319
- });
320
- });
321
-
322
- it('does not call responseObserver on error response', async () => {
323
- const observer = vi.fn();
324
- const fetchMock = vi.fn().mockResolvedValue(
325
- makeResponse(500, JSON.stringify({ message: 'Internal Server Error' })),
326
- );
327
- vi.stubGlobal('fetch', fetchMock);
328
-
329
- const client = new MantisClient('https://mantis.example.com', 'token', observer);
330
- await expect(client.get('issues')).rejects.toBeInstanceOf(MantisApiError);
331
- expect(observer).not.toHaveBeenCalled();
332
- });
333
- });
334
-
335
- // ---------------------------------------------------------------------------
336
- // URL helpers
337
- // ---------------------------------------------------------------------------
338
-
339
- describe('buildIssueViewUrl', () => {
340
- it('builds the correct MantisBT issue view URL', () => {
341
- expect(buildIssueViewUrl('https://mantis.example.com', 42))
342
- .toBe('https://mantis.example.com/view.php?id=42');
343
- });
344
-
345
- it('works with base URLs that have a path prefix', () => {
346
- expect(buildIssueViewUrl('https://example.com/mantis', 1))
347
- .toBe('https://example.com/mantis/view.php?id=1');
348
- });
349
- });
350
-
351
- describe('buildNoteViewUrl', () => {
352
- it('builds the correct MantisBT note anchor URL', () => {
353
- expect(buildNoteViewUrl('https://mantis.example.com', 42, 99))
354
- .toBe('https://mantis.example.com/view.php?id=42#bugnote99');
355
- });
356
- });
357
-
358
- describe('MantisClient – getBaseUrl', () => {
359
- it('returns the normalized base URL (direct constructor)', async () => {
360
- const client = new MantisClient('https://mantis.example.com', 'token');
361
- expect(await client.getBaseUrl()).toBe('https://mantis.example.com');
362
- });
363
-
364
- it('returns the base URL from the credential factory', async () => {
365
- const factory = vi.fn().mockResolvedValue({
366
- baseUrl: 'https://lazy.example.com',
367
- apiKey: 'key',
368
- });
369
- const client = new MantisClient(factory);
370
- expect(await client.getBaseUrl()).toBe('https://lazy.example.com');
371
- });
372
- });
@@ -1,263 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
-
3
- // vi.mock is hoisted — must be at module top level
4
- vi.mock('node:fs/promises');
5
-
6
- import { readFile } from 'node:fs/promises';
7
-
8
- // ---------------------------------------------------------------------------
9
- // Helpers
10
- // ---------------------------------------------------------------------------
11
-
12
- /**
13
- * Reset the module registry so that the `cachedConfig` singleton in config.ts
14
- * is re-initialized for each test. Then import getConfig fresh.
15
- */
16
- async function freshGetConfig(): Promise<(typeof import('../src/config.js'))['getConfig']> {
17
- vi.resetModules();
18
- const mod = await import('../src/config.js');
19
- return mod.getConfig;
20
- }
21
-
22
- async function freshGetStartupConfig(): Promise<(typeof import('../src/config.js'))['getStartupConfig']> {
23
- vi.resetModules();
24
- const mod = await import('../src/config.js');
25
- return mod.getStartupConfig;
26
- }
27
-
28
- // ---------------------------------------------------------------------------
29
- // Setup
30
- // ---------------------------------------------------------------------------
31
-
32
- beforeEach(() => {
33
- vi.resetAllMocks();
34
- vi.unstubAllEnvs();
35
- });
36
-
37
- // ---------------------------------------------------------------------------
38
- // ENV-based configuration
39
- // ---------------------------------------------------------------------------
40
-
41
- describe('getConfig() – ENV variables', () => {
42
- it('reads baseUrl and apiKey from environment variables', async () => {
43
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
44
- vi.stubEnv('MANTIS_API_KEY', 'env-api-key');
45
-
46
- const getConfig = await freshGetConfig();
47
- const config = await getConfig();
48
-
49
- expect(config.baseUrl).toBe('https://mantis.example.com');
50
- expect(config.apiKey).toBe('env-api-key');
51
- });
52
-
53
- it('strips trailing slash from baseUrl', async () => {
54
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com/');
55
- vi.stubEnv('MANTIS_API_KEY', 'key');
56
-
57
- const getConfig = await freshGetConfig();
58
- const config = await getConfig();
59
-
60
- expect(config.baseUrl).toBe('https://mantis.example.com');
61
- });
62
-
63
- it('strips /api/rest suffix from baseUrl when user includes it', async () => {
64
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com/api/rest');
65
- vi.stubEnv('MANTIS_API_KEY', 'key');
66
-
67
- const getConfig = await freshGetConfig();
68
- const config = await getConfig();
69
-
70
- expect(config.baseUrl).toBe('https://mantis.example.com');
71
- });
72
-
73
- it('strips /api/rest/ (with trailing slash) from baseUrl', async () => {
74
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com/api/rest/');
75
- vi.stubEnv('MANTIS_API_KEY', 'key');
76
-
77
- const getConfig = await freshGetConfig();
78
- const config = await getConfig();
79
-
80
- expect(config.baseUrl).toBe('https://mantis.example.com');
81
- });
82
-
83
- it('parses MANTIS_CACHE_TTL as a number', async () => {
84
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
85
- vi.stubEnv('MANTIS_API_KEY', 'key');
86
- vi.stubEnv('MANTIS_CACHE_TTL', '7200');
87
-
88
- const getConfig = await freshGetConfig();
89
- const config = await getConfig();
90
-
91
- expect(config.cacheTtl).toBe(7200);
92
- });
93
-
94
- it('uses 3600 as default cacheTtl when MANTIS_CACHE_TTL is not set', async () => {
95
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
96
- vi.stubEnv('MANTIS_API_KEY', 'key');
97
-
98
- const getConfig = await freshGetConfig();
99
- const config = await getConfig();
100
-
101
- expect(config.cacheTtl).toBe(3600);
102
- });
103
-
104
- it('uses MANTIS_CACHE_DIR when provided', async () => {
105
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
106
- vi.stubEnv('MANTIS_API_KEY', 'key');
107
- vi.stubEnv('MANTIS_CACHE_DIR', '/custom/cache/dir');
108
-
109
- const getConfig = await freshGetConfig();
110
- const config = await getConfig();
111
-
112
- expect(config.cacheDir).toBe('/custom/cache/dir');
113
- });
114
- });
115
-
116
- // ---------------------------------------------------------------------------
117
- // Error cases
118
- // ---------------------------------------------------------------------------
119
-
120
- describe('getConfig() – errors', () => {
121
- it('throws when MANTIS_BASE_URL is not set', async () => {
122
- vi.stubEnv('MANTIS_API_KEY', 'some-key');
123
-
124
- const getConfig = await freshGetConfig();
125
- await expect(getConfig()).rejects.toThrow('MANTIS_BASE_URL');
126
- });
127
-
128
- it('throws when MANTIS_API_KEY is not set', async () => {
129
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
130
-
131
- const getConfig = await freshGetConfig();
132
- await expect(getConfig()).rejects.toThrow('MANTIS_API_KEY');
133
- });
134
-
135
- it('throws when no configuration is provided at all', async () => {
136
- const getConfig = await freshGetConfig();
137
- await expect(getConfig()).rejects.toThrow('Missing required MantisBT configuration');
138
- });
139
- });
140
-
141
- // ---------------------------------------------------------------------------
142
- // HTTP transport configuration
143
- // ---------------------------------------------------------------------------
144
-
145
- describe('getConfig() – HTTP transport', () => {
146
- it('uses 127.0.0.1 as default httpHost', async () => {
147
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
148
- vi.stubEnv('MANTIS_API_KEY', 'key');
149
-
150
- const getConfig = await freshGetConfig();
151
- const config = await getConfig();
152
-
153
- expect(config.httpHost).toBe('127.0.0.1');
154
- });
155
-
156
- it('uses 3000 as default httpPort', async () => {
157
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
158
- vi.stubEnv('MANTIS_API_KEY', 'key');
159
-
160
- const getConfig = await freshGetConfig();
161
- const config = await getConfig();
162
-
163
- expect(config.httpPort).toBe(3000);
164
- });
165
-
166
- it('uses PORT when set', async () => {
167
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
168
- vi.stubEnv('MANTIS_API_KEY', 'key');
169
- vi.stubEnv('PORT', '8080');
170
-
171
- const getConfig = await freshGetConfig();
172
- const config = await getConfig();
173
-
174
- expect(config.httpPort).toBe(8080);
175
- });
176
-
177
- it('uses MCP_HTTP_HOST when set', async () => {
178
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
179
- vi.stubEnv('MANTIS_API_KEY', 'key');
180
- vi.stubEnv('MCP_HTTP_HOST', '0.0.0.0');
181
-
182
- const getConfig = await freshGetConfig();
183
- const config = await getConfig();
184
-
185
- expect(config.httpHost).toBe('0.0.0.0');
186
- });
187
-
188
- it('leaves httpToken undefined when MCP_HTTP_TOKEN is not set', async () => {
189
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
190
- vi.stubEnv('MANTIS_API_KEY', 'key');
191
-
192
- const getConfig = await freshGetConfig();
193
- const config = await getConfig();
194
-
195
- expect(config.httpToken).toBeUndefined();
196
- });
197
-
198
- it('reads httpToken from MCP_HTTP_TOKEN', async () => {
199
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
200
- vi.stubEnv('MANTIS_API_KEY', 'key');
201
- vi.stubEnv('MCP_HTTP_TOKEN', 'secret-token');
202
-
203
- const getConfig = await freshGetConfig();
204
- const config = await getConfig();
205
-
206
- expect(config.httpToken).toBe('secret-token');
207
- });
208
- });
209
-
210
- // ---------------------------------------------------------------------------
211
- // getStartupConfig — never throws without credentials
212
- // ---------------------------------------------------------------------------
213
-
214
- describe('getStartupConfig()', () => {
215
- it('succeeds without MANTIS_BASE_URL and MANTIS_API_KEY', async () => {
216
- vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
217
-
218
- const getStartupConfig = await freshGetStartupConfig();
219
- const config = await getStartupConfig();
220
-
221
- expect(config).toBeDefined();
222
- expect(config.cacheTtl).toBe(3600);
223
- expect(config.httpHost).toBe('127.0.0.1');
224
- expect(config.httpPort).toBe(3000);
225
- });
226
-
227
- it('does not include baseUrl or apiKey', async () => {
228
- vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
229
-
230
- const getStartupConfig = await freshGetStartupConfig();
231
- const config = await getStartupConfig();
232
-
233
- expect(config).not.toHaveProperty('baseUrl');
234
- expect(config).not.toHaveProperty('apiKey');
235
- });
236
-
237
- it('reads MANTIS_CACHE_DIR from environment', async () => {
238
- vi.stubEnv('MANTIS_CACHE_DIR', '/tmp/test-cache');
239
- vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
240
-
241
- const getStartupConfig = await freshGetStartupConfig();
242
- const config = await getStartupConfig();
243
-
244
- expect(config.cacheDir).toBe('/tmp/test-cache');
245
- });
246
- });
247
-
248
- // ---------------------------------------------------------------------------
249
- // Singleton caching
250
- // ---------------------------------------------------------------------------
251
-
252
- describe('getConfig() – singleton', () => {
253
- it('returns the same object on repeated calls within the same module instance', async () => {
254
- vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
255
- vi.stubEnv('MANTIS_API_KEY', 'key');
256
-
257
- const getConfig = await freshGetConfig();
258
- const first = await getConfig();
259
- const second = await getConfig();
260
-
261
- expect(first).toBe(second);
262
- });
263
- });
@@ -1,40 +0,0 @@
1
- {
2
- "id": 51,
3
- "name": "user_1",
4
- "real_name": "User One",
5
- "email": "user1@example.com",
6
- "language": "german",
7
- "timezone": "Europe/Berlin",
8
- "access_level": {
9
- "id": 70,
10
- "name": "manager",
11
- "label": "Manager"
12
- },
13
- "created_at": "2026-02-28T09:08:30+01:00",
14
- "projects": [
15
- { "id": 30, "name": "Project 1" },
16
- { "id": 54, "name": "Project 2" },
17
- { "id": 2, "name": "Project 3" },
18
- { "id": 48, "name": "Project 4" },
19
- { "id": 25, "name": "Project 5" },
20
- { "id": 39, "name": "Project 6" },
21
- { "id": 44, "name": "Project 7" },
22
- { "id": 52, "name": "Project 8" },
23
- { "id": 5, "name": "Project 9" },
24
- { "id": 27, "name": "Project 10" },
25
- { "id": 3, "name": "Project 11" },
26
- { "id": 11, "name": "Project 12" },
27
- { "id": 38, "name": "Project 13" },
28
- { "id": 28, "name": "Project 14" },
29
- { "id": 45, "name": "Project 15" },
30
- { "id": 40, "name": "Project 16" },
31
- { "id": 26, "name": "Project 17" },
32
- { "id": 8, "name": "Project 18" },
33
- { "id": 14, "name": "Project 19" },
34
- { "id": 42, "name": "Project 20" },
35
- { "id": 49, "name": "Project 21" },
36
- { "id": 21, "name": "Project 22" },
37
- { "id": 53, "name": "Project 23" },
38
- { "id": 47, "name": "Project 24" }
39
- ]
40
- }