Agent Skills: Implement /skills reload (#15865)

This commit is contained in:
N. Taylor Mullen
2026-01-05 15:12:51 -08:00
committed by GitHub
parent 8f0324d868
commit 2cb33b2f76
11 changed files with 468 additions and 9 deletions
@@ -1,21 +1,22 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { skillsCommand } from './skillsCommand.js';
import { MessageType } from '../types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import type { CommandContext } from './types.js';
import type { Config } from '@google/gemini-cli-core';
import type { Config, SkillDefinition } from '@google/gemini-cli-core';
import { SettingScope, type LoadedSettings } from '../../config/settings.js';
describe('skillsCommand', () => {
let context: CommandContext;
beforeEach(() => {
vi.useFakeTimers();
const skills = [
{
name: 'skill1',
@@ -35,6 +36,7 @@ describe('skillsCommand', () => {
config: {
getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue(skills),
getSkills: vi.fn().mockReturnValue(skills),
getSkill: vi
.fn()
.mockImplementation(
@@ -51,6 +53,11 @@ describe('skillsCommand', () => {
});
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it('should add a SKILLS_LIST item to UI with descriptions by default', async () => {
await skillsCommand.action!(context, '');
@@ -187,6 +194,170 @@ describe('skillsCommand', () => {
});
});
describe('reload', () => {
it('should reload skills successfully and show success message', async () => {
const reloadCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'reload',
)!;
// Make reload take some time so timer can fire
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 200));
});
// @ts-expect-error Mocking reloadSkills
context.services.config.reloadSkills = reloadSkillsMock;
const actionPromise = reloadCmd.action!(context, '');
// Initially, no pending item (flicker prevention)
expect(context.ui.setPendingItem).not.toHaveBeenCalled();
// Fast forward 100ms to trigger the pending item
await vi.advanceTimersByTimeAsync(100);
expect(context.ui.setPendingItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: 'Reloading agent skills...',
}),
);
// Fast forward another 100ms (reload complete), but pending item should stay
await vi.advanceTimersByTimeAsync(100);
expect(context.ui.setPendingItem).not.toHaveBeenCalledWith(null);
// Fast forward to reach 500ms total
await vi.advanceTimersByTimeAsync(300);
await actionPromise;
expect(reloadSkillsMock).toHaveBeenCalled();
expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: 'Agent skills reloaded successfully.',
}),
expect.any(Number),
);
});
it('should show new skills count after reload', async () => {
const reloadCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'reload',
)!;
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
const skillManager = context.services.config!.getSkillManager();
vi.mocked(skillManager.getSkills).mockReturnValue([
{ name: 'skill1' },
{ name: 'skill2' },
{ name: 'skill3' },
] as SkillDefinition[]);
});
// @ts-expect-error Mocking reloadSkills
context.services.config.reloadSkills = reloadSkillsMock;
await reloadCmd.action!(context, '');
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: 'Agent skills reloaded successfully. 1 newly available skill.',
}),
expect.any(Number),
);
});
it('should show removed skills count after reload', async () => {
const reloadCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'reload',
)!;
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
const skillManager = context.services.config!.getSkillManager();
vi.mocked(skillManager.getSkills).mockReturnValue([
{ name: 'skill1' },
] as SkillDefinition[]);
});
// @ts-expect-error Mocking reloadSkills
context.services.config.reloadSkills = reloadSkillsMock;
await reloadCmd.action!(context, '');
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: 'Agent skills reloaded successfully. 1 skill no longer available.',
}),
expect.any(Number),
);
});
it('should show both added and removed skills count after reload', async () => {
const reloadCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'reload',
)!;
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
const skillManager = context.services.config!.getSkillManager();
vi.mocked(skillManager.getSkills).mockReturnValue([
{ name: 'skill2' }, // skill1 removed, skill3 added
{ name: 'skill3' },
] as SkillDefinition[]);
});
// @ts-expect-error Mocking reloadSkills
context.services.config.reloadSkills = reloadSkillsMock;
await reloadCmd.action!(context, '');
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: 'Agent skills reloaded successfully. 1 newly available skill and 1 skill no longer available.',
}),
expect.any(Number),
);
});
it('should show error if configuration is missing', async () => {
const reloadCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'reload',
)!;
context.services.config = null;
await reloadCmd.action!(context, '');
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: 'Could not retrieve configuration.',
}),
expect.any(Number),
);
});
it('should show error if reload fails', async () => {
const reloadCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'reload',
)!;
const error = new Error('Reload failed');
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
await new Promise((_, reject) => setTimeout(() => reject(error), 200));
});
// @ts-expect-error Mocking reloadSkills
context.services.config.reloadSkills = reloadSkillsMock;
const actionPromise = reloadCmd.action!(context, '');
await vi.advanceTimersByTimeAsync(100);
await vi.advanceTimersByTimeAsync(400);
await actionPromise;
expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: 'Failed to reload skills: Reload failed',
}),
expect.any(Number),
);
});
});
describe('completions', () => {
it('should provide completions for disable (only enabled skills)', async () => {
const disableCmd = skillsCommand.subCommands!.find(