mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
195 lines
6.0 KiB
TypeScript
195 lines
6.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import { ExtensionManager } from './extension-manager.js';
|
|
import { debugLogger, coreEvents } from '@google/gemini-cli-core';
|
|
import { createTestMergedSettings } from './settings.js';
|
|
import { createExtension } from '../test-utils/createExtension.js';
|
|
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
|
|
|
|
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
|
|
const mockIntegrityManager = vi.hoisted(() => ({
|
|
verify: vi.fn().mockResolvedValue('verified'),
|
|
store: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
vi.mock('node:os', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('node:os')>();
|
|
return {
|
|
...actual,
|
|
homedir: mockHomedir,
|
|
};
|
|
});
|
|
|
|
// Mock @google/gemini-cli-core
|
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
|
const actual =
|
|
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
|
return {
|
|
...actual,
|
|
homedir: mockHomedir,
|
|
ExtensionIntegrityManager: vi
|
|
.fn()
|
|
.mockImplementation(() => mockIntegrityManager),
|
|
loadAgentsFromDirectory: vi
|
|
.fn()
|
|
.mockImplementation(async () => ({ agents: [], errors: [] })),
|
|
loadSkillsFromDir: (
|
|
await importOriginal<typeof import('@google/gemini-cli-core')>()
|
|
).loadSkillsFromDir,
|
|
};
|
|
});
|
|
|
|
describe('ExtensionManager skills validation', () => {
|
|
let extensionManager: ExtensionManager;
|
|
let tempDir: string;
|
|
let extensionsDir: string;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.spyOn(coreEvents, 'emitFeedback');
|
|
vi.spyOn(debugLogger, 'debug').mockImplementation(() => {});
|
|
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-test-'));
|
|
mockHomedir.mockReturnValue(tempDir);
|
|
|
|
// Create the extensions directory that ExtensionManager expects
|
|
extensionsDir = path.join(tempDir, '.gemini', EXTENSIONS_DIRECTORY_NAME);
|
|
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
|
|
extensionManager = new ExtensionManager({
|
|
settings: createTestMergedSettings({
|
|
telemetry: { enabled: false },
|
|
}),
|
|
requestConsent: vi.fn().mockResolvedValue(true),
|
|
requestSetting: vi.fn(),
|
|
workspaceDir: tempDir,
|
|
integrityManager: mockIntegrityManager,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
try {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
} catch {
|
|
// ignore
|
|
}
|
|
});
|
|
|
|
it('should emit a warning during install if skills directory is not empty but no skills are loaded', async () => {
|
|
// Create a source extension
|
|
const sourceDir = path.join(tempDir, 'source-ext');
|
|
createExtension({
|
|
extensionsDir: sourceDir, // createExtension appends name
|
|
name: 'skills-ext',
|
|
version: '1.0.0',
|
|
installMetadata: {
|
|
type: 'local',
|
|
source: path.join(sourceDir, 'skills-ext'),
|
|
},
|
|
});
|
|
const extensionPath = path.join(sourceDir, 'skills-ext');
|
|
|
|
// Add invalid skills content
|
|
const skillsDir = path.join(extensionPath, 'skills');
|
|
fs.mkdirSync(skillsDir);
|
|
fs.writeFileSync(path.join(skillsDir, 'not-a-skill.txt'), 'hello');
|
|
|
|
await extensionManager.loadExtensions();
|
|
|
|
await extensionManager.installOrUpdateExtension({
|
|
type: 'local',
|
|
source: extensionPath,
|
|
});
|
|
|
|
expect(debugLogger.debug).toHaveBeenCalledWith(
|
|
expect.stringContaining('Failed to load skills from'),
|
|
);
|
|
});
|
|
|
|
it('should emit a warning during load if skills directory is not empty but no skills are loaded', async () => {
|
|
// 1. Create a source extension
|
|
const sourceDir = path.join(tempDir, 'source-ext-load');
|
|
createExtension({
|
|
extensionsDir: sourceDir,
|
|
name: 'skills-ext-load',
|
|
version: '1.0.0',
|
|
});
|
|
const sourceExtPath = path.join(sourceDir, 'skills-ext-load');
|
|
|
|
// Add invalid skills content
|
|
const skillsDir = path.join(sourceExtPath, 'skills');
|
|
fs.mkdirSync(skillsDir);
|
|
fs.writeFileSync(path.join(skillsDir, 'not-a-skill.txt'), 'hello');
|
|
|
|
// 2. Install it to ensure correct disk state
|
|
await extensionManager.loadExtensions();
|
|
await extensionManager.installOrUpdateExtension({
|
|
type: 'local',
|
|
source: sourceExtPath,
|
|
});
|
|
|
|
// Clear the spy
|
|
vi.mocked(debugLogger.debug).mockClear();
|
|
|
|
// 3. Create a fresh ExtensionManager to force loading from disk
|
|
const newExtensionManager = new ExtensionManager({
|
|
settings: createTestMergedSettings({
|
|
telemetry: { enabled: false },
|
|
}),
|
|
requestConsent: vi.fn().mockResolvedValue(true),
|
|
requestSetting: vi.fn(),
|
|
workspaceDir: tempDir,
|
|
integrityManager: mockIntegrityManager,
|
|
});
|
|
|
|
// 4. Load extensions
|
|
await newExtensionManager.loadExtensions();
|
|
|
|
expect(debugLogger.debug).toHaveBeenCalledWith(
|
|
expect.stringContaining('Failed to load skills from'),
|
|
);
|
|
});
|
|
|
|
it('should succeed if skills are correctly loaded', async () => {
|
|
const sourceDir = path.join(tempDir, 'source-ext-good');
|
|
createExtension({
|
|
extensionsDir: sourceDir,
|
|
name: 'good-skills-ext',
|
|
version: '1.0.0',
|
|
installMetadata: {
|
|
type: 'local',
|
|
source: path.join(sourceDir, 'good-skills-ext'),
|
|
},
|
|
});
|
|
const extensionPath = path.join(sourceDir, 'good-skills-ext');
|
|
|
|
const skillsDir = path.join(extensionPath, 'skills');
|
|
const skillSubdir = path.join(skillsDir, 'test-skill');
|
|
fs.mkdirSync(skillSubdir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(skillSubdir, 'SKILL.md'),
|
|
'---\nname: test-skill\ndescription: test desc\n---\nbody',
|
|
);
|
|
|
|
await extensionManager.loadExtensions();
|
|
|
|
const extension = await extensionManager.installOrUpdateExtension({
|
|
type: 'local',
|
|
source: extensionPath,
|
|
});
|
|
|
|
expect(extension.name).toBe('good-skills-ext');
|
|
expect(debugLogger.debug).not.toHaveBeenCalledWith(
|
|
expect.stringContaining('Failed to load skills from'),
|
|
);
|
|
});
|
|
});
|