@dpesch/mantisbt-mcp-server 1.5.8 → 1.6.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.
@@ -24,9 +24,33 @@ interface ToolDefinition {
24
24
  [key: string]: unknown;
25
25
  }
26
26
 
27
+ export interface PromptMessage {
28
+ role: string;
29
+ content: { type: string; text: string };
30
+ }
31
+
32
+ export interface PromptResult {
33
+ messages: PromptMessage[];
34
+ }
35
+
36
+ export interface ResourceResult {
37
+ contents: Array<{ uri: string; mimeType?: string; text: string }>;
38
+ }
39
+
40
+ type PromptHandler = (args: Record<string, unknown>) => PromptResult;
41
+
42
+ type ResourceHandler = (uri: URL) => Promise<ResourceResult>;
43
+
44
+ interface PromptDefinition {
45
+ argsSchema?: Record<string, z.ZodTypeAny>;
46
+ [key: string]: unknown;
47
+ }
48
+
27
49
  export class MockMcpServer {
28
50
  private readonly handlers = new Map<string, ToolHandler>();
29
51
  private readonly schemas = new Map<string, z.ZodTypeAny>();
52
+ private readonly promptHandlers = new Map<string, PromptHandler>();
53
+ private readonly resourceHandlers = new Map<string, ResourceHandler>();
30
54
 
31
55
  // Nachahmt McpServer.registerTool – fängt Handler und Schema ein
32
56
  registerTool(name: string, definition: ToolDefinition, handler: ToolHandler): void {
@@ -36,6 +60,11 @@ export class MockMcpServer {
36
60
  }
37
61
  }
38
62
 
63
+ // Nachahmt McpServer.registerPrompt
64
+ registerPrompt(name: string, _definition: PromptDefinition, handler: PromptHandler): void {
65
+ this.promptHandlers.set(name, handler);
66
+ }
67
+
39
68
  /**
40
69
  * Ruft den Handler auf. Wenn `validate: true`, wird der Input zuerst
41
70
  * durch das Zod-Schema geparst (wie der echte MCP-Server es tut).
@@ -66,11 +95,43 @@ export class MockMcpServer {
66
95
  return handler(args);
67
96
  }
68
97
 
98
+ callPrompt(name: string, args: Record<string, unknown> = {}): PromptResult {
99
+ const handler = this.promptHandlers.get(name);
100
+ if (!handler) throw new Error(`Prompt not registered: ${name}`);
101
+ return handler(args);
102
+ }
103
+
69
104
  hasToolRegistered(name: string): boolean {
70
105
  return this.handlers.has(name);
71
106
  }
72
107
 
108
+ hasPromptRegistered(name: string): boolean {
109
+ return this.promptHandlers.has(name);
110
+ }
111
+
73
112
  registeredToolNames(): string[] {
74
113
  return [...this.handlers.keys()];
75
114
  }
115
+
116
+ registeredPromptNames(): string[] {
117
+ return [...this.promptHandlers.keys()];
118
+ }
119
+
120
+ registerResource(name: string, uri: string, _config: unknown, handler: ResourceHandler): void {
121
+ this.resourceHandlers.set(uri, handler);
122
+ }
123
+
124
+ async callResource(uri: string): Promise<ResourceResult> {
125
+ const handler = this.resourceHandlers.get(uri);
126
+ if (!handler) throw new Error(`Resource not registered: ${uri}`);
127
+ return handler(new URL(uri));
128
+ }
129
+
130
+ hasResourceRegistered(uri: string): boolean {
131
+ return this.resourceHandlers.has(uri);
132
+ }
133
+
134
+ registeredResourceUris(): string[] {
135
+ return [...this.resourceHandlers.keys()];
136
+ }
76
137
  }
@@ -0,0 +1,242 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { registerPrompts } from '../../src/prompts/index.js';
3
+ import { MockMcpServer } from '../helpers/mock-server.js';
4
+
5
+ let mockServer: MockMcpServer;
6
+
7
+ beforeEach(() => {
8
+ mockServer = new MockMcpServer();
9
+ registerPrompts(mockServer as never);
10
+ });
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Registration
14
+ // ---------------------------------------------------------------------------
15
+
16
+ describe('prompt registration', () => {
17
+ it('registers create-bug-report', () => {
18
+ expect(mockServer.hasPromptRegistered('create-bug-report')).toBe(true);
19
+ });
20
+
21
+ it('registers create-feature-request', () => {
22
+ expect(mockServer.hasPromptRegistered('create-feature-request')).toBe(true);
23
+ });
24
+
25
+ it('registers summarize-issue', () => {
26
+ expect(mockServer.hasPromptRegistered('summarize-issue')).toBe(true);
27
+ });
28
+
29
+ it('registers project-status', () => {
30
+ expect(mockServer.hasPromptRegistered('project-status')).toBe(true);
31
+ });
32
+ });
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // create-bug-report
36
+ // ---------------------------------------------------------------------------
37
+
38
+ describe('create-bug-report', () => {
39
+ it('returns a single user message', () => {
40
+ const result = mockServer.callPrompt('create-bug-report', {
41
+ project_id: 1,
42
+ category: 'General',
43
+ summary: 'Login fails',
44
+ description: 'Cannot log in after password reset',
45
+ });
46
+
47
+ expect(result.messages).toHaveLength(1);
48
+ expect(result.messages[0]!.role).toBe('user');
49
+ expect(result.messages[0]!.content.type).toBe('text');
50
+ });
51
+
52
+ it('includes project_id, category, summary, and description in the text', () => {
53
+ const result = mockServer.callPrompt('create-bug-report', {
54
+ project_id: 7,
55
+ category: 'Authentication',
56
+ summary: 'Session expires too early',
57
+ description: 'Session is invalidated after 5 minutes of inactivity',
58
+ });
59
+
60
+ const text = result.messages[0]!.content.text;
61
+ expect(text).toContain('project 7');
62
+ expect(text).toContain('Authentication');
63
+ expect(text).toContain('Session expires too early');
64
+ expect(text).toContain('Session is invalidated after 5 minutes');
65
+ });
66
+
67
+ it('includes optional steps_to_reproduce when provided', () => {
68
+ const result = mockServer.callPrompt('create-bug-report', {
69
+ project_id: 1,
70
+ category: 'General',
71
+ summary: 'Bug',
72
+ description: 'Desc',
73
+ steps_to_reproduce: '1. Open app\n2. Click login',
74
+ });
75
+
76
+ expect(result.messages[0]!.content.text).toContain('Steps to reproduce');
77
+ expect(result.messages[0]!.content.text).toContain('1. Open app');
78
+ });
79
+
80
+ it('omits optional sections when not provided', () => {
81
+ const result = mockServer.callPrompt('create-bug-report', {
82
+ project_id: 1,
83
+ category: 'General',
84
+ summary: 'Bug',
85
+ description: 'Desc',
86
+ });
87
+
88
+ const text = result.messages[0]!.content.text;
89
+ expect(text).not.toContain('Steps to reproduce');
90
+ expect(text).not.toContain('Expected behavior');
91
+ expect(text).not.toContain('Actual behavior');
92
+ expect(text).not.toContain('Environment');
93
+ });
94
+
95
+ it('includes all optional fields when provided', () => {
96
+ const result = mockServer.callPrompt('create-bug-report', {
97
+ project_id: 1,
98
+ category: 'UI',
99
+ summary: 'Button broken',
100
+ description: 'Save button does nothing',
101
+ steps_to_reproduce: 'Click Save',
102
+ expected: 'Form is saved',
103
+ actual: 'Nothing happens',
104
+ environment: 'Chrome 120, Windows 11',
105
+ });
106
+
107
+ const text = result.messages[0]!.content.text;
108
+ expect(text).toContain('Steps to reproduce');
109
+ expect(text).toContain('Expected behavior');
110
+ expect(text).toContain('Actual behavior');
111
+ expect(text).toContain('Environment');
112
+ expect(text).toContain('Chrome 120');
113
+ });
114
+
115
+ it('mentions create_issue tool', () => {
116
+ const result = mockServer.callPrompt('create-bug-report', {
117
+ project_id: 1,
118
+ category: 'General',
119
+ summary: 'Bug',
120
+ description: 'Desc',
121
+ });
122
+
123
+ expect(result.messages[0]!.content.text).toContain('create_issue');
124
+ });
125
+ });
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // create-feature-request
129
+ // ---------------------------------------------------------------------------
130
+
131
+ describe('create-feature-request', () => {
132
+ it('returns a single user message', () => {
133
+ const result = mockServer.callPrompt('create-feature-request', {
134
+ project_id: 2,
135
+ category: 'General',
136
+ summary: 'Dark mode',
137
+ description: 'Add a dark mode toggle to the UI',
138
+ });
139
+
140
+ expect(result.messages).toHaveLength(1);
141
+ expect(result.messages[0]!.role).toBe('user');
142
+ });
143
+
144
+ it('includes project_id, category, summary, and description', () => {
145
+ const result = mockServer.callPrompt('create-feature-request', {
146
+ project_id: 5,
147
+ category: 'UX',
148
+ summary: 'Export to CSV',
149
+ description: 'Allow exporting issue lists as CSV',
150
+ });
151
+
152
+ const text = result.messages[0]!.content.text;
153
+ expect(text).toContain('project 5');
154
+ expect(text).toContain('UX');
155
+ expect(text).toContain('Export to CSV');
156
+ expect(text).toContain('Allow exporting issue lists');
157
+ });
158
+
159
+ it('includes use_case when provided', () => {
160
+ const result = mockServer.callPrompt('create-feature-request', {
161
+ project_id: 1,
162
+ category: 'General',
163
+ summary: 'Feature',
164
+ description: 'Desc',
165
+ use_case: 'Needed for monthly reporting',
166
+ });
167
+
168
+ expect(result.messages[0]!.content.text).toContain('Needed for monthly reporting');
169
+ });
170
+
171
+ it('omits use_case section when not provided', () => {
172
+ const result = mockServer.callPrompt('create-feature-request', {
173
+ project_id: 1,
174
+ category: 'General',
175
+ summary: 'Feature',
176
+ description: 'Desc',
177
+ });
178
+
179
+ expect(result.messages[0]!.content.text).not.toContain('Use case');
180
+ });
181
+
182
+ it('mentions create_issue tool', () => {
183
+ const result = mockServer.callPrompt('create-feature-request', {
184
+ project_id: 1,
185
+ category: 'General',
186
+ summary: 'Feature',
187
+ description: 'Desc',
188
+ });
189
+
190
+ expect(result.messages[0]!.content.text).toContain('create_issue');
191
+ });
192
+ });
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // summarize-issue
196
+ // ---------------------------------------------------------------------------
197
+
198
+ describe('summarize-issue', () => {
199
+ it('returns a single user message', () => {
200
+ const result = mockServer.callPrompt('summarize-issue', { issue_id: 42 });
201
+
202
+ expect(result.messages).toHaveLength(1);
203
+ expect(result.messages[0]!.role).toBe('user');
204
+ });
205
+
206
+ it('includes the issue ID in the text', () => {
207
+ const result = mockServer.callPrompt('summarize-issue', { issue_id: 1234 });
208
+
209
+ expect(result.messages[0]!.content.text).toContain('1234');
210
+ });
211
+
212
+ it('mentions get_issue tool', () => {
213
+ const result = mockServer.callPrompt('summarize-issue', { issue_id: 1 });
214
+
215
+ expect(result.messages[0]!.content.text).toContain('get_issue');
216
+ });
217
+ });
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // project-status
221
+ // ---------------------------------------------------------------------------
222
+
223
+ describe('project-status', () => {
224
+ it('returns a single user message', () => {
225
+ const result = mockServer.callPrompt('project-status', { project_id: 3 });
226
+
227
+ expect(result.messages).toHaveLength(1);
228
+ expect(result.messages[0]!.role).toBe('user');
229
+ });
230
+
231
+ it('includes the project ID in the text', () => {
232
+ const result = mockServer.callPrompt('project-status', { project_id: 99 });
233
+
234
+ expect(result.messages[0]!.content.text).toContain('99');
235
+ });
236
+
237
+ it('mentions list_issues tool', () => {
238
+ const result = mockServer.callPrompt('project-status', { project_id: 1 });
239
+
240
+ expect(result.messages[0]!.content.text).toContain('list_issues');
241
+ });
242
+ });
@@ -0,0 +1,192 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { MantisClient } from '../../src/client.js';
3
+ import { MetadataCache } from '../../src/cache.js';
4
+ import { registerResources } from '../../src/resources/index.js';
5
+ import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Inline fixtures
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const USER_FIXTURE = { id: 1, name: 'jsmith', real_name: 'John Smith', email: 'jsmith@example.com' };
12
+
13
+ const PROJECTS_FIXTURE = [
14
+ { id: 10, name: 'Alpha', enabled: true },
15
+ { id: 11, name: 'Beta', enabled: true },
16
+ ];
17
+
18
+ const ENUM_FIXTURE = {
19
+ configs: [
20
+ { option: 'severity_enum_string', value: '10:feature,50:minor,80:block' },
21
+ { option: 'status_enum_string', value: '10:new,80:resolved,90:closed' },
22
+ { option: 'priority_enum_string', value: '10:none,30:normal,60:immediate' },
23
+ { option: 'resolution_enum_string', value: '10:open,20:fixed' },
24
+ { option: 'reproducibility_enum_string', value: '10:always,70:have not tried' },
25
+ ],
26
+ };
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Setup
30
+ // ---------------------------------------------------------------------------
31
+
32
+ let mockServer: MockMcpServer;
33
+ let client: MantisClient;
34
+ let cache: MetadataCache;
35
+
36
+ beforeEach(() => {
37
+ mockServer = new MockMcpServer();
38
+ client = new MantisClient('https://mantis.example.com', 'test-token');
39
+ cache = new MetadataCache('/tmp/cache-resources-test', 86400);
40
+ registerResources(mockServer as never, client, cache);
41
+ vi.stubGlobal('fetch', vi.fn());
42
+ });
43
+
44
+ afterEach(async () => {
45
+ vi.unstubAllGlobals();
46
+ await cache.invalidate();
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Registration
51
+ // ---------------------------------------------------------------------------
52
+
53
+ describe('resource registration', () => {
54
+ it('registers mantis://me', () => {
55
+ expect(mockServer.hasResourceRegistered('mantis://me')).toBe(true);
56
+ });
57
+
58
+ it('registers mantis://projects', () => {
59
+ expect(mockServer.hasResourceRegistered('mantis://projects')).toBe(true);
60
+ });
61
+
62
+ it('registers mantis://enums', () => {
63
+ expect(mockServer.hasResourceRegistered('mantis://enums')).toBe(true);
64
+ });
65
+ });
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // mantis://me
69
+ // ---------------------------------------------------------------------------
70
+
71
+ describe('mantis://me', () => {
72
+ it('returns a single content item with application/json', async () => {
73
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
74
+
75
+ const result = await mockServer.callResource('mantis://me');
76
+
77
+ expect(result.contents).toHaveLength(1);
78
+ expect(result.contents[0]!.mimeType).toBe('application/json');
79
+ });
80
+
81
+ it('sets uri to mantis://me', async () => {
82
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
83
+
84
+ const result = await mockServer.callResource('mantis://me');
85
+
86
+ expect(result.contents[0]!.uri).toBe('mantis://me');
87
+ });
88
+
89
+ it('returns valid JSON with id and name', async () => {
90
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
91
+
92
+ const result = await mockServer.callResource('mantis://me');
93
+
94
+ const parsed = JSON.parse(result.contents[0]!.text) as { id: number; name: string };
95
+ expect(parsed.id).toBe(USER_FIXTURE.id);
96
+ expect(parsed.name).toBe(USER_FIXTURE.name);
97
+ });
98
+ });
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // mantis://projects
102
+ // ---------------------------------------------------------------------------
103
+
104
+ describe('mantis://projects', () => {
105
+ it('returns a single content item with application/json', async () => {
106
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
107
+
108
+ const result = await mockServer.callResource('mantis://projects');
109
+
110
+ expect(result.contents).toHaveLength(1);
111
+ expect(result.contents[0]!.mimeType).toBe('application/json');
112
+ });
113
+
114
+ it('sets uri to mantis://projects', async () => {
115
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
116
+
117
+ const result = await mockServer.callResource('mantis://projects');
118
+
119
+ expect(result.contents[0]!.uri).toBe('mantis://projects');
120
+ });
121
+
122
+ it('returns a JSON array of projects from live API when cache is empty', async () => {
123
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
124
+
125
+ const result = await mockServer.callResource('mantis://projects');
126
+
127
+ const parsed = JSON.parse(result.contents[0]!.text) as unknown[];
128
+ expect(Array.isArray(parsed)).toBe(true);
129
+ expect(parsed).toHaveLength(2);
130
+ });
131
+
132
+ it('serves from cache without calling the API when cache is valid', async () => {
133
+ await cache.save({
134
+ timestamp: Date.now(),
135
+ projects: PROJECTS_FIXTURE,
136
+ byProject: {},
137
+ tags: [],
138
+ });
139
+
140
+ const result = await mockServer.callResource('mantis://projects');
141
+
142
+ expect(vi.mocked(fetch)).not.toHaveBeenCalled();
143
+ const parsed = JSON.parse(result.contents[0]!.text) as unknown[];
144
+ expect(parsed).toHaveLength(2);
145
+ });
146
+ });
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // mantis://enums
150
+ // ---------------------------------------------------------------------------
151
+
152
+ describe('mantis://enums', () => {
153
+ it('returns a single content item with application/json', async () => {
154
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
155
+
156
+ const result = await mockServer.callResource('mantis://enums');
157
+
158
+ expect(result.contents).toHaveLength(1);
159
+ expect(result.contents[0]!.mimeType).toBe('application/json');
160
+ });
161
+
162
+ it('sets uri to mantis://enums', async () => {
163
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
164
+
165
+ const result = await mockServer.callResource('mantis://enums');
166
+
167
+ expect(result.contents[0]!.uri).toBe('mantis://enums');
168
+ });
169
+
170
+ it('returns parsed enum groups with id and name entries', async () => {
171
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
172
+
173
+ const result = await mockServer.callResource('mantis://enums');
174
+
175
+ const parsed = JSON.parse(result.contents[0]!.text) as Record<string, Array<{ id: number; name: string }>>;
176
+ for (const key of ['severity', 'priority', 'status', 'resolution', 'reproducibility']) {
177
+ expect(Array.isArray(parsed[key])).toBe(true);
178
+ expect(parsed[key]!.length).toBeGreaterThan(0);
179
+ expect(parsed[key]![0]).toMatchObject({ id: expect.any(Number), name: expect.any(String) });
180
+ }
181
+ });
182
+
183
+ it('parses severity values correctly from fixture', async () => {
184
+ vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
185
+
186
+ const result = await mockServer.callResource('mantis://enums');
187
+
188
+ const parsed = JSON.parse(result.contents[0]!.text) as Record<string, Array<{ id: number; name: string }>>;
189
+ expect(parsed['severity']).toContainEqual({ id: 10, name: 'feature' });
190
+ expect(parsed['severity']).toContainEqual({ id: 50, name: 'minor' });
191
+ });
192
+ });
@@ -112,8 +112,10 @@ describe('create_issue', () => {
112
112
  expect(mockServer.hasToolRegistered('create_issue')).toBe(true);
113
113
  });
114
114
 
115
- it('sends severity: { name: "minor" } by default when no severity is provided', async () => {
115
+ it('sends severity as { id: 50 } (minor) by default when no severity is provided', async () => {
116
116
  // Regression: omitting severity caused MantisBT to store 0 → displayed as "@0@".
117
+ // The server now resolves canonical English names to IDs so MantisBT language setting
118
+ // does not affect how the value is stored.
117
119
  // validate: true ensures Zod defaults are applied before the handler runs.
118
120
  vi.mocked(fetch).mockResolvedValue(
119
121
  makeResponse(201, JSON.stringify({ issue: { id: 100, summary: 'Test' } }))
@@ -126,7 +128,7 @@ describe('create_issue', () => {
126
128
  }, { validate: true });
127
129
 
128
130
  const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as Record<string, unknown>;
129
- expect(body.severity).toEqual({ name: 'minor' });
131
+ expect(body.severity).toEqual({ id: 50 });
130
132
  });
131
133
 
132
134
  it('returns full issue object when API responds with complete issue', async () => {
@@ -137,7 +139,7 @@ describe('create_issue', () => {
137
139
 
138
140
  const result = await mockServer.callTool('create_issue', {
139
141
  summary: 'New issue', project_id: 1, category: 'General',
140
- });
142
+ }, { validate: true });
141
143
 
142
144
  expect(result.isError).toBeUndefined();
143
145
  const parsed = JSON.parse(result.content[0]!.text) as typeof fullIssue;
@@ -155,7 +157,7 @@ describe('create_issue', () => {
155
157
 
156
158
  const result = await mockServer.callTool('create_issue', {
157
159
  summary: 'Created issue', project_id: 1, category: 'General',
158
- });
160
+ }, { validate: true });
159
161
 
160
162
  expect(result.isError).toBeUndefined();
161
163
  const parsed = JSON.parse(result.content[0]!.text) as typeof fullIssue;
@@ -173,7 +175,7 @@ describe('create_issue', () => {
173
175
 
174
176
  const result = await mockServer.callTool('create_issue', {
175
177
  summary: 'Test', project_id: 1, category: 'General',
176
- });
178
+ }, { validate: true });
177
179
 
178
180
  expect(result.isError).toBeUndefined();
179
181
  const parsed = JSON.parse(result.content[0]!.text) as { id: number };
@@ -193,7 +195,21 @@ describe('create_issue', () => {
193
195
  }, { validate: true });
194
196
 
195
197
  const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as Record<string, unknown>;
196
- expect(body.severity).toEqual({ name: 'crash' });
198
+ expect(body.severity).toEqual({ id: 70 });
199
+ });
200
+
201
+ it('returns an error for an unknown severity name', async () => {
202
+ const result = await mockServer.callTool('create_issue', {
203
+ summary: 'Test',
204
+ project_id: 1,
205
+ category: 'General',
206
+ severity: 'schwerer Fehler',
207
+ }, { validate: true });
208
+
209
+ expect(result.isError).toBe(true);
210
+ expect(result.content[0]!.text).toContain('schwerer Fehler');
211
+ expect(result.content[0]!.text).toContain('minor');
212
+ expect(fetch).not.toHaveBeenCalled();
197
213
  });
198
214
  });
199
215
 
@@ -213,7 +229,7 @@ describe('create_issue – handler username', () => {
213
229
 
214
230
  await server.callTool('create_issue', {
215
231
  summary: 'Test', project_id: 1, category: 'General', handler: 'dom',
216
- });
232
+ }, { validate: true });
217
233
 
218
234
  const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as { handler: { id: number } };
219
235
  expect(body.handler).toEqual({ id: 7 });
@@ -230,7 +246,7 @@ describe('create_issue – handler username', () => {
230
246
 
231
247
  await server.callTool('create_issue', {
232
248
  summary: 'Test', project_id: 1, category: 'General', handler: 'John Doe',
233
- });
249
+ }, { validate: true });
234
250
 
235
251
  const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as { handler: { id: number } };
236
252
  expect(body.handler).toEqual({ id: 9 });
@@ -247,7 +263,7 @@ describe('create_issue – handler username', () => {
247
263
 
248
264
  await server.callTool('create_issue', {
249
265
  summary: 'Test', project_id: 1, category: 'General', handler: 'alice',
250
- });
266
+ }, { validate: true });
251
267
 
252
268
  const projectUsersCall = vi.mocked(fetch).mock.calls[0]![0] as string;
253
269
  expect(projectUsersCall).toContain('projects/1/users');
@@ -282,7 +298,7 @@ describe('create_issue – handler username', () => {
282
298
 
283
299
  await server.callTool('create_issue', {
284
300
  summary: 'Test', project_id: 1, category: 'General', handler_id: 99, handler: 'dom',
285
- });
301
+ }, { validate: true });
286
302
 
287
303
  const body = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) as { handler: { id: number } };
288
304
  expect(body.handler).toEqual({ id: 99 });