@dpesch/mantisbt-mcp-server 1.5.9 → 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.
- package/CHANGELOG.md +22 -0
- package/README.de.md +22 -11
- package/README.md +22 -11
- package/dist/client.js +37 -23
- package/dist/config.js +41 -50
- package/dist/index.js +23 -13
- package/dist/prompts/index.js +112 -0
- package/dist/resources/index.js +29 -0
- package/dist/tools/config.js +44 -40
- package/docs/cookbook.de.md +218 -0
- package/docs/cookbook.md +218 -0
- package/docs/examples.de.md +42 -0
- package/docs/examples.md +42 -0
- package/package.json +2 -2
- package/server.json +2 -2
- package/tests/client.test.ts +70 -0
- package/tests/config.test.ts +47 -37
- package/tests/helpers/mock-server.ts +61 -0
- package/tests/prompts/prompts.test.ts +242 -0
- package/tests/resources/resources.test.ts +192 -0
package/server.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"name": "io.github.dpesch/mantisbt-mcp-server",
|
|
4
4
|
"title": "MantisBT MCP Server",
|
|
5
5
|
"description": "MantisBT MCP server – manage issues, notes, files, tags, and relationships. With semantic search.",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.6.0",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.6.0",
|
|
12
12
|
"runtimeHint": "npx",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
package/tests/client.test.ts
CHANGED
|
@@ -209,6 +209,76 @@ describe('MantisClient – HTTP methods', () => {
|
|
|
209
209
|
});
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Credential factory (lazy constructor)
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
describe('MantisClient – credential factory', () => {
|
|
217
|
+
it('does not call the factory until the first API method', async () => {
|
|
218
|
+
const factory = vi.fn().mockResolvedValue({
|
|
219
|
+
baseUrl: 'https://lazy.example.com',
|
|
220
|
+
apiKey: 'lazy-key',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
new MantisClient(factory);
|
|
224
|
+
|
|
225
|
+
expect(factory).not.toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('calls the factory on first API method and uses the returned credentials', async () => {
|
|
229
|
+
const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
|
|
230
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
231
|
+
|
|
232
|
+
const factory = vi.fn().mockResolvedValue({
|
|
233
|
+
baseUrl: 'https://lazy.example.com',
|
|
234
|
+
apiKey: 'lazy-key',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const client = new MantisClient(factory);
|
|
238
|
+
await client.get('issues');
|
|
239
|
+
|
|
240
|
+
expect(factory).toHaveBeenCalledOnce();
|
|
241
|
+
const calledUrl: string = fetchMock.mock.calls[0][0] as string;
|
|
242
|
+
expect(calledUrl).toBe('https://lazy.example.com/api/rest/issues');
|
|
243
|
+
const options = fetchMock.mock.calls[0][1] as RequestInit;
|
|
244
|
+
expect((options.headers as Record<string, string>)['Authorization']).toBe('lazy-key');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('caches credentials after the first call and does not call the factory again', async () => {
|
|
248
|
+
const fetchMock = vi.fn().mockResolvedValue(makeResponse(200, '{}'));
|
|
249
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
250
|
+
|
|
251
|
+
const factory = vi.fn().mockResolvedValue({
|
|
252
|
+
baseUrl: 'https://lazy.example.com',
|
|
253
|
+
apiKey: 'lazy-key',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const client = new MantisClient(factory);
|
|
257
|
+
await client.get('issues');
|
|
258
|
+
await client.get('projects');
|
|
259
|
+
|
|
260
|
+
expect(factory).toHaveBeenCalledOnce();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('forwards the responseObserver when using factory constructor', async () => {
|
|
264
|
+
const observer = vi.fn();
|
|
265
|
+
const fakeResponse = makeResponse(200, '{}');
|
|
266
|
+
const fetchMock = vi.fn().mockResolvedValue(fakeResponse);
|
|
267
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
268
|
+
|
|
269
|
+
const factory = vi.fn().mockResolvedValue({
|
|
270
|
+
baseUrl: 'https://lazy.example.com',
|
|
271
|
+
apiKey: 'lazy-key',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const client = new MantisClient(factory, observer);
|
|
275
|
+
await client.get('issues');
|
|
276
|
+
|
|
277
|
+
expect(observer).toHaveBeenCalledOnce();
|
|
278
|
+
expect(observer).toHaveBeenCalledWith(fakeResponse);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
212
282
|
// ---------------------------------------------------------------------------
|
|
213
283
|
// responseObserver
|
|
214
284
|
// ---------------------------------------------------------------------------
|
package/tests/config.test.ts
CHANGED
|
@@ -19,6 +19,12 @@ async function freshGetConfig(): Promise<(typeof import('../src/config.js'))['ge
|
|
|
19
19
|
return mod.getConfig;
|
|
20
20
|
}
|
|
21
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
|
+
|
|
22
28
|
// ---------------------------------------------------------------------------
|
|
23
29
|
// Setup
|
|
24
30
|
// ---------------------------------------------------------------------------
|
|
@@ -87,60 +93,26 @@ describe('getConfig() – ENV variables', () => {
|
|
|
87
93
|
});
|
|
88
94
|
});
|
|
89
95
|
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
// JSON fallback
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
describe('getConfig() – mantis.json fallback', () => {
|
|
95
|
-
it('falls back to ~/.claude/mantis.json when ENV vars are missing', async () => {
|
|
96
|
-
const json = JSON.stringify({ base_url: 'https://from-json.example.com', api_key: 'json-key' });
|
|
97
|
-
vi.mocked(readFile).mockResolvedValue(json as any);
|
|
98
|
-
|
|
99
|
-
const getConfig = await freshGetConfig();
|
|
100
|
-
const config = await getConfig();
|
|
101
|
-
|
|
102
|
-
expect(config.baseUrl).toBe('https://from-json.example.com');
|
|
103
|
-
expect(config.apiKey).toBe('json-key');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('prefers ENV vars over mantis.json values', async () => {
|
|
107
|
-
vi.stubEnv('MANTIS_BASE_URL', 'https://from-env.example.com');
|
|
108
|
-
vi.stubEnv('MANTIS_API_KEY', 'env-key');
|
|
109
|
-
const json = JSON.stringify({ base_url: 'https://from-json.example.com', api_key: 'json-key' });
|
|
110
|
-
vi.mocked(readFile).mockResolvedValue(json as any);
|
|
111
|
-
|
|
112
|
-
const getConfig = await freshGetConfig();
|
|
113
|
-
const config = await getConfig();
|
|
114
|
-
|
|
115
|
-
expect(config.baseUrl).toBe('https://from-env.example.com');
|
|
116
|
-
expect(config.apiKey).toBe('env-key');
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
96
|
// ---------------------------------------------------------------------------
|
|
121
97
|
// Error cases
|
|
122
98
|
// ---------------------------------------------------------------------------
|
|
123
99
|
|
|
124
100
|
describe('getConfig() – errors', () => {
|
|
125
|
-
it('throws when
|
|
101
|
+
it('throws when MANTIS_BASE_URL is not set', async () => {
|
|
126
102
|
vi.stubEnv('MANTIS_API_KEY', 'some-key');
|
|
127
|
-
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
128
103
|
|
|
129
104
|
const getConfig = await freshGetConfig();
|
|
130
105
|
await expect(getConfig()).rejects.toThrow('MANTIS_BASE_URL');
|
|
131
106
|
});
|
|
132
107
|
|
|
133
|
-
it('throws when
|
|
108
|
+
it('throws when MANTIS_API_KEY is not set', async () => {
|
|
134
109
|
vi.stubEnv('MANTIS_BASE_URL', 'https://mantis.example.com');
|
|
135
|
-
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
136
110
|
|
|
137
111
|
const getConfig = await freshGetConfig();
|
|
138
112
|
await expect(getConfig()).rejects.toThrow('MANTIS_API_KEY');
|
|
139
113
|
});
|
|
140
114
|
|
|
141
|
-
it('throws when no configuration is
|
|
142
|
-
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
143
|
-
|
|
115
|
+
it('throws when no configuration is provided at all', async () => {
|
|
144
116
|
const getConfig = await freshGetConfig();
|
|
145
117
|
await expect(getConfig()).rejects.toThrow('Missing required MantisBT configuration');
|
|
146
118
|
});
|
|
@@ -215,6 +187,44 @@ describe('getConfig() – HTTP transport', () => {
|
|
|
215
187
|
});
|
|
216
188
|
});
|
|
217
189
|
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// getStartupConfig — never throws without credentials
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
describe('getStartupConfig()', () => {
|
|
195
|
+
it('succeeds without MANTIS_BASE_URL and MANTIS_API_KEY', async () => {
|
|
196
|
+
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
197
|
+
|
|
198
|
+
const getStartupConfig = await freshGetStartupConfig();
|
|
199
|
+
const config = await getStartupConfig();
|
|
200
|
+
|
|
201
|
+
expect(config).toBeDefined();
|
|
202
|
+
expect(config.cacheTtl).toBe(3600);
|
|
203
|
+
expect(config.httpHost).toBe('127.0.0.1');
|
|
204
|
+
expect(config.httpPort).toBe(3000);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('does not include baseUrl or apiKey', async () => {
|
|
208
|
+
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
209
|
+
|
|
210
|
+
const getStartupConfig = await freshGetStartupConfig();
|
|
211
|
+
const config = await getStartupConfig();
|
|
212
|
+
|
|
213
|
+
expect(config).not.toHaveProperty('baseUrl');
|
|
214
|
+
expect(config).not.toHaveProperty('apiKey');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('reads MANTIS_CACHE_DIR from environment', async () => {
|
|
218
|
+
vi.stubEnv('MANTIS_CACHE_DIR', '/tmp/test-cache');
|
|
219
|
+
vi.mocked(readFile).mockRejectedValue(new Error('ENOENT'));
|
|
220
|
+
|
|
221
|
+
const getStartupConfig = await freshGetStartupConfig();
|
|
222
|
+
const config = await getStartupConfig();
|
|
223
|
+
|
|
224
|
+
expect(config.cacheDir).toBe('/tmp/test-cache');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
218
228
|
// ---------------------------------------------------------------------------
|
|
219
229
|
// Singleton caching
|
|
220
230
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
+
});
|