@smartbear/mcp 0.4.0 → 0.5.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/README.md +20 -20
- package/dist/{insight-hub → bugsnag}/client/api/CurrentUser.js +2 -1
- package/dist/{insight-hub → bugsnag}/client/api/Error.js +37 -4
- package/dist/{insight-hub → bugsnag}/client/api/Project.js +2 -1
- package/dist/{insight-hub → bugsnag}/client/api/base.js +18 -1
- package/dist/{insight-hub → bugsnag}/client/api/filters.js +2 -2
- package/dist/{insight-hub → bugsnag}/client.js +87 -19
- package/dist/common/info.js +1 -1
- package/dist/common/server.js +8 -5
- package/dist/index.js +9 -9
- package/dist/pactflow/client/ai.js +24 -5
- package/dist/pactflow/client/base.js +6 -1
- package/dist/pactflow/client/tools.js +9 -0
- package/dist/pactflow/client/utils.js +70 -0
- package/dist/pactflow/client.js +58 -10
- package/package.json +6 -3
- package/dist/package.json +0 -60
- package/dist/tests/unit/common/server.test.js +0 -319
- package/dist/tests/unit/insight-hub/api-utilities.test.js +0 -31
- package/dist/tests/unit/insight-hub/client.test.js +0 -852
- package/dist/tests/unit/insight-hub/filters.test.js +0 -93
- package/dist/tests/unit/pactflow/ai.test.js +0 -21
- package/dist/tests/unit/pactflow/client.test.js +0 -67
- package/dist/tests/unit/pactflow/tools.test.js +0 -34
- package/dist/vitest.config.js +0 -57
- /package/dist/{insight-hub → bugsnag}/client/api/index.js +0 -0
- /package/dist/{insight-hub → bugsnag}/client/configuration.js +0 -0
- /package/dist/{insight-hub → bugsnag}/client/index.js +0 -0
|
@@ -1,852 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from '../../../common/info.js';
|
|
3
|
-
import { InsightHubClient } from '../../../insight-hub/client.js';
|
|
4
|
-
// Mock the dependencies
|
|
5
|
-
const mockCurrentUserAPI = {
|
|
6
|
-
listUserOrganizations: vi.fn(),
|
|
7
|
-
getOrganizationProjects: vi.fn()
|
|
8
|
-
};
|
|
9
|
-
const mockErrorAPI = {
|
|
10
|
-
viewErrorOnProject: vi.fn(),
|
|
11
|
-
viewLatestEventOnError: vi.fn(),
|
|
12
|
-
viewEventById: vi.fn(),
|
|
13
|
-
listProjectErrors: vi.fn(),
|
|
14
|
-
updateErrorOnProject: vi.fn(),
|
|
15
|
-
listErrorPivots: vi.fn(),
|
|
16
|
-
listEventsOnProject: vi.fn()
|
|
17
|
-
};
|
|
18
|
-
const mockProjectAPI = {
|
|
19
|
-
listProjectEventFields: vi.fn(),
|
|
20
|
-
createProject: vi.fn()
|
|
21
|
-
};
|
|
22
|
-
const mockCache = {
|
|
23
|
-
set: vi.fn(),
|
|
24
|
-
get: vi.fn(),
|
|
25
|
-
del: vi.fn()
|
|
26
|
-
};
|
|
27
|
-
vi.mock('../../../insight-hub/client/index.js', () => ({
|
|
28
|
-
CurrentUserAPI: vi.fn().mockImplementation(() => mockCurrentUserAPI),
|
|
29
|
-
ErrorAPI: vi.fn().mockImplementation(() => mockErrorAPI),
|
|
30
|
-
Configuration: vi.fn().mockImplementation((config) => config)
|
|
31
|
-
}));
|
|
32
|
-
vi.mock('../../../insight-hub/client/api/Project.js', () => ({
|
|
33
|
-
ProjectAPI: vi.fn().mockImplementation(() => mockProjectAPI)
|
|
34
|
-
}));
|
|
35
|
-
vi.mock('node-cache', () => ({
|
|
36
|
-
default: vi.fn().mockImplementation(() => mockCache)
|
|
37
|
-
}));
|
|
38
|
-
vi.mock('../../../common/bugsnag.js', () => ({
|
|
39
|
-
default: {
|
|
40
|
-
notify: vi.fn()
|
|
41
|
-
}
|
|
42
|
-
}));
|
|
43
|
-
describe('InsightHubClient', () => {
|
|
44
|
-
let client;
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
vi.clearAllMocks();
|
|
47
|
-
client = new InsightHubClient('test-token');
|
|
48
|
-
});
|
|
49
|
-
describe('constructor', () => {
|
|
50
|
-
it('should create client instance with proper dependencies', () => {
|
|
51
|
-
const client = new InsightHubClient('test-token');
|
|
52
|
-
expect(client).toBeInstanceOf(InsightHubClient);
|
|
53
|
-
});
|
|
54
|
-
it('should configure endpoints correctly during construction', async () => {
|
|
55
|
-
const { Configuration } = await import('../../../insight-hub/client/index.js');
|
|
56
|
-
const MockedConfiguration = vi.mocked(Configuration);
|
|
57
|
-
new InsightHubClient('test-token', '00000hub-key');
|
|
58
|
-
expect(MockedConfiguration).toHaveBeenCalledWith(expect.objectContaining({
|
|
59
|
-
basePath: 'https://api.insighthub.smartbear.com',
|
|
60
|
-
authToken: 'test-token',
|
|
61
|
-
headers: expect.objectContaining({
|
|
62
|
-
'User-Agent': `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
|
|
63
|
-
'Content-Type': 'application/json',
|
|
64
|
-
'X-Bugsnag-API': 'true',
|
|
65
|
-
'X-Version': '2'
|
|
66
|
-
})
|
|
67
|
-
}));
|
|
68
|
-
});
|
|
69
|
-
it('should set project API key when provided', () => {
|
|
70
|
-
const client = new InsightHubClient('test-token', 'test-project-key');
|
|
71
|
-
expect(client).toBeInstanceOf(InsightHubClient);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
describe('getEndpoint method', () => {
|
|
75
|
-
let client;
|
|
76
|
-
beforeEach(() => {
|
|
77
|
-
client = new InsightHubClient('test-token');
|
|
78
|
-
});
|
|
79
|
-
describe('without custom endpoint', () => {
|
|
80
|
-
describe('with Hub API key (00000 prefix)', () => {
|
|
81
|
-
it('should return Hub domain for api subdomain', () => {
|
|
82
|
-
const result = client.getEndpoint('api', '00000hub-key');
|
|
83
|
-
expect(result).toBe('https://api.insighthub.smartbear.com');
|
|
84
|
-
});
|
|
85
|
-
it('should return Hub domain for app subdomain', () => {
|
|
86
|
-
const result = client.getEndpoint('app', '00000test-key');
|
|
87
|
-
expect(result).toBe('https://app.insighthub.smartbear.com');
|
|
88
|
-
});
|
|
89
|
-
it('should return Hub domain for custom subdomain', () => {
|
|
90
|
-
const result = client.getEndpoint('custom', '00000key');
|
|
91
|
-
expect(result).toBe('https://custom.insighthub.smartbear.com');
|
|
92
|
-
});
|
|
93
|
-
it('should handle empty string after prefix', () => {
|
|
94
|
-
const result = client.getEndpoint('api', '00000');
|
|
95
|
-
expect(result).toBe('https://api.insighthub.smartbear.com');
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
describe('with regular API key (non-Hub)', () => {
|
|
99
|
-
it('should return Bugsnag domain for api subdomain', () => {
|
|
100
|
-
const result = client.getEndpoint('api', 'regular-key');
|
|
101
|
-
expect(result).toBe('https://api.bugsnag.com');
|
|
102
|
-
});
|
|
103
|
-
it('should return Bugsnag domain for app subdomain', () => {
|
|
104
|
-
const result = client.getEndpoint('app', 'abc123def');
|
|
105
|
-
expect(result).toBe('https://app.bugsnag.com');
|
|
106
|
-
});
|
|
107
|
-
it('should return Bugsnag domain for custom subdomain', () => {
|
|
108
|
-
const result = client.getEndpoint('custom', 'test-key-123');
|
|
109
|
-
expect(result).toBe('https://custom.bugsnag.com');
|
|
110
|
-
});
|
|
111
|
-
it('should handle API key with 00000 in middle', () => {
|
|
112
|
-
const result = client.getEndpoint('api', 'key-00000-middle');
|
|
113
|
-
expect(result).toBe('https://api.bugsnag.com');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe('without API key', () => {
|
|
117
|
-
it('should return Bugsnag domain when API key is undefined', () => {
|
|
118
|
-
const result = client.getEndpoint('api', undefined);
|
|
119
|
-
expect(result).toBe('https://api.bugsnag.com');
|
|
120
|
-
});
|
|
121
|
-
it('should return Bugsnag domain when API key is empty string', () => {
|
|
122
|
-
const result = client.getEndpoint('api', '');
|
|
123
|
-
expect(result).toBe('https://api.bugsnag.com');
|
|
124
|
-
});
|
|
125
|
-
it('should return Bugsnag domain when API key is null', () => {
|
|
126
|
-
const result = client.getEndpoint('api', null);
|
|
127
|
-
expect(result).toBe('https://api.bugsnag.com');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
describe('with custom endpoint', () => {
|
|
132
|
-
describe('Hub domain endpoints (always normalized)', () => {
|
|
133
|
-
it('should normalize to HTTPS subdomain for exact hub domain match', () => {
|
|
134
|
-
const result = client.getEndpoint('api', '00000key', 'https://api.insighthub.smartbear.com');
|
|
135
|
-
expect(result).toBe('https://api.insighthub.smartbear.com');
|
|
136
|
-
});
|
|
137
|
-
it('should normalize to HTTPS subdomain regardless of input protocol', () => {
|
|
138
|
-
const result = client.getEndpoint('api', '00000key', 'http://app.insighthub.smartbear.com');
|
|
139
|
-
expect(result).toBe('https://api.insighthub.smartbear.com');
|
|
140
|
-
});
|
|
141
|
-
it('should normalize to HTTPS subdomain regardless of input subdomain', () => {
|
|
142
|
-
const result = client.getEndpoint('app', '00000key', 'https://api.insighthub.smartbear.com');
|
|
143
|
-
expect(result).toBe('https://app.insighthub.smartbear.com');
|
|
144
|
-
});
|
|
145
|
-
it('should normalize hub domain with port', () => {
|
|
146
|
-
const result = client.getEndpoint('api', '00000key', 'https://custom.insighthub.smartbear.com:8080');
|
|
147
|
-
expect(result).toBe('https://api.insighthub.smartbear.com');
|
|
148
|
-
});
|
|
149
|
-
it('should normalize hub domain with path', () => {
|
|
150
|
-
const result = client.getEndpoint('api', '00000key', 'https://custom.insighthub.smartbear.com/path');
|
|
151
|
-
expect(result).toBe('https://api.insighthub.smartbear.com');
|
|
152
|
-
});
|
|
153
|
-
it('should normalize complex subdomains to standard format', () => {
|
|
154
|
-
const result = client.getEndpoint('api', '00000key', 'https://staging.app.insighthub.smartbear.com');
|
|
155
|
-
expect(result).toBe('https://api.insighthub.smartbear.com');
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe('Bugsnag domain endpoints (always normalized)', () => {
|
|
159
|
-
it('should normalize to HTTPS subdomain for exact bugsnag domain match', () => {
|
|
160
|
-
const result = client.getEndpoint('api', 'regular-key', 'https://api.bugsnag.com');
|
|
161
|
-
expect(result).toBe('https://api.bugsnag.com');
|
|
162
|
-
});
|
|
163
|
-
it('should normalize to HTTPS subdomain regardless of input protocol', () => {
|
|
164
|
-
const result = client.getEndpoint('api', 'regular-key', 'http://app.bugsnag.com');
|
|
165
|
-
expect(result).toBe('https://api.bugsnag.com');
|
|
166
|
-
});
|
|
167
|
-
it('should normalize bugsnag domain with port', () => {
|
|
168
|
-
const result = client.getEndpoint('app', 'regular-key', 'https://api.bugsnag.com:9000');
|
|
169
|
-
expect(result).toBe('https://app.bugsnag.com');
|
|
170
|
-
});
|
|
171
|
-
it('should normalize bugsnag domain with path', () => {
|
|
172
|
-
const result = client.getEndpoint('app', 'regular-key', 'https://api.bugsnag.com/v2');
|
|
173
|
-
expect(result).toBe('https://app.bugsnag.com');
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
describe('Custom domain endpoints (used as-is)', () => {
|
|
177
|
-
it('should return custom endpoint exactly as provided', () => {
|
|
178
|
-
const customEndpoint = 'https://custom.api.com';
|
|
179
|
-
const result = client.getEndpoint('api', '00000key', customEndpoint);
|
|
180
|
-
expect(result).toBe(customEndpoint);
|
|
181
|
-
});
|
|
182
|
-
it('should return custom endpoint as-is regardless of API key type', () => {
|
|
183
|
-
const customEndpoint = 'https://my-custom-domain.com/api';
|
|
184
|
-
const result = client.getEndpoint('api', 'regular-key', customEndpoint);
|
|
185
|
-
expect(result).toBe(customEndpoint);
|
|
186
|
-
});
|
|
187
|
-
it('should preserve HTTP protocol for custom domains', () => {
|
|
188
|
-
const customEndpoint = 'http://localhost:3000';
|
|
189
|
-
const result = client.getEndpoint('api', '00000key', customEndpoint);
|
|
190
|
-
expect(result).toBe(customEndpoint);
|
|
191
|
-
});
|
|
192
|
-
it('should preserve custom domain with ports and paths', () => {
|
|
193
|
-
const customEndpoint = 'https://192.168.1.100:8080/api/v1';
|
|
194
|
-
const result = client.getEndpoint('api', '00000key', customEndpoint);
|
|
195
|
-
expect(result).toBe(customEndpoint);
|
|
196
|
-
});
|
|
197
|
-
it('should preserve custom domain with query parameters', () => {
|
|
198
|
-
const customEndpoint = 'https://custom.domain.com/api?version=1';
|
|
199
|
-
const result = client.getEndpoint('api', '00000key', customEndpoint);
|
|
200
|
-
expect(result).toBe(customEndpoint);
|
|
201
|
-
});
|
|
202
|
-
it('should preserve custom domain with fragments', () => {
|
|
203
|
-
const customEndpoint = 'https://custom.domain.com/api#section';
|
|
204
|
-
const result = client.getEndpoint('api', '00000key', customEndpoint);
|
|
205
|
-
expect(result).toBe(customEndpoint);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
describe('edge cases', () => {
|
|
209
|
-
it('should handle malformed custom endpoints gracefully', () => {
|
|
210
|
-
// This should throw due to invalid URL, which is expected behavior
|
|
211
|
-
expect(() => {
|
|
212
|
-
client.getEndpoint('api', '00000key', 'not-a-valid-url');
|
|
213
|
-
}).toThrow();
|
|
214
|
-
});
|
|
215
|
-
it('should preserve custom endpoints with userinfo', () => {
|
|
216
|
-
const customEndpoint = 'https://user:pass@custom.domain.com';
|
|
217
|
-
const result = client.getEndpoint('api', '00000key', customEndpoint);
|
|
218
|
-
expect(result).toBe(customEndpoint);
|
|
219
|
-
});
|
|
220
|
-
it('should normalize known domains even with userinfo', () => {
|
|
221
|
-
const result = client.getEndpoint('api', '00000key', 'https://user:pass@app.insighthub.smartbear.com');
|
|
222
|
-
expect(result).toBe('https://api.insighthub.smartbear.com');
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
describe('subdomain validation', () => {
|
|
227
|
-
it('should handle empty subdomain', () => {
|
|
228
|
-
const result = client.getEndpoint('', '00000key');
|
|
229
|
-
expect(result).toBe('https://.insighthub.smartbear.com');
|
|
230
|
-
});
|
|
231
|
-
it('should handle subdomain with special characters', () => {
|
|
232
|
-
const result = client.getEndpoint('test-api_v2', '00000key');
|
|
233
|
-
expect(result).toBe('https://test-api_v2.insighthub.smartbear.com');
|
|
234
|
-
});
|
|
235
|
-
it('should handle numeric subdomain', () => {
|
|
236
|
-
const result = client.getEndpoint('v1', 'regular-key');
|
|
237
|
-
expect(result).toBe('https://v1.bugsnag.com');
|
|
238
|
-
});
|
|
239
|
-
it('should handle very long subdomains', () => {
|
|
240
|
-
const longSubdomain = 'very-long-subdomain-name-with-many-characters';
|
|
241
|
-
const result = client.getEndpoint(longSubdomain, '00000key');
|
|
242
|
-
expect(result).toBe(`https://${longSubdomain}.insighthub.smartbear.com`);
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
describe('static utility methods', () => {
|
|
247
|
-
// Test static methods if they exist in the class
|
|
248
|
-
it('should have proper class structure', () => {
|
|
249
|
-
const client = new InsightHubClient('test-token');
|
|
250
|
-
// Verify the client has expected methods
|
|
251
|
-
expect(typeof client.initialize).toBe('function');
|
|
252
|
-
expect(typeof client.registerTools).toBe('function');
|
|
253
|
-
expect(typeof client.registerResources).toBe('function');
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
describe('error handling', () => {
|
|
257
|
-
it('should handle invalid tokens gracefully during construction', () => {
|
|
258
|
-
expect(() => {
|
|
259
|
-
new InsightHubClient('');
|
|
260
|
-
}).not.toThrow();
|
|
261
|
-
expect(() => {
|
|
262
|
-
new InsightHubClient(' ');
|
|
263
|
-
}).not.toThrow();
|
|
264
|
-
});
|
|
265
|
-
it('should handle special characters in project API key', () => {
|
|
266
|
-
expect(() => {
|
|
267
|
-
new InsightHubClient('test-token', '00000-special!@#$%^&*()');
|
|
268
|
-
}).not.toThrow();
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
describe('configuration validation', () => {
|
|
272
|
-
it('should pass correct authToken to Configuration', async () => {
|
|
273
|
-
const { Configuration } = await import('../../../insight-hub/client/index.js');
|
|
274
|
-
const MockedConfiguration = vi.mocked(Configuration);
|
|
275
|
-
const testToken = 'super-secret-token-123';
|
|
276
|
-
new InsightHubClient(testToken);
|
|
277
|
-
expect(MockedConfiguration).toHaveBeenCalledWith(expect.objectContaining({
|
|
278
|
-
authToken: testToken
|
|
279
|
-
}));
|
|
280
|
-
});
|
|
281
|
-
it('should include all required headers', async () => {
|
|
282
|
-
const { Configuration } = await import('../../../insight-hub/client/index.js');
|
|
283
|
-
const MockedConfiguration = vi.mocked(Configuration);
|
|
284
|
-
new InsightHubClient('test-token');
|
|
285
|
-
const configCall = MockedConfiguration.mock.calls[0][0];
|
|
286
|
-
expect(configCall.headers).toEqual({
|
|
287
|
-
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
|
|
288
|
-
"Content-Type": "application/json",
|
|
289
|
-
"X-Bugsnag-API": "true",
|
|
290
|
-
"X-Version": "2",
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
describe('API client initialization', () => {
|
|
295
|
-
it('should initialize all required API clients', async () => {
|
|
296
|
-
const { CurrentUserAPI, ErrorAPI } = await import('../../../insight-hub/client/index.js');
|
|
297
|
-
const { ProjectAPI } = await import('../../../insight-hub/client/api/Project.js');
|
|
298
|
-
const MockedCurrentUserAPI = vi.mocked(CurrentUserAPI);
|
|
299
|
-
const MockedErrorAPI = vi.mocked(ErrorAPI);
|
|
300
|
-
const MockedProjectAPI = vi.mocked(ProjectAPI);
|
|
301
|
-
// Clear previous calls from beforeEach and other tests
|
|
302
|
-
MockedCurrentUserAPI.mockClear();
|
|
303
|
-
MockedErrorAPI.mockClear();
|
|
304
|
-
MockedProjectAPI.mockClear();
|
|
305
|
-
new InsightHubClient('test-token');
|
|
306
|
-
expect(MockedCurrentUserAPI).toHaveBeenCalledOnce();
|
|
307
|
-
expect(MockedErrorAPI).toHaveBeenCalledOnce();
|
|
308
|
-
expect(MockedProjectAPI).toHaveBeenCalledOnce();
|
|
309
|
-
});
|
|
310
|
-
it('should initialize NodeCache', async () => {
|
|
311
|
-
const NodeCacheModule = await import('node-cache');
|
|
312
|
-
const MockedNodeCache = vi.mocked(NodeCacheModule.default);
|
|
313
|
-
// Clear previous calls from beforeEach
|
|
314
|
-
MockedNodeCache.mockClear();
|
|
315
|
-
new InsightHubClient('test-token');
|
|
316
|
-
expect(MockedNodeCache).toHaveBeenCalledOnce();
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
describe('initialization', () => {
|
|
320
|
-
it('should initialize successfully with organizations and projects', async () => {
|
|
321
|
-
const mockOrg = { id: 'org-1', name: 'Test Org' };
|
|
322
|
-
const mockProjects = [
|
|
323
|
-
{ id: 'proj-1', name: 'Project 1', api_key: 'key1' },
|
|
324
|
-
{ id: 'proj-2', name: 'Project 2', api_key: 'key2' }
|
|
325
|
-
];
|
|
326
|
-
mockCache.get.mockReturnValueOnce(null) // No current projects
|
|
327
|
-
.mockReturnValueOnce(null) // No cached projects
|
|
328
|
-
.mockReturnValueOnce(null); // No cached organization
|
|
329
|
-
mockCurrentUserAPI.listUserOrganizations.mockResolvedValue({ body: [mockOrg] });
|
|
330
|
-
mockCurrentUserAPI.getOrganizationProjects.mockResolvedValue({ body: mockProjects });
|
|
331
|
-
await client.initialize();
|
|
332
|
-
expect(mockCurrentUserAPI.listUserOrganizations).toHaveBeenCalledOnce();
|
|
333
|
-
expect(mockCurrentUserAPI.getOrganizationProjects).toHaveBeenCalledWith('org-1', { paginate: true });
|
|
334
|
-
expect(mockCache.set).toHaveBeenCalledWith('insight_hub_org', mockOrg);
|
|
335
|
-
expect(mockCache.set).toHaveBeenCalledWith('insight_hub_projects', mockProjects);
|
|
336
|
-
});
|
|
337
|
-
it('should initialize with project API key and set up event filters', async () => {
|
|
338
|
-
const clientWithApiKey = new InsightHubClient('test-token', 'project-api-key');
|
|
339
|
-
const mockProjects = [
|
|
340
|
-
{ id: 'proj-1', name: 'Project 1', api_key: 'project-api-key' },
|
|
341
|
-
{ id: 'proj-2', name: 'Project 2', api_key: 'other-key' }
|
|
342
|
-
];
|
|
343
|
-
const mockEventFields = [
|
|
344
|
-
{ display_id: 'user.email', custom: false },
|
|
345
|
-
{ display_id: 'error.status', custom: false },
|
|
346
|
-
{ display_id: 'search', custom: false } // This should be filtered out
|
|
347
|
-
];
|
|
348
|
-
mockCache.get.mockReturnValueOnce(mockProjects)
|
|
349
|
-
.mockReturnValueOnce(null)
|
|
350
|
-
.mockReturnValueOnce(mockProjects);
|
|
351
|
-
mockProjectAPI.listProjectEventFields.mockResolvedValue({ body: mockEventFields });
|
|
352
|
-
await clientWithApiKey.initialize();
|
|
353
|
-
expect(mockCache.set).toHaveBeenCalledWith('insight_hub_current_project', mockProjects[0]);
|
|
354
|
-
expect(mockProjectAPI.listProjectEventFields).toHaveBeenCalledWith('proj-1');
|
|
355
|
-
// // Verify that 'search' field is filtered out
|
|
356
|
-
const filteredFields = mockEventFields.filter(field => field.display_id !== 'search');
|
|
357
|
-
expect(mockCache.set).toHaveBeenCalledWith('insight_hub_current_project_event_filters', filteredFields);
|
|
358
|
-
});
|
|
359
|
-
it('should throw error when no organizations found', async () => {
|
|
360
|
-
mockCurrentUserAPI.listUserOrganizations.mockResolvedValue({ body: [] });
|
|
361
|
-
await expect(client.initialize()).rejects.toThrow('No organizations found for the current user.');
|
|
362
|
-
});
|
|
363
|
-
it('should throw error when project with API key not found', async () => {
|
|
364
|
-
const clientWithApiKey = new InsightHubClient('test-token', 'non-existent-key');
|
|
365
|
-
const mockOrg = { id: 'org-1', name: 'Test Org' };
|
|
366
|
-
const mockProject = { id: 'proj-1', name: 'Project 1', api_key: 'other-key' };
|
|
367
|
-
mockCurrentUserAPI.listUserOrganizations.mockResolvedValue({ body: [mockOrg] });
|
|
368
|
-
mockCurrentUserAPI.getOrganizationProjects.mockResolvedValue({ body: [mockProject] });
|
|
369
|
-
await expect(clientWithApiKey.initialize()).rejects.toThrow('Unable to find project with API key non-existent-key in organization.');
|
|
370
|
-
});
|
|
371
|
-
it('should throw error when no event fields found for project', async () => {
|
|
372
|
-
const clientWithApiKey = new InsightHubClient('test-token', 'project-api-key');
|
|
373
|
-
const mockOrg = { id: 'org-1', name: 'Test Org' };
|
|
374
|
-
const mockProjects = [
|
|
375
|
-
{ id: 'proj-1', name: 'Project 1', api_key: 'project-api-key' }
|
|
376
|
-
];
|
|
377
|
-
mockCurrentUserAPI.listUserOrganizations.mockResolvedValue({ body: [mockOrg] });
|
|
378
|
-
mockCurrentUserAPI.getOrganizationProjects.mockResolvedValue({ body: mockProjects });
|
|
379
|
-
mockProjectAPI.listProjectEventFields.mockResolvedValue({ body: [] });
|
|
380
|
-
await expect(clientWithApiKey.initialize()).rejects.toThrow('No event fields found for project Project 1.');
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
describe('API methods', () => {
|
|
384
|
-
describe('getProjects', () => {
|
|
385
|
-
it('should return cached projects when available', async () => {
|
|
386
|
-
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
387
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
388
|
-
const result = await client.getProjects();
|
|
389
|
-
expect(mockCache.get).toHaveBeenCalledWith('insight_hub_projects');
|
|
390
|
-
expect(result).toEqual(mockProjects);
|
|
391
|
-
});
|
|
392
|
-
it('should fetch projects from API when not cached', async () => {
|
|
393
|
-
const mockOrg = { id: 'org-1', name: 'Test Org' };
|
|
394
|
-
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
395
|
-
mockCache.get
|
|
396
|
-
.mockReturnValueOnce(null) // First call for projects
|
|
397
|
-
.mockReturnValueOnce(mockOrg); // Second call for org
|
|
398
|
-
mockCurrentUserAPI.getOrganizationProjects.mockResolvedValue({ body: mockProjects });
|
|
399
|
-
const result = await client.getProjects();
|
|
400
|
-
expect(mockCurrentUserAPI.getOrganizationProjects).toHaveBeenCalledWith('org-1', { paginate: true });
|
|
401
|
-
expect(mockCache.set).toHaveBeenCalledWith('insight_hub_projects', mockProjects);
|
|
402
|
-
expect(result).toEqual(mockProjects);
|
|
403
|
-
});
|
|
404
|
-
it('should return empty array when no projects found', async () => {
|
|
405
|
-
const mockOrg = { id: 'org-1', name: 'Test Org' };
|
|
406
|
-
mockCache.get
|
|
407
|
-
.mockReturnValueOnce(null) // First call for projects
|
|
408
|
-
.mockReturnValueOnce(mockOrg); // Second call for org
|
|
409
|
-
mockCurrentUserAPI.getOrganizationProjects.mockResolvedValue({ body: [] });
|
|
410
|
-
await expect(client.getProjects()).resolves.toEqual([]);
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
describe('getEventById', () => {
|
|
414
|
-
it('should find event across multiple projects', async () => {
|
|
415
|
-
const mockOrgs = [{ id: 'org-1', name: 'Test Org' }];
|
|
416
|
-
const mockProjects = [
|
|
417
|
-
{ id: 'proj-1', name: 'Project 1' },
|
|
418
|
-
{ id: 'proj-2', name: 'Project 2' }
|
|
419
|
-
];
|
|
420
|
-
const mockEvent = { id: 'event-1', project_id: 'proj-2' };
|
|
421
|
-
mockCache.get.mockReturnValueOnce(mockProjects);
|
|
422
|
-
mockCache.get.mockReturnValueOnce(mockOrgs);
|
|
423
|
-
mockCurrentUserAPI.getOrganizationProjects.mockResolvedValue({ body: mockProjects });
|
|
424
|
-
mockErrorAPI.viewEventById
|
|
425
|
-
.mockRejectedValueOnce(new Error('Not found')) // proj-1
|
|
426
|
-
.mockResolvedValueOnce({ body: mockEvent }); // proj-2
|
|
427
|
-
const result = await client.getEvent('event-1');
|
|
428
|
-
expect(mockErrorAPI.viewEventById).toHaveBeenCalledWith('proj-1', 'event-1');
|
|
429
|
-
expect(mockErrorAPI.viewEventById).toHaveBeenCalledWith('proj-2', 'event-1');
|
|
430
|
-
expect(result).toEqual(mockEvent);
|
|
431
|
-
});
|
|
432
|
-
it('should return null when event not found in any project', async () => {
|
|
433
|
-
const mockOrgs = [{ id: 'org-1', name: 'Test Org' }];
|
|
434
|
-
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
435
|
-
mockCurrentUserAPI.listUserOrganizations.mockResolvedValue({ body: mockOrgs });
|
|
436
|
-
mockCurrentUserAPI.getOrganizationProjects.mockResolvedValue({ body: mockProjects });
|
|
437
|
-
mockErrorAPI.viewEventById.mockRejectedValue(new Error('Not found'));
|
|
438
|
-
const result = await client.getEvent('event-1');
|
|
439
|
-
expect(result).toBeNull();
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
describe('tool registration', () => {
|
|
444
|
-
let registerToolsSpy;
|
|
445
|
-
let getInputFunctionSpy;
|
|
446
|
-
beforeEach(() => {
|
|
447
|
-
registerToolsSpy = vi.fn();
|
|
448
|
-
getInputFunctionSpy = vi.fn();
|
|
449
|
-
});
|
|
450
|
-
it('should register list_projects tool when no project API key', () => {
|
|
451
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
452
|
-
expect(registerToolsSpy).toBeCalledWith(expect.any(Object), expect.any(Function));
|
|
453
|
-
});
|
|
454
|
-
it('should not register list_projects tool when project API key is provided', () => {
|
|
455
|
-
const clientWithApiKey = new InsightHubClient('test-token', 'project-api-key');
|
|
456
|
-
clientWithApiKey.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
457
|
-
const registeredTools = registerToolsSpy.mock.calls.map((call) => call[0].title);
|
|
458
|
-
expect(registeredTools).not.toContain('List Projects');
|
|
459
|
-
});
|
|
460
|
-
it('should register common tools regardless of project API key', () => {
|
|
461
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
462
|
-
const registeredTools = registerToolsSpy.mock.calls.map((call) => call[0].title);
|
|
463
|
-
expect(registeredTools).toContain('Get Error');
|
|
464
|
-
expect(registeredTools).toContain('Get Event Details');
|
|
465
|
-
expect(registeredTools).toContain('List Project Errors');
|
|
466
|
-
expect(registeredTools).toContain('List Project Event Filters');
|
|
467
|
-
expect(registeredTools).toContain('Update Error');
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
describe('resource registration', () => {
|
|
471
|
-
let registerResourcesSpy;
|
|
472
|
-
beforeEach(() => {
|
|
473
|
-
registerResourcesSpy = vi.fn();
|
|
474
|
-
});
|
|
475
|
-
it('should register event resource', () => {
|
|
476
|
-
client.registerResources(registerResourcesSpy);
|
|
477
|
-
expect(registerResourcesSpy).toHaveBeenCalledWith('event', '{id}', expect.any(Function));
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
describe('tool handlers', () => {
|
|
481
|
-
let registerToolsSpy;
|
|
482
|
-
let getInputFunctionSpy;
|
|
483
|
-
beforeEach(() => {
|
|
484
|
-
registerToolsSpy = vi.fn();
|
|
485
|
-
getInputFunctionSpy = vi.fn();
|
|
486
|
-
});
|
|
487
|
-
describe('list_projects tool handler', () => {
|
|
488
|
-
it('should return projects with pagination', async () => {
|
|
489
|
-
const mockProjects = [
|
|
490
|
-
{ id: 'proj-1', name: 'Project 1' },
|
|
491
|
-
{ id: 'proj-2', name: 'Project 2' },
|
|
492
|
-
{ id: 'proj-3', name: 'Project 3' }
|
|
493
|
-
];
|
|
494
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
495
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
496
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
497
|
-
.find((call) => call[0].title === 'List Projects')[1];
|
|
498
|
-
const result = await toolHandler({ page_size: 2, page: 1 });
|
|
499
|
-
const expectedResult = {
|
|
500
|
-
data: mockProjects.slice(0, 2),
|
|
501
|
-
count: 2
|
|
502
|
-
};
|
|
503
|
-
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
504
|
-
});
|
|
505
|
-
it('should return all projects when no pagination specified', async () => {
|
|
506
|
-
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
507
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
508
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
509
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
510
|
-
.find((call) => call[0].title === 'List Projects')[1];
|
|
511
|
-
const result = await toolHandler({});
|
|
512
|
-
const expectedResult = {
|
|
513
|
-
data: mockProjects,
|
|
514
|
-
count: 1
|
|
515
|
-
};
|
|
516
|
-
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
517
|
-
});
|
|
518
|
-
it('should handle no projects found', async () => {
|
|
519
|
-
mockCache.get.mockReturnValue([]);
|
|
520
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
521
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
522
|
-
.find((call) => call[0].title === 'List Projects')[1];
|
|
523
|
-
const result = await toolHandler({});
|
|
524
|
-
expect(result.content[0].text).toBe('No projects found.');
|
|
525
|
-
});
|
|
526
|
-
it('should handle pagination with only page_size', async () => {
|
|
527
|
-
const mockProjects = [
|
|
528
|
-
{ id: 'proj-1', name: 'Project 1' },
|
|
529
|
-
{ id: 'proj-2', name: 'Project 2' },
|
|
530
|
-
{ id: 'proj-3', name: 'Project 3' }
|
|
531
|
-
];
|
|
532
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
533
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
534
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
535
|
-
.find((call) => call[0].title === 'List Projects')[1];
|
|
536
|
-
const result = await toolHandler({ page_size: 2 });
|
|
537
|
-
const expectedResult = {
|
|
538
|
-
data: mockProjects.slice(0, 2),
|
|
539
|
-
count: 2
|
|
540
|
-
};
|
|
541
|
-
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
542
|
-
});
|
|
543
|
-
it('should handle pagination with only page', async () => {
|
|
544
|
-
const mockProjects = Array.from({ length: 25 }, (_, i) => ({
|
|
545
|
-
id: `proj-${i + 1}`,
|
|
546
|
-
name: `Project ${i + 1}`
|
|
547
|
-
}));
|
|
548
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
549
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
550
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
551
|
-
.find((call) => call[0].title === 'List Projects')[1];
|
|
552
|
-
const result = await toolHandler({ page: 2 });
|
|
553
|
-
// Default page_size is 10, so page 2 should return projects 10-19
|
|
554
|
-
const expectedResult = {
|
|
555
|
-
data: mockProjects.slice(10, 20),
|
|
556
|
-
count: 10
|
|
557
|
-
};
|
|
558
|
-
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
559
|
-
});
|
|
560
|
-
});
|
|
561
|
-
describe('get_error tool handler', () => {
|
|
562
|
-
it('should get error details with project from cache', async () => {
|
|
563
|
-
const mockProject = { id: 'proj-1', name: 'Project 1', slug: 'my-project' };
|
|
564
|
-
const mockError = { id: 'error-1', message: 'Test error' };
|
|
565
|
-
const mockOrg = { id: 'org-1', name: 'Test Org', slug: 'test-org' };
|
|
566
|
-
const mockEvents = [{ id: 'event-1', timestamp: '2023-01-01' }];
|
|
567
|
-
const mockPivots = [{ id: 'pivot-1', name: 'test-pivot' }];
|
|
568
|
-
mockCache.get.mockReturnValueOnce(mockProject)
|
|
569
|
-
.mockReturnValueOnce(mockOrg);
|
|
570
|
-
mockErrorAPI.viewErrorOnProject.mockResolvedValue({ body: mockError });
|
|
571
|
-
mockErrorAPI.listEventsOnProject.mockResolvedValue({ body: mockEvents });
|
|
572
|
-
mockErrorAPI.listErrorPivots.mockResolvedValue({ body: mockPivots });
|
|
573
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
574
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
575
|
-
.find((call) => call[0].title === 'Get Error')[1];
|
|
576
|
-
const result = await toolHandler({ errorId: 'error-1' });
|
|
577
|
-
const queryString = '?filters[error][][type]=eq&filters[error][][value]=error-1';
|
|
578
|
-
const encodedQueryString = encodeURI(queryString);
|
|
579
|
-
expect(mockErrorAPI.viewErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1');
|
|
580
|
-
expect(result.content[0].text).toBe(JSON.stringify({
|
|
581
|
-
error_details: mockError,
|
|
582
|
-
latest_event: mockEvents[0],
|
|
583
|
-
pivots: mockPivots,
|
|
584
|
-
url: `https://app.bugsnag.com/${mockOrg.slug}/${mockProject.slug}/errors/error-1${encodedQueryString}`
|
|
585
|
-
}));
|
|
586
|
-
});
|
|
587
|
-
it('should throw error when required arguments missing', async () => {
|
|
588
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
589
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
590
|
-
.find((call) => call[0].title === 'Get Error')[1];
|
|
591
|
-
await expect(toolHandler({})).rejects.toThrow('Both projectId and errorId arguments are required');
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
describe('get_insight_hub_event_details tool handler', () => {
|
|
595
|
-
it('should get event details from dashboard URL', async () => {
|
|
596
|
-
const mockProjects = [{ id: 'proj-1', slug: 'my-project', name: 'My Project' }];
|
|
597
|
-
const mockEvent = { id: 'event-1', project_id: 'proj-1' };
|
|
598
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
599
|
-
mockErrorAPI.viewEventById.mockResolvedValue({ body: mockEvent });
|
|
600
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
601
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
602
|
-
.find((call) => call[0].title === 'Get Event Details')[1];
|
|
603
|
-
const result = await toolHandler({
|
|
604
|
-
link: 'https://app.bugsnag.com/my-org/my-project/errors/error-123?event_id=event-1'
|
|
605
|
-
});
|
|
606
|
-
expect(mockErrorAPI.viewEventById).toHaveBeenCalledWith('proj-1', 'event-1');
|
|
607
|
-
expect(result.content[0].text).toBe(JSON.stringify(mockEvent));
|
|
608
|
-
});
|
|
609
|
-
it('should throw error when link is invalid', async () => {
|
|
610
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
611
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
612
|
-
.find((call) => call[0].title === 'Get Event Details')[1];
|
|
613
|
-
await expect(toolHandler({ link: 'invalid-url' })).rejects.toThrow();
|
|
614
|
-
});
|
|
615
|
-
it('should throw error when project not found', async () => {
|
|
616
|
-
mockCache.get.mockReturnValue([{ id: 'proj-1', slug: 'other-project', name: 'Other Project' }]);
|
|
617
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
618
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
619
|
-
.find((call) => call[0].title === 'Get Event Details')[1];
|
|
620
|
-
await expect(toolHandler({
|
|
621
|
-
link: 'https://app.bugsnag.com/my-org/my-project/errors/error-123?event_id=event-1'
|
|
622
|
-
})).rejects.toThrow('Project with the specified slug not found.');
|
|
623
|
-
});
|
|
624
|
-
it('should throw error when URL is missing required parameters', async () => {
|
|
625
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
626
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
627
|
-
.find((call) => call[0].title === 'Get Event Details')[1];
|
|
628
|
-
await expect(toolHandler({
|
|
629
|
-
link: 'https://app.bugsnag.com/my-org/my-project/errors/error-123' // Missing event_id
|
|
630
|
-
})).rejects.toThrow('Both projectSlug and eventId must be present in the link');
|
|
631
|
-
});
|
|
632
|
-
});
|
|
633
|
-
describe('list_project_errors tool handler', () => {
|
|
634
|
-
it('should list project errors with filters', async () => {
|
|
635
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
636
|
-
const mockEventFields = [
|
|
637
|
-
{ display_id: 'error.status', custom: false },
|
|
638
|
-
{ display_id: 'user.email', custom: false }
|
|
639
|
-
];
|
|
640
|
-
const mockErrors = [{ id: 'error-1', message: 'Test error' }];
|
|
641
|
-
const filters = { 'error.status': [{ type: 'eq', value: 'open' }] };
|
|
642
|
-
mockCache.get
|
|
643
|
-
.mockReturnValueOnce(mockProject) // current project
|
|
644
|
-
.mockReturnValueOnce(mockEventFields); // event fields
|
|
645
|
-
mockErrorAPI.listProjectErrors.mockResolvedValue({ body: mockErrors });
|
|
646
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
647
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
648
|
-
.find((call) => call[0].title === 'List Project Errors')[1];
|
|
649
|
-
const result = await toolHandler({ filters });
|
|
650
|
-
expect(mockErrorAPI.listProjectErrors).toHaveBeenCalledWith('proj-1', { filters });
|
|
651
|
-
const expectedResult = {
|
|
652
|
-
data: mockErrors,
|
|
653
|
-
count: 1
|
|
654
|
-
};
|
|
655
|
-
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
656
|
-
});
|
|
657
|
-
it('should validate filter keys against cached event fields', async () => {
|
|
658
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
659
|
-
const mockEventFields = [{ display_id: 'error.status', custom: false }];
|
|
660
|
-
const filters = { 'invalid.field': [{ type: 'eq', value: 'test' }] };
|
|
661
|
-
mockCache.get
|
|
662
|
-
.mockReturnValueOnce(mockProject)
|
|
663
|
-
.mockReturnValueOnce(mockEventFields);
|
|
664
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
665
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
666
|
-
.find((call) => call[0].title === 'List Project Errors')[1];
|
|
667
|
-
await expect(toolHandler({ filters })).rejects.toThrow('Invalid filter key: invalid.field');
|
|
668
|
-
});
|
|
669
|
-
it('should throw error when no project ID available', async () => {
|
|
670
|
-
mockCache.get.mockReturnValue(null);
|
|
671
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
672
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
673
|
-
.find((call) => call[0].title === 'List Project Errors')[1];
|
|
674
|
-
await expect(toolHandler({})).rejects.toThrow('No current project found. Please provide a projectId or configure a project API key.');
|
|
675
|
-
});
|
|
676
|
-
});
|
|
677
|
-
describe('get_project_event_filters tool handler', () => {
|
|
678
|
-
it('should return cached event fields', async () => {
|
|
679
|
-
const mockEventFields = [
|
|
680
|
-
{ display_id: 'error.status', custom: false },
|
|
681
|
-
{ display_id: 'user.email', custom: false }
|
|
682
|
-
];
|
|
683
|
-
mockCache.get.mockReturnValue(mockEventFields);
|
|
684
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
685
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
686
|
-
.find((call) => call[0].title === 'List Project Event Filters')[1];
|
|
687
|
-
const result = await toolHandler({});
|
|
688
|
-
expect(result.content[0].text).toBe(JSON.stringify(mockEventFields));
|
|
689
|
-
});
|
|
690
|
-
it('should throw error when no event filters in cache', async () => {
|
|
691
|
-
mockCache.get.mockReturnValue(null);
|
|
692
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
693
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
694
|
-
.find((call) => call[0].title === 'List Project Event Filters')[1];
|
|
695
|
-
await expect(toolHandler({})).rejects.toThrow('No event filters found in cache.');
|
|
696
|
-
});
|
|
697
|
-
});
|
|
698
|
-
describe('update_error tool handler', () => {
|
|
699
|
-
it('should update error successfully with project from cache', async () => {
|
|
700
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
701
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
702
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
703
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
704
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
705
|
-
.find((call) => call[0].title === 'Update Error')[1];
|
|
706
|
-
const result = await toolHandler({
|
|
707
|
-
errorId: 'error-1',
|
|
708
|
-
operation: 'fix'
|
|
709
|
-
});
|
|
710
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1', { operation: 'fix' });
|
|
711
|
-
expect(result.content[0].text).toBe(JSON.stringify({ success: true }));
|
|
712
|
-
});
|
|
713
|
-
it('should update error successfully with explicit project ID', async () => {
|
|
714
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
715
|
-
const mockProjects = [mockProject];
|
|
716
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
717
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 204 });
|
|
718
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
719
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
720
|
-
.find((call) => call[0].title === 'Update Error')[1];
|
|
721
|
-
const result = await toolHandler({
|
|
722
|
-
projectId: 'proj-1',
|
|
723
|
-
errorId: 'error-1',
|
|
724
|
-
operation: 'ignore'
|
|
725
|
-
});
|
|
726
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1', { operation: 'ignore' });
|
|
727
|
-
expect(result.content[0].text).toBe(JSON.stringify({ success: true }));
|
|
728
|
-
});
|
|
729
|
-
it('should handle all permitted operations', async () => {
|
|
730
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
731
|
-
// Test all operations except override_severity which requires special elicitInput handling
|
|
732
|
-
const operations = ['open', 'fix', 'ignore', 'discard', 'undiscard'];
|
|
733
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
734
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
735
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
736
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
737
|
-
.find((call) => call[0].title === 'Update Error')[1];
|
|
738
|
-
for (const operation of operations) {
|
|
739
|
-
await toolHandler({
|
|
740
|
-
errorId: 'error-1',
|
|
741
|
-
operation: operation
|
|
742
|
-
});
|
|
743
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1', { operation, severity: undefined });
|
|
744
|
-
}
|
|
745
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledTimes(operations.length);
|
|
746
|
-
});
|
|
747
|
-
it('should handle override_severity operation with elicitInput', async () => {
|
|
748
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
749
|
-
getInputFunctionSpy.mockResolvedValue({
|
|
750
|
-
action: 'accept',
|
|
751
|
-
content: { severity: 'warning' }
|
|
752
|
-
});
|
|
753
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
754
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
755
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
756
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
757
|
-
.find((call) => call[0].title === 'Update Error')[1];
|
|
758
|
-
const result = await toolHandler({
|
|
759
|
-
errorId: 'error-1',
|
|
760
|
-
operation: 'override_severity'
|
|
761
|
-
});
|
|
762
|
-
expect(getInputFunctionSpy).toHaveBeenCalledWith({
|
|
763
|
-
message: "Please provide the new severity for the error (e.g. 'info', 'warning', 'error', 'critical')",
|
|
764
|
-
requestedSchema: {
|
|
765
|
-
type: "object",
|
|
766
|
-
properties: {
|
|
767
|
-
severity: {
|
|
768
|
-
type: "string",
|
|
769
|
-
enum: ['info', 'warning', 'error'],
|
|
770
|
-
description: "The new severity level for the error"
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
},
|
|
774
|
-
required: ["severity"]
|
|
775
|
-
});
|
|
776
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1', { operation: 'override_severity', severity: 'warning' });
|
|
777
|
-
expect(result.content[0].text).toBe(JSON.stringify({ success: true }));
|
|
778
|
-
});
|
|
779
|
-
it('should handle override_severity operation when elicitInput is rejected', async () => {
|
|
780
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
781
|
-
getInputFunctionSpy.mockResolvedValue({
|
|
782
|
-
action: 'reject'
|
|
783
|
-
});
|
|
784
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
785
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
786
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
787
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
788
|
-
.find((call) => call[0].title === 'Update Error')[1];
|
|
789
|
-
const result = await toolHandler({
|
|
790
|
-
errorId: 'error-1',
|
|
791
|
-
operation: 'override_severity'
|
|
792
|
-
});
|
|
793
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1', { operation: 'override_severity', severity: undefined });
|
|
794
|
-
expect(result.content[0].text).toBe(JSON.stringify({ success: true }));
|
|
795
|
-
});
|
|
796
|
-
it('should return false when API returns non-success status', async () => {
|
|
797
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
798
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
799
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 400 });
|
|
800
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
801
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
802
|
-
.find((call) => call[0].title === 'Update Error')[1];
|
|
803
|
-
const result = await toolHandler({
|
|
804
|
-
errorId: 'error-1',
|
|
805
|
-
operation: 'fix'
|
|
806
|
-
});
|
|
807
|
-
expect(result.content[0].text).toBe(JSON.stringify({ success: false }));
|
|
808
|
-
});
|
|
809
|
-
it('should throw error when no project found', async () => {
|
|
810
|
-
mockCache.get.mockReturnValue(null);
|
|
811
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
812
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
813
|
-
.find((call) => call[0].title === 'Update Error')[1];
|
|
814
|
-
await expect(toolHandler({
|
|
815
|
-
errorId: 'error-1',
|
|
816
|
-
operation: 'fix'
|
|
817
|
-
})).rejects.toThrow('No current project found. Please provide a projectId or configure a project API key.');
|
|
818
|
-
});
|
|
819
|
-
it('should throw error when project ID not found', async () => {
|
|
820
|
-
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
821
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
822
|
-
client.registerTools(registerToolsSpy, getInputFunctionSpy);
|
|
823
|
-
const toolHandler = registerToolsSpy.mock.calls
|
|
824
|
-
.find((call) => call[0].title === 'Update Error')[1];
|
|
825
|
-
await expect(toolHandler({
|
|
826
|
-
projectId: 'non-existent-project',
|
|
827
|
-
errorId: 'error-1',
|
|
828
|
-
operation: 'fix'
|
|
829
|
-
})).rejects.toThrow('Project with ID non-existent-project not found.');
|
|
830
|
-
});
|
|
831
|
-
});
|
|
832
|
-
});
|
|
833
|
-
describe('resource handlers', () => {
|
|
834
|
-
let registerResourcesSpy;
|
|
835
|
-
beforeEach(() => {
|
|
836
|
-
registerResourcesSpy = vi.fn();
|
|
837
|
-
});
|
|
838
|
-
describe('insight_hub_event resource handler', () => {
|
|
839
|
-
it('should find event by ID across projects', async () => {
|
|
840
|
-
const mockEvent = { id: 'event-1', project_id: 'proj-1' };
|
|
841
|
-
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
842
|
-
mockCache.get.mockReturnValueOnce(mockProjects);
|
|
843
|
-
mockErrorAPI.viewEventById.mockResolvedValue({ body: mockEvent });
|
|
844
|
-
client.registerResources(registerResourcesSpy);
|
|
845
|
-
const resourceHandler = registerResourcesSpy.mock.calls[0][2];
|
|
846
|
-
const result = await resourceHandler({ href: 'insighthub://event/event-1' }, { id: 'event-1' });
|
|
847
|
-
expect(result.contents[0].uri).toBe('insighthub://event/event-1');
|
|
848
|
-
expect(result.contents[0].text).toBe(JSON.stringify(mockEvent));
|
|
849
|
-
});
|
|
850
|
-
});
|
|
851
|
-
});
|
|
852
|
-
});
|