@smartbear/mcp 0.3.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 +86 -25
- package/dist/api-hub/client.js +298 -52
- 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/bugsnag/client.js +687 -0
- package/dist/common/info.js +1 -1
- package/dist/common/server.js +148 -0
- package/dist/index.js +38 -29
- package/dist/pactflow/client/ai.js +146 -0
- package/dist/pactflow/client/base.js +6 -0
- package/dist/pactflow/client/tools.js +55 -0
- package/dist/pactflow/client/utils.js +70 -0
- package/dist/pactflow/client.js +180 -0
- package/dist/reflect/client.js +100 -18
- package/package.json +8 -4
- package/dist/common/templates.js +0 -57
- package/dist/insight-hub/client.js +0 -621
- package/dist/package.json +0 -59
- package/dist/tests/unit/common/templates.test.js +0 -149
- package/dist/tests/unit/insight-hub/api-utilities.test.js +0 -31
- package/dist/tests/unit/insight-hub/client.test.js +0 -920
- package/dist/tests/unit/insight-hub/filters.test.js +0 -93
- 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,920 +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 mockServer;
|
|
445
|
-
beforeEach(() => {
|
|
446
|
-
mockServer = {
|
|
447
|
-
registerTool: vi.fn(),
|
|
448
|
-
resource: vi.fn()
|
|
449
|
-
};
|
|
450
|
-
});
|
|
451
|
-
it('should register list_insight_hub_projects tool when no project API key', () => {
|
|
452
|
-
client.registerTools(mockServer);
|
|
453
|
-
expect(mockServer.registerTool).toHaveBeenCalledWith('list_insight_hub_projects', expect.any(Object), expect.any(Function));
|
|
454
|
-
});
|
|
455
|
-
it('should not register list_insight_hub_projects tool when project API key is provided', () => {
|
|
456
|
-
const clientWithApiKey = new InsightHubClient('test-token', 'project-api-key');
|
|
457
|
-
clientWithApiKey.registerTools(mockServer);
|
|
458
|
-
const registeredTools = mockServer.registerTool.mock.calls.map((call) => call[0]);
|
|
459
|
-
expect(registeredTools).not.toContain('list_insight_hub_projects');
|
|
460
|
-
});
|
|
461
|
-
it('should register common tools regardless of project API key', () => {
|
|
462
|
-
client.registerTools(mockServer);
|
|
463
|
-
const registeredTools = mockServer.registerTool.mock.calls.map((call) => call[0]);
|
|
464
|
-
expect(registeredTools).toContain('get_insight_hub_error');
|
|
465
|
-
expect(registeredTools).toContain('get_insight_hub_event_details');
|
|
466
|
-
expect(registeredTools).toContain('list_insight_hub_project_errors');
|
|
467
|
-
expect(registeredTools).toContain('get_project_event_filters');
|
|
468
|
-
expect(registeredTools).toContain('update_error');
|
|
469
|
-
});
|
|
470
|
-
});
|
|
471
|
-
describe('resource registration', () => {
|
|
472
|
-
let mockServer;
|
|
473
|
-
beforeEach(() => {
|
|
474
|
-
mockServer = {
|
|
475
|
-
registerTool: vi.fn(),
|
|
476
|
-
resource: vi.fn()
|
|
477
|
-
};
|
|
478
|
-
});
|
|
479
|
-
it('should register insight_hub_event resource', () => {
|
|
480
|
-
client.registerResources(mockServer);
|
|
481
|
-
expect(mockServer.resource).toHaveBeenCalledWith('insight_hub_event', expect.any(Object), expect.any(Function));
|
|
482
|
-
});
|
|
483
|
-
});
|
|
484
|
-
describe('tool handlers', () => {
|
|
485
|
-
let mockServer;
|
|
486
|
-
beforeEach(() => {
|
|
487
|
-
mockServer = {
|
|
488
|
-
registerTool: vi.fn(),
|
|
489
|
-
resource: vi.fn()
|
|
490
|
-
};
|
|
491
|
-
});
|
|
492
|
-
describe('list_insight_hub_projects tool handler', () => {
|
|
493
|
-
it('should return projects with pagination', async () => {
|
|
494
|
-
const mockProjects = [
|
|
495
|
-
{ id: 'proj-1', name: 'Project 1' },
|
|
496
|
-
{ id: 'proj-2', name: 'Project 2' },
|
|
497
|
-
{ id: 'proj-3', name: 'Project 3' }
|
|
498
|
-
];
|
|
499
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
500
|
-
client.registerTools(mockServer);
|
|
501
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
502
|
-
.find((call) => call[0] === 'list_insight_hub_projects')[2];
|
|
503
|
-
const result = await toolHandler({ page_size: 2, page: 1 });
|
|
504
|
-
const expectedResult = {
|
|
505
|
-
data: mockProjects.slice(0, 2),
|
|
506
|
-
count: 2
|
|
507
|
-
};
|
|
508
|
-
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
509
|
-
});
|
|
510
|
-
it('should return all projects when no pagination specified', async () => {
|
|
511
|
-
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
512
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
513
|
-
client.registerTools(mockServer);
|
|
514
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
515
|
-
.find((call) => call[0] === 'list_insight_hub_projects')[2];
|
|
516
|
-
const result = await toolHandler({});
|
|
517
|
-
const expectedResult = {
|
|
518
|
-
data: mockProjects,
|
|
519
|
-
count: 1
|
|
520
|
-
};
|
|
521
|
-
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
522
|
-
});
|
|
523
|
-
it('should handle no projects found', async () => {
|
|
524
|
-
mockCache.get.mockReturnValue([]);
|
|
525
|
-
client.registerTools(mockServer);
|
|
526
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
527
|
-
.find((call) => call[0] === 'list_insight_hub_projects')[2];
|
|
528
|
-
const result = await toolHandler({});
|
|
529
|
-
expect(result.content[0].text).toBe('No projects found.');
|
|
530
|
-
});
|
|
531
|
-
it('should handle pagination with only page_size', async () => {
|
|
532
|
-
const mockProjects = [
|
|
533
|
-
{ id: 'proj-1', name: 'Project 1' },
|
|
534
|
-
{ id: 'proj-2', name: 'Project 2' },
|
|
535
|
-
{ id: 'proj-3', name: 'Project 3' }
|
|
536
|
-
];
|
|
537
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
538
|
-
client.registerTools(mockServer);
|
|
539
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
540
|
-
.find((call) => call[0] === 'list_insight_hub_projects')[2];
|
|
541
|
-
const result = await toolHandler({ page_size: 2 });
|
|
542
|
-
const expectedResult = {
|
|
543
|
-
data: mockProjects.slice(0, 2),
|
|
544
|
-
count: 2
|
|
545
|
-
};
|
|
546
|
-
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
547
|
-
});
|
|
548
|
-
it('should handle pagination with only page', async () => {
|
|
549
|
-
const mockProjects = Array.from({ length: 25 }, (_, i) => ({
|
|
550
|
-
id: `proj-${i + 1}`,
|
|
551
|
-
name: `Project ${i + 1}`
|
|
552
|
-
}));
|
|
553
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
554
|
-
client.registerTools(mockServer);
|
|
555
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
556
|
-
.find((call) => call[0] === 'list_insight_hub_projects')[2];
|
|
557
|
-
const result = await toolHandler({ page: 2 });
|
|
558
|
-
// Default page_size is 10, so page 2 should return projects 10-19
|
|
559
|
-
const expectedResult = {
|
|
560
|
-
data: mockProjects.slice(10, 20),
|
|
561
|
-
count: 10
|
|
562
|
-
};
|
|
563
|
-
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
564
|
-
});
|
|
565
|
-
});
|
|
566
|
-
describe('get_insight_hub_error tool handler', () => {
|
|
567
|
-
it('should get error details with project from cache', async () => {
|
|
568
|
-
const mockProject = { id: 'proj-1', name: 'Project 1', slug: 'my-project' };
|
|
569
|
-
const mockError = { id: 'error-1', message: 'Test error' };
|
|
570
|
-
const mockOrg = { id: 'org-1', name: 'Test Org', slug: 'test-org' };
|
|
571
|
-
const mockEvents = [{ id: 'event-1', timestamp: '2023-01-01' }];
|
|
572
|
-
const mockPivots = [{ id: 'pivot-1', name: 'test-pivot' }];
|
|
573
|
-
mockCache.get.mockReturnValueOnce(mockProject)
|
|
574
|
-
.mockReturnValueOnce(mockOrg);
|
|
575
|
-
mockErrorAPI.viewErrorOnProject.mockResolvedValue({ body: mockError });
|
|
576
|
-
mockErrorAPI.listEventsOnProject.mockResolvedValue({ body: mockEvents });
|
|
577
|
-
mockErrorAPI.listErrorPivots.mockResolvedValue({ body: mockPivots });
|
|
578
|
-
client.registerTools(mockServer);
|
|
579
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
580
|
-
.find((call) => call[0] === 'get_insight_hub_error')[2];
|
|
581
|
-
const result = await toolHandler({ errorId: 'error-1' });
|
|
582
|
-
const queryString = '?filters[error][][type]=eq&filters[error][][value]=error-1';
|
|
583
|
-
const encodedQueryString = encodeURI(queryString);
|
|
584
|
-
expect(mockErrorAPI.viewErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1');
|
|
585
|
-
expect(result.content[0].text).toBe(JSON.stringify({
|
|
586
|
-
error_details: mockError,
|
|
587
|
-
latest_event: mockEvents[0],
|
|
588
|
-
pivots: mockPivots,
|
|
589
|
-
url: `https://app.bugsnag.com/${mockOrg.slug}/${mockProject.slug}/errors/error-1${encodedQueryString}`
|
|
590
|
-
}));
|
|
591
|
-
});
|
|
592
|
-
it('should throw error when required arguments missing', async () => {
|
|
593
|
-
client.registerTools(mockServer);
|
|
594
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
595
|
-
.find((call) => call[0] === 'get_insight_hub_error')[2];
|
|
596
|
-
await expect(toolHandler({})).rejects.toThrow('Both projectId and errorId arguments are required');
|
|
597
|
-
});
|
|
598
|
-
});
|
|
599
|
-
describe('get_insight_hub_event_details tool handler', () => {
|
|
600
|
-
it('should get event details from dashboard URL', async () => {
|
|
601
|
-
const mockProjects = [{ id: 'proj-1', slug: 'my-project', name: 'My Project' }];
|
|
602
|
-
const mockEvent = { id: 'event-1', project_id: 'proj-1' };
|
|
603
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
604
|
-
mockErrorAPI.viewEventById.mockResolvedValue({ body: mockEvent });
|
|
605
|
-
client.registerTools(mockServer);
|
|
606
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
607
|
-
.find((call) => call[0] === 'get_insight_hub_event_details')[2];
|
|
608
|
-
const result = await toolHandler({
|
|
609
|
-
link: 'https://app.bugsnag.com/my-org/my-project/errors/error-123?event_id=event-1'
|
|
610
|
-
});
|
|
611
|
-
expect(mockErrorAPI.viewEventById).toHaveBeenCalledWith('proj-1', 'event-1');
|
|
612
|
-
expect(result.content[0].text).toBe(JSON.stringify(mockEvent));
|
|
613
|
-
});
|
|
614
|
-
it('should throw error when link is invalid', async () => {
|
|
615
|
-
client.registerTools(mockServer);
|
|
616
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
617
|
-
.find((call) => call[0] === 'get_insight_hub_event_details')[2];
|
|
618
|
-
await expect(toolHandler({ link: 'invalid-url' })).rejects.toThrow();
|
|
619
|
-
});
|
|
620
|
-
it('should throw error when project not found', async () => {
|
|
621
|
-
mockCache.get.mockReturnValue([{ id: 'proj-1', slug: 'other-project', name: 'Other Project' }]);
|
|
622
|
-
client.registerTools(mockServer);
|
|
623
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
624
|
-
.find((call) => call[0] === 'get_insight_hub_event_details')[2];
|
|
625
|
-
await expect(toolHandler({
|
|
626
|
-
link: 'https://app.bugsnag.com/my-org/my-project/errors/error-123?event_id=event-1'
|
|
627
|
-
})).rejects.toThrow('Project with the specified slug not found.');
|
|
628
|
-
});
|
|
629
|
-
it('should throw error when URL is missing required parameters', async () => {
|
|
630
|
-
client.registerTools(mockServer);
|
|
631
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
632
|
-
.find((call) => call[0] === 'get_insight_hub_event_details')[2];
|
|
633
|
-
await expect(toolHandler({
|
|
634
|
-
link: 'https://app.bugsnag.com/my-org/my-project/errors/error-123' // Missing event_id
|
|
635
|
-
})).rejects.toThrow('Both projectSlug and eventId must be present in the link');
|
|
636
|
-
});
|
|
637
|
-
});
|
|
638
|
-
describe('list_insight_hub_project_errors tool handler', () => {
|
|
639
|
-
it('should list project errors with filters', async () => {
|
|
640
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
641
|
-
const mockEventFields = [
|
|
642
|
-
{ display_id: 'error.status', custom: false },
|
|
643
|
-
{ display_id: 'user.email', custom: false }
|
|
644
|
-
];
|
|
645
|
-
const mockErrors = [{ id: 'error-1', message: 'Test error' }];
|
|
646
|
-
const filters = { 'error.status': [{ type: 'eq', value: 'open' }] };
|
|
647
|
-
mockCache.get
|
|
648
|
-
.mockReturnValueOnce(mockProject) // current project
|
|
649
|
-
.mockReturnValueOnce(mockEventFields); // event fields
|
|
650
|
-
mockErrorAPI.listProjectErrors.mockResolvedValue({ body: mockErrors });
|
|
651
|
-
client.registerTools(mockServer);
|
|
652
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
653
|
-
.find((call) => call[0] === 'list_insight_hub_project_errors')[2];
|
|
654
|
-
const result = await toolHandler({ filters });
|
|
655
|
-
expect(mockErrorAPI.listProjectErrors).toHaveBeenCalledWith('proj-1', { filters });
|
|
656
|
-
const expectedResult = {
|
|
657
|
-
data: mockErrors,
|
|
658
|
-
count: 1
|
|
659
|
-
};
|
|
660
|
-
expect(result.content[0].text).toBe(JSON.stringify(expectedResult));
|
|
661
|
-
});
|
|
662
|
-
it('should validate filter keys against cached event fields', async () => {
|
|
663
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
664
|
-
const mockEventFields = [{ display_id: 'error.status', custom: false }];
|
|
665
|
-
const filters = { 'invalid.field': [{ type: 'eq', value: 'test' }] };
|
|
666
|
-
mockCache.get
|
|
667
|
-
.mockReturnValueOnce(mockProject)
|
|
668
|
-
.mockReturnValueOnce(mockEventFields);
|
|
669
|
-
client.registerTools(mockServer);
|
|
670
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
671
|
-
.find((call) => call[0] === 'list_insight_hub_project_errors')[2];
|
|
672
|
-
await expect(toolHandler({ filters })).rejects.toThrow('Invalid filter key: invalid.field');
|
|
673
|
-
});
|
|
674
|
-
it('should throw error when no project ID available', async () => {
|
|
675
|
-
mockCache.get.mockReturnValue(null);
|
|
676
|
-
client.registerTools(mockServer);
|
|
677
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
678
|
-
.find((call) => call[0] === 'list_insight_hub_project_errors')[2];
|
|
679
|
-
await expect(toolHandler({})).rejects.toThrow('No current project found. Please provide a projectId or configure a project API key.');
|
|
680
|
-
});
|
|
681
|
-
});
|
|
682
|
-
describe('get_project_event_filters tool handler', () => {
|
|
683
|
-
it('should return cached event fields', async () => {
|
|
684
|
-
const mockEventFields = [
|
|
685
|
-
{ display_id: 'error.status', custom: false },
|
|
686
|
-
{ display_id: 'user.email', custom: false }
|
|
687
|
-
];
|
|
688
|
-
mockCache.get.mockReturnValue(mockEventFields);
|
|
689
|
-
client.registerTools(mockServer);
|
|
690
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
691
|
-
.find((call) => call[0] === 'get_project_event_filters')[2];
|
|
692
|
-
const result = await toolHandler({});
|
|
693
|
-
expect(result.content[0].text).toBe(JSON.stringify(mockEventFields));
|
|
694
|
-
});
|
|
695
|
-
it('should throw error when no event filters in cache', async () => {
|
|
696
|
-
mockCache.get.mockReturnValue(null);
|
|
697
|
-
client.registerTools(mockServer);
|
|
698
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
699
|
-
.find((call) => call[0] === 'get_project_event_filters')[2];
|
|
700
|
-
await expect(toolHandler({})).rejects.toThrow('No event filters found in cache.');
|
|
701
|
-
});
|
|
702
|
-
});
|
|
703
|
-
describe('update_error tool handler', () => {
|
|
704
|
-
it('should update error successfully with project from cache', async () => {
|
|
705
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
706
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
707
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
708
|
-
client.registerTools(mockServer);
|
|
709
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
710
|
-
.find((call) => call[0] === 'update_error')[2];
|
|
711
|
-
const result = await toolHandler({
|
|
712
|
-
errorId: 'error-1',
|
|
713
|
-
operation: 'fix'
|
|
714
|
-
});
|
|
715
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1', { operation: 'fix' });
|
|
716
|
-
expect(result.content[0].text).toBe(JSON.stringify({ success: true }));
|
|
717
|
-
});
|
|
718
|
-
it('should update error successfully with explicit project ID', async () => {
|
|
719
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
720
|
-
const mockProjects = [mockProject];
|
|
721
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
722
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 204 });
|
|
723
|
-
client.registerTools(mockServer);
|
|
724
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
725
|
-
.find((call) => call[0] === 'update_error')[2];
|
|
726
|
-
const result = await toolHandler({
|
|
727
|
-
projectId: 'proj-1',
|
|
728
|
-
errorId: 'error-1',
|
|
729
|
-
operation: 'ignore'
|
|
730
|
-
});
|
|
731
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1', { operation: 'ignore' });
|
|
732
|
-
expect(result.content[0].text).toBe(JSON.stringify({ success: true }));
|
|
733
|
-
});
|
|
734
|
-
it('should handle all permitted operations', async () => {
|
|
735
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
736
|
-
// Test all operations except override_severity which requires special elicitInput handling
|
|
737
|
-
const operations = ['open', 'fix', 'ignore', 'discard', 'undiscard'];
|
|
738
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
739
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
740
|
-
client.registerTools(mockServer);
|
|
741
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
742
|
-
.find((call) => call[0] === 'update_error')[2];
|
|
743
|
-
for (const operation of operations) {
|
|
744
|
-
await toolHandler({
|
|
745
|
-
errorId: 'error-1',
|
|
746
|
-
operation: operation
|
|
747
|
-
});
|
|
748
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1', { operation, severity: undefined });
|
|
749
|
-
}
|
|
750
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledTimes(operations.length);
|
|
751
|
-
});
|
|
752
|
-
it('should handle override_severity operation with elicitInput', async () => {
|
|
753
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
754
|
-
const mockServerWithElicitInput = {
|
|
755
|
-
registerTool: vi.fn(),
|
|
756
|
-
server: {
|
|
757
|
-
elicitInput: vi.fn().mockResolvedValue({
|
|
758
|
-
action: 'accept',
|
|
759
|
-
content: { severity: 'warning' }
|
|
760
|
-
})
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
764
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
765
|
-
client.registerTools(mockServerWithElicitInput);
|
|
766
|
-
const toolHandler = mockServerWithElicitInput.registerTool.mock.calls
|
|
767
|
-
.find((call) => call[0] === 'update_error')[2];
|
|
768
|
-
const result = await toolHandler({
|
|
769
|
-
errorId: 'error-1',
|
|
770
|
-
operation: 'override_severity'
|
|
771
|
-
});
|
|
772
|
-
expect(mockServerWithElicitInput.server.elicitInput).toHaveBeenCalledWith({
|
|
773
|
-
message: "Please provide the new severity for the error (e.g. 'info', 'warning', 'error', 'critical')",
|
|
774
|
-
requestedSchema: {
|
|
775
|
-
type: "object",
|
|
776
|
-
properties: {
|
|
777
|
-
severity: {
|
|
778
|
-
type: "string",
|
|
779
|
-
enum: ['info', 'warning', 'error'],
|
|
780
|
-
description: "The new severity level for the error"
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
},
|
|
784
|
-
required: ["severity"]
|
|
785
|
-
});
|
|
786
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1', { operation: 'override_severity', severity: 'warning' });
|
|
787
|
-
expect(result.content[0].text).toBe(JSON.stringify({ success: true }));
|
|
788
|
-
});
|
|
789
|
-
it('should handle override_severity operation when elicitInput is rejected', async () => {
|
|
790
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
791
|
-
const mockServerWithElicitInput = {
|
|
792
|
-
registerTool: vi.fn(),
|
|
793
|
-
server: {
|
|
794
|
-
elicitInput: vi.fn().mockResolvedValue({
|
|
795
|
-
action: 'reject'
|
|
796
|
-
})
|
|
797
|
-
}
|
|
798
|
-
};
|
|
799
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
800
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 200 });
|
|
801
|
-
client.registerTools(mockServerWithElicitInput);
|
|
802
|
-
const toolHandler = mockServerWithElicitInput.registerTool.mock.calls
|
|
803
|
-
.find((call) => call[0] === 'update_error')[2];
|
|
804
|
-
const result = await toolHandler({
|
|
805
|
-
errorId: 'error-1',
|
|
806
|
-
operation: 'override_severity'
|
|
807
|
-
});
|
|
808
|
-
expect(mockErrorAPI.updateErrorOnProject).toHaveBeenCalledWith('proj-1', 'error-1', { operation: 'override_severity', severity: undefined });
|
|
809
|
-
expect(result.content[0].text).toBe(JSON.stringify({ success: true }));
|
|
810
|
-
});
|
|
811
|
-
it('should return false when API returns non-success status', async () => {
|
|
812
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
813
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
814
|
-
mockErrorAPI.updateErrorOnProject.mockResolvedValue({ status: 400 });
|
|
815
|
-
client.registerTools(mockServer);
|
|
816
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
817
|
-
.find((call) => call[0] === 'update_error')[2];
|
|
818
|
-
const result = await toolHandler({
|
|
819
|
-
errorId: 'error-1',
|
|
820
|
-
operation: 'fix'
|
|
821
|
-
});
|
|
822
|
-
expect(result.content[0].text).toBe(JSON.stringify({ success: false }));
|
|
823
|
-
});
|
|
824
|
-
it('should throw error when no project found', async () => {
|
|
825
|
-
mockCache.get.mockReturnValue(null);
|
|
826
|
-
client.registerTools(mockServer);
|
|
827
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
828
|
-
.find((call) => call[0] === 'update_error')[2];
|
|
829
|
-
await expect(toolHandler({
|
|
830
|
-
errorId: 'error-1',
|
|
831
|
-
operation: 'fix'
|
|
832
|
-
})).rejects.toThrow('No current project found. Please provide a projectId or configure a project API key.');
|
|
833
|
-
});
|
|
834
|
-
it('should throw error when project ID not found', async () => {
|
|
835
|
-
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
836
|
-
mockCache.get.mockReturnValue(mockProjects);
|
|
837
|
-
client.registerTools(mockServer);
|
|
838
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
839
|
-
.find((call) => call[0] === 'update_error')[2];
|
|
840
|
-
await expect(toolHandler({
|
|
841
|
-
projectId: 'non-existent-project',
|
|
842
|
-
errorId: 'error-1',
|
|
843
|
-
operation: 'fix'
|
|
844
|
-
})).rejects.toThrow('Project with ID non-existent-project not found.');
|
|
845
|
-
});
|
|
846
|
-
it('should notify Bugsnag when API call fails', async () => {
|
|
847
|
-
const Bugsnag = (await import('../../../common/bugsnag.js')).default;
|
|
848
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
849
|
-
const mockError = new Error('API error');
|
|
850
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
851
|
-
mockErrorAPI.updateErrorOnProject.mockRejectedValue(mockError);
|
|
852
|
-
client.registerTools(mockServer);
|
|
853
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
854
|
-
.find((call) => call[0] === 'update_error')[2];
|
|
855
|
-
await expect(toolHandler({
|
|
856
|
-
errorId: 'error-1',
|
|
857
|
-
operation: 'fix'
|
|
858
|
-
})).rejects.toThrow('API error');
|
|
859
|
-
expect(Bugsnag.notify).toHaveBeenCalledWith(mockError);
|
|
860
|
-
});
|
|
861
|
-
});
|
|
862
|
-
describe('error handling in tool handlers', () => {
|
|
863
|
-
it('should notify Bugsnag when error occurs in list_insight_hub_projects', async () => {
|
|
864
|
-
const Bugsnag = (await import('../../../common/bugsnag.js')).default;
|
|
865
|
-
const mockError = new Error('Test error');
|
|
866
|
-
mockCache.get.mockImplementation(() => {
|
|
867
|
-
throw mockError;
|
|
868
|
-
});
|
|
869
|
-
client.registerTools(mockServer);
|
|
870
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
871
|
-
.find((call) => call[0] === 'list_insight_hub_projects')[2];
|
|
872
|
-
await expect(toolHandler({})).rejects.toThrow('Test error');
|
|
873
|
-
expect(Bugsnag.notify).toHaveBeenCalledWith(mockError);
|
|
874
|
-
});
|
|
875
|
-
it('should notify Bugsnag when error occurs in get_insight_hub_error', async () => {
|
|
876
|
-
const Bugsnag = (await import('../../../common/bugsnag.js')).default;
|
|
877
|
-
const mockError = new Error('API error');
|
|
878
|
-
const mockProject = { id: 'proj-1', name: 'Project 1' };
|
|
879
|
-
mockCache.get.mockReturnValue(mockProject);
|
|
880
|
-
mockErrorAPI.viewErrorOnProject.mockRejectedValue(mockError);
|
|
881
|
-
client.registerTools(mockServer);
|
|
882
|
-
const toolHandler = mockServer.registerTool.mock.calls
|
|
883
|
-
.find((call) => call[0] === 'get_insight_hub_error')[2];
|
|
884
|
-
await expect(toolHandler({ errorId: 'error-1' })).rejects.toThrow('API error');
|
|
885
|
-
expect(Bugsnag.notify).toHaveBeenCalledWith(mockError);
|
|
886
|
-
});
|
|
887
|
-
it('should notify Bugsnag when error occurs in resource handler', async () => {
|
|
888
|
-
const Bugsnag = (await import('../../../common/bugsnag.js')).default;
|
|
889
|
-
const mockError = new Error('Resource error');
|
|
890
|
-
mockCache.get.mockRejectedValue(mockError);
|
|
891
|
-
client.registerResources(mockServer);
|
|
892
|
-
const resourceHandler = mockServer.resource.mock.calls[0][2];
|
|
893
|
-
await expect(resourceHandler({ href: 'insighthub://event/event-1' }, { id: 'event-1' })).rejects.toThrow('Resource error');
|
|
894
|
-
expect(Bugsnag.notify).toHaveBeenCalledWith(mockError);
|
|
895
|
-
});
|
|
896
|
-
});
|
|
897
|
-
});
|
|
898
|
-
describe('resource handlers', () => {
|
|
899
|
-
let mockServer;
|
|
900
|
-
beforeEach(() => {
|
|
901
|
-
mockServer = {
|
|
902
|
-
registerTool: vi.fn(),
|
|
903
|
-
resource: vi.fn()
|
|
904
|
-
};
|
|
905
|
-
});
|
|
906
|
-
describe('insight_hub_event resource handler', () => {
|
|
907
|
-
it('should find event by ID across projects', async () => {
|
|
908
|
-
const mockEvent = { id: 'event-1', project_id: 'proj-1' };
|
|
909
|
-
const mockProjects = [{ id: 'proj-1', name: 'Project 1' }];
|
|
910
|
-
mockCache.get.mockReturnValueOnce(mockProjects);
|
|
911
|
-
mockErrorAPI.viewEventById.mockResolvedValue({ body: mockEvent });
|
|
912
|
-
client.registerResources(mockServer);
|
|
913
|
-
const resourceHandler = mockServer.resource.mock.calls[0][2];
|
|
914
|
-
const result = await resourceHandler({ href: 'insighthub://event/event-1' }, { id: 'event-1' });
|
|
915
|
-
expect(result.contents[0].uri).toBe('insighthub://event/event-1');
|
|
916
|
-
expect(result.contents[0].text).toBe(JSON.stringify(mockEvent));
|
|
917
|
-
});
|
|
918
|
-
});
|
|
919
|
-
});
|
|
920
|
-
});
|