Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentPlugins/test/common/pluginParsers.test.ts
13399 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 assert from 'assert';
7
import { URI } from '../../../../base/common/uri.js';
8
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
9
import { McpServerType } from '../../../mcp/common/mcpPlatformTypes.js';
10
import {
11
parseComponentPathConfig,
12
resolveComponentDirs,
13
normalizeMcpServerConfiguration,
14
shellQuotePluginRootInCommand,
15
convertBareEnvVarsToVsCodeSyntax,
16
} from '../../common/pluginParsers.js';
17
18
suite('pluginParsers', () => {
19
20
ensureNoDisposablesAreLeakedInTestSuite();
21
22
// ---- parseComponentPathConfig ---------------------------------------
23
24
suite('parseComponentPathConfig', () => {
25
26
test('returns empty config for undefined', () => {
27
const result = parseComponentPathConfig(undefined);
28
assert.deepStrictEqual(result, { paths: [], exclusive: false });
29
});
30
31
test('returns empty config for null', () => {
32
const result = parseComponentPathConfig(null);
33
assert.deepStrictEqual(result, { paths: [], exclusive: false });
34
});
35
36
test('parses a string to single-element paths', () => {
37
const result = parseComponentPathConfig('custom/skills');
38
assert.deepStrictEqual(result, { paths: ['custom/skills'], exclusive: false });
39
});
40
41
test('trims whitespace from string', () => {
42
const result = parseComponentPathConfig(' spaced ');
43
assert.deepStrictEqual(result, { paths: ['spaced'], exclusive: false });
44
});
45
46
test('returns empty for blank string', () => {
47
const result = parseComponentPathConfig(' ');
48
assert.deepStrictEqual(result, { paths: [], exclusive: false });
49
});
50
51
test('parses a string array', () => {
52
const result = parseComponentPathConfig(['a', 'b', 'c']);
53
assert.deepStrictEqual(result, { paths: ['a', 'b', 'c'], exclusive: false });
54
});
55
56
test('filters non-string entries from arrays', () => {
57
const result = parseComponentPathConfig(['valid', 42, null, 'ok']);
58
assert.deepStrictEqual(result, { paths: ['valid', 'ok'], exclusive: false });
59
});
60
61
test('parses object with paths and exclusive', () => {
62
const result = parseComponentPathConfig({ paths: ['x', 'y'], exclusive: true });
63
assert.deepStrictEqual(result, { paths: ['x', 'y'], exclusive: true });
64
});
65
66
test('object without exclusive defaults to false', () => {
67
const result = parseComponentPathConfig({ paths: ['z'] });
68
assert.deepStrictEqual(result, { paths: ['z'], exclusive: false });
69
});
70
71
test('returns empty for unrecognized types', () => {
72
const result = parseComponentPathConfig(42);
73
assert.deepStrictEqual(result, { paths: [], exclusive: false });
74
});
75
});
76
77
// ---- resolveComponentDirs -------------------------------------------
78
79
suite('resolveComponentDirs', () => {
80
81
const pluginUri = URI.file('/workspace/.plugin-root');
82
83
test('includes default directory when not exclusive', () => {
84
const dirs = resolveComponentDirs(pluginUri, 'skills', { paths: [], exclusive: false });
85
assert.strictEqual(dirs.length, 1);
86
assert.ok(dirs[0].path.endsWith('/skills'));
87
});
88
89
test('excludes default directory when exclusive', () => {
90
const dirs = resolveComponentDirs(pluginUri, 'skills', { paths: ['custom'], exclusive: true });
91
assert.ok(!dirs.some(d => d.path.endsWith('/skills')));
92
assert.ok(dirs.some(d => d.path.endsWith('/custom')));
93
});
94
95
test('resolves relative paths from plugin root', () => {
96
const dirs = resolveComponentDirs(pluginUri, 'skills', { paths: ['other/skills'], exclusive: false });
97
assert.strictEqual(dirs.length, 2);
98
assert.ok(dirs[1].path.endsWith('/other/skills'));
99
});
100
101
test('rejects paths that escape plugin root', () => {
102
const dirs = resolveComponentDirs(pluginUri, 'skills', { paths: ['../../outside'], exclusive: false });
103
// Should only have the default dir, the traversal path is rejected
104
assert.strictEqual(dirs.length, 1);
105
});
106
});
107
108
// ---- normalizeMcpServerConfiguration --------------------------------
109
110
suite('normalizeMcpServerConfiguration', () => {
111
112
test('returns undefined for non-object input', () => {
113
assert.strictEqual(normalizeMcpServerConfiguration(null), undefined);
114
assert.strictEqual(normalizeMcpServerConfiguration('string'), undefined);
115
assert.strictEqual(normalizeMcpServerConfiguration(42), undefined);
116
});
117
118
test('parses local server with command', () => {
119
const result = normalizeMcpServerConfiguration({
120
type: 'stdio',
121
command: 'node',
122
args: ['server.js'],
123
env: { KEY: 'value' },
124
cwd: '/workspace',
125
});
126
assert.ok(result);
127
assert.strictEqual(result!.type, McpServerType.LOCAL);
128
assert.strictEqual((result as { command: string }).command, 'node');
129
});
130
131
test('infers local type from command without explicit type', () => {
132
const result = normalizeMcpServerConfiguration({ command: 'python' });
133
assert.ok(result);
134
assert.strictEqual(result!.type, McpServerType.LOCAL);
135
});
136
137
test('parses remote server with url', () => {
138
const result = normalizeMcpServerConfiguration({
139
type: 'sse',
140
url: 'https://example.com',
141
headers: { 'X-Key': 'val' },
142
});
143
assert.ok(result);
144
assert.strictEqual(result!.type, McpServerType.REMOTE);
145
});
146
147
test('infers remote type from url without explicit type', () => {
148
const result = normalizeMcpServerConfiguration({ url: 'https://example.com' });
149
assert.ok(result);
150
assert.strictEqual(result!.type, McpServerType.REMOTE);
151
});
152
153
test('rejects ws type', () => {
154
const result = normalizeMcpServerConfiguration({ type: 'ws', url: 'ws://localhost:3000' });
155
assert.strictEqual(result, undefined);
156
});
157
158
test('rejects local type without command', () => {
159
const result = normalizeMcpServerConfiguration({ type: 'stdio' });
160
assert.strictEqual(result, undefined);
161
});
162
163
test('filters non-string args', () => {
164
const result = normalizeMcpServerConfiguration({
165
command: 'test',
166
args: ['valid', 42, null, 'also-valid'],
167
});
168
assert.ok(result);
169
const args = (result as { args?: string[] }).args;
170
assert.deepStrictEqual(args, ['valid', 'also-valid']);
171
});
172
});
173
174
// ---- shellQuotePluginRootInCommand -----------------------------------
175
176
suite('shellQuotePluginRootInCommand', () => {
177
178
test('replaces token with path when no special chars', () => {
179
const result = shellQuotePluginRootInCommand(
180
'cd ${PLUGIN_ROOT} && run',
181
'/simple/path',
182
'${PLUGIN_ROOT}'
183
);
184
assert.strictEqual(result, 'cd /simple/path && run');
185
});
186
187
test('quotes path with spaces', () => {
188
const result = shellQuotePluginRootInCommand(
189
'cd ${PLUGIN_ROOT} && run',
190
'/path with spaces',
191
'${PLUGIN_ROOT}'
192
);
193
assert.ok(result.includes('"'), 'should add quotes for path with spaces');
194
assert.ok(result.includes('/path with spaces'));
195
});
196
197
test('returns unchanged when token not present', () => {
198
const result = shellQuotePluginRootInCommand('echo hello', '/path', '${PLUGIN_ROOT}');
199
assert.strictEqual(result, 'echo hello');
200
});
201
202
test('handles already-quoted token', () => {
203
const result = shellQuotePluginRootInCommand(
204
'"${PLUGIN_ROOT}/script.sh"',
205
'/path with spaces',
206
'${PLUGIN_ROOT}'
207
);
208
assert.ok(!result.includes('""'), 'should not double-quote');
209
});
210
});
211
212
// ---- convertBareEnvVarsToVsCodeSyntax -------------------------------
213
214
suite('convertBareEnvVarsToVsCodeSyntax', () => {
215
216
test('converts bare env vars to VS Code syntax', () => {
217
const def = {
218
name: 'test',
219
uri: URI.file('/plugin'),
220
configuration: {
221
type: McpServerType.LOCAL as const,
222
command: '${MY_TOOL}',
223
args: ['--key=${API_KEY}'],
224
},
225
};
226
const result = convertBareEnvVarsToVsCodeSyntax(def);
227
assert.strictEqual((result.configuration as { command: string }).command, '${env:MY_TOOL}');
228
assert.deepStrictEqual((result.configuration as unknown as { args: string[] }).args, ['--key=${env:API_KEY}']);
229
});
230
231
test('does not convert already-qualified vars', () => {
232
const def = {
233
name: 'test',
234
uri: URI.file('/plugin'),
235
configuration: {
236
type: McpServerType.LOCAL as const,
237
command: '${env:ALREADY_QUALIFIED}',
238
},
239
};
240
const result = convertBareEnvVarsToVsCodeSyntax(def);
241
assert.strictEqual((result.configuration as { command: string }).command, '${env:ALREADY_QUALIFIED}');
242
});
243
244
test('ignores lowercase vars', () => {
245
const def = {
246
name: 'test',
247
uri: URI.file('/plugin'),
248
configuration: {
249
type: McpServerType.LOCAL as const,
250
command: '${lowercase}',
251
},
252
};
253
const result = convertBareEnvVarsToVsCodeSyntax(def);
254
assert.strictEqual((result.configuration as { command: string }).command, '${lowercase}');
255
});
256
});
257
});
258
259