Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts
5260 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as assert from 'assert';
7
import * as sinon from 'sinon';
8
import { upcast } from '../../../../../base/common/types.js';
9
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
10
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
11
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
12
import { ILoggerService } from '../../../../../platform/log/common/log.js';
13
import { IProductService } from '../../../../../platform/product/common/productService.js';
14
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
15
import { TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';
16
import { IMcpHostDelegate } from '../../common/mcpRegistryTypes.js';
17
import { McpServerRequestHandler, McpTask } from '../../common/mcpServerRequestHandler.js';
18
import { McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../common/mcpTypes.js';
19
import { MCP } from '../../common/modelContextProtocol.js';
20
import { TestMcpMessageTransport } from './mcpRegistryTypes.js';
21
import { IOutputService } from '../../../../services/output/common/output.js';
22
import { Disposable } from '../../../../../base/common/lifecycle.js';
23
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
24
import { McpTaskManager } from '../../common/mcpTaskManager.js';
25
import { upcastPartial } from '../../../../../base/test/common/mock.js';
26
27
class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate {
28
private readonly _transport: TestMcpMessageTransport;
29
30
priority = 0;
31
32
constructor() {
33
super();
34
this._transport = this._register(new TestMcpMessageTransport());
35
}
36
37
38
substituteVariables(serverDefinition: McpServerDefinition, launch: McpServerLaunch): Promise<McpServerLaunch> {
39
return Promise.resolve(launch);
40
}
41
42
canStart(): boolean {
43
return true;
44
}
45
46
start(): TestMcpMessageTransport {
47
return this._transport;
48
}
49
50
getTransport(): TestMcpMessageTransport {
51
return this._transport;
52
}
53
54
waitForInitialProviderPromises(): Promise<void> {
55
return Promise.resolve();
56
}
57
}
58
59
suite('Workbench - MCP - ServerRequestHandler', () => {
60
const store = ensureNoDisposablesAreLeakedInTestSuite();
61
62
let instantiationService: TestInstantiationService;
63
let delegate: TestMcpHostDelegate;
64
let transport: TestMcpMessageTransport;
65
let handler: McpServerRequestHandler;
66
let cts: CancellationTokenSource;
67
68
setup(async () => {
69
delegate = store.add(new TestMcpHostDelegate());
70
transport = delegate.getTransport();
71
cts = store.add(new CancellationTokenSource());
72
73
// Setup test services
74
const services = new ServiceCollection(
75
[ILoggerService, store.add(new TestLoggerService())],
76
[IOutputService, upcast({ showChannel: () => { } })],
77
[IStorageService, store.add(new TestStorageService())],
78
[IProductService, TestProductService],
79
);
80
81
instantiationService = store.add(new TestInstantiationService(services));
82
83
transport.setConnectionState({ state: McpConnectionState.Kind.Running });
84
85
// Manually create the handler since we need the transport already set up
86
const logger = store.add((instantiationService.get(ILoggerService) as TestLoggerService)
87
.createLogger('mcpServerTest', { hidden: true, name: 'MCP Test' }));
88
89
// Start the handler creation
90
const handlerPromise = McpServerRequestHandler.create(instantiationService, { logger, launch: transport, taskManager: store.add(new McpTaskManager()) }, cts.token);
91
92
handler = await handlerPromise;
93
store.add(handler);
94
});
95
96
test('should send and receive JSON-RPC requests', async () => {
97
// Setup request
98
const requestPromise = handler.listResources();
99
100
// Get the sent message and verify it
101
const sentMessages = transport.getSentMessages();
102
assert.strictEqual(sentMessages.length, 3); // initialize + listResources
103
104
// Verify listResources request format
105
const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest;
106
assert.strictEqual(listResourcesRequest.method, 'resources/list');
107
assert.strictEqual(listResourcesRequest.jsonrpc, MCP.JSONRPC_VERSION);
108
assert.ok(typeof listResourcesRequest.id === 'number');
109
110
// Simulate server response with mock resources that match the expected Resource interface
111
transport.simulateReceiveMessage({
112
jsonrpc: MCP.JSONRPC_VERSION,
113
id: listResourcesRequest.id,
114
result: {
115
resources: [
116
{ uri: 'resource1', type: 'text/plain', name: 'Test Resource 1' },
117
{ uri: 'resource2', type: 'text/plain', name: 'Test Resource 2' }
118
]
119
}
120
});
121
122
// Verify the result
123
const resources = await requestPromise;
124
assert.strictEqual(resources.length, 2);
125
assert.strictEqual(resources[0].uri, 'resource1');
126
assert.strictEqual(resources[1].name, 'Test Resource 2');
127
});
128
129
test('should handle paginated requests', async () => {
130
// Setup request
131
const requestPromise = handler.listResources();
132
133
// Get the first request and respond with pagination
134
const sentMessages = transport.getSentMessages();
135
const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest;
136
137
// Send first page with nextCursor
138
transport.simulateReceiveMessage({
139
jsonrpc: MCP.JSONRPC_VERSION,
140
id: listResourcesRequest.id,
141
result: {
142
resources: [
143
{ uri: 'resource1', type: 'text/plain', name: 'Test Resource 1' }
144
],
145
nextCursor: 'page2'
146
}
147
});
148
149
// Clear the sent messages to only capture the next page request
150
transport.clearSentMessages();
151
152
// Wait a bit to allow the handler to process and send the next request
153
await new Promise(resolve => setTimeout(resolve, 0));
154
155
// Get the second request and verify cursor is included
156
const sentMessages2 = transport.getSentMessages();
157
assert.strictEqual(sentMessages2.length, 1);
158
159
const listResourcesRequest2 = sentMessages2[0] as MCP.JSONRPCRequest;
160
assert.strictEqual(listResourcesRequest2.method, 'resources/list');
161
assert.deepStrictEqual(listResourcesRequest2.params, { cursor: 'page2' });
162
163
// Send final page with no nextCursor
164
transport.simulateReceiveMessage({
165
jsonrpc: MCP.JSONRPC_VERSION,
166
id: listResourcesRequest2.id,
167
result: {
168
resources: [
169
{ uri: 'resource2', type: 'text/plain', name: 'Test Resource 2' }
170
]
171
}
172
});
173
174
// Verify the combined result
175
const resources = await requestPromise;
176
assert.strictEqual(resources.length, 2);
177
assert.strictEqual(resources[0].uri, 'resource1');
178
assert.strictEqual(resources[1].uri, 'resource2');
179
});
180
181
test('should handle error responses', async () => {
182
// Setup request
183
const requestPromise = handler.readResource({ uri: 'non-existent' });
184
185
// Get the sent message
186
const sentMessages = transport.getSentMessages();
187
const readResourceRequest = sentMessages[2] as MCP.JSONRPCRequest; // [0] is initialize
188
189
// Simulate error response
190
transport.simulateReceiveMessage({
191
jsonrpc: MCP.JSONRPC_VERSION,
192
id: readResourceRequest.id,
193
error: {
194
code: MCP.METHOD_NOT_FOUND,
195
message: 'Resource not found'
196
}
197
});
198
199
// Verify the error is thrown correctly
200
try {
201
await requestPromise;
202
assert.fail('Expected error was not thrown');
203
} catch (e: unknown) {
204
assert.strictEqual((e as Error).message, 'MPC -32601: Resource not found');
205
assert.strictEqual((e as { code: number }).code, MCP.METHOD_NOT_FOUND);
206
}
207
});
208
209
test('should handle server requests', async () => {
210
// Simulate ping request from server
211
const pingRequest: MCP.JSONRPCRequest & MCP.PingRequest = {
212
jsonrpc: MCP.JSONRPC_VERSION,
213
id: 100,
214
method: 'ping'
215
};
216
217
transport.simulateReceiveMessage(pingRequest);
218
219
// The handler should have sent a response
220
const sentMessages = transport.getSentMessages();
221
const pingResponse = sentMessages.find(m =>
222
'id' in m && m.id === pingRequest.id && 'result' in m
223
) as MCP.JSONRPCResultResponse;
224
225
assert.ok(pingResponse, 'No ping response was sent');
226
assert.deepStrictEqual(pingResponse.result, {});
227
});
228
229
test('should handle roots list requests', async () => {
230
// Set roots
231
handler.roots = [
232
{ uri: 'file:///test/root1', name: 'Root 1' },
233
{ uri: 'file:///test/root2', name: 'Root 2' }
234
];
235
236
// Simulate roots/list request from server
237
const rootsRequest: MCP.JSONRPCRequest & MCP.ListRootsRequest = {
238
jsonrpc: MCP.JSONRPC_VERSION,
239
id: 101,
240
method: 'roots/list'
241
};
242
243
transport.simulateReceiveMessage(rootsRequest);
244
245
// The handler should have sent a response
246
const sentMessages = transport.getSentMessages();
247
const rootsResponse = sentMessages.find(m =>
248
'id' in m && m.id === rootsRequest.id && 'result' in m
249
) as MCP.JSONRPCResultResponse;
250
251
assert.ok(rootsResponse, 'No roots/list response was sent');
252
assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots.length, 2);
253
assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots[0].uri, 'file:///test/root1');
254
});
255
256
test('should handle server notifications', async () => {
257
let progressNotificationReceived = false;
258
store.add(handler.onDidReceiveProgressNotification(notification => {
259
progressNotificationReceived = true;
260
assert.strictEqual(notification.method, 'notifications/progress');
261
assert.strictEqual(notification.params.progressToken, 'token1');
262
assert.strictEqual(notification.params.progress, 50);
263
}));
264
265
// Simulate progress notification with correct format
266
const progressNotification: MCP.JSONRPCNotification & MCP.ProgressNotification = {
267
jsonrpc: MCP.JSONRPC_VERSION,
268
method: 'notifications/progress',
269
params: {
270
progressToken: 'token1',
271
progress: 50,
272
total: 100
273
}
274
};
275
276
transport.simulateReceiveMessage(progressNotification);
277
assert.strictEqual(progressNotificationReceived, true);
278
});
279
280
test('should handle cancellation', async () => {
281
// Setup a new cancellation token source for this specific test
282
const testCts = store.add(new CancellationTokenSource());
283
const requestPromise = handler.listResources(undefined, testCts.token);
284
285
// Get the request ID
286
const sentMessages = transport.getSentMessages();
287
const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest;
288
const requestId = listResourcesRequest.id;
289
290
// Cancel the request
291
testCts.cancel();
292
293
// Check that a cancellation notification was sent
294
const cancelNotification = transport.getSentMessages().find(m =>
295
!('id' in m) &&
296
'method' in m &&
297
m.method === 'notifications/cancelled' &&
298
'params' in m &&
299
m.params && m.params.requestId === requestId
300
);
301
302
assert.ok(cancelNotification, 'No cancellation notification was sent');
303
304
// Verify the promise was cancelled
305
try {
306
await requestPromise;
307
assert.fail('Promise should have been cancelled');
308
} catch (e) {
309
assert.strictEqual(e.name, 'Canceled');
310
}
311
});
312
313
test('should handle cancelled notification from server', async () => {
314
// Setup request
315
const requestPromise = handler.listResources();
316
317
// Get the request ID
318
const sentMessages = transport.getSentMessages();
319
const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest;
320
const requestId = listResourcesRequest.id;
321
322
// Simulate cancelled notification from server
323
const cancelledNotification: MCP.JSONRPCNotification & MCP.CancelledNotification = {
324
jsonrpc: MCP.JSONRPC_VERSION,
325
method: 'notifications/cancelled',
326
params: {
327
requestId
328
}
329
};
330
331
transport.simulateReceiveMessage(cancelledNotification);
332
333
// Verify the promise was cancelled
334
try {
335
await requestPromise;
336
assert.fail('Promise should have been cancelled');
337
} catch (e) {
338
assert.strictEqual(e.name, 'Canceled');
339
}
340
});
341
342
test('should dispose properly and cancel pending requests', async () => {
343
// Setup multiple requests
344
const request1 = handler.listResources();
345
const request2 = handler.listTools();
346
347
// Dispose the handler
348
handler.dispose();
349
350
// Verify all promises were cancelled
351
try {
352
await request1;
353
assert.fail('Promise 1 should have been cancelled');
354
} catch (e) {
355
assert.strictEqual(e.name, 'Canceled');
356
}
357
358
try {
359
await request2;
360
assert.fail('Promise 2 should have been cancelled');
361
} catch (e) {
362
assert.strictEqual(e.name, 'Canceled');
363
}
364
});
365
366
test('should handle connection error by cancelling requests', async () => {
367
// Setup request
368
const requestPromise = handler.listResources();
369
370
// Simulate connection error
371
transport.setConnectionState({
372
state: McpConnectionState.Kind.Error,
373
message: 'Connection lost'
374
});
375
376
// Verify the promise was cancelled
377
try {
378
await requestPromise;
379
assert.fail('Promise should have been cancelled');
380
} catch (e) {
381
assert.strictEqual(e.name, 'Canceled');
382
}
383
});
384
});
385
386
suite.skip('Workbench - MCP - McpTask', () => { // TODO@connor4312 https://github.com/microsoft/vscode/issues/280126
387
const store = ensureNoDisposablesAreLeakedInTestSuite();
388
let clock: sinon.SinonFakeTimers;
389
390
setup(() => {
391
clock = sinon.useFakeTimers();
392
});
393
394
teardown(() => {
395
clock.restore();
396
});
397
398
function createTask(overrides: Partial<MCP.Task> = {}): MCP.Task {
399
return {
400
taskId: 'task1',
401
status: 'working',
402
createdAt: new Date().toISOString(),
403
lastUpdatedAt: new Date().toISOString(),
404
ttl: null,
405
...overrides
406
};
407
}
408
409
test('should resolve when task completes', async () => {
410
const mockHandler = upcastPartial<McpServerRequestHandler>({
411
getTask: sinon.stub().resolves(createTask({ status: 'completed' })),
412
getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] })
413
});
414
415
const task = store.add(new McpTask(createTask()));
416
task.setHandler(mockHandler);
417
418
// Advance time to trigger polling
419
await clock.tickAsync(2000);
420
421
// Update to completed state
422
task.onDidUpdateState(createTask({ status: 'completed' }));
423
424
const result = await task.result;
425
assert.deepStrictEqual(result, { content: [{ type: 'text', text: 'result' }] });
426
assert.ok((mockHandler.getTaskResult as sinon.SinonStub).calledWith({ taskId: 'task1' }));
427
});
428
429
test('should poll for task updates', async () => {
430
const getTaskStub = sinon.stub();
431
getTaskStub.onCall(0).resolves(createTask({ status: 'working' }));
432
getTaskStub.onCall(1).resolves(createTask({ status: 'working' }));
433
getTaskStub.onCall(2).resolves(createTask({ status: 'completed' }));
434
435
const mockHandler = upcastPartial<McpServerRequestHandler>({
436
getTask: getTaskStub,
437
getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] })
438
});
439
440
const task = store.add(new McpTask(createTask({ pollInterval: 1000 })));
441
task.setHandler(mockHandler);
442
443
// First poll
444
await clock.tickAsync(1000);
445
assert.strictEqual(getTaskStub.callCount, 1);
446
447
// Second poll
448
await clock.tickAsync(1000);
449
assert.strictEqual(getTaskStub.callCount, 2);
450
451
// Third poll - completes
452
await clock.tickAsync(1000);
453
assert.strictEqual(getTaskStub.callCount, 3);
454
455
const result = await task.result;
456
assert.deepStrictEqual(result, { content: [{ type: 'text', text: 'result' }] });
457
});
458
459
test('should use default poll interval if not specified', async () => {
460
const getTaskStub = sinon.stub();
461
getTaskStub.resolves(createTask({ status: 'working' }));
462
463
const mockHandler = upcastPartial<McpServerRequestHandler>({
464
getTask: getTaskStub,
465
});
466
467
const task = store.add(new McpTask(createTask()));
468
task.setHandler(mockHandler);
469
470
// Default poll interval is 2000ms
471
await clock.tickAsync(2000);
472
assert.strictEqual(getTaskStub.callCount, 1);
473
474
await clock.tickAsync(2000);
475
assert.strictEqual(getTaskStub.callCount, 2);
476
477
task.dispose();
478
});
479
480
test('should reject when task fails', async () => {
481
const mockHandler = upcastPartial<McpServerRequestHandler>({
482
getTask: sinon.stub().resolves(createTask({
483
status: 'failed',
484
statusMessage: 'Something went wrong'
485
}))
486
});
487
488
const task = store.add(new McpTask(createTask()));
489
task.setHandler(mockHandler);
490
491
// Update to failed state
492
task.onDidUpdateState(createTask({
493
status: 'failed',
494
statusMessage: 'Something went wrong'
495
}));
496
497
await assert.rejects(
498
task.result,
499
(error: Error) => {
500
assert.ok(error.message.includes('Task task1 failed'));
501
assert.ok(error.message.includes('Something went wrong'));
502
return true;
503
}
504
);
505
});
506
507
test('should cancel when task is cancelled', async () => {
508
const task = store.add(new McpTask(createTask()));
509
510
// Update to cancelled state
511
task.onDidUpdateState(createTask({ status: 'cancelled' }));
512
513
await assert.rejects(
514
task.result,
515
(error: Error) => {
516
assert.strictEqual(error.name, 'Canceled');
517
return true;
518
}
519
);
520
});
521
522
test('should cancel when cancellation token is triggered', async () => {
523
const cts = store.add(new CancellationTokenSource());
524
const task = store.add(new McpTask(createTask(), cts.token));
525
526
// Cancel the token
527
cts.cancel();
528
529
await assert.rejects(
530
task.result,
531
(error: Error) => {
532
assert.strictEqual(error.name, 'Canceled');
533
return true;
534
}
535
);
536
});
537
538
test('should handle TTL expiration', async () => {
539
const now = Date.now();
540
clock.setSystemTime(now);
541
542
const task = store.add(new McpTask(createTask({ ttl: 5000 })));
543
544
// Advance time past TTL
545
await clock.tickAsync(6000);
546
547
await assert.rejects(
548
task.result,
549
(error: Error) => {
550
assert.strictEqual(error.name, 'Canceled');
551
return true;
552
}
553
);
554
});
555
556
test('should stop polling when in terminal state', async () => {
557
const getTaskStub = sinon.stub();
558
getTaskStub.resolves(createTask({ status: 'completed' }));
559
560
const mockHandler = upcastPartial<McpServerRequestHandler>({
561
getTask: getTaskStub,
562
getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] })
563
});
564
565
const task = store.add(new McpTask(createTask({ pollInterval: 1000 })));
566
task.setHandler(mockHandler);
567
568
// Update to completed state immediately
569
task.onDidUpdateState(createTask({ status: 'completed' }));
570
571
await task.result;
572
573
// Advance time - should not poll anymore
574
const initialCallCount = getTaskStub.callCount;
575
await clock.tickAsync(5000);
576
assert.strictEqual(getTaskStub.callCount, initialCallCount);
577
});
578
579
test('should handle handler reconnection', async () => {
580
const getTaskStub1 = sinon.stub();
581
getTaskStub1.resolves(createTask({ status: 'working' }));
582
583
const mockHandler1 = upcastPartial<McpServerRequestHandler>({
584
getTask: getTaskStub1,
585
});
586
587
const task = store.add(new McpTask(createTask({ pollInterval: 1000 })));
588
task.setHandler(mockHandler1);
589
590
// First poll with handler1
591
await clock.tickAsync(1000);
592
assert.strictEqual(getTaskStub1.callCount, 1);
593
594
// Switch to a new handler
595
const getTaskStub2 = sinon.stub();
596
getTaskStub2.resolves(createTask({ status: 'completed' }));
597
598
const mockHandler2 = upcastPartial<McpServerRequestHandler>({
599
getTask: getTaskStub2,
600
getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] })
601
});
602
603
task.setHandler(mockHandler2);
604
605
// Second poll with handler2
606
await clock.tickAsync(1000);
607
assert.strictEqual(getTaskStub1.callCount, 1); // No more calls to old handler
608
assert.strictEqual(getTaskStub2.callCount, 1); // New handler is called
609
610
const result = await task.result;
611
assert.deepStrictEqual(result, { content: [{ type: 'text', text: 'result' }] });
612
});
613
614
test('should not poll when handler is undefined', async () => {
615
const task = store.add(new McpTask(createTask({ pollInterval: 1000 })));
616
617
// Advance time - should not crash
618
await clock.tickAsync(5000);
619
620
// Now set a handler and it should start polling
621
const getTaskStub = sinon.stub();
622
getTaskStub.resolves(createTask({ status: 'completed' }));
623
624
const mockHandler = upcastPartial<McpServerRequestHandler>({
625
getTask: getTaskStub,
626
getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] })
627
});
628
629
task.setHandler(mockHandler);
630
await clock.tickAsync(1000);
631
assert.strictEqual(getTaskStub.callCount, 1);
632
633
task.dispose();
634
});
635
636
test('should handle input_required state', async () => {
637
const getTaskStub = sinon.stub();
638
// getTask call returns completed (triggered by input_required handling)
639
getTaskStub.resolves(createTask({ status: 'completed' }));
640
641
const mockHandler = upcastPartial<McpServerRequestHandler>({
642
getTask: getTaskStub,
643
getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] })
644
});
645
646
const task = store.add(new McpTask(createTask({ pollInterval: 1000 })));
647
task.setHandler(mockHandler);
648
649
// Update to input_required - this triggers a getTask call
650
task.onDidUpdateState(createTask({ status: 'input_required' }));
651
652
// Allow the promise to settle
653
await clock.tickAsync(0);
654
655
// Verify getTask was called
656
assert.strictEqual(getTaskStub.callCount, 1);
657
658
// Once getTask resolves with completed, should fetch result
659
const result = await task.result;
660
assert.deepStrictEqual(result, { content: [{ type: 'text', text: 'result' }] });
661
});
662
663
test('should handle getTask returning cancelled during polling', async () => {
664
const getTaskStub = sinon.stub();
665
getTaskStub.resolves(createTask({ status: 'cancelled' }));
666
667
const mockHandler = upcastPartial<McpServerRequestHandler>({
668
getTask: getTaskStub,
669
});
670
671
const task = store.add(new McpTask(createTask({ pollInterval: 1000 })));
672
task.setHandler(mockHandler);
673
674
// Advance time to trigger polling
675
await clock.tickAsync(1000);
676
677
await assert.rejects(
678
task.result,
679
(error: Error) => {
680
assert.strictEqual(error.name, 'Canceled');
681
return true;
682
}
683
);
684
});
685
686
test('should return correct task id', () => {
687
const task = store.add(new McpTask(createTask({ taskId: 'my-task-id' })));
688
assert.strictEqual(task.id, 'my-task-id');
689
});
690
691
test('should dispose cleanly', async () => {
692
const getTaskStub = sinon.stub();
693
getTaskStub.resolves(createTask({ status: 'working' }));
694
695
const mockHandler = upcastPartial<McpServerRequestHandler>({
696
getTask: getTaskStub,
697
});
698
699
const task = store.add(new McpTask(createTask({ pollInterval: 1000 })));
700
task.setHandler(mockHandler);
701
702
// Poll once
703
await clock.tickAsync(1000);
704
const callCountBeforeDispose = getTaskStub.callCount;
705
706
// Dispose
707
task.dispose();
708
709
// Advance time - should not poll anymore
710
await clock.tickAsync(5000);
711
assert.strictEqual(getTaskStub.callCount, callCountBeforeDispose);
712
});
713
});
714
715