Path: blob/main/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as assert from 'assert';6import { timeout } from '../../../../../base/common/async.js';7import { Disposable } from '../../../../../base/common/lifecycle.js';8import { autorun, observableValue } from '../../../../../base/common/observable.js';9import { upcast } from '../../../../../base/common/types.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';11import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';12import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';13import { ILogger, ILoggerService, LogLevel, NullLogger } from '../../../../../platform/log/common/log.js';14import { IProductService } from '../../../../../platform/product/common/productService.js';15import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';16import { IOutputService } from '../../../../services/output/common/output.js';17import { TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';18import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js';19import { McpServerConnection } from '../../common/mcpServerConnection.js';20import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerTransportType, McpServerTrust } from '../../common/mcpTypes.js';21import { TestMcpMessageTransport } from './mcpRegistryTypes.js';22import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js';23import { Event } from '../../../../../base/common/event.js';2425class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate {26private readonly _transport: TestMcpMessageTransport;27private _canStartValue = true;2829priority = 0;3031constructor() {32super();33this._transport = this._register(new TestMcpMessageTransport());34}3536canStart(): boolean {37return this._canStartValue;38}3940start(): IMcpMessageTransport {41if (!this._canStartValue) {42throw new Error('Cannot start server');43}44return this._transport;45}4647getTransport(): TestMcpMessageTransport {48return this._transport;49}5051setCanStart(value: boolean): void {52this._canStartValue = value;53}5455waitForInitialProviderPromises(): Promise<void> {56return Promise.resolve();57}58}5960suite('Workbench - MCP - ServerConnection', () => {61const store = ensureNoDisposablesAreLeakedInTestSuite();6263let instantiationService: TestInstantiationService;64let delegate: TestMcpHostDelegate;65let transport: TestMcpMessageTransport;66let collection: McpCollectionDefinition;67let serverDefinition: McpServerDefinition;6869setup(() => {70delegate = store.add(new TestMcpHostDelegate());71transport = delegate.getTransport();7273// Setup test services74const services = new ServiceCollection(75[ILoggerService, store.add(new TestLoggerService())],76[IOutputService, upcast({ showChannel: () => { } })],77[IStorageService, store.add(new TestStorageService())],78[IProductService, TestProductService],79);8081instantiationService = store.add(new TestInstantiationService(services));8283// Create test collection84collection = {85id: 'test-collection',86label: 'Test Collection',87remoteAuthority: null,88serverDefinitions: observableValue('serverDefs', []),89trustBehavior: McpServerTrust.Kind.Trusted,90scope: StorageScope.APPLICATION,91configTarget: ConfigurationTarget.USER,92};9394// Create server definition95serverDefinition = {96id: 'test-server',97label: 'Test Server',98cacheNonce: 'a',99launch: {100type: McpServerTransportType.Stdio,101command: 'test-command',102args: [],103env: {},104envFile: undefined,105cwd: '/test'106}107};108});109110function waitForHandler(cnx: McpServerConnection) {111const handler = cnx.handler.get();112if (handler) {113return Promise.resolve(handler);114}115116return new Promise(resolve => {117const disposable = autorun(reader => {118const handler = cnx.handler.read(reader);119if (handler) {120disposable.dispose();121resolve(handler);122}123});124});125}126127test('should start and set state to Running when transport succeeds', async () => {128// Create server connection129const connection = instantiationService.createInstance(130McpServerConnection,131collection,132serverDefinition,133delegate,134serverDefinition.launch,135new NullLogger(),136);137store.add(connection);138139// Start the connection140const startPromise = connection.start({});141142// Simulate successful connection143transport.setConnectionState({ state: McpConnectionState.Kind.Running });144145const state = await startPromise;146assert.strictEqual(state.state, McpConnectionState.Kind.Running);147148transport.simulateInitialized();149assert.ok(await waitForHandler(connection));150});151152test('should handle errors during start', async () => {153// Setup delegate to fail on start154delegate.setCanStart(false);155156// Create server connection157const connection = instantiationService.createInstance(158McpServerConnection,159collection,160serverDefinition,161delegate,162serverDefinition.launch,163new NullLogger(),164);165store.add(connection);166167// Start the connection168const state = await connection.start({});169170assert.strictEqual(state.state, McpConnectionState.Kind.Error);171assert.ok(state.message);172});173174test('should handle transport errors', async () => {175// Create server connection176const connection = instantiationService.createInstance(177McpServerConnection,178collection,179serverDefinition,180delegate,181serverDefinition.launch,182new NullLogger(),183);184store.add(connection);185186// Start the connection187const startPromise = connection.start({});188189// Simulate error in transport190transport.setConnectionState({191state: McpConnectionState.Kind.Error,192message: 'Test error message'193});194195const state = await startPromise;196assert.strictEqual(state.state, McpConnectionState.Kind.Error);197assert.strictEqual(state.message, 'Test error message');198});199200test('should stop and set state to Stopped', async () => {201// Create server connection202const connection = instantiationService.createInstance(203McpServerConnection,204collection,205serverDefinition,206delegate,207serverDefinition.launch,208new NullLogger(),209);210store.add(connection);211212// Start the connection213const startPromise = connection.start({});214transport.setConnectionState({ state: McpConnectionState.Kind.Running });215await startPromise;216217// Stop the connection218const stopPromise = connection.stop();219await stopPromise;220221assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped);222});223224test('should not restart if already starting', async () => {225// Create server connection226const connection = instantiationService.createInstance(227McpServerConnection,228collection,229serverDefinition,230delegate,231serverDefinition.launch,232new NullLogger(),233);234store.add(connection);235236// Start the connection237const startPromise1 = connection.start({});238239// Try to start again while starting240const startPromise2 = connection.start({});241242// Simulate successful connection243transport.setConnectionState({ state: McpConnectionState.Kind.Running });244245const state1 = await startPromise1;246const state2 = await startPromise2;247248// Both promises should resolve to the same state249assert.strictEqual(state1.state, McpConnectionState.Kind.Running);250assert.strictEqual(state2.state, McpConnectionState.Kind.Running);251252transport.simulateInitialized();253assert.ok(await waitForHandler(connection));254255connection.dispose();256});257258test('should clean up when disposed', async () => {259// Create server connection260const connection = instantiationService.createInstance(261McpServerConnection,262collection,263serverDefinition,264delegate,265serverDefinition.launch,266new NullLogger(),267);268269// Start the connection270const startPromise = connection.start({});271transport.setConnectionState({ state: McpConnectionState.Kind.Running });272await startPromise;273274// Dispose the connection275connection.dispose();276277assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped);278});279280test('should log transport messages', async () => {281// Track logged messages282const loggedMessages: string[] = [];283284// Create server connection285const connection = instantiationService.createInstance(286McpServerConnection,287collection,288serverDefinition,289delegate,290serverDefinition.launch,291{292onDidChangeLogLevel: Event.None,293getLevel: () => LogLevel.Debug,294info: (message: string) => {295loggedMessages.push(message);296},297error: () => { },298dispose: () => { }299} as Partial<ILogger> as ILogger,300);301store.add(connection);302303// Start the connection304const startPromise = connection.start({});305306// Simulate log message from transport307transport.simulateLog('Test log message');308309// Set connection to running310transport.setConnectionState({ state: McpConnectionState.Kind.Running });311await startPromise;312313// Check that the message was logged314assert.ok(loggedMessages.some(msg => msg === 'Test log message'));315316connection.dispose();317await timeout(10);318});319320test('should correctly handle transitions to and from error state', async () => {321// Create server connection322const connection = instantiationService.createInstance(323McpServerConnection,324collection,325serverDefinition,326delegate,327serverDefinition.launch,328new NullLogger(),329);330store.add(connection);331332// Start the connection333const startPromise = connection.start({});334335// Transition to error state336const errorState: McpConnectionState = {337state: McpConnectionState.Kind.Error,338message: 'Temporary error'339};340transport.setConnectionState(errorState);341342let state = await startPromise;343assert.equal(state, errorState);344345346transport.setConnectionState({ state: McpConnectionState.Kind.Stopped });347348// Transition back to running state349const startPromise2 = connection.start({});350transport.setConnectionState({ state: McpConnectionState.Kind.Running });351state = await startPromise2;352assert.deepStrictEqual(state, { state: McpConnectionState.Kind.Running });353354connection.dispose();355await timeout(10);356});357358test('should handle multiple start/stop cycles', async () => {359// Create server connection360const connection = instantiationService.createInstance(361McpServerConnection,362collection,363serverDefinition,364delegate,365serverDefinition.launch,366new NullLogger(),367);368store.add(connection);369370// First cycle371let startPromise = connection.start({});372transport.setConnectionState({ state: McpConnectionState.Kind.Running });373await startPromise;374375await connection.stop();376assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Stopped });377378// Second cycle379startPromise = connection.start({});380transport.setConnectionState({ state: McpConnectionState.Kind.Running });381await startPromise;382383assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Running });384385await connection.stop();386387assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Stopped });388389connection.dispose();390await timeout(10);391});392});393394395