Download packages/core/src/skills/skillManager.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 14.2 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/skills/skillManager.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/skills/skillManager.test.ts
-
curl -L -o skillManager.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/skills/skillManager.test.ts
14.2 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; | |
| import * as fs from 'node:fs/promises'; | |
| import * as os from 'node:os'; | |
| import * as path from 'node:path'; | |
| import { SkillManager } from './skillManager.js'; | |
| import { Storage } from '../config/storage.js'; | |
| import { type GeminiCLIExtension } from '../config/config.js'; | |
| import { loadSkillsFromDir, type SkillDefinition } from './skillLoader.js'; | |
| import { coreEvents } from '../utils/events.js'; | |
| import { debugLogger } from '../utils/debugLogger.js'; | |
| vi.mock('./skillLoader.js', async (importOriginal) => { | |
| const actual = await importOriginal<typeof import('./skillLoader.js')>(); | |
| return { | |
| ...actual, | |
| loadSkillsFromDir: vi.fn(actual.loadSkillsFromDir), | |
| }; | |
| }); | |
| describe('SkillManager', () => { | |
| let testRootDir: string; | |
| beforeEach(async () => { | |
| testRootDir = await fs.mkdtemp( | |
| path.join(os.tmpdir(), 'skill-manager-test-'), | |
| ); | |
| }); | |
| afterEach(async () => { | |
| await fs.rm(testRootDir, { recursive: true, force: true }); | |
| vi.restoreAllMocks(); | |
| }); | |
| it('should discover skills from extensions, user, and workspace with precedence', async () => { | |
| const userDir = path.join(testRootDir, 'user'); | |
| const projectDir = path.join(testRootDir, 'workspace'); | |
| await fs.mkdir(path.join(userDir, 'skill-a'), { recursive: true }); | |
| await fs.mkdir(path.join(projectDir, 'skill-b'), { recursive: true }); | |
| await fs.writeFile( | |
| path.join(userDir, 'skill-a', 'SKILL.md'), | |
| `--- | |
| name: skill-user | |
| description: user-desc | |
| --- | |
| `, | |
| ); | |
| await fs.writeFile( | |
| path.join(projectDir, 'skill-b', 'SKILL.md'), | |
| `--- | |
| name: skill-project | |
| description: project-desc | |
| --- | |
| `, | |
| ); | |
| const mockExtension: GeminiCLIExtension = { | |
| name: 'test-ext', | |
| version: '1.0.0', | |
| isActive: true, | |
| path: '/ext', | |
| contextFiles: [], | |
| id: 'ext-id', | |
| skills: [ | |
| { | |
| name: 'skill-extension', | |
| description: 'ext-desc', | |
| location: '/ext/skills/SKILL.md', | |
| body: 'body', | |
| }, | |
| ], | |
| }; | |
| vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue(userDir); | |
| vi.spyOn(Storage, 'getUserAgentSkillsDir').mockReturnValue( | |
| '/non-existent-user-agent', | |
| ); | |
| const storage = new Storage('/dummy'); | |
| vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir); | |
| vi.spyOn(storage, 'getProjectAgentSkillsDir').mockReturnValue( | |
| '/non-existent-project-agent', | |
| ); | |
| const service = new SkillManager(); | |
| // @ts-expect-error accessing private method for testing | |
| vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined); | |
| await service.discoverSkills(storage, [mockExtension], true); | |
| const skills = service.getSkills(); | |
| expect(skills).toHaveLength(3); | |
| const names = skills.map((s) => s.name); | |
| expect(names).toContain('skill-extension'); | |
| expect(names).toContain('skill-user'); | |
| expect(names).toContain('skill-project'); | |
| }); | |
| it('should respect precedence: Workspace > User > Extension', async () => { | |
| const userDir = path.join(testRootDir, 'user'); | |
| const projectDir = path.join(testRootDir, 'workspace'); | |
| await fs.mkdir(path.join(userDir, 'skill'), { recursive: true }); | |
| await fs.mkdir(path.join(projectDir, 'skill'), { recursive: true }); | |
| await fs.writeFile( | |
| path.join(userDir, 'skill', 'SKILL.md'), | |
| `--- | |
| name: same-name | |
| description: user-desc | |
| --- | |
| `, | |
| ); | |
| await fs.writeFile( | |
| path.join(projectDir, 'skill', 'SKILL.md'), | |
| `--- | |
| name: same-name | |
| description: project-desc | |
| --- | |
| `, | |
| ); | |
| const mockExtension: GeminiCLIExtension = { | |
| name: 'test-ext', | |
| version: '1.0.0', | |
| isActive: true, | |
| path: '/ext', | |
| contextFiles: [], | |
| id: 'ext-id', | |
| skills: [ | |
| { | |
| name: 'same-name', | |
| description: 'ext-desc', | |
| location: '/ext/skills/SKILL.md', | |
| body: 'body', | |
| }, | |
| ], | |
| }; | |
| vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue(userDir); | |
| vi.spyOn(Storage, 'getUserAgentSkillsDir').mockReturnValue( | |
| '/non-existent-user-agent', | |
| ); | |
| const storage = new Storage('/dummy'); | |
| vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir); | |
| vi.spyOn(storage, 'getProjectAgentSkillsDir').mockReturnValue( | |
| '/non-existent-project-agent', | |
| ); | |
| const service = new SkillManager(); | |
| // @ts-expect-error accessing private method for testing | |
| vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined); | |
| await service.discoverSkills(storage, [mockExtension], true); | |
| const skills = service.getSkills(); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].description).toBe('project-desc'); | |
| // Test User > Extension | |
| vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue('/non-existent'); | |
| await service.discoverSkills(storage, [mockExtension], true); | |
| expect(service.getSkills()[0].description).toBe('user-desc'); | |
| }); | |
| it('should discover built-in skills', async () => { | |
| const service = new SkillManager(); | |
| const mockBuiltinSkill: SkillDefinition = { | |
| name: 'builtin-skill', | |
| description: 'builtin-desc', | |
| location: 'builtin-loc', | |
| body: 'builtin-body', | |
| }; | |
| vi.mocked(loadSkillsFromDir).mockImplementation(async (dir) => { | |
| if (dir.endsWith('builtin')) { | |
| return [{ ...mockBuiltinSkill }]; | |
| } | |
| return []; | |
| }); | |
| const storage = new Storage('/dummy'); | |
| vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue('/non-existent'); | |
| vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue('/non-existent'); | |
| await service.discoverSkills(storage, [], true); | |
| const skills = service.getSkills(); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].name).toBe('builtin-skill'); | |
| expect(skills[0].isBuiltin).toBe(true); | |
| }); | |
| it('should filter disabled skills in getSkills but not in getAllSkills', async () => { | |
| const skillDir = path.join(testRootDir, 'skill1'); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| await fs.writeFile( | |
| path.join(skillDir, 'SKILL.md'), | |
| `--- | |
| name: skill1 | |
| description: desc1 | |
| --- | |
| body1`, | |
| ); | |
| const storage = new Storage('/dummy'); | |
| vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(testRootDir); | |
| vi.spyOn(storage, 'getProjectAgentSkillsDir').mockReturnValue( | |
| '/non-existent-project-agent', | |
| ); | |
| vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue('/non-existent'); | |
| vi.spyOn(Storage, 'getUserAgentSkillsDir').mockReturnValue( | |
| '/non-existent-user-agent', | |
| ); | |
| const service = new SkillManager(); | |
| // @ts-expect-error accessing private method for testing | |
| vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined); | |
| await service.discoverSkills(storage, [], true); | |
| service.setDisabledSkills(['skill1']); | |
| expect(service.getSkills()).toHaveLength(0); | |
| expect(service.getAllSkills()).toHaveLength(1); | |
| expect(service.getAllSkills()[0].disabled).toBe(true); | |
| }); | |
| it('should skip workspace skills if folder is not trusted', async () => { | |
| const projectDir = path.join(testRootDir, 'workspace'); | |
| await fs.mkdir(path.join(projectDir, 'skill-project'), { recursive: true }); | |
| await fs.writeFile( | |
| path.join(projectDir, 'skill-project', 'SKILL.md'), | |
| `--- | |
| name: skill-project | |
| description: project-desc | |
| --- | |
| `, | |
| ); | |
| const storage = new Storage('/dummy'); | |
| vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir); | |
| vi.spyOn(storage, 'getProjectAgentSkillsDir').mockReturnValue( | |
| '/non-existent-project-agent', | |
| ); | |
| vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue('/non-existent'); | |
| vi.spyOn(Storage, 'getUserAgentSkillsDir').mockReturnValue( | |
| '/non-existent-user-agent', | |
| ); | |
| const service = new SkillManager(); | |
| // @ts-expect-error accessing private method for testing | |
| vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined); | |
| // Call with isTrusted = false | |
| await service.discoverSkills(storage, [], false); | |
| const skills = service.getSkills(); | |
| expect(skills).toHaveLength(0); | |
| }); | |
| it('should filter built-in skills in getDisplayableSkills', async () => { | |
| const service = new SkillManager(); | |
| // @ts-expect-error accessing private property for testing | |
| service.skills = [ | |
| { | |
| name: 'regular-skill', | |
| description: 'regular', | |
| location: 'loc1', | |
| body: 'body', | |
| isBuiltin: false, | |
| }, | |
| { | |
| name: 'builtin-skill', | |
| description: 'builtin', | |
| location: 'loc2', | |
| body: 'body', | |
| isBuiltin: true, | |
| }, | |
| { | |
| name: 'disabled-builtin', | |
| description: 'disabled builtin', | |
| location: 'loc3', | |
| body: 'body', | |
| isBuiltin: true, | |
| disabled: true, | |
| }, | |
| ]; | |
| const displayable = service.getDisplayableSkills(); | |
| expect(displayable).toHaveLength(1); | |
| expect(displayable[0].name).toBe('regular-skill'); | |
| const all = service.getAllSkills(); | |
| expect(all).toHaveLength(3); | |
| const enabled = service.getSkills(); | |
| expect(enabled).toHaveLength(2); | |
| expect(enabled.map((s) => s.name)).toContain('builtin-skill'); | |
| }); | |
| it('should maintain admin settings state', async () => { | |
| const service = new SkillManager(); | |
| // Case 1: Enabled by admin | |
| service.setAdminSettings(true); | |
| expect(service.isAdminEnabled()).toBe(true); | |
| // Case 2: Disabled by admin | |
| service.setAdminSettings(false); | |
| expect(service.isAdminEnabled()).toBe(false); | |
| }); | |
| it('should reset active skill names', () => { | |
| const service = new SkillManager(); | |
| service.activateSkill('skill-1'); | |
| service.activateSkill('skill-2'); | |
| expect(service.isSkillActive('skill-1')).toBe(true); | |
| expect(service.isSkillActive('skill-2')).toBe(true); | |
| service.reset(); | |
| expect(service.isSkillActive('skill-1')).toBe(false); | |
| expect(service.isSkillActive('skill-2')).toBe(false); | |
| }); | |
| describe('Conflict Detection', () => { | |
| it('should emit UI warning when a non-built-in skill is overridden', async () => { | |
| const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback'); | |
| const userDir = path.join(testRootDir, 'user'); | |
| const projectDir = path.join(testRootDir, 'workspace'); | |
| await fs.mkdir(userDir, { recursive: true }); | |
| await fs.mkdir(projectDir, { recursive: true }); | |
| const skillName = 'conflicting-skill'; | |
| const userSkillPath = path.join(userDir, 'SKILL.md'); | |
| const projectSkillPath = path.join(projectDir, 'SKILL.md'); | |
| vi.mocked(loadSkillsFromDir).mockImplementation(async (dir) => { | |
| if (dir === userDir) { | |
| return [ | |
| { | |
| name: skillName, | |
| description: 'user-desc', | |
| location: userSkillPath, | |
| body: '', | |
| }, | |
| ]; | |
| } | |
| if (dir === projectDir) { | |
| return [ | |
| { | |
| name: skillName, | |
| description: 'project-desc', | |
| location: projectSkillPath, | |
| body: '', | |
| }, | |
| ]; | |
| } | |
| return []; | |
| }); | |
| vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue(userDir); | |
| vi.spyOn(Storage, 'getUserAgentSkillsDir').mockReturnValue( | |
| '/non-existent-user-agent', | |
| ); | |
| const storage = new Storage('/dummy'); | |
| vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir); | |
| vi.spyOn(storage, 'getProjectAgentSkillsDir').mockReturnValue( | |
| '/non-existent-project-agent', | |
| ); | |
| const service = new SkillManager(); | |
| // @ts-expect-error accessing private method for testing | |
| vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined); | |
| await service.discoverSkills(storage, [], true); | |
| expect(emitFeedbackSpy).toHaveBeenCalledWith( | |
| 'warning', | |
| expect.stringContaining( | |
| `Skill conflict detected: "${skillName}" from "${projectSkillPath}" is overriding the same skill from "${userSkillPath}".`, | |
| ), | |
| ); | |
| }); | |
| it('should log warning but NOT emit UI warning when a built-in skill is overridden', async () => { | |
| const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback'); | |
| const debugWarnSpy = vi.spyOn(debugLogger, 'warn'); | |
| const userDir = path.join(testRootDir, 'user'); | |
| await fs.mkdir(userDir, { recursive: true }); | |
| const skillName = 'builtin-skill'; | |
| const userSkillPath = path.join(userDir, 'SKILL.md'); | |
| const builtinSkillPath = 'builtin/loc'; | |
| vi.mocked(loadSkillsFromDir).mockImplementation(async (dir) => { | |
| if (dir.endsWith('builtin')) { | |
| return [ | |
| { | |
| name: skillName, | |
| description: 'builtin-desc', | |
| location: builtinSkillPath, | |
| body: '', | |
| isBuiltin: true, | |
| }, | |
| ]; | |
| } | |
| if (dir === userDir) { | |
| return [ | |
| { | |
| name: skillName, | |
| description: 'user-desc', | |
| location: userSkillPath, | |
| body: '', | |
| }, | |
| ]; | |
| } | |
| return []; | |
| }); | |
| vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue(userDir); | |
| vi.spyOn(Storage, 'getUserAgentSkillsDir').mockReturnValue( | |
| '/non-existent-user-agent', | |
| ); | |
| const storage = new Storage('/dummy'); | |
| vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue('/non-existent'); | |
| vi.spyOn(storage, 'getProjectAgentSkillsDir').mockReturnValue( | |
| '/non-existent-project-agent', | |
| ); | |
| const service = new SkillManager(); | |
| await service.discoverSkills(storage, [], true); | |
| // UI warning should not be called | |
| expect(emitFeedbackSpy).not.toHaveBeenCalled(); | |
| // Debug warning should be called | |
| expect(debugWarnSpy).toHaveBeenCalledWith( | |
| expect.stringContaining( | |
| `Skill "${skillName}" from "${userSkillPath}" is overriding the built-in skill.`, | |
| ), | |
| ); | |
| }); | |
| }); | |
| }); | |