@dpesch/mantisbt-mcp-server 1.10.3 → 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.
- package/CHANGELOG.md +24 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/.gitea/PULL_REQUEST_TEMPLATE.md +0 -17
- package/.gitea/workflows/ci.yml +0 -95
- package/.github/workflows/ci.yml +0 -32
- package/scripts/hooks/pre-push.mjs +0 -220
- package/scripts/init.mjs +0 -95
- package/scripts/record-fixtures.ts +0 -160
- package/tests/cache.test.ts +0 -265
- package/tests/client.test.ts +0 -372
- package/tests/config.test.ts +0 -263
- package/tests/fixtures/get_current_user.json +0 -40
- package/tests/fixtures/get_issue.json +0 -133
- package/tests/fixtures/get_issue_enums.json +0 -67
- package/tests/fixtures/get_issue_fields_sample.json +0 -76
- package/tests/fixtures/get_project_categories.json +0 -157
- package/tests/fixtures/get_project_versions.json +0 -3
- package/tests/fixtures/get_project_versions_with_data.json +0 -52
- package/tests/fixtures/list_issues.json +0 -95
- package/tests/fixtures/list_projects.json +0 -65
- package/tests/helpers/mock-server.ts +0 -166
- package/tests/helpers/search-mocks.ts +0 -84
- package/tests/prompts/prompts.test.ts +0 -242
- package/tests/resources/resources.test.ts +0 -309
- package/tests/search/embedder.test.ts +0 -81
- package/tests/search/highlight.test.ts +0 -129
- package/tests/search/store.test.ts +0 -193
- package/tests/search/sync.test.ts +0 -249
- package/tests/search/tools.test.ts +0 -661
- package/tests/tools/config.test.ts +0 -212
- package/tests/tools/files.test.ts +0 -343
- package/tests/tools/issues.test.ts +0 -1180
- package/tests/tools/metadata.test.ts +0 -509
- package/tests/tools/monitors.test.ts +0 -101
- package/tests/tools/projects.test.ts +0 -338
- package/tests/tools/relationships.test.ts +0 -177
- package/tests/tools/string-coercion.test.ts +0 -317
- package/tests/tools/users.test.ts +0 -62
- package/tests/utils/date-filter.test.ts +0 -169
- package/tests/version-hint.test.ts +0 -230
- package/tsconfig.build.json +0 -8
- package/vitest.config.ts +0 -8
|
@@ -1,242 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,309 +0,0 @@
|
|
|
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
|
-
// Simulates a raw MantisBT API response with extra fields that must be stripped
|
|
19
|
-
const PROJECTS_RAW_FIXTURE = [
|
|
20
|
-
{
|
|
21
|
-
id: 10,
|
|
22
|
-
name: 'Alpha',
|
|
23
|
-
enabled: true,
|
|
24
|
-
status: { id: 10, name: 'development', label: 'Entwicklung' },
|
|
25
|
-
view_state: { id: 10, name: 'public', label: 'Öffentlich' },
|
|
26
|
-
custom_fields: [
|
|
27
|
-
{ id: 1, name: 'Reklamieren', type: 'checkbox', default_value: '', possible_values: 'Ja' },
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const ENUM_FIXTURE = {
|
|
33
|
-
configs: [
|
|
34
|
-
{ option: 'severity_enum_string', value: '10:feature,50:minor,80:block' },
|
|
35
|
-
{ option: 'status_enum_string', value: '10:new,80:resolved,90:closed' },
|
|
36
|
-
{ option: 'priority_enum_string', value: '10:none,30:normal,60:immediate' },
|
|
37
|
-
{ option: 'resolution_enum_string', value: '10:open,20:fixed' },
|
|
38
|
-
{ option: 'reproducibility_enum_string', value: '10:always,70:have not tried' },
|
|
39
|
-
],
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// Setup
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
let mockServer: MockMcpServer;
|
|
47
|
-
let client: MantisClient;
|
|
48
|
-
let cache: MetadataCache;
|
|
49
|
-
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
mockServer = new MockMcpServer();
|
|
52
|
-
client = new MantisClient('https://mantis.example.com', 'test-token');
|
|
53
|
-
cache = new MetadataCache('/tmp/cache-resources-test', 86400);
|
|
54
|
-
registerResources(mockServer as never, client, cache);
|
|
55
|
-
vi.stubGlobal('fetch', vi.fn());
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
afterEach(async () => {
|
|
59
|
-
vi.unstubAllGlobals();
|
|
60
|
-
await cache.invalidate();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Registration
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
describe('resource registration', () => {
|
|
68
|
-
it('registers mantis://me', () => {
|
|
69
|
-
expect(mockServer.hasResourceRegistered('mantis://me')).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('registers mantis://projects', () => {
|
|
73
|
-
expect(mockServer.hasResourceRegistered('mantis://projects')).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('registers mantis://enums', () => {
|
|
77
|
-
expect(mockServer.hasResourceRegistered('mantis://enums')).toBe(true);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// mantis://me
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
describe('mantis://me', () => {
|
|
86
|
-
it('returns a single content item with application/json', async () => {
|
|
87
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
|
|
88
|
-
|
|
89
|
-
const result = await mockServer.callResource('mantis://me');
|
|
90
|
-
|
|
91
|
-
expect(result.contents).toHaveLength(1);
|
|
92
|
-
expect(result.contents[0]!.mimeType).toBe('application/json');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('sets uri to mantis://me', async () => {
|
|
96
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
|
|
97
|
-
|
|
98
|
-
const result = await mockServer.callResource('mantis://me');
|
|
99
|
-
|
|
100
|
-
expect(result.contents[0]!.uri).toBe('mantis://me');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('returns valid JSON with id and name', async () => {
|
|
104
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(USER_FIXTURE)));
|
|
105
|
-
|
|
106
|
-
const result = await mockServer.callResource('mantis://me');
|
|
107
|
-
|
|
108
|
-
const parsed = JSON.parse(result.contents[0]!.text) as { id: number; name: string };
|
|
109
|
-
expect(parsed.id).toBe(USER_FIXTURE.id);
|
|
110
|
-
expect(parsed.name).toBe(USER_FIXTURE.name);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
// mantis://projects
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
|
|
118
|
-
describe('mantis://projects', () => {
|
|
119
|
-
it('returns a single content item with application/json', async () => {
|
|
120
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
|
|
121
|
-
|
|
122
|
-
const result = await mockServer.callResource('mantis://projects');
|
|
123
|
-
|
|
124
|
-
expect(result.contents).toHaveLength(1);
|
|
125
|
-
expect(result.contents[0]!.mimeType).toBe('application/json');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('sets uri to mantis://projects', async () => {
|
|
129
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
|
|
130
|
-
|
|
131
|
-
const result = await mockServer.callResource('mantis://projects');
|
|
132
|
-
|
|
133
|
-
expect(result.contents[0]!.uri).toBe('mantis://projects');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('returns a JSON array of projects from live API when cache is empty', async () => {
|
|
137
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
|
|
138
|
-
|
|
139
|
-
const result = await mockServer.callResource('mantis://projects');
|
|
140
|
-
|
|
141
|
-
const parsed = JSON.parse(result.contents[0]!.text) as unknown[];
|
|
142
|
-
expect(Array.isArray(parsed)).toBe(true);
|
|
143
|
-
expect(parsed).toHaveLength(2);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('returns minified JSON (no indentation)', async () => {
|
|
147
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_FIXTURE })));
|
|
148
|
-
|
|
149
|
-
const result = await mockServer.callResource('mantis://projects');
|
|
150
|
-
|
|
151
|
-
expect(result.contents[0]!.text).not.toContain('\n');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('strips custom_fields from live API response', async () => {
|
|
155
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_RAW_FIXTURE })));
|
|
156
|
-
|
|
157
|
-
const result = await mockServer.callResource('mantis://projects');
|
|
158
|
-
|
|
159
|
-
const parsed = JSON.parse(result.contents[0]!.text) as Array<Record<string, unknown>>;
|
|
160
|
-
expect(parsed[0]).not.toHaveProperty('custom_fields');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('preserves label on status and view_state from live API response', async () => {
|
|
164
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: PROJECTS_RAW_FIXTURE })));
|
|
165
|
-
|
|
166
|
-
const result = await mockServer.callResource('mantis://projects');
|
|
167
|
-
|
|
168
|
-
const parsed = JSON.parse(result.contents[0]!.text) as Array<Record<string, unknown>>;
|
|
169
|
-
expect((parsed[0]!['status'] as Record<string, unknown>)['label']).toBe('Entwicklung');
|
|
170
|
-
expect((parsed[0]!['view_state'] as Record<string, unknown>)['label']).toBe('Öffentlich');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('serves from cache without calling the API when cache is valid', async () => {
|
|
174
|
-
await cache.save({
|
|
175
|
-
timestamp: Date.now(),
|
|
176
|
-
projects: PROJECTS_FIXTURE,
|
|
177
|
-
byProject: {},
|
|
178
|
-
tags: [],
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
const result = await mockServer.callResource('mantis://projects');
|
|
182
|
-
|
|
183
|
-
expect(vi.mocked(fetch)).not.toHaveBeenCalled();
|
|
184
|
-
const parsed = JSON.parse(result.contents[0]!.text) as unknown[];
|
|
185
|
-
expect(parsed).toHaveLength(2);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
// mantis://projects/{id}
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
|
|
193
|
-
describe('mantis://projects/{id}', () => {
|
|
194
|
-
it('registers the template resource', () => {
|
|
195
|
-
expect(mockServer.hasResourceRegistered('mantis://projects/{id}')).toBe(true);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('returns combined project view from cache (users, versions, categories)', async () => {
|
|
199
|
-
const projectsFixture = [{ id: 10, name: 'Alpha', enabled: true }];
|
|
200
|
-
await cache.save({
|
|
201
|
-
timestamp: Date.now(),
|
|
202
|
-
projects: projectsFixture,
|
|
203
|
-
byProject: {
|
|
204
|
-
10: {
|
|
205
|
-
users: [{ id: 1, name: 'jsmith' }],
|
|
206
|
-
versions: [{ id: 5, name: '1.0.0', released: true }],
|
|
207
|
-
categories: [{ id: 3, name: 'Bugs' }],
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
tags: [],
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const result = await mockServer.callResource('mantis://projects/10');
|
|
214
|
-
|
|
215
|
-
const parsed = JSON.parse(result.contents[0]!.text) as Record<string, unknown>;
|
|
216
|
-
expect(parsed['id']).toBe(10);
|
|
217
|
-
expect(parsed['name']).toBe('Alpha');
|
|
218
|
-
expect(parsed['users']).toHaveLength(1);
|
|
219
|
-
expect(parsed['versions']).toHaveLength(1);
|
|
220
|
-
expect(parsed['categories']).toHaveLength(1);
|
|
221
|
-
expect(vi.mocked(fetch)).not.toHaveBeenCalled();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('fetches live data in parallel when cache is cold', async () => {
|
|
225
|
-
vi.mocked(fetch)
|
|
226
|
-
.mockResolvedValueOnce(makeResponse(200, JSON.stringify({ projects: [{ id: 10, name: 'Alpha', categories: [{ id: 3, name: 'Bugs' }] }] })))
|
|
227
|
-
.mockResolvedValueOnce(makeResponse(200, JSON.stringify({ users: [{ id: 1, name: 'jsmith' }] })))
|
|
228
|
-
.mockResolvedValueOnce(makeResponse(200, JSON.stringify({ versions: [{ id: 5, name: '1.0.0' }] })));
|
|
229
|
-
|
|
230
|
-
const result = await mockServer.callResource('mantis://projects/10');
|
|
231
|
-
|
|
232
|
-
const parsed = JSON.parse(result.contents[0]!.text) as Record<string, unknown>;
|
|
233
|
-
expect(parsed['id']).toBe(10);
|
|
234
|
-
expect((parsed['users'] as unknown[]).length).toBeGreaterThan(0);
|
|
235
|
-
expect((parsed['versions'] as unknown[]).length).toBeGreaterThan(0);
|
|
236
|
-
expect((parsed['categories'] as unknown[]).length).toBeGreaterThan(0);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('strips [All Projects] prefix from category names on live fetch', async () => {
|
|
240
|
-
vi.mocked(fetch)
|
|
241
|
-
.mockResolvedValueOnce(makeResponse(200, JSON.stringify({ projects: [{ id: 10, name: 'Alpha', categories: [{ id: 1, name: '[All Projects] General' }] }] })))
|
|
242
|
-
.mockResolvedValueOnce(makeResponse(200, JSON.stringify({ users: [] })))
|
|
243
|
-
.mockResolvedValueOnce(makeResponse(200, JSON.stringify({ versions: [] })));
|
|
244
|
-
|
|
245
|
-
const result = await mockServer.callResource('mantis://projects/10');
|
|
246
|
-
|
|
247
|
-
const parsed = JSON.parse(result.contents[0]!.text) as { categories: Array<{ name: string }> };
|
|
248
|
-
expect(parsed.categories[0]!.name).toBe('General');
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('returns minified JSON', async () => {
|
|
252
|
-
await cache.save({
|
|
253
|
-
timestamp: Date.now(),
|
|
254
|
-
projects: [{ id: 10, name: 'Alpha' }],
|
|
255
|
-
byProject: { 10: { users: [], versions: [], categories: [] } },
|
|
256
|
-
tags: [],
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
const result = await mockServer.callResource('mantis://projects/10');
|
|
260
|
-
|
|
261
|
-
expect(result.contents[0]!.text).not.toContain('\n');
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// mantis://enums
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
|
|
269
|
-
describe('mantis://enums', () => {
|
|
270
|
-
it('returns a single content item with application/json', async () => {
|
|
271
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
272
|
-
|
|
273
|
-
const result = await mockServer.callResource('mantis://enums');
|
|
274
|
-
|
|
275
|
-
expect(result.contents).toHaveLength(1);
|
|
276
|
-
expect(result.contents[0]!.mimeType).toBe('application/json');
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('sets uri to mantis://enums', async () => {
|
|
280
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
281
|
-
|
|
282
|
-
const result = await mockServer.callResource('mantis://enums');
|
|
283
|
-
|
|
284
|
-
expect(result.contents[0]!.uri).toBe('mantis://enums');
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('returns parsed enum groups with id and name entries', async () => {
|
|
288
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
289
|
-
|
|
290
|
-
const result = await mockServer.callResource('mantis://enums');
|
|
291
|
-
|
|
292
|
-
const parsed = JSON.parse(result.contents[0]!.text) as Record<string, Array<{ id: number; name: string }>>;
|
|
293
|
-
for (const key of ['severity', 'priority', 'status', 'resolution', 'reproducibility']) {
|
|
294
|
-
expect(Array.isArray(parsed[key])).toBe(true);
|
|
295
|
-
expect(parsed[key]!.length).toBeGreaterThan(0);
|
|
296
|
-
expect(parsed[key]![0]).toMatchObject({ id: expect.any(Number), name: expect.any(String) });
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('parses severity values correctly from fixture', async () => {
|
|
301
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(ENUM_FIXTURE)));
|
|
302
|
-
|
|
303
|
-
const result = await mockServer.callResource('mantis://enums');
|
|
304
|
-
|
|
305
|
-
const parsed = JSON.parse(result.contents[0]!.text) as Record<string, Array<{ id: number; name: string }>>;
|
|
306
|
-
expect(parsed['severity']).toContainEqual({ id: 10, name: 'feature' });
|
|
307
|
-
expect(parsed['severity']).toContainEqual({ id: 50, name: 'minor' });
|
|
308
|
-
});
|
|
309
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { Embedder } from '../../src/search/embedder.js';
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Mock @huggingface/transformers (dynamic import)
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
const mockPipelineFn = vi.fn(async (texts: string | string[]) => {
|
|
9
|
-
if (Array.isArray(texts)) {
|
|
10
|
-
return texts.map(() => ({ data: new Float32Array(4).fill(0.1), dims: [1, 4] }));
|
|
11
|
-
}
|
|
12
|
-
return { data: new Float32Array(4).fill(0.1), dims: [4] };
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const mockPipelineFactory = vi.fn(async (_task: string, _model: string, _opts?: unknown) => mockPipelineFn);
|
|
16
|
-
|
|
17
|
-
vi.mock('@huggingface/transformers', () => ({
|
|
18
|
-
pipeline: mockPipelineFactory,
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Thread configuration
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
describe('Embedder – thread configuration', () => {
|
|
30
|
-
it('passes intra_op_num_threads=1 by default', async () => {
|
|
31
|
-
const embedder = new Embedder('test-model');
|
|
32
|
-
await embedder.embed('hello');
|
|
33
|
-
|
|
34
|
-
expect(mockPipelineFactory).toHaveBeenCalledWith(
|
|
35
|
-
'feature-extraction',
|
|
36
|
-
'test-model',
|
|
37
|
-
expect.objectContaining({
|
|
38
|
-
session_options: { intra_op_num_threads: 1, inter_op_num_threads: 1 },
|
|
39
|
-
}),
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('passes configured numThreads to intra_op_num_threads; inter stays 1', async () => {
|
|
44
|
-
const embedder = new Embedder('test-model', 4);
|
|
45
|
-
await embedder.embed('hello');
|
|
46
|
-
|
|
47
|
-
expect(mockPipelineFactory).toHaveBeenCalledWith(
|
|
48
|
-
'feature-extraction',
|
|
49
|
-
'test-model',
|
|
50
|
-
expect.objectContaining({
|
|
51
|
-
session_options: { intra_op_num_threads: 4, inter_op_num_threads: 1 },
|
|
52
|
-
}),
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('loads the pipeline only once (lazy singleton)', async () => {
|
|
57
|
-
const embedder = new Embedder('test-model', 1);
|
|
58
|
-
await embedder.embed('first');
|
|
59
|
-
await embedder.embed('second');
|
|
60
|
-
|
|
61
|
-
expect(mockPipelineFactory).toHaveBeenCalledTimes(1);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// Embedder default — numThreads omitted
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
describe('Embedder – numThreads default', () => {
|
|
70
|
-
it('uses intra_op_num_threads=1 when numThreads is not passed', async () => {
|
|
71
|
-
const embedder = new Embedder('m');
|
|
72
|
-
await embedder.embed('x');
|
|
73
|
-
expect(mockPipelineFactory).toHaveBeenCalledWith(
|
|
74
|
-
'feature-extraction',
|
|
75
|
-
'm',
|
|
76
|
-
expect.objectContaining({
|
|
77
|
-
session_options: expect.objectContaining({ intra_op_num_threads: 1 }),
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
});
|