Path: blob/main/src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts
13399 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 assert from 'assert';6import { URI } from '../../../../base/common/uri.js';7import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';8import { McpServerType } from '../../../mcp/common/mcpPlatformTypes.js';9import {10parseComponentPathConfig,11resolveComponentDirs,12normalizeMcpServerConfiguration,13shellQuotePluginRootInCommand,14convertBareEnvVarsToVsCodeSyntax,15} from '../../common/pluginParsers.js';1617suite('pluginParsers', () => {1819ensureNoDisposablesAreLeakedInTestSuite();2021// ---- parseComponentPathConfig ---------------------------------------2223suite('parseComponentPathConfig', () => {2425test('returns empty config for undefined', () => {26const result = parseComponentPathConfig(undefined);27assert.deepStrictEqual(result, { paths: [], exclusive: false });28});2930test('returns empty config for null', () => {31const result = parseComponentPathConfig(null);32assert.deepStrictEqual(result, { paths: [], exclusive: false });33});3435test('parses a string to single-element paths', () => {36const result = parseComponentPathConfig('custom/skills');37assert.deepStrictEqual(result, { paths: ['custom/skills'], exclusive: false });38});3940test('trims whitespace from string', () => {41const result = parseComponentPathConfig(' spaced ');42assert.deepStrictEqual(result, { paths: ['spaced'], exclusive: false });43});4445test('returns empty for blank string', () => {46const result = parseComponentPathConfig(' ');47assert.deepStrictEqual(result, { paths: [], exclusive: false });48});4950test('parses a string array', () => {51const result = parseComponentPathConfig(['a', 'b', 'c']);52assert.deepStrictEqual(result, { paths: ['a', 'b', 'c'], exclusive: false });53});5455test('filters non-string entries from arrays', () => {56const result = parseComponentPathConfig(['valid', 42, null, 'ok']);57assert.deepStrictEqual(result, { paths: ['valid', 'ok'], exclusive: false });58});5960test('parses object with paths and exclusive', () => {61const result = parseComponentPathConfig({ paths: ['x', 'y'], exclusive: true });62assert.deepStrictEqual(result, { paths: ['x', 'y'], exclusive: true });63});6465test('object without exclusive defaults to false', () => {66const result = parseComponentPathConfig({ paths: ['z'] });67assert.deepStrictEqual(result, { paths: ['z'], exclusive: false });68});6970test('returns empty for unrecognized types', () => {71const result = parseComponentPathConfig(42);72assert.deepStrictEqual(result, { paths: [], exclusive: false });73});74});7576// ---- resolveComponentDirs -------------------------------------------7778suite('resolveComponentDirs', () => {7980const pluginUri = URI.file('/workspace/.plugin-root');8182test('includes default directory when not exclusive', () => {83const dirs = resolveComponentDirs(pluginUri, 'skills', { paths: [], exclusive: false });84assert.strictEqual(dirs.length, 1);85assert.ok(dirs[0].path.endsWith('/skills'));86});8788test('excludes default directory when exclusive', () => {89const dirs = resolveComponentDirs(pluginUri, 'skills', { paths: ['custom'], exclusive: true });90assert.ok(!dirs.some(d => d.path.endsWith('/skills')));91assert.ok(dirs.some(d => d.path.endsWith('/custom')));92});9394test('resolves relative paths from plugin root', () => {95const dirs = resolveComponentDirs(pluginUri, 'skills', { paths: ['other/skills'], exclusive: false });96assert.strictEqual(dirs.length, 2);97assert.ok(dirs[1].path.endsWith('/other/skills'));98});99100test('rejects paths that escape plugin root', () => {101const dirs = resolveComponentDirs(pluginUri, 'skills', { paths: ['../../outside'], exclusive: false });102// Should only have the default dir, the traversal path is rejected103assert.strictEqual(dirs.length, 1);104});105});106107// ---- normalizeMcpServerConfiguration --------------------------------108109suite('normalizeMcpServerConfiguration', () => {110111test('returns undefined for non-object input', () => {112assert.strictEqual(normalizeMcpServerConfiguration(null), undefined);113assert.strictEqual(normalizeMcpServerConfiguration('string'), undefined);114assert.strictEqual(normalizeMcpServerConfiguration(42), undefined);115});116117test('parses local server with command', () => {118const result = normalizeMcpServerConfiguration({119type: 'stdio',120command: 'node',121args: ['server.js'],122env: { KEY: 'value' },123cwd: '/workspace',124});125assert.ok(result);126assert.strictEqual(result!.type, McpServerType.LOCAL);127assert.strictEqual((result as { command: string }).command, 'node');128});129130test('infers local type from command without explicit type', () => {131const result = normalizeMcpServerConfiguration({ command: 'python' });132assert.ok(result);133assert.strictEqual(result!.type, McpServerType.LOCAL);134});135136test('parses remote server with url', () => {137const result = normalizeMcpServerConfiguration({138type: 'sse',139url: 'https://example.com',140headers: { 'X-Key': 'val' },141});142assert.ok(result);143assert.strictEqual(result!.type, McpServerType.REMOTE);144});145146test('infers remote type from url without explicit type', () => {147const result = normalizeMcpServerConfiguration({ url: 'https://example.com' });148assert.ok(result);149assert.strictEqual(result!.type, McpServerType.REMOTE);150});151152test('rejects ws type', () => {153const result = normalizeMcpServerConfiguration({ type: 'ws', url: 'ws://localhost:3000' });154assert.strictEqual(result, undefined);155});156157test('rejects local type without command', () => {158const result = normalizeMcpServerConfiguration({ type: 'stdio' });159assert.strictEqual(result, undefined);160});161162test('filters non-string args', () => {163const result = normalizeMcpServerConfiguration({164command: 'test',165args: ['valid', 42, null, 'also-valid'],166});167assert.ok(result);168const args = (result as { args?: string[] }).args;169assert.deepStrictEqual(args, ['valid', 'also-valid']);170});171});172173// ---- shellQuotePluginRootInCommand -----------------------------------174175suite('shellQuotePluginRootInCommand', () => {176177test('replaces token with path when no special chars', () => {178const result = shellQuotePluginRootInCommand(179'cd ${PLUGIN_ROOT} && run',180'/simple/path',181'${PLUGIN_ROOT}'182);183assert.strictEqual(result, 'cd /simple/path && run');184});185186test('quotes path with spaces', () => {187const result = shellQuotePluginRootInCommand(188'cd ${PLUGIN_ROOT} && run',189'/path with spaces',190'${PLUGIN_ROOT}'191);192assert.ok(result.includes('"'), 'should add quotes for path with spaces');193assert.ok(result.includes('/path with spaces'));194});195196test('returns unchanged when token not present', () => {197const result = shellQuotePluginRootInCommand('echo hello', '/path', '${PLUGIN_ROOT}');198assert.strictEqual(result, 'echo hello');199});200201test('handles already-quoted token', () => {202const result = shellQuotePluginRootInCommand(203'"${PLUGIN_ROOT}/script.sh"',204'/path with spaces',205'${PLUGIN_ROOT}'206);207assert.ok(!result.includes('""'), 'should not double-quote');208});209});210211// ---- convertBareEnvVarsToVsCodeSyntax -------------------------------212213suite('convertBareEnvVarsToVsCodeSyntax', () => {214215test('converts bare env vars to VS Code syntax', () => {216const def = {217name: 'test',218uri: URI.file('/plugin'),219configuration: {220type: McpServerType.LOCAL as const,221command: '${MY_TOOL}',222args: ['--key=${API_KEY}'],223},224};225const result = convertBareEnvVarsToVsCodeSyntax(def);226assert.strictEqual((result.configuration as { command: string }).command, '${env:MY_TOOL}');227assert.deepStrictEqual((result.configuration as unknown as { args: string[] }).args, ['--key=${env:API_KEY}']);228});229230test('does not convert already-qualified vars', () => {231const def = {232name: 'test',233uri: URI.file('/plugin'),234configuration: {235type: McpServerType.LOCAL as const,236command: '${env:ALREADY_QUALIFIED}',237},238};239const result = convertBareEnvVarsToVsCodeSyntax(def);240assert.strictEqual((result.configuration as { command: string }).command, '${env:ALREADY_QUALIFIED}');241});242243test('ignores lowercase vars', () => {244const def = {245name: 'test',246uri: URI.file('/plugin'),247configuration: {248type: McpServerType.LOCAL as const,249command: '${lowercase}',250},251};252const result = convertBareEnvVarsToVsCodeSyntax(def);253assert.strictEqual((result.configuration as { command: string }).command, '${lowercase}');254});255});256});257258259