mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 06:31:01 -07:00
216 lines
7.4 KiB
TypeScript
216 lines
7.4 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'node:fs/promises';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import { installSkill, linkSkill } from './skillUtils.js';
|
|
|
|
describe('skillUtils', () => {
|
|
let tempDir: string;
|
|
const projectRoot = path.resolve(__dirname, '../../../../../');
|
|
|
|
beforeEach(async () => {
|
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skill-utils-test-'));
|
|
vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const itif = (condition: boolean) => (condition ? it : it.skip);
|
|
|
|
describe('linkSkill', () => {
|
|
// TODO: issue 19388 - Enable linkSkill tests on Windows
|
|
itif(process.platform !== 'win32')(
|
|
'should successfully link from a local directory',
|
|
async () => {
|
|
// Create a mock skill directory
|
|
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
|
|
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
|
|
await fs.mkdir(skillSubDir, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(skillSubDir, 'SKILL.md'),
|
|
'---\nname: test-skill\ndescription: test\n---\nbody',
|
|
);
|
|
|
|
const skills = await linkSkill(
|
|
mockSkillSourceDir,
|
|
'workspace',
|
|
() => {},
|
|
);
|
|
expect(skills.length).toBe(1);
|
|
expect(skills[0].name).toBe('test-skill');
|
|
|
|
const linkedPath = path.join(tempDir, '.gemini/skills', 'test-skill');
|
|
const stats = await fs.lstat(linkedPath);
|
|
expect(stats.isSymbolicLink()).toBe(true);
|
|
|
|
const linkTarget = await fs.readlink(linkedPath);
|
|
expect(path.resolve(linkTarget)).toBe(path.resolve(skillSubDir));
|
|
},
|
|
);
|
|
|
|
itif(process.platform !== 'win32')(
|
|
'should overwrite existing skill at destination',
|
|
async () => {
|
|
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
|
|
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
|
|
await fs.mkdir(skillSubDir, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(skillSubDir, 'SKILL.md'),
|
|
'---\nname: test-skill\ndescription: test\n---\nbody',
|
|
);
|
|
|
|
const targetDir = path.join(tempDir, '.gemini/skills');
|
|
await fs.mkdir(targetDir, { recursive: true });
|
|
const existingPath = path.join(targetDir, 'test-skill');
|
|
await fs.mkdir(existingPath);
|
|
|
|
const skills = await linkSkill(
|
|
mockSkillSourceDir,
|
|
'workspace',
|
|
() => {},
|
|
);
|
|
expect(skills.length).toBe(1);
|
|
|
|
const stats = await fs.lstat(existingPath);
|
|
expect(stats.isSymbolicLink()).toBe(true);
|
|
},
|
|
);
|
|
|
|
it('should abort linking if consent is rejected', async () => {
|
|
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
|
|
const skillSubDir = path.join(mockSkillSourceDir, 'test-skill');
|
|
await fs.mkdir(skillSubDir, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(skillSubDir, 'SKILL.md'),
|
|
'---\nname: test-skill\ndescription: test\n---\nbody',
|
|
);
|
|
|
|
const requestConsent = vi.fn().mockResolvedValue(false);
|
|
|
|
await expect(
|
|
linkSkill(mockSkillSourceDir, 'workspace', () => {}, requestConsent),
|
|
).rejects.toThrow('Skill linking cancelled by user.');
|
|
|
|
expect(requestConsent).toHaveBeenCalled();
|
|
|
|
// Verify it was NOT linked
|
|
const linkedPath = path.join(tempDir, '.gemini/skills', 'test-skill');
|
|
const exists = await fs.lstat(linkedPath).catch(() => null);
|
|
expect(exists).toBeNull();
|
|
});
|
|
|
|
it('should throw error if multiple skills with same name are discovered', async () => {
|
|
const mockSkillSourceDir = path.join(tempDir, 'mock-skill-source');
|
|
const skillDir1 = path.join(mockSkillSourceDir, 'skill1');
|
|
const skillDir2 = path.join(mockSkillSourceDir, 'skill2');
|
|
await fs.mkdir(skillDir1, { recursive: true });
|
|
await fs.mkdir(skillDir2, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(skillDir1, 'SKILL.md'),
|
|
'---\nname: duplicate-skill\ndescription: desc1\n---\nbody1',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(skillDir2, 'SKILL.md'),
|
|
'---\nname: duplicate-skill\ndescription: desc2\n---\nbody2',
|
|
);
|
|
|
|
await expect(
|
|
linkSkill(mockSkillSourceDir, 'workspace', () => {}),
|
|
).rejects.toThrow('Duplicate skill name "duplicate-skill" found');
|
|
});
|
|
});
|
|
|
|
it('should successfully install from a .skill file', async () => {
|
|
const skillPath = path.join(projectRoot, 'weather-skill.skill');
|
|
|
|
// Ensure the file exists
|
|
const exists = await fs.stat(skillPath).catch(() => null);
|
|
if (!exists) {
|
|
// If we can't find it in CI or other environments, we skip or use a mock.
|
|
// For now, since it exists in the user's environment, this test will pass there.
|
|
return;
|
|
}
|
|
|
|
const skills = await installSkill(
|
|
skillPath,
|
|
'workspace',
|
|
undefined,
|
|
async () => {},
|
|
);
|
|
expect(skills.length).toBeGreaterThan(0);
|
|
expect(skills[0].name).toBe('weather-skill');
|
|
|
|
// Verify it was copied to the workspace skills dir
|
|
const installedPath = path.join(tempDir, '.gemini/skills', 'weather-skill');
|
|
const installedExists = await fs.stat(installedPath).catch(() => null);
|
|
expect(installedExists?.isDirectory()).toBe(true);
|
|
|
|
const skillMdExists = await fs
|
|
.stat(path.join(installedPath, 'SKILL.md'))
|
|
.catch(() => null);
|
|
expect(skillMdExists?.isFile()).toBe(true);
|
|
});
|
|
|
|
it('should successfully install from a local directory', async () => {
|
|
// Create a mock skill directory
|
|
const mockSkillDir = path.join(tempDir, 'mock-skill-source');
|
|
const skillSubDir = path.join(mockSkillDir, 'test-skill');
|
|
await fs.mkdir(skillSubDir, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(skillSubDir, 'SKILL.md'),
|
|
'---\nname: test-skill\ndescription: test\n---\nbody',
|
|
);
|
|
|
|
const skills = await installSkill(
|
|
mockSkillDir,
|
|
'workspace',
|
|
undefined,
|
|
async () => {},
|
|
);
|
|
expect(skills.length).toBe(1);
|
|
expect(skills[0].name).toBe('test-skill');
|
|
|
|
const installedPath = path.join(tempDir, '.gemini/skills', 'test-skill');
|
|
const installedExists = await fs.stat(installedPath).catch(() => null);
|
|
expect(installedExists?.isDirectory()).toBe(true);
|
|
});
|
|
|
|
it('should abort installation if consent is rejected', async () => {
|
|
const mockSkillDir = path.join(tempDir, 'mock-skill-source');
|
|
const skillSubDir = path.join(mockSkillDir, 'test-skill');
|
|
await fs.mkdir(skillSubDir, { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(skillSubDir, 'SKILL.md'),
|
|
'---\nname: test-skill\ndescription: test\n---\nbody',
|
|
);
|
|
|
|
const requestConsent = vi.fn().mockResolvedValue(false);
|
|
|
|
await expect(
|
|
installSkill(
|
|
mockSkillDir,
|
|
'workspace',
|
|
undefined,
|
|
async () => {},
|
|
requestConsent,
|
|
),
|
|
).rejects.toThrow('Skill installation cancelled by user.');
|
|
|
|
expect(requestConsent).toHaveBeenCalled();
|
|
|
|
// Verify it was NOT copied
|
|
const installedPath = path.join(tempDir, '.gemini/skills', 'test-skill');
|
|
const installedExists = await fs.stat(installedPath).catch(() => null);
|
|
expect(installedExists).toBeNull();
|
|
});
|
|
});
|