Skip to content

Commit 7f6c046

Browse files
committed
Add timeout reset on progress notifications
1 parent 41e0d26 commit 7f6c046

File tree

2 files changed

+292
-50
lines changed

2 files changed

+292
-50
lines changed

src/shared/protocol.test.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,182 @@ describe("protocol tests", () => {
6262
await transport.close();
6363
expect(oncloseMock).toHaveBeenCalled();
6464
});
65+
66+
test("should reset timeout when progress notification is received", async () => {
67+
jest.useFakeTimers();
68+
69+
await protocol.connect(transport);
70+
const request = { method: "example", params: {} };
71+
const mockSchema: ZodType<{ result: string }> = z.object({
72+
result: z.string(),
73+
});
74+
75+
const onProgressMock = jest.fn();
76+
const requestPromise = protocol.request(request, mockSchema, {
77+
timeout: 1000, // Increased timeout for more reliable testing
78+
resetTimeoutOnProgress: true,
79+
onprogress: onProgressMock,
80+
});
81+
82+
// Advance time close to timeout
83+
jest.advanceTimersByTime(800);
84+
85+
// Send progress notification
86+
if (transport.onmessage) {
87+
transport.onmessage({
88+
jsonrpc: "2.0",
89+
method: "notifications/progress",
90+
params: {
91+
progressToken: 0,
92+
progress: 50,
93+
total: 100,
94+
},
95+
});
96+
}
97+
98+
// Run all pending promises to ensure progress handler is called
99+
await Promise.resolve();
100+
101+
// Verify progress handler was called
102+
expect(onProgressMock).toHaveBeenCalledWith({
103+
progress: 50,
104+
total: 100,
105+
});
106+
107+
// Send success response
108+
if (transport.onmessage) {
109+
transport.onmessage({
110+
jsonrpc: "2.0",
111+
id: 0,
112+
result: { result: "success" },
113+
});
114+
}
115+
116+
// Run all pending promises
117+
await Promise.resolve();
118+
119+
await expect(requestPromise).resolves.toEqual({ result: "success" });
120+
121+
jest.useRealTimers();
122+
});
123+
124+
test("should respect maxTotalTimeout", async () => {
125+
jest.useFakeTimers();
126+
127+
await protocol.connect(transport);
128+
const request = { method: "example", params: {} };
129+
const mockSchema: ZodType<{ result: string }> = z.object({
130+
result: z.string(),
131+
});
132+
133+
const onProgressMock = jest.fn();
134+
const requestPromise = protocol.request(request, mockSchema, {
135+
timeout: 1000,
136+
maxTotalTimeout: 100,
137+
resetTimeoutOnProgress: true,
138+
onprogress: onProgressMock,
139+
});
140+
141+
// Advance time beyond maxTotalTimeout
142+
jest.advanceTimersByTime(150);
143+
144+
// Send progress notification after maxTotalTimeout
145+
if (transport.onmessage) {
146+
transport.onmessage({
147+
jsonrpc: "2.0",
148+
method: "notifications/progress",
149+
params: {
150+
progressToken: 0,
151+
progress: 50,
152+
total: 100,
153+
},
154+
});
155+
}
156+
157+
await expect(requestPromise).rejects.toThrow("Maximum total timeout exceeded");
158+
expect(onProgressMock).not.toHaveBeenCalled();
159+
160+
jest.useRealTimers();
161+
});
162+
163+
test("should timeout if no progress received within timeout period", async () => {
164+
jest.useFakeTimers();
165+
166+
await protocol.connect(transport);
167+
const request = { method: "example", params: {} };
168+
const mockSchema: ZodType<{ result: string }> = z.object({
169+
result: z.string(),
170+
});
171+
172+
const requestPromise = protocol.request(request, mockSchema, {
173+
timeout: 100,
174+
resetTimeoutOnProgress: true,
175+
});
176+
177+
// Advance time beyond timeout
178+
jest.advanceTimersByTime(101);
179+
180+
await expect(requestPromise).rejects.toThrow("Request timed out");
181+
182+
jest.useRealTimers();
183+
});
184+
185+
test("should handle multiple progress notifications correctly", async () => {
186+
jest.useFakeTimers();
187+
188+
await protocol.connect(transport);
189+
const request = { method: "example", params: {} };
190+
const mockSchema: ZodType<{ result: string }> = z.object({
191+
result: z.string(),
192+
});
193+
194+
const onProgressMock = jest.fn();
195+
const requestPromise = protocol.request(request, mockSchema, {
196+
timeout: 1000,
197+
resetTimeoutOnProgress: true,
198+
onprogress: onProgressMock,
199+
});
200+
201+
// Simulate multiple progress updates
202+
for (let i = 1; i <= 3; i++) {
203+
// Advance close to timeout
204+
jest.advanceTimersByTime(800);
205+
206+
// Send progress notification
207+
if (transport.onmessage) {
208+
transport.onmessage({
209+
jsonrpc: "2.0",
210+
method: "notifications/progress",
211+
params: {
212+
progressToken: 0,
213+
progress: i * 25,
214+
total: 100,
215+
},
216+
});
217+
}
218+
219+
// Verify progress handler was called
220+
await Promise.resolve();
221+
expect(onProgressMock).toHaveBeenNthCalledWith(i, {
222+
progress: i * 25,
223+
total: 100,
224+
});
225+
}
226+
227+
// Send success response
228+
if (transport.onmessage) {
229+
transport.onmessage({
230+
jsonrpc: "2.0",
231+
id: 0,
232+
result: { result: "success" },
233+
});
234+
}
235+
236+
await Promise.resolve();
237+
await expect(requestPromise).resolves.toEqual({ result: "success" });
238+
239+
jest.useRealTimers();
240+
});
65241
});
66242

67243
describe("mergeCapabilities", () => {

0 commit comments

Comments
 (0)