Download packages/core/src/skills/skillLoader.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 9.4 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/skills/skillLoader.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/skills/skillLoader.test.ts
-
curl -L -o skillLoader.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/skills/skillLoader.test.ts
9.4 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 { loadSkillsFromDir } from './skillLoader.js'; | |
| import { coreEvents } from '../utils/events.js'; | |
| import { debugLogger } from '../utils/debugLogger.js'; | |
| describe('skillLoader', () => { | |
| let testRootDir: string; | |
| beforeEach(async () => { | |
| testRootDir = await fs.mkdtemp( | |
| path.join(os.tmpdir(), 'skill-loader-test-'), | |
| ); | |
| vi.spyOn(coreEvents, 'emitFeedback'); | |
| vi.spyOn(debugLogger, 'debug').mockImplementation(() => {}); | |
| }); | |
| afterEach(async () => { | |
| await fs.rm(testRootDir, { recursive: true, force: true }); | |
| vi.restoreAllMocks(); | |
| }); | |
| it('should load skills from a directory with valid SKILL.md', async () => { | |
| const skillDir = path.join(testRootDir, 'my-skill'); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| const skillFile = path.join(skillDir, 'SKILL.md'); | |
| await fs.writeFile( | |
| skillFile, | |
| `---\nname: my-skill\ndescription: A test skill\n---\n# Instructions\nDo something.\n`, | |
| ); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].name).toBe('my-skill'); | |
| expect(skills[0].description).toBe('A test skill'); | |
| expect(skills[0].location).toBe(skillFile); | |
| expect(skills[0].body).toBe('# Instructions\nDo something.'); | |
| expect(coreEvents.emitFeedback).not.toHaveBeenCalled(); | |
| }); | |
| it('should emit feedback when no valid skills are found in a non-empty directory', async () => { | |
| const notASkillDir = path.join(testRootDir, 'not-a-skill'); | |
| await fs.mkdir(notASkillDir, { recursive: true }); | |
| await fs.writeFile(path.join(notASkillDir, 'some-file.txt'), 'hello'); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(0); | |
| expect(debugLogger.debug).toHaveBeenCalledWith( | |
| expect.stringContaining('Failed to load skills from'), | |
| ); | |
| }); | |
| it('should ignore empty directories and not emit feedback', async () => { | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(0); | |
| expect(coreEvents.emitFeedback).not.toHaveBeenCalled(); | |
| }); | |
| it('should ignore directories without SKILL.md', async () => { | |
| const notASkillDir = path.join(testRootDir, 'not-a-skill'); | |
| await fs.mkdir(notASkillDir, { recursive: true }); | |
| // With a subdirectory, even if empty, it might still trigger readdir | |
| // But my current logic is if discoveredSkills.length === 0, then check readdir | |
| // If readdir is empty, it's fine. | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(0); | |
| // If notASkillDir is empty, no warning. | |
| }); | |
| it('should ignore SKILL.md without valid frontmatter and emit warning if directory is not empty', async () => { | |
| const skillDir = path.join(testRootDir, 'invalid-skill'); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| const skillFile = path.join(skillDir, 'SKILL.md'); | |
| await fs.writeFile(skillFile, '# No frontmatter here'); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(0); | |
| expect(debugLogger.debug).toHaveBeenCalledWith( | |
| expect.stringContaining('Failed to load skills from'), | |
| ); | |
| }); | |
| it('should return empty array for non-existent directory', async () => { | |
| const skills = await loadSkillsFromDir('/non/existent/path'); | |
| expect(skills).toEqual([]); | |
| expect(coreEvents.emitFeedback).not.toHaveBeenCalled(); | |
| }); | |
| it('should parse skill with colon in description (issue #16323)', async () => { | |
| const skillDir = path.join(testRootDir, 'colon-skill'); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| const skillFile = path.join(skillDir, 'SKILL.md'); | |
| await fs.writeFile( | |
| skillFile, | |
| `--- | |
| name: foo | |
| description: Simple story generation assistant for fiction writing. Use for creating characters, scenes, storylines, and prose. Trigger words: character, scene, storyline, story, prose, fiction, writing. | |
| --- | |
| # Instructions | |
| Do something. | |
| `, | |
| ); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].name).toBe('foo'); | |
| expect(skills[0].description).toContain('Trigger words:'); | |
| }); | |
| it('should parse skill with multiple colons in description', async () => { | |
| const skillDir = path.join(testRootDir, 'multi-colon-skill'); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| const skillFile = path.join(skillDir, 'SKILL.md'); | |
| await fs.writeFile( | |
| skillFile, | |
| `--- | |
| name: multi-colon | |
| description: Use this for tasks like: coding, reviewing, testing. Keywords: async, await, promise. | |
| --- | |
| # Instructions | |
| Do something. | |
| `, | |
| ); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].name).toBe('multi-colon'); | |
| expect(skills[0].description).toContain('tasks like:'); | |
| expect(skills[0].description).toContain('Keywords:'); | |
| }); | |
| it('should parse skill with quoted YAML description (backward compatibility)', async () => { | |
| const skillDir = path.join(testRootDir, 'quoted-skill'); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| const skillFile = path.join(skillDir, 'SKILL.md'); | |
| await fs.writeFile( | |
| skillFile, | |
| `--- | |
| name: quoted-skill | |
| description: "A skill with colons: like this one: and another." | |
| --- | |
| # Instructions | |
| Do something. | |
| `, | |
| ); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].name).toBe('quoted-skill'); | |
| expect(skills[0].description).toBe( | |
| 'A skill with colons: like this one: and another.', | |
| ); | |
| }); | |
| it('should parse skill with multi-line YAML description', async () => { | |
| const skillDir = path.join(testRootDir, 'multiline-skill'); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| const skillFile = path.join(skillDir, 'SKILL.md'); | |
| await fs.writeFile( | |
| skillFile, | |
| `--- | |
| name: multiline-skill | |
| description: | |
| Expertise in reviewing code for style, security, and performance. Use when the | |
| user asks for "feedback," a "review," or to "check" their changes. | |
| --- | |
| # Instructions | |
| Do something. | |
| `, | |
| ); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].name).toBe('multiline-skill'); | |
| expect(skills[0].description).toContain('Expertise in reviewing code'); | |
| expect(skills[0].description).toContain('check'); | |
| }); | |
| it('should handle empty name or description', async () => { | |
| const skillDir = path.join(testRootDir, 'empty-skill'); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| const skillFile = path.join(skillDir, 'SKILL.md'); | |
| await fs.writeFile( | |
| skillFile, | |
| `--- | |
| name: | |
| description: | |
| --- | |
| `, | |
| ); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].name).toBe(''); | |
| expect(skills[0].description).toBe(''); | |
| }); | |
| it('should handle indented name and description fields', async () => { | |
| const skillDir = path.join(testRootDir, 'indented-fields'); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| const skillFile = path.join(skillDir, 'SKILL.md'); | |
| await fs.writeFile( | |
| skillFile, | |
| `--- | |
| name: indented-name | |
| description: indented-desc | |
| --- | |
| `, | |
| ); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].name).toBe('indented-name'); | |
| expect(skills[0].description).toBe('indented-desc'); | |
| }); | |
| it('should handle missing space after colon', async () => { | |
| const skillDir = path.join(testRootDir, 'no-space'); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| const skillFile = path.join(skillDir, 'SKILL.md'); | |
| await fs.writeFile( | |
| skillFile, | |
| `--- | |
| name:no-space-name | |
| description:no-space-desc | |
| --- | |
| `, | |
| ); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].name).toBe('no-space-name'); | |
| expect(skills[0].description).toBe('no-space-desc'); | |
| }); | |
| it('should sanitize skill names containing invalid filename characters', async () => { | |
| const skillFile = path.join(testRootDir, 'SKILL.md'); | |
| await fs.writeFile( | |
| skillFile, | |
| `--- | |
| name: gke:prs-troubleshooter | |
| description: Test sanitization | |
| --- | |
| `, | |
| ); | |
| const skills = await loadSkillsFromDir(testRootDir); | |
| expect(skills).toHaveLength(1); | |
| expect(skills[0].name).toBe('gke-prs-troubleshooter'); | |
| }); | |
| it('should load real built-in antigravity-support skill successfully', async () => { | |
| const { fileURLToPath } = await import('node:url'); | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const builtinDir = path.resolve(__dirname, 'builtin'); | |
| const skills = await loadSkillsFromDir(builtinDir); | |
| const antigravitySkill = skills.find( | |
| (s) => s.name === 'antigravity-support', | |
| ); | |
| expect(antigravitySkill).toBeDefined(); | |
| expect(antigravitySkill!.description).toContain('Antigravity CLI'); | |
| expect(antigravitySkill!.body).toContain( | |
| 'https://antigravity.google/docs/cli-getting-started', | |
| ); | |
| }); | |
| }); | |