Path: blob/main/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.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 * as sinon from 'sinon';7import { timeout } from '../../../../../base/common/async.js';8import { ISettableObservable, observableValue } from '../../../../../base/common/observable.js';9import { upcast } from '../../../../../base/common/types.js';10import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';11import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';12import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';13import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';14import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';15import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';16import { ILogger, ILoggerService, ILogService, NullLogger, NullLogService } from '../../../../../platform/log/common/log.js';17import { mcpAccessConfig, McpAccessValue } from '../../../../../platform/mcp/common/mcpManagement.js';18import { IProductService } from '../../../../../platform/product/common/productService.js';19import { ISecretStorageService } from '../../../../../platform/secrets/common/secrets.js';20import { TestSecretStorageService } from '../../../../../platform/secrets/test/common/testSecretStorageService.js';21import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';22import { IConfigurationResolverService } from '../../../../services/configurationResolver/common/configurationResolver.js';23import { ConfigurationResolverExpression } from '../../../../services/configurationResolver/common/configurationResolverExpression.js';24import { IOutputService } from '../../../../services/output/common/output.js';25import { TestLoggerService, TestStorageService } from '../../../../test/common/workbenchTestServices.js';26import { McpRegistry } from '../../common/mcpRegistry.js';27import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js';28import { McpServerConnection } from '../../common/mcpServerConnection.js';29import { LazyCollectionState, McpCollectionDefinition, McpServerDefinition, McpServerTransportStdio, McpServerTransportType, McpServerTrust, McpStartServerInteraction } from '../../common/mcpTypes.js';30import { TestMcpMessageTransport } from './mcpRegistryTypes.js';3132class TestConfigurationResolverService implements Partial<IConfigurationResolverService> {33declare readonly _serviceBrand: undefined;3435private interactiveCounter = 0;3637// Used to simulate stored/resolved variables38private readonly resolvedVariables = new Map<string, string>();3940constructor() {41// Add some test variables42this.resolvedVariables.set('workspaceFolder', '/test/workspace');43this.resolvedVariables.set('fileBasename', 'test.txt');44}4546resolveAsync(folder: any, value: any): Promise<any> {47const parsed = ConfigurationResolverExpression.parse(value);48for (const variable of parsed.unresolved()) {49const resolved = this.resolvedVariables.get(variable.inner);50if (resolved) {51parsed.resolve(variable, resolved);52}53}5455return Promise.resolve(parsed.toObject());56}5758resolveWithInteraction(folder: any, config: any, section?: string, variables?: Record<string, string>, target?: ConfigurationTarget): Promise<Map<string, string> | undefined> {59const parsed = ConfigurationResolverExpression.parse(config);60// For testing, we simulate interaction by returning a map with some variables61const result = new Map<string, string>();62result.set('input:testInteractive', `interactiveValue${this.interactiveCounter++}`);63result.set('command:testCommand', `commandOutput${this.interactiveCounter++}}`);6465// If variables are provided, include those too66for (const [k, v] of result.entries()) {67parsed.resolve({ id: '${' + k + '}' } as any, v);68}6970return Promise.resolve(result);71}72}7374class TestMcpHostDelegate implements IMcpHostDelegate {75priority = 0;7677canStart(): boolean {78return true;79}8081start(): IMcpMessageTransport {82return new TestMcpMessageTransport();83}8485waitForInitialProviderPromises(): Promise<void> {86return Promise.resolve();87}88}8990class TestDialogService implements Partial<IDialogService> {91declare readonly _serviceBrand: undefined;9293private _promptResult: boolean | undefined;94private _promptSpy: sinon.SinonStub;9596constructor() {97this._promptSpy = sinon.stub();98this._promptSpy.callsFake(() => {99return Promise.resolve({ result: this._promptResult });100});101}102103setPromptResult(result: boolean | undefined): void {104this._promptResult = result;105}106107get promptSpy(): sinon.SinonStub {108return this._promptSpy;109}110111prompt(options: any): Promise<any> {112return this._promptSpy(options);113}114}115116class TestMcpRegistry extends McpRegistry {117public nextDefinitionIdsToTrust: string[] | undefined;118119protected override _promptForTrustOpenDialog(): Promise<string[] | undefined> {120return Promise.resolve(this.nextDefinitionIdsToTrust);121}122}123124suite('Workbench - MCP - Registry', () => {125const store = ensureNoDisposablesAreLeakedInTestSuite();126127let registry: TestMcpRegistry;128let testStorageService: TestStorageService;129let testConfigResolverService: TestConfigurationResolverService;130let testDialogService: TestDialogService;131let testCollection: McpCollectionDefinition & { serverDefinitions: ISettableObservable<McpServerDefinition[]> };132let baseDefinition: McpServerDefinition;133let configurationService: TestConfigurationService;134let logger: ILogger;135let trustNonceBearer: { trustedAtNonce: string | undefined };136137setup(() => {138testConfigResolverService = new TestConfigurationResolverService();139testStorageService = store.add(new TestStorageService());140testDialogService = new TestDialogService();141configurationService = new TestConfigurationService({ [mcpAccessConfig]: McpAccessValue.All });142trustNonceBearer = { trustedAtNonce: undefined };143144const services = new ServiceCollection(145[IConfigurationService, configurationService],146[IConfigurationResolverService, testConfigResolverService],147[IStorageService, testStorageService],148[ISecretStorageService, new TestSecretStorageService()],149[ILoggerService, store.add(new TestLoggerService())],150[ILogService, store.add(new NullLogService())],151[IOutputService, upcast({ showChannel: () => { } })],152[IDialogService, testDialogService],153[IProductService, {}],154);155156logger = new NullLogger();157158const instaService = store.add(new TestInstantiationService(services));159registry = store.add(instaService.createInstance(TestMcpRegistry));160161// Create test collection that can be reused162testCollection = {163id: 'test-collection',164label: 'Test Collection',165remoteAuthority: null,166serverDefinitions: observableValue('serverDefs', []),167trustBehavior: McpServerTrust.Kind.Trusted,168scope: StorageScope.APPLICATION,169configTarget: ConfigurationTarget.USER,170};171172// Create base definition that can be reused173baseDefinition = {174id: 'test-server',175label: 'Test Server',176cacheNonce: 'a',177launch: {178type: McpServerTransportType.Stdio,179command: 'test-command',180args: [],181env: {},182envFile: undefined,183cwd: '/test',184}185};186});187188test('registerCollection adds collection to registry', () => {189const disposable = registry.registerCollection(testCollection);190store.add(disposable);191192assert.strictEqual(registry.collections.get().length, 1);193assert.strictEqual(registry.collections.get()[0], testCollection);194195disposable.dispose();196assert.strictEqual(registry.collections.get().length, 0);197});198199test('collections are not visible when not enabled', () => {200const disposable = registry.registerCollection(testCollection);201store.add(disposable);202203assert.strictEqual(registry.collections.get().length, 1);204205configurationService.setUserConfiguration(mcpAccessConfig, McpAccessValue.None);206configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any);207208assert.strictEqual(registry.collections.get().length, 0);209210configurationService.setUserConfiguration(mcpAccessConfig, McpAccessValue.All);211configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true } as any);212});213214test('registerDelegate adds delegate to registry', () => {215const delegate = new TestMcpHostDelegate();216const disposable = registry.registerDelegate(delegate);217store.add(disposable);218219assert.strictEqual(registry.delegates.get().length, 1);220assert.strictEqual(registry.delegates.get()[0], delegate);221222disposable.dispose();223assert.strictEqual(registry.delegates.get().length, 0);224});225226test('resolveConnection creates connection with resolved variables and memorizes them until cleared', async () => {227const definition: McpServerDefinition = {228...baseDefinition,229launch: {230type: McpServerTransportType.Stdio,231command: '${workspaceFolder}/cmd',232args: ['--file', '${fileBasename}'],233env: {234PATH: '${input:testInteractive}'235},236envFile: undefined,237cwd: '/test',238},239variableReplacement: {240section: 'mcp',241target: ConfigurationTarget.WORKSPACE,242}243};244245const delegate = new TestMcpHostDelegate();246store.add(registry.registerDelegate(delegate));247testCollection.serverDefinitions.set([definition], undefined);248store.add(registry.registerCollection(testCollection));249250const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer }) as McpServerConnection;251252assert.ok(connection);253assert.strictEqual(connection.definition, definition);254assert.strictEqual((connection.launchDefinition as any).command, '/test/workspace/cmd');255assert.strictEqual((connection.launchDefinition as any).env.PATH, 'interactiveValue0');256connection.dispose();257258const connection2 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer }) as McpServerConnection;259260assert.ok(connection2);261assert.strictEqual((connection2.launchDefinition as any).env.PATH, 'interactiveValue0');262connection2.dispose();263264registry.clearSavedInputs(StorageScope.WORKSPACE);265266const connection3 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer }) as McpServerConnection;267268assert.ok(connection3);269assert.strictEqual((connection3.launchDefinition as any).env.PATH, 'interactiveValue4');270connection3.dispose();271});272273test('resolveConnection uses user-provided launch configuration', async () => {274// Create a collection with custom launch resolver275const customCollection: McpCollectionDefinition = {276...testCollection,277resolveServerLanch: async (def) => {278return {279...(def.launch as McpServerTransportStdio),280env: { CUSTOM_ENV: 'value' },281};282}283};284285// Create a definition with variable replacement286const definition: McpServerDefinition = {287...baseDefinition,288variableReplacement: {289section: 'mcp',290target: ConfigurationTarget.WORKSPACE,291}292};293294const delegate = new TestMcpHostDelegate();295store.add(registry.registerDelegate(delegate));296testCollection.serverDefinitions.set([definition], undefined);297store.add(registry.registerCollection(customCollection));298299// Resolve connection should use the custom launch configuration300const connection = await registry.resolveConnection({301collectionRef: customCollection,302definitionRef: definition,303logger,304trustNonceBearer,305}) as McpServerConnection;306307assert.ok(connection);308309// Verify the launch configuration passed to _replaceVariablesInLaunch was the custom one310assert.deepStrictEqual((connection.launchDefinition as McpServerTransportStdio).env, { CUSTOM_ENV: 'value' });311312connection.dispose();313});314315suite('Lazy Collections', () => {316let lazyCollection: McpCollectionDefinition;317let normalCollection: McpCollectionDefinition;318let removedCalled: boolean;319320setup(() => {321removedCalled = false;322lazyCollection = {323...testCollection,324id: 'lazy-collection',325lazy: {326isCached: false,327load: () => Promise.resolve(),328removed: () => { removedCalled = true; }329}330};331normalCollection = {332...testCollection,333id: 'lazy-collection',334serverDefinitions: observableValue('serverDefs', [baseDefinition])335};336});337338test('registers lazy collection', () => {339const disposable = registry.registerCollection(lazyCollection);340store.add(disposable);341342assert.strictEqual(registry.collections.get().length, 1);343assert.strictEqual(registry.collections.get()[0], lazyCollection);344assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.HasUnknown);345});346347test('lazy collection is replaced by normal collection', () => {348store.add(registry.registerCollection(lazyCollection));349store.add(registry.registerCollection(normalCollection));350351const collections = registry.collections.get();352assert.strictEqual(collections.length, 1);353assert.strictEqual(collections[0], normalCollection);354assert.strictEqual(collections[0].lazy, undefined);355assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.AllKnown);356});357358test('lazyCollectionState updates correctly during loading', async () => {359lazyCollection = {360...lazyCollection,361lazy: {362...lazyCollection.lazy!,363load: async () => {364await timeout(0);365store.add(registry.registerCollection(normalCollection));366return Promise.resolve();367}368}369};370371store.add(registry.registerCollection(lazyCollection));372assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.HasUnknown);373374const loadingPromise = registry.discoverCollections();375assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.LoadingUnknown);376377await loadingPromise;378379// The collection wasn't replaced, so it should be removed380assert.strictEqual(registry.collections.get().length, 1);381assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.AllKnown);382assert.strictEqual(removedCalled, false);383});384385test('removed callback is called when lazy collection is not replaced', async () => {386store.add(registry.registerCollection(lazyCollection));387await registry.discoverCollections();388389assert.strictEqual(removedCalled, true);390});391392test('cached lazy collections are tracked correctly', () => {393lazyCollection.lazy!.isCached = true;394store.add(registry.registerCollection(lazyCollection));395396assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.AllKnown);397398// Adding an uncached lazy collection changes the state399const uncachedLazy = {400...lazyCollection,401id: 'uncached-lazy',402lazy: {403...lazyCollection.lazy!,404isCached: false405}406};407store.add(registry.registerCollection(uncachedLazy));408409assert.strictEqual(registry.lazyCollectionState.get().state, LazyCollectionState.HasUnknown);410});411});412413suite('Trust Flow', () => {414/**415* Helper to create a test MCP collection with a specific trust behavior416*/417function createTestCollection(trustBehavior: McpServerTrust.Kind.Trusted | McpServerTrust.Kind.TrustedOnNonce, id = 'test-collection'): McpCollectionDefinition & { serverDefinitions: ISettableObservable<McpServerDefinition[]> } {418return {419id,420label: 'Test Collection',421remoteAuthority: null,422serverDefinitions: observableValue('serverDefs', []),423trustBehavior,424scope: StorageScope.APPLICATION,425configTarget: ConfigurationTarget.USER,426};427}428429/**430* Helper to create a test server definition with a specific cache nonce431*/432function createTestDefinition(id = 'test-server', cacheNonce = 'nonce-a'): McpServerDefinition {433return {434id,435label: 'Test Server',436cacheNonce,437launch: {438type: McpServerTransportType.Stdio,439command: 'test-command',440args: [],441env: {},442envFile: undefined,443cwd: '/test',444}445};446}447448/**449* Helper to set up a basic registry with delegate and collection450*/451function setupRegistry(trustBehavior: McpServerTrust.Kind.Trusted | McpServerTrust.Kind.TrustedOnNonce = McpServerTrust.Kind.TrustedOnNonce, cacheNonce = 'nonce-a') {452const delegate = new TestMcpHostDelegate();453store.add(registry.registerDelegate(delegate));454455const collection = createTestCollection(trustBehavior);456const definition = createTestDefinition('test-server', cacheNonce);457collection.serverDefinitions.set([definition], undefined);458store.add(registry.registerCollection(collection));459460return { collection, definition, delegate };461}462463test('trusted collection allows connection without prompting', async () => {464const { collection, definition } = setupRegistry(McpServerTrust.Kind.Trusted);465466const connection = await registry.resolveConnection({467collectionRef: collection,468definitionRef: definition,469logger,470trustNonceBearer,471});472473assert.ok(connection, 'Connection should be created for trusted collection');474assert.strictEqual(registry.nextDefinitionIdsToTrust, undefined, 'Trust dialog should not have been called');475connection!.dispose();476});477478test('nonce-based trust allows connection when nonce matches', async () => {479const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-a');480trustNonceBearer.trustedAtNonce = 'nonce-a';481482const connection = await registry.resolveConnection({483collectionRef: collection,484definitionRef: definition,485logger,486trustNonceBearer,487});488489assert.ok(connection, 'Connection should be created when nonce matches');490assert.strictEqual(registry.nextDefinitionIdsToTrust, undefined, 'Trust dialog should not have been called');491connection!.dispose();492});493494test('nonce-based trust prompts when nonce changes', async () => {495const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');496trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce497registry.nextDefinitionIdsToTrust = [definition.id]; // User trusts the server498499const connection = await registry.resolveConnection({500collectionRef: collection,501definitionRef: definition,502logger,503trustNonceBearer,504});505506assert.ok(connection, 'Connection should be created when user trusts');507assert.strictEqual(trustNonceBearer.trustedAtNonce, 'nonce-b', 'Nonce should be updated');508connection!.dispose();509});510511test('nonce-based trust denies connection when user rejects', async () => {512const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');513trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce514registry.nextDefinitionIdsToTrust = []; // User does not trust the server515516const connection = await registry.resolveConnection({517collectionRef: collection,518definitionRef: definition,519logger,520trustNonceBearer,521});522523assert.strictEqual(connection, undefined, 'Connection should not be created when user rejects');524assert.strictEqual(trustNonceBearer.trustedAtNonce, '__vscode_not_trusted', 'Should mark as explicitly not trusted');525});526527test('autoTrustChanges bypasses prompt when nonce changes', async () => {528const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');529trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce530531const connection = await registry.resolveConnection({532collectionRef: collection,533definitionRef: definition,534logger,535trustNonceBearer,536autoTrustChanges: true,537});538539assert.ok(connection, 'Connection should be created with autoTrustChanges');540assert.strictEqual(trustNonceBearer.trustedAtNonce, 'nonce-b', 'Nonce should be updated');541assert.strictEqual(registry.nextDefinitionIdsToTrust, undefined, 'Trust dialog should not have been called');542connection!.dispose();543});544545test('promptType "never" skips prompt and fails silently', async () => {546const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');547trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce548549const connection = await registry.resolveConnection({550collectionRef: collection,551definitionRef: definition,552logger,553trustNonceBearer,554promptType: 'never',555});556557assert.strictEqual(connection, undefined, 'Connection should not be created with promptType "never"');558assert.strictEqual(registry.nextDefinitionIdsToTrust, undefined, 'Trust dialog should not have been called');559});560561test('promptType "only-new" skips previously untrusted servers', async () => {562const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');563trustNonceBearer.trustedAtNonce = '__vscode_not_trusted'; // Previously explicitly denied564565const connection = await registry.resolveConnection({566collectionRef: collection,567definitionRef: definition,568logger,569trustNonceBearer,570promptType: 'only-new',571});572573assert.strictEqual(connection, undefined, 'Connection should not be created for previously untrusted server');574assert.strictEqual(registry.nextDefinitionIdsToTrust, undefined, 'Trust dialog should not have been called');575});576577test('promptType "all-untrusted" prompts for previously untrusted servers', async () => {578const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');579trustNonceBearer.trustedAtNonce = '__vscode_not_trusted'; // Previously explicitly denied580registry.nextDefinitionIdsToTrust = [definition.id]; // User now trusts the server581582const connection = await registry.resolveConnection({583collectionRef: collection,584definitionRef: definition,585logger,586trustNonceBearer,587promptType: 'all-untrusted',588});589590assert.ok(connection, 'Connection should be created when user trusts previously untrusted server');591assert.strictEqual(trustNonceBearer.trustedAtNonce, 'nonce-b', 'Nonce should be updated');592connection!.dispose();593});594595test('concurrent resolveConnection calls with same interaction are grouped', async () => {596const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');597trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce598599// Create a second definition that also needs trust600const definition2 = createTestDefinition('test-server-2', 'nonce-c');601collection.serverDefinitions.set([definition, definition2], undefined);602603// Create shared interaction604const interaction = new McpStartServerInteraction();605606// Manually set participants as mentioned in the requirements607interaction.participants.set(definition.id, { s: 'unknown' });608interaction.participants.set(definition2.id, { s: 'unknown' });609610const trustNonceBearer2 = { trustedAtNonce: 'nonce-b' }; // Different nonce for second server611612// Trust both servers613registry.nextDefinitionIdsToTrust = [definition.id, definition2.id];614615// Start both connections concurrently with the same interaction616const [connection1, connection2] = await Promise.all([617registry.resolveConnection({618collectionRef: collection,619definitionRef: definition,620logger,621trustNonceBearer,622interaction,623}),624registry.resolveConnection({625collectionRef: collection,626definitionRef: definition2,627logger,628trustNonceBearer: trustNonceBearer2,629interaction,630})631]);632633assert.ok(connection1, 'First connection should be created');634assert.ok(connection2, 'Second connection should be created');635assert.strictEqual(trustNonceBearer.trustedAtNonce, 'nonce-b', 'First nonce should be updated');636assert.strictEqual(trustNonceBearer2.trustedAtNonce, 'nonce-c', 'Second nonce should be updated');637638connection1!.dispose();639connection2!.dispose();640});641642test('user cancelling trust dialog returns undefined for all pending connections', async () => {643const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');644trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce645646// Create a second definition that also needs trust647const definition2 = createTestDefinition('test-server-2', 'nonce-c');648collection.serverDefinitions.set([definition, definition2], undefined);649650// Create shared interaction651const interaction = new McpStartServerInteraction();652653// Manually set participants as mentioned in the requirements654interaction.participants.set(definition.id, { s: 'unknown' });655interaction.participants.set(definition2.id, { s: 'unknown' });656657const trustNonceBearer2 = { trustedAtNonce: 'nonce-b' }; // Different nonce for second server658659// User cancels the dialog660registry.nextDefinitionIdsToTrust = undefined;661662// Start both connections concurrently with the same interaction663const [connection1, connection2] = await Promise.all([664registry.resolveConnection({665collectionRef: collection,666definitionRef: definition,667logger,668trustNonceBearer,669interaction,670}),671registry.resolveConnection({672collectionRef: collection,673definitionRef: definition2,674logger,675trustNonceBearer: trustNonceBearer2,676interaction,677})678]);679680assert.strictEqual(connection1, undefined, 'First connection should not be created when user cancels');681assert.strictEqual(connection2, undefined, 'Second connection should not be created when user cancels');682});683684test('partial trust selection in grouped interaction', async () => {685const { collection, definition } = setupRegistry(McpServerTrust.Kind.TrustedOnNonce, 'nonce-b');686trustNonceBearer.trustedAtNonce = 'nonce-a'; // Different nonce687688// Create a second definition that also needs trust689const definition2 = createTestDefinition('test-server-2', 'nonce-c');690collection.serverDefinitions.set([definition, definition2], undefined);691692// Create shared interaction693const interaction = new McpStartServerInteraction();694695// Manually set participants as mentioned in the requirements696interaction.participants.set(definition.id, { s: 'unknown' });697interaction.participants.set(definition2.id, { s: 'unknown' });698699const trustNonceBearer2 = { trustedAtNonce: 'nonce-b' }; // Different nonce for second server700701// User trusts only the first server702registry.nextDefinitionIdsToTrust = [definition.id];703704// Start both connections concurrently with the same interaction705const [connection1, connection2] = await Promise.all([706registry.resolveConnection({707collectionRef: collection,708definitionRef: definition,709logger,710trustNonceBearer,711interaction,712}),713registry.resolveConnection({714collectionRef: collection,715definitionRef: definition2,716logger,717trustNonceBearer: trustNonceBearer2,718interaction,719})720]);721722assert.ok(connection1, 'First connection should be created when trusted');723assert.strictEqual(connection2, undefined, 'Second connection should not be created when not trusted');724assert.strictEqual(trustNonceBearer.trustedAtNonce, 'nonce-b', 'First nonce should be updated');725assert.strictEqual(trustNonceBearer2.trustedAtNonce, '__vscode_not_trusted', 'Second nonce should be marked as not trusted');726727connection1!.dispose();728});729});730731});732733734