Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/customInstructions/test/node/customInstructionsService.spec.ts
13405 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 { afterEach, beforeEach, expect, suite, test } from 'vitest';
7
import { URI } from '../../../../util/vs/base/common/uri';
8
import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors';
9
import { IConfigurationService } from '../../../configuration/common/configurationService';
10
import { DefaultsOnlyConfigurationService } from '../../../configuration/common/defaultsOnlyConfigurationService';
11
import { InMemoryConfigurationService } from '../../../configuration/test/common/inMemoryConfigurationService';
12
import { createPlatformServices, ITestingServicesAccessor } from '../../../test/node/services';
13
import { TestWorkspaceService } from '../../../test/node/testWorkspaceService';
14
import { IWorkspaceService } from '../../../workspace/common/workspaceService';
15
import { ICustomInstructionsService } from '../../common/customInstructionsService';
16
17
suite('CustomInstructionsService - Skills', () => {
18
let accessor: ITestingServicesAccessor;
19
let customInstructionsService: ICustomInstructionsService;
20
let configService: InMemoryConfigurationService;
21
22
beforeEach(async () => {
23
const services = createPlatformServices();
24
25
// Setup workspace with a workspace folder
26
const workspaceFolders = [URI.file('/workspace')];
27
services.define(IWorkspaceService, new SyncDescriptor(
28
TestWorkspaceService,
29
[workspaceFolders, []]
30
));
31
32
// Create a configuration service that allows setting values
33
configService = new InMemoryConfigurationService(new DefaultsOnlyConfigurationService());
34
services.define(IConfigurationService, configService);
35
36
// Enable the agent skills setting
37
await configService.setNonExtensionConfig('chat.useAgentSkills', true);
38
39
accessor = services.createTestingAccessor();
40
customInstructionsService = accessor.get(ICustomInstructionsService);
41
});
42
43
afterEach(() => {
44
accessor?.dispose();
45
});
46
47
suite('getSkillInfo', () => {
48
test('should return skill info for file in .github/skills folder', () => {
49
const skillFileUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');
50
const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);
51
52
expect(skillInfo).toBeDefined();
53
expect(skillInfo?.skillName).toBe('myskill');
54
expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/workspace/.github/skills/myskill').toString());
55
});
56
57
test('should return skill info for file in .claude/skills folder', () => {
58
const skillFileUri = URI.file('/workspace/.claude/skills/myskill/SKILL.md');
59
const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);
60
61
expect(skillInfo).toBeDefined();
62
expect(skillInfo?.skillName).toBe('myskill');
63
expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/workspace/.claude/skills/myskill').toString());
64
});
65
66
test('should return skill info for nested file in skill folder', () => {
67
const skillFileUri = URI.file('/workspace/.github/skills/myskill/subfolder/helper.ts');
68
const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);
69
70
expect(skillInfo).toBeDefined();
71
expect(skillInfo?.skillName).toBe('myskill');
72
expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/workspace/.github/skills/myskill').toString());
73
});
74
75
test('should return undefined for non-skill file', () => {
76
const regularFileUri = URI.file('/workspace/src/file.ts');
77
const skillInfo = customInstructionsService.getSkillInfo(regularFileUri);
78
79
expect(skillInfo).toBeUndefined();
80
});
81
82
test('should return undefined when useAgentSkills setting is disabled', async () => {
83
// Disable the setting
84
await configService.setNonExtensionConfig('chat.useAgentSkills', false);
85
86
const skillFileUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');
87
const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);
88
89
expect(skillInfo).toBeUndefined();
90
});
91
92
test('should return skill info for skill with hyphenated name', () => {
93
const skillFileUri = URI.file('/workspace/.github/skills/my-skill-name/SKILL.md');
94
const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);
95
96
expect(skillInfo).toBeDefined();
97
expect(skillInfo?.skillName).toBe('my-skill-name');
98
});
99
});
100
101
suite('isSkillFile', () => {
102
test('should return true for file in skill folder', () => {
103
const skillFileUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');
104
expect(customInstructionsService.isSkillFile(skillFileUri)).toBe(true);
105
});
106
107
test('should return true for nested file in skill folder', () => {
108
const skillFileUri = URI.file('/workspace/.github/skills/myskill/subfolder/code.ts');
109
expect(customInstructionsService.isSkillFile(skillFileUri)).toBe(true);
110
});
111
112
test('should return false for non-skill file', () => {
113
const regularFileUri = URI.file('/workspace/src/file.ts');
114
expect(customInstructionsService.isSkillFile(regularFileUri)).toBe(false);
115
});
116
117
test('should return false when useAgentSkills setting is disabled', async () => {
118
await configService.setNonExtensionConfig('chat.useAgentSkills', false);
119
120
const skillFileUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');
121
expect(customInstructionsService.isSkillFile(skillFileUri)).toBe(false);
122
});
123
124
test('should return true for file in .claude/skills folder', () => {
125
const skillFileUri = URI.file('/workspace/.claude/skills/test/file.ts');
126
expect(customInstructionsService.isSkillFile(skillFileUri)).toBe(true);
127
});
128
});
129
130
suite('isSkillMdFile', () => {
131
test('should return true for SKILL.md in skill folder', () => {
132
const skillMdUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');
133
expect(customInstructionsService.isSkillMdFile(skillMdUri)).toBe(true);
134
});
135
136
test('should return true for skill.md with lowercase', () => {
137
const skillMdUri = URI.file('/workspace/.github/skills/myskill/skill.md');
138
expect(customInstructionsService.isSkillMdFile(skillMdUri)).toBe(true);
139
});
140
141
test('should return true for mixed case sKiLl.Md', () => {
142
const skillMdUri = URI.file('/workspace/.github/skills/myskill/sKiLl.Md');
143
expect(customInstructionsService.isSkillMdFile(skillMdUri)).toBe(true);
144
});
145
146
test('should return false for other .md files in skill folder', () => {
147
const otherMdUri = URI.file('/workspace/.github/skills/myskill/README.md');
148
expect(customInstructionsService.isSkillMdFile(otherMdUri)).toBe(false);
149
});
150
151
test('should return false for non-md files in skill folder', () => {
152
const codeFileUri = URI.file('/workspace/.github/skills/myskill/code.ts');
153
expect(customInstructionsService.isSkillMdFile(codeFileUri)).toBe(false);
154
});
155
156
test('should return false for SKILL.md outside skill folder', () => {
157
const nonSkillUri = URI.file('/workspace/docs/SKILL.md');
158
expect(customInstructionsService.isSkillMdFile(nonSkillUri)).toBe(false);
159
});
160
161
test('should return false when useAgentSkills setting is disabled', async () => {
162
await configService.setNonExtensionConfig('chat.useAgentSkills', false);
163
164
const skillMdUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');
165
expect(customInstructionsService.isSkillMdFile(skillMdUri)).toBe(false);
166
});
167
});
168
169
suite('isExternalInstructionsFile', () => {
170
test('should return true for skill files', async () => {
171
const skillFileUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');
172
expect(await customInstructionsService.isExternalInstructionsFile(skillFileUri)).toBe(true);
173
});
174
175
test('should return false for regular files', async () => {
176
const regularFileUri = URI.file('/workspace/src/file.ts');
177
expect(await customInstructionsService.isExternalInstructionsFile(regularFileUri)).toBe(false);
178
});
179
});
180
181
suite('isExternalInstructionsFolder', () => {
182
test('should return true for skill folder', () => {
183
const skillFolderUri = URI.file('/workspace/.github/skills/myskill');
184
expect(customInstructionsService.isExternalInstructionsFolder(skillFolderUri)).toBe(true);
185
});
186
187
test('should return true for nested folder in skill', () => {
188
const nestedFolderUri = URI.file('/workspace/.github/skills/myskill/subfolder');
189
expect(customInstructionsService.isExternalInstructionsFolder(nestedFolderUri)).toBe(true);
190
});
191
192
test('should return false for regular folder', () => {
193
const regularFolderUri = URI.file('/workspace/src');
194
expect(customInstructionsService.isExternalInstructionsFolder(regularFolderUri)).toBe(false);
195
});
196
});
197
198
suite('chat.agentSkillsLocations config', () => {
199
test('should return skill info for file in absolute path skill location', async () => {
200
await configService.setNonExtensionConfig('chat.agentSkillsLocations', {
201
'/custom/skills': true
202
});
203
204
const skillFileUri = URI.file('/custom/skills/myskill/SKILL.md');
205
const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);
206
207
expect(skillInfo).toBeDefined();
208
expect(skillInfo?.skillName).toBe('myskill');
209
expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/custom/skills/myskill').toString());
210
});
211
212
test('should return skill info for nested file in absolute path skill location', async () => {
213
await configService.setNonExtensionConfig('chat.agentSkillsLocations', {
214
'/custom/skills': true
215
});
216
217
const skillFileUri = URI.file('/custom/skills/myskill/subfolder/code.ts');
218
const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);
219
220
expect(skillInfo).toBeDefined();
221
expect(skillInfo?.skillName).toBe('myskill');
222
expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/custom/skills/myskill').toString());
223
});
224
225
test('should return skill info for file in tilde path skill location', async () => {
226
await configService.setNonExtensionConfig('chat.agentSkillsLocations', {
227
'~/my-skills': true
228
});
229
230
// userHome is /home/testuser in NullNativeEnvService
231
const skillFileUri = URI.file('/home/testuser/my-skills/myskill/SKILL.md');
232
const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);
233
234
expect(skillInfo).toBeDefined();
235
expect(skillInfo?.skillName).toBe('myskill');
236
expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/home/testuser/my-skills/myskill').toString());
237
});
238
239
test('should return skill info for file in relative path skill location', async () => {
240
await configService.setNonExtensionConfig('chat.agentSkillsLocations', {
241
'custom-skills': true
242
});
243
244
// Relative paths are joined to workspace folder (/workspace)
245
const skillFileUri = URI.file('/workspace/custom-skills/myskill/SKILL.md');
246
const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);
247
248
expect(skillInfo).toBeDefined();
249
expect(skillInfo?.skillName).toBe('myskill');
250
expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/workspace/custom-skills/myskill').toString());
251
});
252
253
test('should ignore disabled skill locations (value !== true)', async () => {
254
await configService.setNonExtensionConfig('chat.agentSkillsLocations', {
255
'/custom/skills': false
256
});
257
258
const skillFileUri = URI.file('/custom/skills/myskill/SKILL.md');
259
const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);
260
261
expect(skillInfo).toBeUndefined();
262
});
263
264
test('should handle multiple skill locations', async () => {
265
await configService.setNonExtensionConfig('chat.agentSkillsLocations', {
266
'/custom/skills': true,
267
'~/my-skills': true,
268
'local-skills': true
269
});
270
271
// Check absolute path
272
const skill1 = customInstructionsService.getSkillInfo(URI.file('/custom/skills/skill1/SKILL.md'));
273
expect(skill1?.skillName).toBe('skill1');
274
275
// Check tilde path
276
const skill2 = customInstructionsService.getSkillInfo(URI.file('/home/testuser/my-skills/skill2/SKILL.md'));
277
expect(skill2?.skillName).toBe('skill2');
278
279
// Check relative path
280
const skill3 = customInstructionsService.getSkillInfo(URI.file('/workspace/local-skills/skill3/SKILL.md'));
281
expect(skill3?.skillName).toBe('skill3');
282
});
283
284
test('should return true for isSkillFile with config-based location', async () => {
285
await configService.setNonExtensionConfig('chat.agentSkillsLocations', {
286
'/custom/skills': true
287
});
288
289
const skillFileUri = URI.file('/custom/skills/myskill/code.ts');
290
expect(customInstructionsService.isSkillFile(skillFileUri)).toBe(true);
291
});
292
293
test('should return true for isSkillMdFile with config-based location', async () => {
294
await configService.setNonExtensionConfig('chat.agentSkillsLocations', {
295
'/custom/skills': true
296
});
297
298
const skillMdUri = URI.file('/custom/skills/myskill/SKILL.md');
299
expect(customInstructionsService.isSkillMdFile(skillMdUri)).toBe(true);
300
});
301
302
test('should return true for isExternalInstructionsFolder with config-based location', async () => {
303
await configService.setNonExtensionConfig('chat.agentSkillsLocations', {
304
'/custom/skills': true
305
});
306
307
const skillFolderUri = URI.file('/custom/skills/myskill');
308
expect(customInstructionsService.isExternalInstructionsFolder(skillFolderUri)).toBe(true);
309
});
310
311
test('config-based locations should not interfere with default locations', async () => {
312
await configService.setNonExtensionConfig('chat.agentSkillsLocations', {
313
'/custom/skills': true
314
});
315
316
// Default .github/skills should still work
317
const defaultSkillFile = URI.file('/workspace/.github/skills/myskill/SKILL.md');
318
const skillInfo = customInstructionsService.getSkillInfo(defaultSkillFile);
319
320
expect(skillInfo).toBeDefined();
321
expect(skillInfo?.skillName).toBe('myskill');
322
});
323
});
324
325
suite('chat.instructionsFilesLocations config', () => {
326
test('should recognize instruction file in absolute path location', async () => {
327
await configService.setNonExtensionConfig('chat.instructionsFilesLocations', {
328
'/custom/instructions': true
329
});
330
331
const instructionFileUri = URI.file('/custom/instructions/setup.instructions.md');
332
expect(await customInstructionsService.isExternalInstructionsFile(instructionFileUri)).toBe(true);
333
});
334
335
test('should recognize instruction file in tilde path location', async () => {
336
await configService.setNonExtensionConfig('chat.instructionsFilesLocations', {
337
'~/.copilot/instructions': true
338
});
339
340
// userHome is /home/testuser in NullNativeEnvService
341
const instructionFileUri = URI.file('/home/testuser/.copilot/instructions/setup.instructions.md');
342
expect(await customInstructionsService.isExternalInstructionsFile(instructionFileUri)).toBe(true);
343
});
344
345
test('should not recognize non-instruction file in tilde path location', async () => {
346
await configService.setNonExtensionConfig('chat.instructionsFilesLocations', {
347
'~/.copilot/instructions': true
348
});
349
350
const regularFileUri = URI.file('/home/testuser/.copilot/instructions/notes.txt');
351
expect(await customInstructionsService.isExternalInstructionsFile(regularFileUri)).toBe(false);
352
});
353
354
test('should ignore disabled instruction locations', async () => {
355
await configService.setNonExtensionConfig('chat.instructionsFilesLocations', {
356
'/custom/instructions': false
357
});
358
359
const instructionFileUri = URI.file('/custom/instructions/setup.instructions.md');
360
expect(await customInstructionsService.isExternalInstructionsFile(instructionFileUri)).toBe(false);
361
});
362
363
test('should handle both absolute and tilde paths together', async () => {
364
await configService.setNonExtensionConfig('chat.instructionsFilesLocations', {
365
'/custom/instructions': true,
366
'~/.copilot/instructions': true
367
});
368
369
const absoluteFileUri = URI.file('/custom/instructions/setup.instructions.md');
370
expect(await customInstructionsService.isExternalInstructionsFile(absoluteFileUri)).toBe(true);
371
372
// userHome is /home/testuser in NullNativeEnvService
373
const tildeFileUri = URI.file('/home/testuser/.copilot/instructions/setup.instructions.md');
374
expect(await customInstructionsService.isExternalInstructionsFile(tildeFileUri)).toBe(true);
375
});
376
});
377
});
378
379