@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,338 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { readFileSync, existsSync } from 'node:fs';
3
- import { join, dirname } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
-
6
- // vi.mock must be at module top level — vitest hoists it automatically
7
- vi.mock('node:fs/promises');
8
-
9
- import { readFile } from 'node:fs/promises';
10
- import { MantisClient } from '../../src/client.js';
11
- import { MetadataCache, type CachedMetadata } from '../../src/cache.js';
12
- import type { MantisUser } from '../../src/types.js';
13
- import { registerProjectTools } from '../../src/tools/projects.js';
14
- import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
15
-
16
- const __filename = fileURLToPath(import.meta.url);
17
- const __dirname = dirname(__filename);
18
- const fixturesDir = join(__dirname, '..', 'fixtures');
19
-
20
- // ---------------------------------------------------------------------------
21
- // Fixtures laden (mit Inline-Fallback)
22
- // ---------------------------------------------------------------------------
23
-
24
- const listProjectsFixturePath = join(fixturesDir, 'list_projects.json');
25
-
26
- const listProjectsFixture = existsSync(listProjectsFixturePath)
27
- ? (JSON.parse(readFileSync(listProjectsFixturePath, 'utf-8')) as { projects: Array<{ id: number; name: string }> })
28
- : { projects: [{ id: 1, name: 'Test Project' }] };
29
-
30
- const firstProjectId = listProjectsFixture.projects[0]?.id ?? 1;
31
-
32
- // ---------------------------------------------------------------------------
33
- // Setup
34
- // ---------------------------------------------------------------------------
35
-
36
- let mockServer: MockMcpServer;
37
- let client: MantisClient;
38
- let cache: MetadataCache;
39
-
40
- beforeEach(() => {
41
- vi.resetAllMocks();
42
- mockServer = new MockMcpServer();
43
- client = new MantisClient('https://mantis.example.com', 'test-token');
44
- cache = new MetadataCache('/tmp/test-cache-projects', 3600);
45
- registerProjectTools(mockServer as never, client, cache);
46
- vi.stubGlobal('fetch', vi.fn());
47
- });
48
-
49
- afterEach(() => {
50
- vi.unstubAllGlobals();
51
- });
52
-
53
- // ---------------------------------------------------------------------------
54
- // Helpers
55
- // ---------------------------------------------------------------------------
56
-
57
- async function seedCache(users: MantisUser[]): Promise<void> {
58
- const data: CachedMetadata = {
59
- timestamp: Date.now(),
60
- projects: [{ id: 7, name: 'TestProject' }],
61
- byProject: { 7: { users, versions: [], categories: [] } },
62
- tags: [],
63
- };
64
- vi.mocked(readFile).mockResolvedValue(JSON.stringify({ timestamp: Date.now(), data }) as any);
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // list_projects
69
- // ---------------------------------------------------------------------------
70
-
71
- describe('list_projects', () => {
72
- it('ist registriert', () => {
73
- expect(mockServer.hasToolRegistered('list_projects')).toBe(true);
74
- });
75
-
76
- it('gibt Projekte-Array zurück', async () => {
77
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(listProjectsFixture)));
78
-
79
- const result = await mockServer.callTool('list_projects', {});
80
-
81
- expect(result.isError).toBeUndefined();
82
- const parsed = JSON.parse(result.content[0]!.text) as unknown[];
83
- expect(Array.isArray(parsed)).toBe(true);
84
- expect(parsed.length).toBeGreaterThan(0);
85
- });
86
-
87
- it('gibt isError: true bei 401 zurück', async () => {
88
- vi.mocked(fetch).mockResolvedValue(
89
- makeResponse(401, JSON.stringify({ message: 'Unauthorized' })),
90
- );
91
-
92
- const result = await mockServer.callTool('list_projects', {});
93
-
94
- expect(result.isError).toBe(true);
95
- expect(result.content[0]!.text).toContain('Error:');
96
- });
97
-
98
- it('strips custom_fields from projects', async () => {
99
- vi.mocked(fetch).mockResolvedValueOnce(
100
- makeResponse(200, JSON.stringify({
101
- projects: [{ id: 1, name: 'Alpha', custom_fields: [{ field: { id: 9, name: 'cf' }, value: 'x' }] }],
102
- }))
103
- );
104
- const result = await mockServer.callTool('list_projects', {});
105
- const parsed = JSON.parse(result.content[0]!.text) as Array<{ custom_fields?: unknown }>;
106
- expect(parsed[0]!.custom_fields).toBeUndefined();
107
- });
108
-
109
- it('preserves status, enabled, view_state after normalization', async () => {
110
- vi.mocked(fetch).mockResolvedValueOnce(
111
- makeResponse(200, JSON.stringify({
112
- projects: [{ id: 1, name: 'Alpha', enabled: true, status: { id: 10, name: 'development', label: 'Dev' }, view_state: { id: 10, name: 'public', label: 'Public' } }],
113
- }))
114
- );
115
- const result = await mockServer.callTool('list_projects', {});
116
- const parsed = JSON.parse(result.content[0]!.text) as Array<Record<string, unknown>>;
117
- expect(parsed[0]!['enabled']).toBe(true);
118
- expect((parsed[0]!['status'] as Record<string, unknown>)['id']).toBe(10);
119
- expect((parsed[0]!['view_state'] as Record<string, unknown>)['id']).toBe(10);
120
- });
121
-
122
- it('normalizes subprojects recursively', async () => {
123
- vi.mocked(fetch).mockResolvedValueOnce(
124
- makeResponse(200, JSON.stringify({
125
- projects: [{ id: 1, name: 'Parent', subprojects: [{ id: 2, name: 'Child', custom_fields: [{ field: { id: 9 }, value: 'x' }] }] }],
126
- }))
127
- );
128
- const result = await mockServer.callTool('list_projects', {});
129
- const parsed = JSON.parse(result.content[0]!.text) as Array<{ subprojects?: Array<{ custom_fields?: unknown }> }>;
130
- expect(parsed[0]!.subprojects![0]!.custom_fields).toBeUndefined();
131
- });
132
- });
133
-
134
- // ---------------------------------------------------------------------------
135
- // get_project_versions
136
- // ---------------------------------------------------------------------------
137
-
138
- describe('get_project_versions', () => {
139
- it('ist registriert', () => {
140
- expect(mockServer.hasToolRegistered('get_project_versions')).toBe(true);
141
- });
142
-
143
- it('ruft den richtigen Endpoint auf', async () => {
144
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ versions: [] })));
145
-
146
- await mockServer.callTool('get_project_versions', { project_id: firstProjectId });
147
-
148
- const calledUrl = vi.mocked(fetch).mock.calls[0]![0] as string;
149
- expect(calledUrl).toContain(`projects/${firstProjectId}/versions`);
150
- });
151
-
152
- it('does not pass obsolete/inherit by default', async () => {
153
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ versions: [] })));
154
-
155
- await mockServer.callTool('get_project_versions', { project_id: firstProjectId }, { validate: true });
156
-
157
- const calledUrl = vi.mocked(fetch).mock.calls[0]![0] as string;
158
- const url = new URL(calledUrl);
159
- expect(url.searchParams.has('obsolete')).toBe(false);
160
- expect(url.searchParams.has('inherit')).toBe(false);
161
- });
162
-
163
- it('passes obsolete=1 when obsolete: true', async () => {
164
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ versions: [] })));
165
-
166
- await mockServer.callTool('get_project_versions', { project_id: firstProjectId, obsolete: true }, { validate: true });
167
-
168
- const calledUrl = vi.mocked(fetch).mock.calls[0]![0] as string;
169
- const url = new URL(calledUrl);
170
- expect(url.searchParams.get('obsolete')).toBe('1');
171
- });
172
-
173
- it('passes inherit=1 when inherit: true', async () => {
174
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ versions: [] })));
175
-
176
- await mockServer.callTool('get_project_versions', { project_id: firstProjectId, inherit: true }, { validate: true });
177
-
178
- const calledUrl = vi.mocked(fetch).mock.calls[0]![0] as string;
179
- const url = new URL(calledUrl);
180
- expect(url.searchParams.get('inherit')).toBe('1');
181
- });
182
-
183
- it('gibt leeres Array zurück wenn keine Versionen vorhanden', async () => {
184
- const fixture = JSON.parse(readFileSync(join(fixturesDir, 'get_project_versions.json'), 'utf-8')) as { versions: unknown[] };
185
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(fixture)));
186
-
187
- const result = await mockServer.callTool('get_project_versions', { project_id: firstProjectId });
188
-
189
- expect(result.isError).toBeUndefined();
190
- const parsed = JSON.parse(result.content[0]!.text) as unknown[];
191
- expect(Array.isArray(parsed)).toBe(true);
192
- expect(parsed).toHaveLength(0);
193
- });
194
-
195
- it('gibt Versionen zurück wenn vorhanden', async () => {
196
- const fixture = JSON.parse(readFileSync(join(fixturesDir, 'get_project_versions_with_data.json'), 'utf-8')) as { versions: Array<{ id: number; name: string; released: boolean }> };
197
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(fixture)));
198
-
199
- const result = await mockServer.callTool('get_project_versions', { project_id: firstProjectId });
200
-
201
- expect(result.isError).toBeUndefined();
202
- const parsed = JSON.parse(result.content[0]!.text) as Array<{ id: number; name: string; released: boolean }>;
203
- expect(Array.isArray(parsed)).toBe(true);
204
- expect(parsed.length).toBeGreaterThan(0);
205
- expect(parsed[0]).toHaveProperty('id');
206
- expect(parsed[0]).toHaveProperty('name');
207
- expect(parsed[0]).toHaveProperty('released');
208
- });
209
- });
210
-
211
- // ---------------------------------------------------------------------------
212
- // get_project_categories
213
- // ---------------------------------------------------------------------------
214
-
215
- describe('get_project_categories', () => {
216
- it('ist registriert', () => {
217
- expect(mockServer.hasToolRegistered('get_project_categories')).toBe(true);
218
- });
219
-
220
- it('strippt den [All Projects] Prefix', async () => {
221
- const categoriesFixture = {
222
- projects: [{
223
- id: firstProjectId,
224
- name: 'Test Project',
225
- categories: [
226
- { id: 1, name: '[All Projects] General' },
227
- { id: 2, name: 'Backend' },
228
- ],
229
- }],
230
- };
231
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(categoriesFixture)));
232
-
233
- const result = await mockServer.callTool('get_project_categories', { project_id: firstProjectId });
234
-
235
- expect(result.isError).toBeUndefined();
236
- const parsed = JSON.parse(result.content[0]!.text) as Array<{ id: number; name: string }>;
237
- expect(Array.isArray(parsed)).toBe(true);
238
- // Erstes Element soll den Prefix nicht mehr haben
239
- const firstCategory = parsed.find((c) => c.id === 1);
240
- expect(firstCategory?.name).toBe('General');
241
- // Zweites Element ohne Prefix bleibt unverändert
242
- const secondCategory = parsed.find((c) => c.id === 2);
243
- expect(secondCategory?.name).toBe('Backend');
244
- });
245
- });
246
-
247
- // ---------------------------------------------------------------------------
248
- // find_project_member
249
- // ---------------------------------------------------------------------------
250
-
251
- describe('find_project_member', () => {
252
- it('is registered', () => {
253
- expect(mockServer.hasToolRegistered('find_project_member')).toBe(true);
254
- });
255
-
256
- it('returns cached users without fetch', async () => {
257
- const users: MantisUser[] = [{ id: 1, name: 'alice' }, { id: 2, name: 'bob' }];
258
- await seedCache(users);
259
- const result = await mockServer.callTool('find_project_member', { project_id: 7 });
260
- const parsed = JSON.parse(result.content[0]!.text) as MantisUser[];
261
- expect(parsed).toHaveLength(2);
262
- expect(vi.mocked(fetch)).not.toHaveBeenCalled();
263
- });
264
-
265
- it('filters case-insensitively by name', async () => {
266
- await seedCache([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]);
267
- const result = await mockServer.callTool('find_project_member', { project_id: 7, query: 'alice' });
268
- const parsed = JSON.parse(result.content[0]!.text) as MantisUser[];
269
- expect(parsed).toHaveLength(1);
270
- expect(parsed[0]!.name).toBe('Alice');
271
- });
272
-
273
- it('filters by real_name', async () => {
274
- await seedCache([{ id: 1, name: 'a', real_name: 'Alice Smith' }, { id: 2, name: 'b', real_name: 'Bob Jones' }]);
275
- const result = await mockServer.callTool('find_project_member', { project_id: 7, query: 'smith' });
276
- const parsed = JSON.parse(result.content[0]!.text) as MantisUser[];
277
- expect(parsed).toHaveLength(1);
278
- expect(parsed[0]!.real_name).toBe('Alice Smith');
279
- });
280
-
281
- it('filters by email', async () => {
282
- await seedCache([{ id: 1, name: 'a', email: 'alice@example.com' }, { id: 2, name: 'b', email: 'bob@example.com' }]);
283
- const result = await mockServer.callTool('find_project_member', { project_id: 7, query: 'alice@' });
284
- const parsed = JSON.parse(result.content[0]!.text) as MantisUser[];
285
- expect(parsed).toHaveLength(1);
286
- });
287
-
288
- it('applies default limit of 10', async () => {
289
- const users: MantisUser[] = Array.from({ length: 15 }, (_, i) => ({ id: i + 1, name: `user${i + 1}` }));
290
- await seedCache(users);
291
- const result = await mockServer.callTool('find_project_member', { project_id: 7 });
292
- const parsed = JSON.parse(result.content[0]!.text) as MantisUser[];
293
- expect(parsed).toHaveLength(10);
294
- });
295
-
296
- it('respects explicit limit', async () => {
297
- const users: MantisUser[] = Array.from({ length: 15 }, (_, i) => ({ id: i + 1, name: `user${i + 1}` }));
298
- await seedCache(users);
299
- const result = await mockServer.callTool('find_project_member', { project_id: 7, limit: 5 });
300
- const parsed = JSON.parse(result.content[0]!.text) as MantisUser[];
301
- expect(parsed).toHaveLength(5);
302
- });
303
-
304
- it('returns empty array when no match', async () => {
305
- await seedCache([{ id: 1, name: 'alice' }]);
306
- const result = await mockServer.callTool('find_project_member', { project_id: 7, query: 'zzznomatch' });
307
- const parsed = JSON.parse(result.content[0]!.text) as MantisUser[];
308
- expect(parsed).toHaveLength(0);
309
- });
310
-
311
- it('falls back to live API when cache is cold', async () => {
312
- vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
313
- vi.mocked(fetch).mockResolvedValueOnce(
314
- makeResponse(200, JSON.stringify({ users: [{ id: 1, name: 'alice' }] }))
315
- );
316
- const result = await mockServer.callTool('find_project_member', { project_id: 7 });
317
- const parsed = JSON.parse(result.content[0]!.text) as MantisUser[];
318
- expect(parsed).toHaveLength(1);
319
- expect(vi.mocked(fetch)).toHaveBeenCalledOnce();
320
- });
321
-
322
- it('returns isError on API failure', async () => {
323
- vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
324
- vi.mocked(fetch).mockResolvedValueOnce(makeResponse(500, JSON.stringify({ message: 'Internal Server Error' })));
325
- const result = await mockServer.callTool('find_project_member', { project_id: 7 });
326
- expect(result.isError).toBe(true);
327
- });
328
-
329
- it('rejects invalid project_id', async () => {
330
- const result = await mockServer.callTool('find_project_member', { project_id: -1 }, { validate: true });
331
- expect(result.isError).toBe(true);
332
- });
333
-
334
- it('rejects limit < 1', async () => {
335
- const result = await mockServer.callTool('find_project_member', { project_id: 7, limit: 0 }, { validate: true });
336
- expect(result.isError).toBe(true);
337
- });
338
- });
@@ -1,177 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { MantisClient } from '../../src/client.js';
3
- import { registerRelationshipTools } from '../../src/tools/relationships.js';
4
- import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
5
-
6
- // ---------------------------------------------------------------------------
7
- // Setup
8
- // ---------------------------------------------------------------------------
9
-
10
- let mockServer: MockMcpServer;
11
- let client: MantisClient;
12
-
13
- beforeEach(() => {
14
- mockServer = new MockMcpServer();
15
- client = new MantisClient('https://mantis.example.com', 'test-token');
16
- registerRelationshipTools(mockServer as never, client);
17
- vi.stubGlobal('fetch', vi.fn());
18
- });
19
-
20
- afterEach(() => {
21
- vi.unstubAllGlobals();
22
- });
23
-
24
- // ---------------------------------------------------------------------------
25
- // add_relationship
26
- // ---------------------------------------------------------------------------
27
-
28
- describe('add_relationship', () => {
29
- it('is registered', () => {
30
- expect(mockServer.hasToolRegistered('add_relationship')).toBe(true);
31
- });
32
-
33
- it('posts to the correct endpoint', async () => {
34
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 10 })));
35
-
36
- await mockServer.callTool('add_relationship', { issue_id: 42, target_id: 99, type_id: 1 });
37
-
38
- const calledUrl = vi.mocked(fetch).mock.calls[0]![0] as string;
39
- expect(calledUrl).toContain('issues/42/relationships');
40
- });
41
-
42
- it('returns the API response', async () => {
43
- const apiResponse = { id: 10, issue: { id: 99, name: '#99' }, type: { id: 1, label: 'related_to' } };
44
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(apiResponse)));
45
-
46
- const result = await mockServer.callTool('add_relationship', { issue_id: 42, target_id: 99, type_id: 1 });
47
-
48
- expect(result.isError).toBeUndefined();
49
- const parsed = JSON.parse(result.content[0]!.text) as { id: number };
50
- expect(parsed.id).toBe(10);
51
- });
52
-
53
- it('returns isError on API error', async () => {
54
- vi.mocked(fetch).mockResolvedValue(makeResponse(404, JSON.stringify({ message: 'Issue not found' })));
55
-
56
- const result = await mockServer.callTool('add_relationship', { issue_id: 999, target_id: 1, type_id: 0 });
57
-
58
- expect(result.isError).toBe(true);
59
- expect(result.content[0]!.text).toContain('Error:');
60
- });
61
- });
62
-
63
- // ---------------------------------------------------------------------------
64
- // add_relationship – type_name parameter
65
- // ---------------------------------------------------------------------------
66
-
67
- describe('add_relationship – type_name', () => {
68
- it('accepts "related_to" and sends type_id 1', async () => {
69
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
70
-
71
- await mockServer.callTool('add_relationship', { issue_id: 10, target_id: 20, type_name: 'related_to' });
72
-
73
- const body = JSON.parse((vi.mocked(fetch).mock.calls[0]![1] as RequestInit).body as string) as { type: { id: number } };
74
- expect(body.type.id).toBe(1);
75
- });
76
-
77
- it('accepts "related-to" (dash variant) and sends type_id 1', async () => {
78
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
79
-
80
- await mockServer.callTool('add_relationship', { issue_id: 10, target_id: 20, type_name: 'related-to' });
81
-
82
- const body = JSON.parse((vi.mocked(fetch).mock.calls[0]![1] as RequestInit).body as string) as { type: { id: number } };
83
- expect(body.type.id).toBe(1);
84
- });
85
-
86
- it('accepts "duplicate_of" and sends type_id 0', async () => {
87
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
88
-
89
- await mockServer.callTool('add_relationship', { issue_id: 10, target_id: 20, type_name: 'duplicate_of' });
90
-
91
- const body = JSON.parse((vi.mocked(fetch).mock.calls[0]![1] as RequestInit).body as string) as { type: { id: number } };
92
- expect(body.type.id).toBe(0);
93
- });
94
-
95
- it('accepts "depends_on" as alias for parent_of (type_id 2)', async () => {
96
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
97
-
98
- await mockServer.callTool('add_relationship', { issue_id: 10, target_id: 20, type_name: 'depends_on' });
99
-
100
- const body = JSON.parse((vi.mocked(fetch).mock.calls[0]![1] as RequestInit).body as string) as { type: { id: number } };
101
- expect(body.type.id).toBe(2);
102
- });
103
-
104
- it('accepts "blocks" as alias for child_of (type_id 3)', async () => {
105
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
106
-
107
- await mockServer.callTool('add_relationship', { issue_id: 10, target_id: 20, type_name: 'blocks' });
108
-
109
- const body = JSON.parse((vi.mocked(fetch).mock.calls[0]![1] as RequestInit).body as string) as { type: { id: number } };
110
- expect(body.type.id).toBe(3);
111
- });
112
-
113
- it('type_id takes precedence when both type_id and type_name are given', async () => {
114
- vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 5 })));
115
-
116
- await mockServer.callTool('add_relationship', { issue_id: 10, target_id: 20, type_id: 4, type_name: 'related_to' });
117
-
118
- const body = JSON.parse((vi.mocked(fetch).mock.calls[0]![1] as RequestInit).body as string) as { type: { id: number } };
119
- expect(body.type.id).toBe(4);
120
- });
121
-
122
- it('returns error for unknown type_name', async () => {
123
- const result = await mockServer.callTool('add_relationship', { issue_id: 10, target_id: 20, type_name: 'nonsense' });
124
-
125
- expect(result.isError).toBe(true);
126
- expect(result.content[0]!.text).toContain('nonsense');
127
- expect(fetch).not.toHaveBeenCalled();
128
- });
129
-
130
- it('returns error when neither type_id nor type_name is given', async () => {
131
- const result = await mockServer.callTool('add_relationship', { issue_id: 10, target_id: 20 });
132
-
133
- expect(result.isError).toBe(true);
134
- expect(fetch).not.toHaveBeenCalled();
135
- });
136
- });
137
-
138
- // ---------------------------------------------------------------------------
139
- // remove_relationship
140
- // ---------------------------------------------------------------------------
141
-
142
- describe('remove_relationship', () => {
143
- it('is registered', () => {
144
- expect(mockServer.hasToolRegistered('remove_relationship')).toBe(true);
145
- });
146
-
147
- it('sends DELETE to the correct endpoint', async () => {
148
- vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
149
-
150
- await mockServer.callTool('remove_relationship', { issue_id: 42, relationship_id: 7 });
151
-
152
- const call = vi.mocked(fetch).mock.calls[0]!;
153
- const calledUrl = call[0] as string;
154
- const options = call[1] as RequestInit;
155
- expect(calledUrl).toContain('issues/42/relationships/7');
156
- expect(options.method).toBe('DELETE');
157
- });
158
-
159
- it('returns { success: true } on 204', async () => {
160
- vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
161
-
162
- const result = await mockServer.callTool('remove_relationship', { issue_id: 42, relationship_id: 7 });
163
-
164
- expect(result.isError).toBeUndefined();
165
- const parsed = JSON.parse(result.content[0]!.text) as { success: boolean };
166
- expect(parsed.success).toBe(true);
167
- });
168
-
169
- it('returns isError on API error', async () => {
170
- vi.mocked(fetch).mockResolvedValue(makeResponse(404, JSON.stringify({ message: 'Relationship not found' })));
171
-
172
- const result = await mockServer.callTool('remove_relationship', { issue_id: 42, relationship_id: 999 });
173
-
174
- expect(result.isError).toBe(true);
175
- expect(result.content[0]!.text).toContain('Error:');
176
- });
177
- });