mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-03 06:00:49 +00:00
feat(skills): add bundled /review skill for out-of-the-box code review (#2348)
feat(skills): add bundled /review skill for out-of-the-box code review
This commit is contained in:
parent
f1ee4638b7
commit
1359563f45
11 changed files with 525 additions and 27 deletions
128
packages/cli/src/services/BundledSkillLoader.test.ts
Normal file
128
packages/cli/src/services/BundledSkillLoader.test.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { BundledSkillLoader } from './BundledSkillLoader.js';
|
||||
import { CommandKind } from '../ui/commands/types.js';
|
||||
import type { Config, SkillConfig } from '@qwen-code/qwen-code-core';
|
||||
|
||||
function makeSkill(overrides: Partial<SkillConfig> = {}): SkillConfig {
|
||||
return {
|
||||
name: 'review',
|
||||
description: 'Review code changes',
|
||||
level: 'bundled',
|
||||
filePath: '/bundled/review/SKILL.md',
|
||||
body: 'You are an expert code reviewer.',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('BundledSkillLoader', () => {
|
||||
let mockConfig: Config;
|
||||
let mockSkillManager: {
|
||||
listSkills: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSkillManager = {
|
||||
listSkills: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
mockConfig = {
|
||||
getSkillManager: vi.fn().mockReturnValue(mockSkillManager),
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
it('should return empty array when config is null', async () => {
|
||||
const loader = new BundledSkillLoader(null);
|
||||
const commands = await loader.loadCommands(signal);
|
||||
expect(commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when SkillManager is not available', async () => {
|
||||
const config = {
|
||||
getSkillManager: vi.fn().mockReturnValue(null),
|
||||
} as unknown as Config;
|
||||
const loader = new BundledSkillLoader(config);
|
||||
const commands = await loader.loadCommands(signal);
|
||||
expect(commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('should load bundled skills as slash commands', async () => {
|
||||
const skill = makeSkill();
|
||||
mockSkillManager.listSkills.mockResolvedValue([skill]);
|
||||
|
||||
const loader = new BundledSkillLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(signal);
|
||||
|
||||
expect(commands).toHaveLength(1);
|
||||
expect(commands[0].name).toBe('review');
|
||||
expect(commands[0].description).toBe('Review code changes');
|
||||
expect(commands[0].kind).toBe(CommandKind.SKILL);
|
||||
expect(mockSkillManager.listSkills).toHaveBeenCalledWith({
|
||||
level: 'bundled',
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit skill body as prompt without args', async () => {
|
||||
const skill = makeSkill();
|
||||
mockSkillManager.listSkills.mockResolvedValue([skill]);
|
||||
|
||||
const loader = new BundledSkillLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(signal);
|
||||
const result = await commands[0].action!(
|
||||
{ invocation: { raw: '/review', args: '' } } as never,
|
||||
'',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'submit_prompt',
|
||||
content: [{ text: 'You are an expert code reviewer.' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should append raw invocation when args are provided', async () => {
|
||||
const skill = makeSkill();
|
||||
mockSkillManager.listSkills.mockResolvedValue([skill]);
|
||||
|
||||
const loader = new BundledSkillLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(signal);
|
||||
const result = await commands[0].action!(
|
||||
{ invocation: { raw: '/review 123', args: '123' } } as never,
|
||||
'123',
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'submit_prompt',
|
||||
content: [{ text: 'You are an expert code reviewer.\n\n/review 123' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array when listSkills throws', async () => {
|
||||
mockSkillManager.listSkills.mockRejectedValue(new Error('load failed'));
|
||||
|
||||
const loader = new BundledSkillLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(signal);
|
||||
|
||||
expect(commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('should load multiple bundled skills', async () => {
|
||||
const skills = [
|
||||
makeSkill({ name: 'review', description: 'Review code' }),
|
||||
makeSkill({ name: 'deploy', description: 'Deploy app' }),
|
||||
];
|
||||
mockSkillManager.listSkills.mockResolvedValue(skills);
|
||||
|
||||
const loader = new BundledSkillLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(signal);
|
||||
|
||||
expect(commands).toHaveLength(2);
|
||||
expect(commands.map((c) => c.name)).toEqual(['review', 'deploy']);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue