Skip to content

Commit 1dd4353

Browse files
test(mcp): implement resilience pipeline suite
1 parent df97084 commit 1dd4353

File tree

2 files changed

+136
-0
lines changed

2 files changed

+136
-0
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/// <reference types="bun-types" />
2+
import { test, expect, describe, mock } from 'bun:test'
3+
import { ResiliencePipeline } from './pipeline'
4+
import type { McpExecutionContext, McpMiddleware, McpMiddlewareNext } from './types'
5+
6+
const infoInfo = mock()
7+
const errorError = mock()
8+
9+
// Mock logger before any imports of telemetry
10+
mock.module('@sim/logger', () => ({
11+
createLogger: () => ({
12+
info: infoInfo,
13+
error: errorError,
14+
warn: mock(),
15+
debug: mock()
16+
})
17+
}))
18+
19+
// Dynamically import TelemetryMiddleware so the mock applies
20+
const { TelemetryMiddleware } = await import('./telemetry')
21+
import { createLogger } from '@sim/logger'
22+
23+
describe('ResiliencePipeline', () => {
24+
const mockContext: McpExecutionContext = {
25+
toolCall: { name: 'test_tool', arguments: {} },
26+
serverId: 'server-1',
27+
userId: 'user-1',
28+
workspaceId: 'workspace-1'
29+
}
30+
31+
test('should execute middlewares in order', async () => {
32+
const pipeline = new ResiliencePipeline()
33+
const order: number[] = []
34+
35+
const m1: McpMiddleware = {
36+
execute: async (ctx, next) => {
37+
order.push(1)
38+
const res = await next(ctx)
39+
order.push(4)
40+
return res
41+
}
42+
}
43+
44+
const m2: McpMiddleware = {
45+
execute: async (ctx, next) => {
46+
order.push(2)
47+
const res = await next(ctx)
48+
order.push(3)
49+
return res
50+
}
51+
}
52+
53+
pipeline.use(m1).use(m2)
54+
55+
const finalHandler: McpMiddlewareNext = async () => {
56+
return { content: [{ type: 'text', text: 'success' }] }
57+
}
58+
59+
const result = await pipeline.execute(mockContext, finalHandler)
60+
61+
expect(order).toEqual([1, 2, 3, 4])
62+
expect(result.content?.[0].text).toBe('success')
63+
})
64+
})
65+
66+
describe('TelemetryMiddleware', () => {
67+
const mockContext: McpExecutionContext = {
68+
toolCall: { name: 'telemetry_tool', arguments: {} },
69+
serverId: 'server-2',
70+
userId: 'user-2',
71+
workspaceId: 'workspace-2'
72+
}
73+
74+
test('should log success with latency', async () => {
75+
infoInfo.mockClear()
76+
77+
const telemetry = new TelemetryMiddleware()
78+
79+
const finalHandler: McpMiddlewareNext = async () => {
80+
// simulate some latency
81+
await new Promise(r => setTimeout(r, 10))
82+
return { content: [] }
83+
}
84+
85+
await telemetry.execute(mockContext, finalHandler)
86+
87+
expect(infoInfo).toHaveBeenCalled()
88+
const logMsg = infoInfo.mock.calls[0][0]
89+
const logCtx = infoInfo.mock.calls[0][1]
90+
expect(logMsg).toBe('MCP Tool Execution Completed')
91+
expect(logCtx.toolName).toBe('telemetry_tool')
92+
expect(logCtx.latency_ms).toBeGreaterThanOrEqual(10)
93+
expect(logCtx.success).toBe(true)
94+
})
95+
96+
test('should log TOOL_ERROR when tool result has isError: true', async () => {
97+
infoInfo.mockClear()
98+
99+
const telemetry = new TelemetryMiddleware()
100+
101+
const finalHandler: McpMiddlewareNext = async () => {
102+
return { isError: true, content: [] }
103+
}
104+
105+
await telemetry.execute(mockContext, finalHandler)
106+
107+
expect(infoInfo).toHaveBeenCalled()
108+
const logCtx = infoInfo.mock.calls[0][1]
109+
expect(logCtx.success).toBe(false)
110+
expect(logCtx.failure_reason).toBe('TOOL_ERROR')
111+
})
112+
113+
test('should log exception and rethrow with TIMEOUT explanation', async () => {
114+
errorError.mockClear()
115+
116+
const telemetry = new TelemetryMiddleware()
117+
118+
const finalHandler: McpMiddlewareNext = async () => {
119+
throw new Error('Connection timeout occurred')
120+
}
121+
122+
let caughtError: Error | null = null
123+
try {
124+
await telemetry.execute(mockContext, finalHandler)
125+
} catch (e: any) {
126+
caughtError = e
127+
}
128+
129+
expect(caughtError).toBeDefined()
130+
expect(errorError).toHaveBeenCalled()
131+
const logMsg = errorError.mock.calls[0][0]
132+
const logCtx = errorError.mock.calls[0][1]
133+
expect(logMsg).toBe('MCP Tool Execution Failed')
134+
expect(logCtx.failure_reason).toBe('TIMEOUT')
135+
})
136+
})

apps/sim/test/setup.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)