feat: 添加 Commands 系统支持斜杠命令
实现类似 OpenCode 的 Commands 功能: - 支持 Markdown 格式定义命令,带 YAML frontmatter - 变量替换:$ARGUMENTS, $1/$2, @filepath, !`shell` - 三级加载:builtin < user < project - 7 个内置命令:init, review, test, fix, explain, commit, help - 集成终端 UI 支持 /commands 列表和命令执行 - 完整单元测试覆盖 (46 tests)
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as childProcess from 'child_process';
|
||||
import {
|
||||
CommandExecutor,
|
||||
createCommandExecutor,
|
||||
getCommandRegistry,
|
||||
resetCommandRegistry,
|
||||
} from '../../../src/commands/index.js';
|
||||
import type { Command } from '../../../src/commands/types.js';
|
||||
|
||||
// Mock fs
|
||||
vi.mock('fs/promises', () => ({
|
||||
readFile: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock child_process
|
||||
vi.mock('child_process', () => ({
|
||||
exec: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock util.promisify to return our mocked exec
|
||||
vi.mock('util', () => ({
|
||||
promisify: (fn: unknown) => {
|
||||
if (fn === childProcess.exec) {
|
||||
return vi.fn().mockImplementation((cmd: string, _options: unknown) => {
|
||||
// 默认返回命令输出
|
||||
return Promise.resolve({ stdout: `output of: ${cmd}`, stderr: '' });
|
||||
});
|
||||
}
|
||||
return fn;
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock loader and builtin commands
|
||||
vi.mock('../../../src/commands/loader.js', () => ({
|
||||
commandLoader: {
|
||||
loadFromDirectory: vi.fn().mockResolvedValue([]),
|
||||
getUserCommandsDir: vi.fn().mockReturnValue('/mock/user/commands'),
|
||||
getProjectCommandsDir: vi.fn().mockReturnValue('/mock/project/commands'),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/commands/builtin/index.js', () => ({
|
||||
builtinCommands: [],
|
||||
}));
|
||||
|
||||
describe('CommandExecutor - Command 执行器', () => {
|
||||
let executor: CommandExecutor;
|
||||
|
||||
beforeEach(() => {
|
||||
resetCommandRegistry();
|
||||
executor = new CommandExecutor('/test/workdir');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('parseInput - 解析输入', () => {
|
||||
it('解析简单命令', () => {
|
||||
const result = executor.parseInput('/test');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.command).toBe('test');
|
||||
expect(result?.arguments).toBe('');
|
||||
expect(result?.args).toEqual([]);
|
||||
});
|
||||
|
||||
it('解析带参数的命令', () => {
|
||||
const result = executor.parseInput('/review main..feature');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.command).toBe('review');
|
||||
expect(result?.arguments).toBe('main..feature');
|
||||
expect(result?.args).toEqual(['main..feature']);
|
||||
});
|
||||
|
||||
it('解析多个参数', () => {
|
||||
const result = executor.parseInput('/deploy staging production');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.command).toBe('deploy');
|
||||
expect(result?.arguments).toBe('staging production');
|
||||
expect(result?.args).toEqual(['staging', 'production']);
|
||||
});
|
||||
|
||||
it('解析带引号的参数', () => {
|
||||
const result = executor.parseInput('/commit "fix: bug fix"');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.command).toBe('commit');
|
||||
expect(result?.args).toEqual(['fix: bug fix']);
|
||||
});
|
||||
|
||||
it('解析带单引号的参数', () => {
|
||||
const result = executor.parseInput("/test 'hello world' foo");
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.args).toEqual(['hello world', 'foo']);
|
||||
});
|
||||
|
||||
it('不是命令时返回 null', () => {
|
||||
const result = executor.parseInput('not a command');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('空输入返回 null', () => {
|
||||
const result = executor.parseInput('');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('包含工作目录', () => {
|
||||
const result = executor.parseInput('/test');
|
||||
expect(result?.workdir).toBe('/test/workdir');
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute - 执行命令', () => {
|
||||
beforeEach(async () => {
|
||||
const registry = getCommandRegistry();
|
||||
await registry.initialize('/test');
|
||||
|
||||
// 手动注册测试命令
|
||||
const testCommand: Command = {
|
||||
name: 'greet',
|
||||
description: '打招呼',
|
||||
template: 'Hello, $ARGUMENTS!',
|
||||
source: 'builtin',
|
||||
};
|
||||
registry.register(testCommand);
|
||||
});
|
||||
|
||||
it('执行存在的命令', async () => {
|
||||
const input = executor.parseInput('/greet World');
|
||||
expect(input).not.toBeNull();
|
||||
|
||||
const result = await executor.execute(input!);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.prompt).toBe('Hello, World!');
|
||||
});
|
||||
|
||||
it('命令不存在时返回错误', async () => {
|
||||
const input = executor.parseInput('/nonexistent');
|
||||
expect(input).not.toBeNull();
|
||||
|
||||
const result = await executor.execute(input!);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Command 不存在');
|
||||
});
|
||||
|
||||
it('返回 agent 配置', async () => {
|
||||
const registry = getCommandRegistry();
|
||||
const agentCommand: Command = {
|
||||
name: 'code',
|
||||
description: '编码',
|
||||
template: 'Write code',
|
||||
agent: 'coder',
|
||||
source: 'builtin',
|
||||
};
|
||||
registry.register(agentCommand);
|
||||
|
||||
const input = executor.parseInput('/code');
|
||||
const result = await executor.execute(input!);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.agent).toBe('coder');
|
||||
});
|
||||
|
||||
it('返回 model 配置', async () => {
|
||||
const registry = getCommandRegistry();
|
||||
const modelCommand: Command = {
|
||||
name: 'complex',
|
||||
description: '复杂任务',
|
||||
template: 'Complex task',
|
||||
model: 'opus',
|
||||
source: 'builtin',
|
||||
};
|
||||
registry.register(modelCommand);
|
||||
|
||||
const input = executor.parseInput('/complex');
|
||||
const result = await executor.execute(input!);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.model).toBe('opus');
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderTemplate - 模板渲染', () => {
|
||||
it('替换 $ARGUMENTS', async () => {
|
||||
const input = {
|
||||
command: 'test',
|
||||
arguments: 'hello world',
|
||||
args: ['hello', 'world'],
|
||||
workdir: '/test',
|
||||
};
|
||||
|
||||
const result = await executor.renderTemplate('Say: $ARGUMENTS', input);
|
||||
expect(result).toBe('Say: hello world');
|
||||
});
|
||||
|
||||
it('替换位置参数 $1(单个参数时获取全部)', async () => {
|
||||
const input = {
|
||||
command: 'test',
|
||||
arguments: 'foo bar',
|
||||
args: ['foo', 'bar'],
|
||||
workdir: '/test',
|
||||
};
|
||||
|
||||
// 当 $1 是最大参数索引时,获取所有剩余参数
|
||||
const result = await executor.renderTemplate('First: $1', input);
|
||||
expect(result).toBe('First: foo bar');
|
||||
});
|
||||
|
||||
it('替换多个位置参数', async () => {
|
||||
const input = {
|
||||
command: 'test',
|
||||
arguments: 'a b c',
|
||||
args: ['a', 'b', 'c'],
|
||||
workdir: '/test',
|
||||
};
|
||||
|
||||
const result = await executor.renderTemplate('$1 and $2', input);
|
||||
expect(result).toBe('a and b c');
|
||||
});
|
||||
|
||||
it('最后一个位置参数获取剩余所有', async () => {
|
||||
const input = {
|
||||
command: 'test',
|
||||
arguments: 'a b c d',
|
||||
args: ['a', 'b', 'c', 'd'],
|
||||
workdir: '/test',
|
||||
};
|
||||
|
||||
const result = await executor.renderTemplate('First: $1, Rest: $2', input);
|
||||
expect(result).toBe('First: a, Rest: b c d');
|
||||
});
|
||||
|
||||
it('缺少参数时替换为空字符串', async () => {
|
||||
const input = {
|
||||
command: 'test',
|
||||
arguments: '',
|
||||
args: [],
|
||||
workdir: '/test',
|
||||
};
|
||||
|
||||
const result = await executor.renderTemplate('Value: $1', input);
|
||||
expect(result).toBe('Value: ');
|
||||
});
|
||||
|
||||
it('处理文件引用 @filepath', async () => {
|
||||
const mockContent = 'file content here';
|
||||
vi.mocked(fs.readFile).mockResolvedValue(mockContent);
|
||||
|
||||
const input = {
|
||||
command: 'test',
|
||||
arguments: '',
|
||||
args: [],
|
||||
workdir: '/test',
|
||||
};
|
||||
|
||||
const result = await executor.renderTemplate('Review: @src/file.ts', input);
|
||||
expect(result).toContain('file content here');
|
||||
expect(result).toContain('```ts');
|
||||
});
|
||||
|
||||
it('文件不存在时显示提示', async () => {
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const input = {
|
||||
command: 'test',
|
||||
arguments: '',
|
||||
args: [],
|
||||
workdir: '/test',
|
||||
};
|
||||
|
||||
const result = await executor.renderTemplate('Review: @nonexistent.ts', input);
|
||||
expect(result).toContain('[文件不存在: nonexistent.ts]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCommandExecutor - 工厂函数', () => {
|
||||
it('创建执行器实例', () => {
|
||||
const exec = createCommandExecutor('/custom/path');
|
||||
expect(exec).toBeInstanceOf(CommandExecutor);
|
||||
});
|
||||
|
||||
it('默认使用 process.cwd', () => {
|
||||
const exec = createCommandExecutor();
|
||||
const input = exec.parseInput('/test');
|
||||
expect(input?.workdir).toBe(process.cwd());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { CommandLoader } from '../../../src/commands/loader.js';
|
||||
|
||||
// Mock fs/promises
|
||||
vi.mock('fs/promises', () => ({
|
||||
access: vi.fn(),
|
||||
readdir: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
describe('CommandLoader - Command 加载器', () => {
|
||||
let loader: CommandLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
loader = new CommandLoader();
|
||||
});
|
||||
|
||||
describe('parseMarkdownCommand - 解析 Markdown', () => {
|
||||
it('解析带 frontmatter 的 Markdown', () => {
|
||||
const content = `---
|
||||
description: 代码审查
|
||||
agent: explore
|
||||
model: sonnet
|
||||
subtask: true
|
||||
---
|
||||
|
||||
You are a code reviewer.
|
||||
|
||||
Input: $ARGUMENTS
|
||||
`;
|
||||
|
||||
const command = loader.parseMarkdownCommand(content, 'review', 'user');
|
||||
|
||||
expect(command).not.toBeNull();
|
||||
expect(command?.name).toBe('review');
|
||||
expect(command?.description).toBe('代码审查');
|
||||
expect(command?.agent).toBe('explore');
|
||||
expect(command?.model).toBe('sonnet');
|
||||
expect(command?.subtask).toBe(true);
|
||||
expect(command?.template).toContain('You are a code reviewer');
|
||||
expect(command?.template).toContain('$ARGUMENTS');
|
||||
expect(command?.source).toBe('user');
|
||||
});
|
||||
|
||||
it('解析不带 frontmatter 的 Markdown', () => {
|
||||
const content = `# Simple Command
|
||||
|
||||
This is a simple prompt template.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
|
||||
const command = loader.parseMarkdownCommand(content, 'simple', 'project');
|
||||
|
||||
expect(command).not.toBeNull();
|
||||
expect(command?.name).toBe('simple');
|
||||
expect(command?.description).toBeUndefined();
|
||||
expect(command?.template).toContain('Simple Command');
|
||||
expect(command?.template).toContain('$ARGUMENTS');
|
||||
expect(command?.source).toBe('project');
|
||||
});
|
||||
|
||||
it('空模板返回 null', () => {
|
||||
const content = `---
|
||||
description: Empty
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
const command = loader.parseMarkdownCommand(content, 'empty', 'user');
|
||||
|
||||
expect(command).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadFromFile - 从文件加载', () => {
|
||||
it('从 Markdown 文件加载 Command', async () => {
|
||||
const mdContent = `---
|
||||
description: Test command
|
||||
---
|
||||
|
||||
Test template $ARGUMENTS
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(mdContent);
|
||||
|
||||
const command = await loader.loadFromFile(
|
||||
'/base/commands/test.md',
|
||||
'/base/commands',
|
||||
'user'
|
||||
);
|
||||
|
||||
expect(command).not.toBeNull();
|
||||
expect(command?.name).toBe('test');
|
||||
expect(command?.description).toBe('Test command');
|
||||
});
|
||||
|
||||
it('处理嵌套目录路径', async () => {
|
||||
const mdContent = `---
|
||||
description: Deploy staging
|
||||
---
|
||||
|
||||
Deploy to staging
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(mdContent);
|
||||
|
||||
const command = await loader.loadFromFile(
|
||||
'/base/commands/deploy/staging.md',
|
||||
'/base/commands',
|
||||
'project'
|
||||
);
|
||||
|
||||
expect(command).not.toBeNull();
|
||||
expect(command?.name).toBe('deploy/staging');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadFromDirectory - 从目录加载', () => {
|
||||
it('目录不存在时返回空数组', async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
|
||||
|
||||
const commands = await loader.loadFromDirectory('/non-existent', 'user');
|
||||
|
||||
expect(commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('加载目录中的所有 Markdown 文件', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
{ name: 'cmd1.md', isFile: () => true, isDirectory: () => false },
|
||||
{ name: 'cmd2.md', isFile: () => true, isDirectory: () => false },
|
||||
{ name: 'readme.txt', isFile: () => true, isDirectory: () => false }, // 非 .md
|
||||
] as any);
|
||||
|
||||
vi.mocked(fs.readFile)
|
||||
.mockResolvedValueOnce('---\ndescription: Cmd1\n---\nTemplate 1')
|
||||
.mockResolvedValueOnce('---\ndescription: Cmd2\n---\nTemplate 2');
|
||||
|
||||
const commands = await loader.loadFromDirectory('/test', 'user');
|
||||
|
||||
expect(commands.length).toBe(2);
|
||||
expect(commands.map((c) => c.name)).toContain('cmd1');
|
||||
expect(commands.map((c) => c.name)).toContain('cmd2');
|
||||
});
|
||||
|
||||
it('递归加载子目录', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.readdir)
|
||||
.mockResolvedValueOnce([
|
||||
{ name: 'root.md', isFile: () => true, isDirectory: () => false },
|
||||
{ name: 'subdir', isFile: () => false, isDirectory: () => true },
|
||||
] as any)
|
||||
.mockResolvedValueOnce([
|
||||
{ name: 'sub.md', isFile: () => true, isDirectory: () => false },
|
||||
] as any);
|
||||
|
||||
vi.mocked(fs.readFile)
|
||||
.mockResolvedValueOnce('Root template')
|
||||
.mockResolvedValueOnce('Sub template');
|
||||
|
||||
const commands = await loader.loadFromDirectory('/test', 'project');
|
||||
|
||||
expect(commands.length).toBe(2);
|
||||
expect(commands.map((c) => c.name)).toContain('root');
|
||||
expect(commands.map((c) => c.name)).toContain('subdir/sub');
|
||||
});
|
||||
|
||||
it('跳过隐藏目录', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
{ name: '.hidden', isFile: () => false, isDirectory: () => true },
|
||||
{ name: 'visible.md', isFile: () => true, isDirectory: () => false },
|
||||
] as any);
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue('Template');
|
||||
|
||||
const commands = await loader.loadFromDirectory('/test', 'user');
|
||||
|
||||
expect(commands.length).toBe(1);
|
||||
expect(commands[0].name).toBe('visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('目录路径', () => {
|
||||
it('getUserCommandsDir 返回用户 Commands 目录', () => {
|
||||
const originalHome = process.env.HOME;
|
||||
process.env.HOME = '/home/testuser';
|
||||
|
||||
const dir = loader.getUserCommandsDir();
|
||||
|
||||
expect(dir).toBe('/home/testuser/.config/ai-terminal/commands');
|
||||
|
||||
process.env.HOME = originalHome;
|
||||
});
|
||||
|
||||
it('getProjectCommandsDir 返回项目 Commands 目录', () => {
|
||||
const dir = loader.getProjectCommandsDir('/workspace');
|
||||
|
||||
expect(dir).toBe('/workspace/.ai-terminal/commands');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,250 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
CommandRegistry,
|
||||
getCommandRegistry,
|
||||
resetCommandRegistry,
|
||||
} from '../../../src/commands/registry.js';
|
||||
import type { Command } from '../../../src/commands/types.js';
|
||||
|
||||
// Mock loader
|
||||
vi.mock('../../../src/commands/loader.js', () => ({
|
||||
commandLoader: {
|
||||
loadFromDirectory: vi.fn().mockResolvedValue([]),
|
||||
getUserCommandsDir: vi.fn().mockReturnValue('/mock/user/commands'),
|
||||
getProjectCommandsDir: vi.fn().mockReturnValue('/mock/project/commands'),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock builtin commands
|
||||
vi.mock('../../../src/commands/builtin/index.js', () => ({
|
||||
builtinCommands: [
|
||||
{
|
||||
name: 'test-builtin',
|
||||
description: '内置测试命令',
|
||||
template: '测试模板 $ARGUMENTS',
|
||||
source: 'builtin',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
describe('CommandRegistry - Command 注册表', () => {
|
||||
let registry: CommandRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
resetCommandRegistry();
|
||||
registry = new CommandRegistry();
|
||||
});
|
||||
|
||||
describe('register - 注册', () => {
|
||||
it('成功注册 Command', () => {
|
||||
const command: Command = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
template: '模板',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
registry.register(command);
|
||||
expect(registry.has('test')).toBe(true);
|
||||
});
|
||||
|
||||
it('高优先级可以覆盖低优先级', () => {
|
||||
const builtin: Command = {
|
||||
name: 'same',
|
||||
description: '内置版本',
|
||||
template: '内置模板',
|
||||
source: 'builtin',
|
||||
};
|
||||
|
||||
const project: Command = {
|
||||
name: 'same',
|
||||
description: '项目版本',
|
||||
template: '项目模板',
|
||||
source: 'project',
|
||||
};
|
||||
|
||||
registry.register(builtin);
|
||||
registry.register(project);
|
||||
|
||||
const result = registry.get('same');
|
||||
expect(result?.source).toBe('project');
|
||||
expect(result?.description).toBe('项目版本');
|
||||
});
|
||||
|
||||
it('低优先级不能覆盖高优先级', () => {
|
||||
const project: Command = {
|
||||
name: 'same',
|
||||
description: '项目版本',
|
||||
template: '项目模板',
|
||||
source: 'project',
|
||||
};
|
||||
|
||||
const builtin: Command = {
|
||||
name: 'same',
|
||||
description: '内置版本',
|
||||
template: '内置模板',
|
||||
source: 'builtin',
|
||||
};
|
||||
|
||||
registry.register(project);
|
||||
registry.register(builtin);
|
||||
|
||||
const result = registry.get('same');
|
||||
expect(result?.source).toBe('project');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get - 获取', () => {
|
||||
it('获取存在的 Command', () => {
|
||||
const command: Command = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
template: '模板',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
registry.register(command);
|
||||
const result = registry.get('test');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.name).toBe('test');
|
||||
});
|
||||
|
||||
it('获取不存在的 Command 返回 undefined', () => {
|
||||
const result = registry.get('non-existent');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search - 搜索', () => {
|
||||
beforeEach(() => {
|
||||
const commands: Command[] = [
|
||||
{
|
||||
name: 'review',
|
||||
description: '代码审查',
|
||||
template: '审查模板',
|
||||
source: 'builtin',
|
||||
},
|
||||
{
|
||||
name: 'test',
|
||||
description: '运行测试',
|
||||
template: '测试模板',
|
||||
source: 'builtin',
|
||||
},
|
||||
{
|
||||
name: 'deploy/staging',
|
||||
description: '部署到 staging',
|
||||
template: '部署模板',
|
||||
source: 'project',
|
||||
},
|
||||
];
|
||||
|
||||
for (const cmd of commands) {
|
||||
registry.register(cmd);
|
||||
}
|
||||
});
|
||||
|
||||
it('按名称精确匹配', () => {
|
||||
const results = registry.search('review');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].command.name).toBe('review');
|
||||
expect(results[0].score).toBe(100);
|
||||
});
|
||||
|
||||
it('按名称前缀匹配', () => {
|
||||
const results = registry.search('deploy');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].command.name).toBe('deploy/staging');
|
||||
});
|
||||
|
||||
it('按描述匹配', () => {
|
||||
const results = registry.search('代码');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].command.name).toBe('review');
|
||||
});
|
||||
|
||||
it('限制结果数量', () => {
|
||||
const results = registry.search('', 1);
|
||||
expect(results.length).toBeLessThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list - 列表', () => {
|
||||
it('返回所有 Commands 的摘要', () => {
|
||||
const command: Command = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
template: '模板',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
registry.register(command);
|
||||
const list = registry.list();
|
||||
|
||||
expect(list.length).toBe(1);
|
||||
expect(list[0].name).toBe('test');
|
||||
expect(list[0].description).toBe('测试');
|
||||
expect(list[0].source).toBe('user');
|
||||
});
|
||||
|
||||
it('按名称排序', () => {
|
||||
const commands: Command[] = [
|
||||
{ name: 'zebra', description: '', template: '', source: 'user' },
|
||||
{ name: 'alpha', description: '', template: '', source: 'user' },
|
||||
{ name: 'beta', description: '', template: '', source: 'user' },
|
||||
];
|
||||
|
||||
for (const cmd of commands) {
|
||||
registry.register(cmd);
|
||||
}
|
||||
|
||||
const list = registry.list();
|
||||
expect(list[0].name).toBe('alpha');
|
||||
expect(list[1].name).toBe('beta');
|
||||
expect(list[2].name).toBe('zebra');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats - 统计', () => {
|
||||
it('返回正确的统计信息', () => {
|
||||
const commands: Command[] = [
|
||||
{ name: 'b1', description: '', template: '', source: 'builtin' },
|
||||
{ name: 'b2', description: '', template: '', source: 'builtin' },
|
||||
{ name: 'u1', description: '', template: '', source: 'user' },
|
||||
{ name: 'p1', description: '', template: '', source: 'project' },
|
||||
];
|
||||
|
||||
for (const cmd of commands) {
|
||||
registry.register(cmd);
|
||||
}
|
||||
|
||||
const stats = registry.getStats();
|
||||
|
||||
expect(stats.total).toBe(4);
|
||||
expect(stats.bySource.builtin).toBe(2);
|
||||
expect(stats.bySource.user).toBe(1);
|
||||
expect(stats.bySource.project).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCommandRegistry - 全局注册表', () => {
|
||||
beforeEach(() => {
|
||||
resetCommandRegistry();
|
||||
});
|
||||
|
||||
it('返回单例实例', () => {
|
||||
const registry1 = getCommandRegistry();
|
||||
const registry2 = getCommandRegistry();
|
||||
|
||||
expect(registry1).toBe(registry2);
|
||||
});
|
||||
|
||||
it('resetCommandRegistry 重置实例', () => {
|
||||
const registry1 = getCommandRegistry();
|
||||
resetCommandRegistry();
|
||||
const registry2 = getCommandRegistry();
|
||||
|
||||
expect(registry1).not.toBe(registry2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user