mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 12:40:44 +00:00
fix ci test
This commit is contained in:
parent
143beb51ed
commit
b0c3e5d884
11 changed files with 381 additions and 293 deletions
|
|
@ -4,324 +4,340 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import { describe, it, expect, afterEach, vi } from 'vitest';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import mock from 'mock-fs';
|
||||
import { FileCommandLoader } from './FileCommandLoader.js';
|
||||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import { Storage } from '@qwen-code/qwen-code-core';
|
||||
|
||||
describe('FileCommandLoader - Extension Commands Support', () => {
|
||||
let tempDir: string;
|
||||
let mockConfig: Partial<Config>;
|
||||
const projectRoot = '/test/project';
|
||||
const userCommandsDir = Storage.getUserCommandsDir();
|
||||
const projectCommandsDir = path.join(projectRoot, '.qwen', 'commands');
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'file-command-loader-ext-test-'),
|
||||
);
|
||||
|
||||
mockConfig = {
|
||||
getFolderTrustFeature: () => false,
|
||||
getFolderTrust: () => true,
|
||||
getProjectRoot: () => tempDir,
|
||||
storage: new Storage(tempDir),
|
||||
getExtensions: () => [],
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('should load commands from extension with config.commands path', async () => {
|
||||
// Setup extension structure
|
||||
const extensionDir = path.join(tempDir, '.qwen', 'extensions', 'test-ext');
|
||||
const customCommandsDir = path.join(extensionDir, 'custom-cmds');
|
||||
await fs.promises.mkdir(customCommandsDir, { recursive: true });
|
||||
const extensionDir = path.join(
|
||||
projectRoot,
|
||||
'.qwen',
|
||||
'extensions',
|
||||
'test-ext',
|
||||
);
|
||||
|
||||
// Create extension config with custom commands path
|
||||
const extensionConfig = {
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
commands: 'custom-cmds',
|
||||
};
|
||||
await fs.promises.writeFile(
|
||||
path.join(extensionDir, 'qwen-extension.json'),
|
||||
JSON.stringify(extensionConfig),
|
||||
);
|
||||
|
||||
// Create a test command in custom directory
|
||||
const commandContent =
|
||||
'---\ndescription: Test command from extension\n---\nDo something';
|
||||
await fs.promises.writeFile(
|
||||
path.join(customCommandsDir, 'test.md'),
|
||||
commandContent,
|
||||
);
|
||||
|
||||
// Mock config to return the extension
|
||||
mockConfig.getExtensions = () => [
|
||||
{
|
||||
id: 'test-ext',
|
||||
config: extensionConfig,
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: extensionDir,
|
||||
contextFiles: [],
|
||||
mock({
|
||||
[userCommandsDir]: {},
|
||||
[projectCommandsDir]: {},
|
||||
[extensionDir]: {
|
||||
'qwen-extension.json': JSON.stringify(extensionConfig),
|
||||
'custom-cmds': {
|
||||
'test.md':
|
||||
'---\ndescription: Test command from extension\n---\nDo something',
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig as Config);
|
||||
const mockConfig = {
|
||||
getFolderTrustFeature: vi.fn(() => false),
|
||||
getFolderTrust: vi.fn(() => true),
|
||||
getProjectRoot: vi.fn(() => projectRoot),
|
||||
storage: new Storage(projectRoot),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
id: 'test-ext',
|
||||
config: extensionConfig,
|
||||
name: 'test-ext',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: extensionDir,
|
||||
contextFiles: [],
|
||||
},
|
||||
]),
|
||||
} as unknown as Config;
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(new AbortController().signal);
|
||||
|
||||
expect(commands).toHaveLength(1);
|
||||
expect(commands[0].name).toBe('test-ext:test');
|
||||
expect(commands[0].name).toBe('test');
|
||||
expect(commands[0].extensionName).toBe('test-ext');
|
||||
expect(commands[0].description).toBe(
|
||||
'[test-ext] Test command from extension',
|
||||
);
|
||||
});
|
||||
|
||||
it('should load commands from extension with multiple commands paths', async () => {
|
||||
// Setup extension structure
|
||||
const extensionDir = path.join(tempDir, '.qwen', 'extensions', 'multi-ext');
|
||||
const cmdsDir1 = path.join(extensionDir, 'commands1');
|
||||
const cmdsDir2 = path.join(extensionDir, 'commands2');
|
||||
await fs.promises.mkdir(cmdsDir1, { recursive: true });
|
||||
await fs.promises.mkdir(cmdsDir2, { recursive: true });
|
||||
const extensionDir = path.join(
|
||||
projectRoot,
|
||||
'.qwen',
|
||||
'extensions',
|
||||
'multi-ext',
|
||||
);
|
||||
|
||||
// Create extension config with multiple commands paths
|
||||
const extensionConfig = {
|
||||
name: 'multi-ext',
|
||||
version: '1.0.0',
|
||||
commands: ['commands1', 'commands2'],
|
||||
};
|
||||
await fs.promises.writeFile(
|
||||
path.join(extensionDir, 'qwen-extension.json'),
|
||||
JSON.stringify(extensionConfig),
|
||||
);
|
||||
|
||||
// Create test commands in both directories
|
||||
await fs.promises.writeFile(
|
||||
path.join(cmdsDir1, 'cmd1.md'),
|
||||
'---\n---\nCommand 1',
|
||||
);
|
||||
await fs.promises.writeFile(
|
||||
path.join(cmdsDir2, 'cmd2.md'),
|
||||
'---\n---\nCommand 2',
|
||||
);
|
||||
|
||||
// Mock config to return the extension
|
||||
mockConfig.getExtensions = () => [
|
||||
{
|
||||
id: 'multi-ext',
|
||||
config: extensionConfig,
|
||||
contextFiles: [],
|
||||
name: 'multi-ext',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: extensionDir,
|
||||
mock({
|
||||
[userCommandsDir]: {},
|
||||
[projectCommandsDir]: {},
|
||||
[extensionDir]: {
|
||||
'qwen-extension.json': JSON.stringify(extensionConfig),
|
||||
commands1: {
|
||||
'cmd1.md': '---\n---\nCommand 1',
|
||||
},
|
||||
commands2: {
|
||||
'cmd2.md': '---\n---\nCommand 2',
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig as Config);
|
||||
const mockConfig = {
|
||||
getFolderTrustFeature: vi.fn(() => false),
|
||||
getFolderTrust: vi.fn(() => true),
|
||||
getProjectRoot: vi.fn(() => projectRoot),
|
||||
storage: new Storage(projectRoot),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
id: 'multi-ext',
|
||||
config: extensionConfig,
|
||||
contextFiles: [],
|
||||
name: 'multi-ext',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: extensionDir,
|
||||
},
|
||||
]),
|
||||
} as unknown as Config;
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(new AbortController().signal);
|
||||
|
||||
expect(commands).toHaveLength(2);
|
||||
const commandNames = commands.map((c) => c.name).sort();
|
||||
expect(commandNames).toEqual(['multi-ext:cmd1', 'multi-ext:cmd2']);
|
||||
expect(commandNames).toEqual(['cmd1', 'cmd2']);
|
||||
expect(commands.every((c) => c.extensionName === 'multi-ext')).toBe(true);
|
||||
});
|
||||
|
||||
it('should fallback to default "commands" directory when config.commands not specified', async () => {
|
||||
// Setup extension structure with default commands directory
|
||||
const extensionDir = path.join(
|
||||
tempDir,
|
||||
projectRoot,
|
||||
'.qwen',
|
||||
'extensions',
|
||||
'default-ext',
|
||||
);
|
||||
const defaultCommandsDir = path.join(extensionDir, 'commands');
|
||||
await fs.promises.mkdir(defaultCommandsDir, { recursive: true });
|
||||
|
||||
// Create extension config without commands field
|
||||
const extensionConfig = {
|
||||
name: 'default-ext',
|
||||
version: '1.0.0',
|
||||
};
|
||||
await fs.promises.writeFile(
|
||||
path.join(extensionDir, 'qwen-extension.json'),
|
||||
JSON.stringify(extensionConfig),
|
||||
);
|
||||
|
||||
// Create a test command in default directory
|
||||
await fs.promises.writeFile(
|
||||
path.join(defaultCommandsDir, 'default.md'),
|
||||
'---\n---\nDefault command',
|
||||
);
|
||||
|
||||
// Mock config to return the extension
|
||||
mockConfig.getExtensions = () => [
|
||||
{
|
||||
id: 'default-ext',
|
||||
config: extensionConfig,
|
||||
contextFiles: [],
|
||||
name: 'default-ext',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: extensionDir,
|
||||
mock({
|
||||
[userCommandsDir]: {},
|
||||
[projectCommandsDir]: {},
|
||||
[extensionDir]: {
|
||||
'qwen-extension.json': JSON.stringify(extensionConfig),
|
||||
commands: {
|
||||
'default.md': '---\n---\nDefault command',
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig as Config);
|
||||
const mockConfig = {
|
||||
getFolderTrustFeature: vi.fn(() => false),
|
||||
getFolderTrust: vi.fn(() => true),
|
||||
getProjectRoot: vi.fn(() => projectRoot),
|
||||
storage: new Storage(projectRoot),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
id: 'default-ext',
|
||||
config: extensionConfig,
|
||||
contextFiles: [],
|
||||
name: 'default-ext',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: extensionDir,
|
||||
},
|
||||
]),
|
||||
} as unknown as Config;
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(new AbortController().signal);
|
||||
|
||||
expect(commands).toHaveLength(1);
|
||||
expect(commands[0].name).toBe('default-ext:default');
|
||||
expect(commands[0].name).toBe('default');
|
||||
expect(commands[0].extensionName).toBe('default-ext');
|
||||
});
|
||||
|
||||
it('should handle extension without commands directory gracefully', async () => {
|
||||
// Setup extension structure without commands directory
|
||||
const extensionDir = path.join(
|
||||
tempDir,
|
||||
projectRoot,
|
||||
'.qwen',
|
||||
'extensions',
|
||||
'no-cmds-ext',
|
||||
);
|
||||
await fs.promises.mkdir(extensionDir, { recursive: true });
|
||||
|
||||
// Create extension config
|
||||
const extensionConfig = {
|
||||
name: 'no-cmds-ext',
|
||||
version: '1.0.0',
|
||||
};
|
||||
await fs.promises.writeFile(
|
||||
path.join(extensionDir, 'qwen-extension.json'),
|
||||
JSON.stringify(extensionConfig),
|
||||
);
|
||||
|
||||
// Mock config to return the extension
|
||||
mockConfig.getExtensions = () => [
|
||||
{
|
||||
id: 'no-cmds-ext',
|
||||
config: extensionConfig,
|
||||
contextFiles: [],
|
||||
name: 'no-cmds-ext',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: extensionDir,
|
||||
mock({
|
||||
[userCommandsDir]: {},
|
||||
[projectCommandsDir]: {},
|
||||
[extensionDir]: {
|
||||
'qwen-extension.json': JSON.stringify(extensionConfig),
|
||||
// No commands directory
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig as Config);
|
||||
const mockConfig = {
|
||||
getFolderTrustFeature: vi.fn(() => false),
|
||||
getFolderTrust: vi.fn(() => true),
|
||||
getProjectRoot: vi.fn(() => projectRoot),
|
||||
storage: new Storage(projectRoot),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
id: 'no-cmds-ext',
|
||||
config: extensionConfig,
|
||||
contextFiles: [],
|
||||
name: 'no-cmds-ext',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: extensionDir,
|
||||
},
|
||||
]),
|
||||
} as unknown as Config;
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(new AbortController().signal);
|
||||
|
||||
// Should not throw and return empty array
|
||||
expect(commands).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should prefix extension commands with extension name', async () => {
|
||||
// Setup extension
|
||||
it('should set extensionName property for extension commands', async () => {
|
||||
const extensionDir = path.join(
|
||||
tempDir,
|
||||
projectRoot,
|
||||
'.qwen',
|
||||
'extensions',
|
||||
'prefix-ext',
|
||||
);
|
||||
const commandsDir = path.join(extensionDir, 'commands');
|
||||
await fs.promises.mkdir(commandsDir, { recursive: true });
|
||||
|
||||
const extensionConfig = {
|
||||
name: 'prefix-ext',
|
||||
version: '1.0.0',
|
||||
};
|
||||
await fs.promises.writeFile(
|
||||
path.join(extensionDir, 'qwen-extension.json'),
|
||||
JSON.stringify(extensionConfig),
|
||||
);
|
||||
|
||||
await fs.promises.writeFile(
|
||||
path.join(commandsDir, 'mycommand.md'),
|
||||
'---\n---\nMy command',
|
||||
);
|
||||
|
||||
mockConfig.getExtensions = () => [
|
||||
{
|
||||
id: 'prefix-ext',
|
||||
config: extensionConfig,
|
||||
contextFiles: [],
|
||||
name: 'prefix-ext',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: extensionDir,
|
||||
mock({
|
||||
[userCommandsDir]: {},
|
||||
[projectCommandsDir]: {},
|
||||
[extensionDir]: {
|
||||
'qwen-extension.json': JSON.stringify(extensionConfig),
|
||||
commands: {
|
||||
'mycommand.md': '---\n---\nMy command',
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig as Config);
|
||||
const mockConfig = {
|
||||
getFolderTrustFeature: vi.fn(() => false),
|
||||
getFolderTrust: vi.fn(() => true),
|
||||
getProjectRoot: vi.fn(() => projectRoot),
|
||||
storage: new Storage(projectRoot),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
id: 'prefix-ext',
|
||||
config: extensionConfig,
|
||||
contextFiles: [],
|
||||
name: 'prefix-ext',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: extensionDir,
|
||||
},
|
||||
]),
|
||||
} as unknown as Config;
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(new AbortController().signal);
|
||||
|
||||
expect(commands).toHaveLength(1);
|
||||
expect(commands[0].name).toBe('prefix-ext:mycommand');
|
||||
expect(commands[0].name).toBe('mycommand');
|
||||
expect(commands[0].extensionName).toBe('prefix-ext');
|
||||
expect(commands[0].description).toMatch(/^\[prefix-ext\]/);
|
||||
});
|
||||
|
||||
it('should load commands from multiple extensions in alphabetical order', async () => {
|
||||
// Setup two extensions
|
||||
const ext1Dir = path.join(tempDir, '.qwen', 'extensions', 'ext-b');
|
||||
const ext2Dir = path.join(tempDir, '.qwen', 'extensions', 'ext-a');
|
||||
const ext1Dir = path.join(projectRoot, '.qwen', 'extensions', 'ext-b');
|
||||
const ext2Dir = path.join(projectRoot, '.qwen', 'extensions', 'ext-a');
|
||||
|
||||
await fs.promises.mkdir(path.join(ext1Dir, 'commands'), {
|
||||
recursive: true,
|
||||
});
|
||||
await fs.promises.mkdir(path.join(ext2Dir, 'commands'), {
|
||||
recursive: true,
|
||||
mock({
|
||||
[userCommandsDir]: {},
|
||||
[projectCommandsDir]: {},
|
||||
[ext1Dir]: {
|
||||
'qwen-extension.json': JSON.stringify({
|
||||
name: 'ext-b',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
commands: {
|
||||
'cmd.md': '---\n---\nCommand B',
|
||||
},
|
||||
},
|
||||
[ext2Dir]: {
|
||||
'qwen-extension.json': JSON.stringify({
|
||||
name: 'ext-a',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
commands: {
|
||||
'cmd.md': '---\n---\nCommand A',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Extension B
|
||||
await fs.promises.writeFile(
|
||||
path.join(ext1Dir, 'qwen-extension.json'),
|
||||
JSON.stringify({ name: 'ext-b', version: '1.0.0' }),
|
||||
);
|
||||
await fs.promises.writeFile(
|
||||
path.join(ext1Dir, 'commands', 'cmd.md'),
|
||||
'---\n---\nCommand B',
|
||||
);
|
||||
const mockConfig = {
|
||||
getFolderTrustFeature: vi.fn(() => false),
|
||||
getFolderTrust: vi.fn(() => true),
|
||||
getProjectRoot: vi.fn(() => projectRoot),
|
||||
storage: new Storage(projectRoot),
|
||||
getExtensions: vi.fn(() => [
|
||||
{
|
||||
id: 'ext-b',
|
||||
config: { name: 'ext-b', version: '1.0.0' },
|
||||
contextFiles: [],
|
||||
name: 'ext-b',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: ext1Dir,
|
||||
},
|
||||
{
|
||||
id: 'ext-a',
|
||||
config: { name: 'ext-a', version: '1.0.0' },
|
||||
contextFiles: [],
|
||||
name: 'ext-a',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: ext2Dir,
|
||||
},
|
||||
]),
|
||||
} as unknown as Config;
|
||||
|
||||
// Extension A
|
||||
await fs.promises.writeFile(
|
||||
path.join(ext2Dir, 'qwen-extension.json'),
|
||||
JSON.stringify({ name: 'ext-a', version: '1.0.0' }),
|
||||
);
|
||||
await fs.promises.writeFile(
|
||||
path.join(ext2Dir, 'commands', 'cmd.md'),
|
||||
'---\n---\nCommand A',
|
||||
);
|
||||
|
||||
mockConfig.getExtensions = () => [
|
||||
{
|
||||
id: 'ext-b',
|
||||
config: { name: 'ext-b', version: '1.0.0' },
|
||||
contextFiles: [],
|
||||
name: 'ext-b',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: ext1Dir,
|
||||
},
|
||||
{
|
||||
id: 'ext-a',
|
||||
config: { name: 'ext-a', version: '1.0.0' },
|
||||
contextFiles: [],
|
||||
name: 'ext-a',
|
||||
version: '1.0.0',
|
||||
isActive: true,
|
||||
path: ext2Dir,
|
||||
},
|
||||
];
|
||||
|
||||
const loader = new FileCommandLoader(mockConfig as Config);
|
||||
const loader = new FileCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(new AbortController().signal);
|
||||
|
||||
expect(commands).toHaveLength(2);
|
||||
// Extensions are sorted alphabetically, so ext-a comes before ext-b
|
||||
expect(commands[0].name).toBe('ext-a:cmd');
|
||||
expect(commands[1].name).toBe('ext-b:cmd');
|
||||
expect(commands[0].extensionName).toBe('ext-a');
|
||||
expect(commands[1].extensionName).toBe('ext-b');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue