qwen-code/packages/cli/src/services/FileCommandLoader-extension.test.ts
2026-01-20 11:59:14 +08:00

343 lines
9.3 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, afterEach, vi } from 'vitest';
import * as path from 'node:path';
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', () => {
const projectRoot = '/test/project';
const userCommandsDir = Storage.getUserCommandsDir();
const projectCommandsDir = path.join(projectRoot, '.qwen', 'commands');
afterEach(() => {
mock.restore();
});
it('should load commands from extension with config.commands path', async () => {
const extensionDir = path.join(
projectRoot,
'.qwen',
'extensions',
'test-ext',
);
const extensionConfig = {
name: 'test-ext',
version: '1.0.0',
commands: 'custom-cmds',
};
mock({
[userCommandsDir]: {},
[projectCommandsDir]: {},
[extensionDir]: {
'qwen-extension.json': JSON.stringify(extensionConfig),
'custom-cmds': {
'test.md':
'---\ndescription: Test command from extension\n---\nDo something',
},
},
});
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');
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 () => {
const extensionDir = path.join(
projectRoot,
'.qwen',
'extensions',
'multi-ext',
);
const extensionConfig = {
name: 'multi-ext',
version: '1.0.0',
commands: ['commands1', 'commands2'],
};
mock({
[userCommandsDir]: {},
[projectCommandsDir]: {},
[extensionDir]: {
'qwen-extension.json': JSON.stringify(extensionConfig),
commands1: {
'cmd1.md': '---\n---\nCommand 1',
},
commands2: {
'cmd2.md': '---\n---\nCommand 2',
},
},
});
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(['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 () => {
const extensionDir = path.join(
projectRoot,
'.qwen',
'extensions',
'default-ext',
);
const extensionConfig = {
name: 'default-ext',
version: '1.0.0',
};
mock({
[userCommandsDir]: {},
[projectCommandsDir]: {},
[extensionDir]: {
'qwen-extension.json': JSON.stringify(extensionConfig),
commands: {
'default.md': '---\n---\nDefault command',
},
},
});
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');
expect(commands[0].extensionName).toBe('default-ext');
});
it('should handle extension without commands directory gracefully', async () => {
const extensionDir = path.join(
projectRoot,
'.qwen',
'extensions',
'no-cmds-ext',
);
const extensionConfig = {
name: 'no-cmds-ext',
version: '1.0.0',
};
mock({
[userCommandsDir]: {},
[projectCommandsDir]: {},
[extensionDir]: {
'qwen-extension.json': JSON.stringify(extensionConfig),
// No commands directory
},
});
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 set extensionName property for extension commands', async () => {
const extensionDir = path.join(
projectRoot,
'.qwen',
'extensions',
'prefix-ext',
);
const extensionConfig = {
name: 'prefix-ext',
version: '1.0.0',
};
mock({
[userCommandsDir]: {},
[projectCommandsDir]: {},
[extensionDir]: {
'qwen-extension.json': JSON.stringify(extensionConfig),
commands: {
'mycommand.md': '---\n---\nMy command',
},
},
});
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('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 () => {
const ext1Dir = path.join(projectRoot, '.qwen', 'extensions', 'ext-b');
const ext2Dir = path.join(projectRoot, '.qwen', 'extensions', 'ext-a');
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',
},
},
});
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;
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].extensionName).toBe('ext-a');
expect(commands[1].extensionName).toBe('ext-b');
});
});