Path: blob/main/extensions/copilot/src/platform/customInstructions/test/node/customInstructionsService.spec.ts
13405 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 { afterEach, beforeEach, expect, suite, test } from 'vitest';6import { URI } from '../../../../util/vs/base/common/uri';7import { SyncDescriptor } from '../../../../util/vs/platform/instantiation/common/descriptors';8import { IConfigurationService } from '../../../configuration/common/configurationService';9import { DefaultsOnlyConfigurationService } from '../../../configuration/common/defaultsOnlyConfigurationService';10import { InMemoryConfigurationService } from '../../../configuration/test/common/inMemoryConfigurationService';11import { createPlatformServices, ITestingServicesAccessor } from '../../../test/node/services';12import { TestWorkspaceService } from '../../../test/node/testWorkspaceService';13import { IWorkspaceService } from '../../../workspace/common/workspaceService';14import { ICustomInstructionsService } from '../../common/customInstructionsService';1516suite('CustomInstructionsService - Skills', () => {17let accessor: ITestingServicesAccessor;18let customInstructionsService: ICustomInstructionsService;19let configService: InMemoryConfigurationService;2021beforeEach(async () => {22const services = createPlatformServices();2324// Setup workspace with a workspace folder25const workspaceFolders = [URI.file('/workspace')];26services.define(IWorkspaceService, new SyncDescriptor(27TestWorkspaceService,28[workspaceFolders, []]29));3031// Create a configuration service that allows setting values32configService = new InMemoryConfigurationService(new DefaultsOnlyConfigurationService());33services.define(IConfigurationService, configService);3435// Enable the agent skills setting36await configService.setNonExtensionConfig('chat.useAgentSkills', true);3738accessor = services.createTestingAccessor();39customInstructionsService = accessor.get(ICustomInstructionsService);40});4142afterEach(() => {43accessor?.dispose();44});4546suite('getSkillInfo', () => {47test('should return skill info for file in .github/skills folder', () => {48const skillFileUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');49const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);5051expect(skillInfo).toBeDefined();52expect(skillInfo?.skillName).toBe('myskill');53expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/workspace/.github/skills/myskill').toString());54});5556test('should return skill info for file in .claude/skills folder', () => {57const skillFileUri = URI.file('/workspace/.claude/skills/myskill/SKILL.md');58const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);5960expect(skillInfo).toBeDefined();61expect(skillInfo?.skillName).toBe('myskill');62expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/workspace/.claude/skills/myskill').toString());63});6465test('should return skill info for nested file in skill folder', () => {66const skillFileUri = URI.file('/workspace/.github/skills/myskill/subfolder/helper.ts');67const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);6869expect(skillInfo).toBeDefined();70expect(skillInfo?.skillName).toBe('myskill');71expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/workspace/.github/skills/myskill').toString());72});7374test('should return undefined for non-skill file', () => {75const regularFileUri = URI.file('/workspace/src/file.ts');76const skillInfo = customInstructionsService.getSkillInfo(regularFileUri);7778expect(skillInfo).toBeUndefined();79});8081test('should return undefined when useAgentSkills setting is disabled', async () => {82// Disable the setting83await configService.setNonExtensionConfig('chat.useAgentSkills', false);8485const skillFileUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');86const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);8788expect(skillInfo).toBeUndefined();89});9091test('should return skill info for skill with hyphenated name', () => {92const skillFileUri = URI.file('/workspace/.github/skills/my-skill-name/SKILL.md');93const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);9495expect(skillInfo).toBeDefined();96expect(skillInfo?.skillName).toBe('my-skill-name');97});98});99100suite('isSkillFile', () => {101test('should return true for file in skill folder', () => {102const skillFileUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');103expect(customInstructionsService.isSkillFile(skillFileUri)).toBe(true);104});105106test('should return true for nested file in skill folder', () => {107const skillFileUri = URI.file('/workspace/.github/skills/myskill/subfolder/code.ts');108expect(customInstructionsService.isSkillFile(skillFileUri)).toBe(true);109});110111test('should return false for non-skill file', () => {112const regularFileUri = URI.file('/workspace/src/file.ts');113expect(customInstructionsService.isSkillFile(regularFileUri)).toBe(false);114});115116test('should return false when useAgentSkills setting is disabled', async () => {117await configService.setNonExtensionConfig('chat.useAgentSkills', false);118119const skillFileUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');120expect(customInstructionsService.isSkillFile(skillFileUri)).toBe(false);121});122123test('should return true for file in .claude/skills folder', () => {124const skillFileUri = URI.file('/workspace/.claude/skills/test/file.ts');125expect(customInstructionsService.isSkillFile(skillFileUri)).toBe(true);126});127});128129suite('isSkillMdFile', () => {130test('should return true for SKILL.md in skill folder', () => {131const skillMdUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');132expect(customInstructionsService.isSkillMdFile(skillMdUri)).toBe(true);133});134135test('should return true for skill.md with lowercase', () => {136const skillMdUri = URI.file('/workspace/.github/skills/myskill/skill.md');137expect(customInstructionsService.isSkillMdFile(skillMdUri)).toBe(true);138});139140test('should return true for mixed case sKiLl.Md', () => {141const skillMdUri = URI.file('/workspace/.github/skills/myskill/sKiLl.Md');142expect(customInstructionsService.isSkillMdFile(skillMdUri)).toBe(true);143});144145test('should return false for other .md files in skill folder', () => {146const otherMdUri = URI.file('/workspace/.github/skills/myskill/README.md');147expect(customInstructionsService.isSkillMdFile(otherMdUri)).toBe(false);148});149150test('should return false for non-md files in skill folder', () => {151const codeFileUri = URI.file('/workspace/.github/skills/myskill/code.ts');152expect(customInstructionsService.isSkillMdFile(codeFileUri)).toBe(false);153});154155test('should return false for SKILL.md outside skill folder', () => {156const nonSkillUri = URI.file('/workspace/docs/SKILL.md');157expect(customInstructionsService.isSkillMdFile(nonSkillUri)).toBe(false);158});159160test('should return false when useAgentSkills setting is disabled', async () => {161await configService.setNonExtensionConfig('chat.useAgentSkills', false);162163const skillMdUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');164expect(customInstructionsService.isSkillMdFile(skillMdUri)).toBe(false);165});166});167168suite('isExternalInstructionsFile', () => {169test('should return true for skill files', async () => {170const skillFileUri = URI.file('/workspace/.github/skills/myskill/SKILL.md');171expect(await customInstructionsService.isExternalInstructionsFile(skillFileUri)).toBe(true);172});173174test('should return false for regular files', async () => {175const regularFileUri = URI.file('/workspace/src/file.ts');176expect(await customInstructionsService.isExternalInstructionsFile(regularFileUri)).toBe(false);177});178});179180suite('isExternalInstructionsFolder', () => {181test('should return true for skill folder', () => {182const skillFolderUri = URI.file('/workspace/.github/skills/myskill');183expect(customInstructionsService.isExternalInstructionsFolder(skillFolderUri)).toBe(true);184});185186test('should return true for nested folder in skill', () => {187const nestedFolderUri = URI.file('/workspace/.github/skills/myskill/subfolder');188expect(customInstructionsService.isExternalInstructionsFolder(nestedFolderUri)).toBe(true);189});190191test('should return false for regular folder', () => {192const regularFolderUri = URI.file('/workspace/src');193expect(customInstructionsService.isExternalInstructionsFolder(regularFolderUri)).toBe(false);194});195});196197suite('chat.agentSkillsLocations config', () => {198test('should return skill info for file in absolute path skill location', async () => {199await configService.setNonExtensionConfig('chat.agentSkillsLocations', {200'/custom/skills': true201});202203const skillFileUri = URI.file('/custom/skills/myskill/SKILL.md');204const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);205206expect(skillInfo).toBeDefined();207expect(skillInfo?.skillName).toBe('myskill');208expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/custom/skills/myskill').toString());209});210211test('should return skill info for nested file in absolute path skill location', async () => {212await configService.setNonExtensionConfig('chat.agentSkillsLocations', {213'/custom/skills': true214});215216const skillFileUri = URI.file('/custom/skills/myskill/subfolder/code.ts');217const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);218219expect(skillInfo).toBeDefined();220expect(skillInfo?.skillName).toBe('myskill');221expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/custom/skills/myskill').toString());222});223224test('should return skill info for file in tilde path skill location', async () => {225await configService.setNonExtensionConfig('chat.agentSkillsLocations', {226'~/my-skills': true227});228229// userHome is /home/testuser in NullNativeEnvService230const skillFileUri = URI.file('/home/testuser/my-skills/myskill/SKILL.md');231const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);232233expect(skillInfo).toBeDefined();234expect(skillInfo?.skillName).toBe('myskill');235expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/home/testuser/my-skills/myskill').toString());236});237238test('should return skill info for file in relative path skill location', async () => {239await configService.setNonExtensionConfig('chat.agentSkillsLocations', {240'custom-skills': true241});242243// Relative paths are joined to workspace folder (/workspace)244const skillFileUri = URI.file('/workspace/custom-skills/myskill/SKILL.md');245const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);246247expect(skillInfo).toBeDefined();248expect(skillInfo?.skillName).toBe('myskill');249expect(skillInfo?.skillFolderUri.toString()).toBe(URI.file('/workspace/custom-skills/myskill').toString());250});251252test('should ignore disabled skill locations (value !== true)', async () => {253await configService.setNonExtensionConfig('chat.agentSkillsLocations', {254'/custom/skills': false255});256257const skillFileUri = URI.file('/custom/skills/myskill/SKILL.md');258const skillInfo = customInstructionsService.getSkillInfo(skillFileUri);259260expect(skillInfo).toBeUndefined();261});262263test('should handle multiple skill locations', async () => {264await configService.setNonExtensionConfig('chat.agentSkillsLocations', {265'/custom/skills': true,266'~/my-skills': true,267'local-skills': true268});269270// Check absolute path271const skill1 = customInstructionsService.getSkillInfo(URI.file('/custom/skills/skill1/SKILL.md'));272expect(skill1?.skillName).toBe('skill1');273274// Check tilde path275const skill2 = customInstructionsService.getSkillInfo(URI.file('/home/testuser/my-skills/skill2/SKILL.md'));276expect(skill2?.skillName).toBe('skill2');277278// Check relative path279const skill3 = customInstructionsService.getSkillInfo(URI.file('/workspace/local-skills/skill3/SKILL.md'));280expect(skill3?.skillName).toBe('skill3');281});282283test('should return true for isSkillFile with config-based location', async () => {284await configService.setNonExtensionConfig('chat.agentSkillsLocations', {285'/custom/skills': true286});287288const skillFileUri = URI.file('/custom/skills/myskill/code.ts');289expect(customInstructionsService.isSkillFile(skillFileUri)).toBe(true);290});291292test('should return true for isSkillMdFile with config-based location', async () => {293await configService.setNonExtensionConfig('chat.agentSkillsLocations', {294'/custom/skills': true295});296297const skillMdUri = URI.file('/custom/skills/myskill/SKILL.md');298expect(customInstructionsService.isSkillMdFile(skillMdUri)).toBe(true);299});300301test('should return true for isExternalInstructionsFolder with config-based location', async () => {302await configService.setNonExtensionConfig('chat.agentSkillsLocations', {303'/custom/skills': true304});305306const skillFolderUri = URI.file('/custom/skills/myskill');307expect(customInstructionsService.isExternalInstructionsFolder(skillFolderUri)).toBe(true);308});309310test('config-based locations should not interfere with default locations', async () => {311await configService.setNonExtensionConfig('chat.agentSkillsLocations', {312'/custom/skills': true313});314315// Default .github/skills should still work316const defaultSkillFile = URI.file('/workspace/.github/skills/myskill/SKILL.md');317const skillInfo = customInstructionsService.getSkillInfo(defaultSkillFile);318319expect(skillInfo).toBeDefined();320expect(skillInfo?.skillName).toBe('myskill');321});322});323324suite('chat.instructionsFilesLocations config', () => {325test('should recognize instruction file in absolute path location', async () => {326await configService.setNonExtensionConfig('chat.instructionsFilesLocations', {327'/custom/instructions': true328});329330const instructionFileUri = URI.file('/custom/instructions/setup.instructions.md');331expect(await customInstructionsService.isExternalInstructionsFile(instructionFileUri)).toBe(true);332});333334test('should recognize instruction file in tilde path location', async () => {335await configService.setNonExtensionConfig('chat.instructionsFilesLocations', {336'~/.copilot/instructions': true337});338339// userHome is /home/testuser in NullNativeEnvService340const instructionFileUri = URI.file('/home/testuser/.copilot/instructions/setup.instructions.md');341expect(await customInstructionsService.isExternalInstructionsFile(instructionFileUri)).toBe(true);342});343344test('should not recognize non-instruction file in tilde path location', async () => {345await configService.setNonExtensionConfig('chat.instructionsFilesLocations', {346'~/.copilot/instructions': true347});348349const regularFileUri = URI.file('/home/testuser/.copilot/instructions/notes.txt');350expect(await customInstructionsService.isExternalInstructionsFile(regularFileUri)).toBe(false);351});352353test('should ignore disabled instruction locations', async () => {354await configService.setNonExtensionConfig('chat.instructionsFilesLocations', {355'/custom/instructions': false356});357358const instructionFileUri = URI.file('/custom/instructions/setup.instructions.md');359expect(await customInstructionsService.isExternalInstructionsFile(instructionFileUri)).toBe(false);360});361362test('should handle both absolute and tilde paths together', async () => {363await configService.setNonExtensionConfig('chat.instructionsFilesLocations', {364'/custom/instructions': true,365'~/.copilot/instructions': true366});367368const absoluteFileUri = URI.file('/custom/instructions/setup.instructions.md');369expect(await customInstructionsService.isExternalInstructionsFile(absoluteFileUri)).toBe(true);370371// userHome is /home/testuser in NullNativeEnvService372const tildeFileUri = URI.file('/home/testuser/.copilot/instructions/setup.instructions.md');373expect(await customInstructionsService.isExternalInstructionsFile(tildeFileUri)).toBe(true);374});375});376});377378379