Skip to content

Commit f73fdc6

Browse files
committed
add tests
1 parent 670e6b1 commit f73fdc6

File tree

7 files changed

+1420
-966
lines changed

7 files changed

+1420
-966
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import * as currentScopes from '../../../../src/currentScopes';
3+
import * as exports from '../../../../src/exports';
4+
import { captureError } from '../../../../src/integrations/mcp-server/errorCapture';
5+
import { wrapMcpServerWithSentry } from '../../../../src/integrations/mcp-server';
6+
import { createMockMcpServer } from './testUtils';
7+
8+
describe('MCP Server Error Capture', () => {
9+
const captureExceptionSpy = vi.spyOn(exports, 'captureException');
10+
const getClientSpy = vi.spyOn(currentScopes, 'getClient');
11+
12+
beforeEach(() => {
13+
vi.clearAllMocks();
14+
getClientSpy.mockReturnValue({
15+
getOptions: () => ({ sendDefaultPii: true }),
16+
} as ReturnType<typeof currentScopes.getClient>);
17+
});
18+
19+
describe('captureError', () => {
20+
it('should capture errors with default error type', () => {
21+
const error = new Error('Test error');
22+
23+
captureError(error);
24+
25+
expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
26+
tags: {
27+
mcp_error_type: 'handler_execution',
28+
},
29+
});
30+
});
31+
32+
it('should capture errors with custom error type', () => {
33+
const error = new Error('Tool execution failed');
34+
35+
captureError(error, 'tool_execution');
36+
37+
expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
38+
tags: {
39+
mcp_error_type: 'tool_execution',
40+
},
41+
});
42+
});
43+
44+
it('should capture transport errors', () => {
45+
const error = new Error('Connection failed');
46+
47+
captureError(error, 'transport');
48+
49+
expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
50+
tags: {
51+
mcp_error_type: 'transport',
52+
},
53+
});
54+
});
55+
56+
it('should capture protocol errors', () => {
57+
const error = new Error('Invalid JSON-RPC request');
58+
59+
captureError(error, 'protocol');
60+
61+
expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
62+
tags: {
63+
mcp_error_type: 'protocol',
64+
},
65+
});
66+
});
67+
68+
it('should capture validation errors', () => {
69+
const error = new Error('Invalid parameters');
70+
71+
captureError(error, 'validation');
72+
73+
expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
74+
tags: {
75+
mcp_error_type: 'validation',
76+
},
77+
});
78+
});
79+
80+
it('should capture timeout errors', () => {
81+
const error = new Error('Operation timed out');
82+
83+
captureError(error, 'timeout');
84+
85+
expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
86+
tags: {
87+
mcp_error_type: 'timeout',
88+
},
89+
});
90+
});
91+
92+
it('should not capture when no client is available', () => {
93+
getClientSpy.mockReturnValue(undefined);
94+
95+
const error = new Error('Test error');
96+
97+
captureError(error, 'tool_execution');
98+
99+
expect(captureExceptionSpy).not.toHaveBeenCalled();
100+
});
101+
102+
it('should handle Sentry capture errors gracefully', () => {
103+
captureExceptionSpy.mockImplementation(() => {
104+
throw new Error('Sentry error');
105+
});
106+
107+
const error = new Error('Test error');
108+
109+
// Should not throw
110+
expect(() => captureError(error, 'tool_execution')).not.toThrow();
111+
});
112+
113+
it('should handle undefined client gracefully', () => {
114+
getClientSpy.mockReturnValue(undefined);
115+
116+
const error = new Error('Test error');
117+
118+
// Should not throw and not capture
119+
expect(() => captureError(error, 'tool_execution')).not.toThrow();
120+
expect(captureExceptionSpy).not.toHaveBeenCalled();
121+
});
122+
});
123+
124+
describe('Error Capture Integration', () => {
125+
let mockMcpServer: ReturnType<typeof createMockMcpServer>;
126+
let wrappedMcpServer: ReturnType<typeof createMockMcpServer>;
127+
128+
beforeEach(() => {
129+
mockMcpServer = createMockMcpServer();
130+
wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
131+
});
132+
133+
it('should capture tool execution errors and continue normal flow', async () => {
134+
const toolError = new Error('Tool execution failed');
135+
const mockToolHandler = vi.fn().mockRejectedValue(toolError);
136+
137+
wrappedMcpServer.tool('failing-tool', mockToolHandler);
138+
139+
await expect(mockToolHandler({ input: 'test' }, { requestId: 'req-123', sessionId: 'sess-456' })).rejects.toThrow(
140+
'Tool execution failed',
141+
);
142+
143+
// The capture should be set up correctly
144+
expect(captureExceptionSpy).toHaveBeenCalledTimes(0); // No capture yet since we didn't call the wrapped handler
145+
});
146+
147+
it('should handle Sentry capture errors gracefully', async () => {
148+
captureExceptionSpy.mockImplementation(() => {
149+
throw new Error('Sentry error');
150+
});
151+
152+
// Test that the capture function itself doesn't throw
153+
const toolError = new Error('Tool execution failed');
154+
const mockToolHandler = vi.fn().mockRejectedValue(toolError);
155+
156+
wrappedMcpServer.tool('failing-tool', mockToolHandler);
157+
158+
// The error capture should be resilient to Sentry errors
159+
expect(captureExceptionSpy).toHaveBeenCalledTimes(0);
160+
});
161+
});
162+
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import * as currentScopes from '../../../../src/currentScopes';
3+
import { wrapMcpServerWithSentry } from '../../../../src/integrations/mcp-server';
4+
import * as tracingModule from '../../../../src/tracing';
5+
import { createMockMcpServer } from './testUtils';
6+
7+
describe('wrapMcpServerWithSentry', () => {
8+
const startSpanSpy = vi.spyOn(tracingModule, 'startSpan');
9+
const startInactiveSpanSpy = vi.spyOn(tracingModule, 'startInactiveSpan');
10+
const getClientSpy = vi.spyOn(currentScopes, 'getClient');
11+
12+
beforeEach(() => {
13+
vi.clearAllMocks();
14+
// Mock client to return sendDefaultPii:
15+
getClientSpy.mockReturnValue({
16+
getOptions: () => ({ sendDefaultPii: true }),
17+
getDsn: () => ({ publicKey: 'test-key', host: 'test-host' }),
18+
emit: vi.fn(),
19+
} as any);
20+
});
21+
22+
it('should return the same instance (modified) if it is a valid MCP server instance', () => {
23+
const mockMcpServer = createMockMcpServer();
24+
const wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
25+
26+
expect(wrappedMcpServer).toBe(mockMcpServer);
27+
});
28+
29+
it('should return the input unchanged if it is not a valid MCP server instance', () => {
30+
const invalidMcpServer = {
31+
resource: () => {},
32+
tool: () => {},
33+
// Missing required methods
34+
};
35+
36+
const result = wrapMcpServerWithSentry(invalidMcpServer);
37+
expect(result).toBe(invalidMcpServer);
38+
39+
// Methods should not be wrapped
40+
expect(result.resource).toBe(invalidMcpServer.resource);
41+
expect(result.tool).toBe(invalidMcpServer.tool);
42+
43+
// No calls to startSpan or startInactiveSpan
44+
expect(startSpanSpy).not.toHaveBeenCalled();
45+
expect(startInactiveSpanSpy).not.toHaveBeenCalled();
46+
});
47+
48+
it('should not wrap the same instance twice', () => {
49+
const mockMcpServer = createMockMcpServer();
50+
51+
const wrappedOnce = wrapMcpServerWithSentry(mockMcpServer);
52+
const wrappedTwice = wrapMcpServerWithSentry(wrappedOnce);
53+
54+
expect(wrappedTwice).toBe(wrappedOnce);
55+
});
56+
57+
it('should wrap the connect method to intercept transport', () => {
58+
const mockMcpServer = createMockMcpServer();
59+
const originalConnect = mockMcpServer.connect;
60+
61+
const wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
62+
63+
expect(wrappedMcpServer.connect).not.toBe(originalConnect);
64+
expect(typeof wrappedMcpServer.connect).toBe('function');
65+
});
66+
67+
it('should wrap handler methods (tool, resource, prompt)', () => {
68+
const mockMcpServer = createMockMcpServer();
69+
const originalTool = mockMcpServer.tool;
70+
const originalResource = mockMcpServer.resource;
71+
const originalPrompt = mockMcpServer.prompt;
72+
73+
const wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
74+
75+
expect(wrappedMcpServer.tool).not.toBe(originalTool);
76+
expect(wrappedMcpServer.resource).not.toBe(originalResource);
77+
expect(wrappedMcpServer.prompt).not.toBe(originalPrompt);
78+
});
79+
80+
describe('Handler Wrapping', () => {
81+
let mockMcpServer: ReturnType<typeof createMockMcpServer>;
82+
let wrappedMcpServer: ReturnType<typeof createMockMcpServer>;
83+
84+
beforeEach(() => {
85+
mockMcpServer = createMockMcpServer();
86+
wrappedMcpServer = wrapMcpServerWithSentry(mockMcpServer);
87+
});
88+
89+
it('should register tool handlers without throwing errors', () => {
90+
const toolHandler = vi.fn();
91+
92+
expect(() => {
93+
wrappedMcpServer.tool('test-tool', toolHandler);
94+
}).not.toThrow();
95+
});
96+
97+
it('should register resource handlers without throwing errors', () => {
98+
const resourceHandler = vi.fn();
99+
100+
expect(() => {
101+
wrappedMcpServer.resource('test-resource', resourceHandler);
102+
}).not.toThrow();
103+
});
104+
105+
it('should register prompt handlers without throwing errors', () => {
106+
const promptHandler = vi.fn();
107+
108+
expect(() => {
109+
wrappedMcpServer.prompt('test-prompt', promptHandler);
110+
}).not.toThrow();
111+
});
112+
113+
it('should handle multiple arguments when registering handlers', () => {
114+
const nonFunctionArg = { config: 'value' };
115+
116+
expect(() => {
117+
wrappedMcpServer.tool('test-tool', nonFunctionArg, 'other-arg');
118+
}).not.toThrow();
119+
});
120+
});
121+
});

0 commit comments

Comments
 (0)