2026-04-01 18:54:02 +08:00
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
|
import { parseCommand, parseCommandSchema, validateCommand } from '../../src/utils/command';
|
|
|
|
|
|
|
|
|
|
describe('parseCommandSchema', () => {
|
|
|
|
|
it('should parse empty schema', () => {
|
|
|
|
|
const schema = parseCommandSchema('');
|
|
|
|
|
expect(schema).toEqual({
|
|
|
|
|
name: '',
|
|
|
|
|
params: [],
|
2026-04-02 08:29:40 +08:00
|
|
|
options: {},
|
|
|
|
|
flags: {},
|
2026-04-01 18:54:02 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse required params', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> <to>');
|
|
|
|
|
expect(schema.params).toEqual([
|
|
|
|
|
{ name: 'from', required: true, variadic: false },
|
|
|
|
|
{ name: 'to', required: true, variadic: false },
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse optional params', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> [to]');
|
|
|
|
|
expect(schema.params).toEqual([
|
|
|
|
|
{ name: 'from', required: true, variadic: false },
|
|
|
|
|
{ name: 'to', required: false, variadic: false },
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse variadic params', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> [targets...]');
|
|
|
|
|
expect(schema.params).toEqual([
|
|
|
|
|
{ name: 'from', required: true, variadic: false },
|
|
|
|
|
{ name: 'targets', required: false, variadic: true },
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse required variadic params', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <targets...>');
|
|
|
|
|
expect(schema.params).toEqual([
|
|
|
|
|
{ name: 'targets', required: true, variadic: true },
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse long flags', () => {
|
|
|
|
|
const schema = parseCommandSchema('move [--force] [--quiet]');
|
2026-04-02 08:29:40 +08:00
|
|
|
expect(schema.flags).toEqual({
|
|
|
|
|
force: { name: 'force', short: undefined },
|
|
|
|
|
quiet: { name: 'quiet', short: undefined },
|
|
|
|
|
});
|
2026-04-01 18:54:02 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse short flags', () => {
|
|
|
|
|
const schema = parseCommandSchema('move [-f] [-q]');
|
2026-04-02 08:29:40 +08:00
|
|
|
expect(schema.flags).toEqual({
|
|
|
|
|
f: { name: 'f', short: 'f' },
|
|
|
|
|
q: { name: 'q', short: 'q' },
|
|
|
|
|
});
|
2026-04-01 18:54:02 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse long options', () => {
|
2026-04-01 21:18:58 +08:00
|
|
|
const schema = parseCommandSchema('move --x: string [--y: string]');
|
2026-04-02 08:29:40 +08:00
|
|
|
expect(Object.keys(schema.options)).toEqual(['x', 'y']);
|
|
|
|
|
expect(schema.options.x).toMatchObject({ name: 'x', required: true });
|
|
|
|
|
expect(schema.options.x.schema).toBeDefined();
|
|
|
|
|
expect(schema.options.y).toMatchObject({ name: 'y', required: false });
|
|
|
|
|
expect(schema.options.y.schema).toBeDefined();
|
2026-04-01 18:54:02 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse short options', () => {
|
2026-04-01 21:18:58 +08:00
|
|
|
const schema = parseCommandSchema('move -x: string [-y: string]');
|
2026-04-02 08:29:40 +08:00
|
|
|
expect(Object.keys(schema.options)).toEqual(['x', 'y']);
|
|
|
|
|
expect(schema.options.x).toMatchObject({ name: 'x', short: 'x', required: true });
|
|
|
|
|
expect(schema.options.x.schema).toBeDefined();
|
|
|
|
|
expect(schema.options.y).toMatchObject({ name: 'y', short: 'y', required: false });
|
|
|
|
|
expect(schema.options.y.schema).toBeDefined();
|
2026-04-01 18:54:02 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse mixed schema', () => {
|
2026-04-01 21:18:58 +08:00
|
|
|
const schema = parseCommandSchema('move <from> <to> [--force] [-f] [--speed: string -s]');
|
2026-04-01 18:54:02 +08:00
|
|
|
expect(schema).toEqual({
|
|
|
|
|
name: 'move',
|
|
|
|
|
params: [
|
2026-04-01 21:18:58 +08:00
|
|
|
{ name: 'from', required: true, variadic: false, schema: undefined },
|
|
|
|
|
{ name: 'to', required: true, variadic: false, schema: undefined },
|
2026-04-01 18:54:02 +08:00
|
|
|
],
|
2026-04-02 08:29:40 +08:00
|
|
|
flags: {
|
|
|
|
|
force: { name: 'force', short: undefined },
|
|
|
|
|
f: { name: 'f', short: 'f' },
|
|
|
|
|
},
|
|
|
|
|
options: {
|
|
|
|
|
speed: { name: 'speed', short: 's', required: false, schema: expect.any(Object), defaultValue: undefined },
|
|
|
|
|
},
|
2026-04-01 18:54:02 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle complex schema', () => {
|
2026-04-01 21:18:58 +08:00
|
|
|
const schema = parseCommandSchema('place <piece> <region> [x...] [--rotate: number] [--force] [-f]');
|
2026-04-01 18:54:02 +08:00
|
|
|
expect(schema.name).toBe('place');
|
|
|
|
|
expect(schema.params).toHaveLength(3);
|
2026-04-02 08:29:40 +08:00
|
|
|
expect(Object.keys(schema.flags)).toHaveLength(2);
|
|
|
|
|
expect(Object.keys(schema.options)).toHaveLength(1);
|
2026-04-01 18:54:02 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('validateCommand', () => {
|
|
|
|
|
it('should validate correct command', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> <to>');
|
|
|
|
|
const command = parseCommand('move meeple1 region1');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result).toEqual({ valid: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should reject wrong command name', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from>');
|
|
|
|
|
const command = parseCommand('place meeple1');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
valid: false,
|
|
|
|
|
errors: expect.arrayContaining([
|
|
|
|
|
expect.stringContaining('命令名称不匹配'),
|
|
|
|
|
]),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should reject missing required params', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> <to>');
|
|
|
|
|
const command = parseCommand('move meeple1');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
valid: false,
|
|
|
|
|
errors: expect.arrayContaining([
|
|
|
|
|
expect.stringContaining('参数不足'),
|
|
|
|
|
]),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should accept optional params missing', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> [to]');
|
|
|
|
|
const command = parseCommand('move meeple1');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result).toEqual({ valid: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should reject extra params without variadic', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> <to>');
|
|
|
|
|
const command = parseCommand('move meeple1 region1 extra');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
valid: false,
|
|
|
|
|
errors: expect.arrayContaining([
|
|
|
|
|
expect.stringContaining('参数过多'),
|
|
|
|
|
]),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should accept extra params with variadic', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> [targets...]');
|
|
|
|
|
const command = parseCommand('move meeple1 region1 region2 region3');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result).toEqual({ valid: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should reject missing required option', () => {
|
2026-04-01 21:18:58 +08:00
|
|
|
const schema = parseCommandSchema('move <from> --speed: string');
|
2026-04-01 18:54:02 +08:00
|
|
|
const command = parseCommand('move meeple1');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result).toEqual({
|
|
|
|
|
valid: false,
|
|
|
|
|
errors: expect.arrayContaining([
|
|
|
|
|
expect.stringContaining('缺少必需选项'),
|
|
|
|
|
]),
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should accept present required option', () => {
|
2026-04-01 21:18:58 +08:00
|
|
|
const schema = parseCommandSchema('move <from> --speed: string');
|
2026-04-01 18:54:02 +08:00
|
|
|
const command = parseCommand('move meeple1 --speed 10');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result).toEqual({ valid: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should accept optional option missing', () => {
|
2026-04-01 21:18:58 +08:00
|
|
|
const schema = parseCommandSchema('move <from> [--speed: string]');
|
2026-04-01 18:54:02 +08:00
|
|
|
const command = parseCommand('move meeple1');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result).toEqual({ valid: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should accept flags present or not', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> [--force]');
|
|
|
|
|
const cmd1 = parseCommand('move meeple1');
|
|
|
|
|
const cmd2 = parseCommand('move meeple1 --force');
|
|
|
|
|
expect(validateCommand(cmd1, schema)).toEqual({ valid: true });
|
|
|
|
|
expect(validateCommand(cmd2, schema)).toEqual({ valid: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should validate short form option', () => {
|
2026-04-01 21:18:58 +08:00
|
|
|
const schema = parseCommandSchema('move <from> -s: string');
|
2026-04-01 18:54:02 +08:00
|
|
|
const command = parseCommand('move meeple1 -s 10');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result).toEqual({ valid: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should provide detailed error messages', () => {
|
2026-04-01 21:18:58 +08:00
|
|
|
const schema = parseCommandSchema('place <piece> <region> --rotate: string');
|
2026-04-01 18:54:02 +08:00
|
|
|
const command = parseCommand('place meeple1');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result.valid).toBe(false);
|
|
|
|
|
if (!result.valid) {
|
|
|
|
|
expect(result.errors.length).toBeGreaterThanOrEqual(1);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('integration', () => {
|
|
|
|
|
it('should work together parse and validate', () => {
|
2026-04-01 21:18:58 +08:00
|
|
|
const schemaStr = 'place <piece> <region> [--x: string] [--y: string] [--force] [-f]';
|
2026-04-01 18:54:02 +08:00
|
|
|
const schema = parseCommandSchema(schemaStr);
|
|
|
|
|
|
|
|
|
|
const validCmd = parseCommand('place meeple1 board --x 5 --force');
|
|
|
|
|
expect(validateCommand(validCmd, schema)).toEqual({ valid: true });
|
|
|
|
|
|
|
|
|
|
const invalidCmd = parseCommand('place meeple1');
|
|
|
|
|
const result = validateCommand(invalidCmd, schema);
|
|
|
|
|
expect(result.valid).toBe(false);
|
|
|
|
|
});
|
2026-04-01 21:18:58 +08:00
|
|
|
|
|
|
|
|
it('should parse short alias syntax', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> [--verbose: boolean -v]');
|
2026-04-02 08:29:40 +08:00
|
|
|
expect(Object.keys(schema.flags)).toHaveLength(1);
|
|
|
|
|
expect(schema.flags.verbose).toEqual({ name: 'verbose', short: 'v' });
|
2026-04-01 21:18:58 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse command with short alias', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> [--verbose -v]');
|
|
|
|
|
const command = parseCommand('move meeple1 -v');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result.valid).toBe(true);
|
|
|
|
|
expect(command.flags.v).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should parse command with short alias option', () => {
|
|
|
|
|
const schema = parseCommandSchema('move <from> [--speed: number -s]');
|
|
|
|
|
const command = parseCommand('move meeple1 -s 100');
|
|
|
|
|
const result = validateCommand(command, schema);
|
|
|
|
|
expect(result.valid).toBe(true);
|
|
|
|
|
expect(command.options.s).toBe('100');
|
|
|
|
|
});
|
2026-04-01 18:54:02 +08:00
|
|
|
});
|
2026-04-02 08:29:40 +08:00
|
|
|
|
|
|
|
|
|