| |
| |
| |
| |
| |
|
|
| import { describe, it, expect } from 'vitest'; |
| import { parseSlashCommand } from './commands.js'; |
| import { CommandKind, type SlashCommand } from '../ui/commands/types.js'; |
|
|
| |
| const mockCommands: readonly SlashCommand[] = [ |
| { |
| name: 'help', |
| description: 'Show help', |
| action: async () => {}, |
| kind: CommandKind.BUILT_IN, |
| }, |
| { |
| name: 'commit', |
| description: 'Commit changes', |
| action: async () => {}, |
| kind: CommandKind.USER_FILE, |
| }, |
| { |
| name: 'memory', |
| description: 'Manage memory', |
| altNames: ['mem'], |
| subCommands: [ |
| { |
| name: 'list', |
| description: 'List memory files', |
| action: async () => {}, |
| kind: CommandKind.BUILT_IN, |
| }, |
| { |
| name: 'clear', |
| description: 'Clear memory', |
| altNames: ['c'], |
| action: async () => {}, |
| kind: CommandKind.BUILT_IN, |
| }, |
| ], |
| kind: CommandKind.BUILT_IN, |
| }, |
| ]; |
|
|
| describe('parseSlashCommand', () => { |
| it('should parse a simple command without arguments', () => { |
| const result = parseSlashCommand('/help', mockCommands); |
| expect(result.commandToExecute?.name).toBe('help'); |
| expect(result.args).toBe(''); |
| expect(result.canonicalPath).toEqual(['help']); |
| }); |
|
|
| it('should parse a simple command with arguments', () => { |
| const result = parseSlashCommand( |
| '/commit -m "Initial commit"', |
| mockCommands, |
| ); |
| expect(result.commandToExecute?.name).toBe('commit'); |
| expect(result.args).toBe('-m "Initial commit"'); |
| expect(result.canonicalPath).toEqual(['commit']); |
| }); |
|
|
| it('should parse a subcommand', () => { |
| const result = parseSlashCommand('/memory list', mockCommands); |
| expect(result.commandToExecute?.name).toBe('list'); |
| expect(result.args).toBe(''); |
| expect(result.canonicalPath).toEqual(['memory', 'list']); |
| }); |
|
|
| it('should parse a subcommand with arguments', () => { |
| const result = parseSlashCommand( |
| '/memory list some important data', |
| mockCommands, |
| ); |
| expect(result.commandToExecute?.name).toBe('list'); |
| expect(result.args).toBe('some important data'); |
| expect(result.canonicalPath).toEqual(['memory', 'list']); |
| }); |
|
|
| it('should handle a command alias', () => { |
| const result = parseSlashCommand('/mem list some data', mockCommands); |
| expect(result.commandToExecute?.name).toBe('list'); |
| expect(result.args).toBe('some data'); |
| expect(result.canonicalPath).toEqual(['memory', 'list']); |
| }); |
|
|
| it('should handle a subcommand alias', () => { |
| const result = parseSlashCommand('/memory c', mockCommands); |
| expect(result.commandToExecute?.name).toBe('clear'); |
| expect(result.args).toBe(''); |
| expect(result.canonicalPath).toEqual(['memory', 'clear']); |
| }); |
|
|
| it('should return undefined for an unknown command', () => { |
| const result = parseSlashCommand('/unknown', mockCommands); |
| expect(result.commandToExecute).toBeUndefined(); |
| expect(result.args).toBe('unknown'); |
| expect(result.canonicalPath).toEqual([]); |
| }); |
|
|
| it('should return the parent command if subcommand is unknown', () => { |
| const result = parseSlashCommand( |
| '/memory unknownsub some args', |
| mockCommands, |
| ); |
| expect(result.commandToExecute?.name).toBe('memory'); |
| expect(result.args).toBe('unknownsub some args'); |
| expect(result.canonicalPath).toEqual(['memory']); |
| }); |
|
|
| it('should handle extra whitespace', () => { |
| const result = parseSlashCommand( |
| ' /memory list some data ', |
| mockCommands, |
| ); |
| expect(result.commandToExecute?.name).toBe('list'); |
| expect(result.args).toBe('some data'); |
| expect(result.canonicalPath).toEqual(['memory', 'list']); |
| }); |
|
|
| it('should return undefined if query does not start with a slash', () => { |
| const result = parseSlashCommand('help', mockCommands); |
| expect(result.commandToExecute).toBeUndefined(); |
| }); |
|
|
| it('should handle an empty query', () => { |
| const result = parseSlashCommand('', mockCommands); |
| expect(result.commandToExecute).toBeUndefined(); |
| }); |
|
|
| it('should handle a query with only a slash', () => { |
| const result = parseSlashCommand('/', mockCommands); |
| expect(result.commandToExecute).toBeUndefined(); |
| expect(result.args).toBe(''); |
| expect(result.canonicalPath).toEqual([]); |
| }); |
|
|
| describe('backtracking', () => { |
| const backtrackingCommands: readonly SlashCommand[] = [ |
| { |
| name: 'parent', |
| description: 'Parent command', |
| kind: CommandKind.BUILT_IN, |
| action: async () => {}, |
| subCommands: [ |
| { |
| name: 'notakes', |
| description: 'Subcommand that does not take arguments', |
| kind: CommandKind.BUILT_IN, |
| takesArgs: false, |
| action: async () => {}, |
| }, |
| { |
| name: 'takes', |
| description: 'Subcommand that takes arguments', |
| kind: CommandKind.BUILT_IN, |
| takesArgs: true, |
| action: async () => {}, |
| }, |
| ], |
| }, |
| ]; |
|
|
| it('should backtrack to parent if subcommand has takesArgs: false and args are provided', () => { |
| const result = parseSlashCommand( |
| '/parent notakes some prompt', |
| backtrackingCommands, |
| ); |
| expect(result.commandToExecute?.name).toBe('parent'); |
| expect(result.args).toBe('notakes some prompt'); |
| expect(result.canonicalPath).toEqual(['parent']); |
| }); |
|
|
| it('should NOT backtrack if subcommand has takesArgs: false but NO args are provided', () => { |
| const result = parseSlashCommand('/parent notakes', backtrackingCommands); |
| expect(result.commandToExecute?.name).toBe('notakes'); |
| expect(result.args).toBe(''); |
| expect(result.canonicalPath).toEqual(['parent', 'notakes']); |
| }); |
|
|
| it('should NOT backtrack if subcommand has takesArgs: true and args are provided', () => { |
| const result = parseSlashCommand( |
| '/parent takes some args', |
| backtrackingCommands, |
| ); |
| expect(result.commandToExecute?.name).toBe('takes'); |
| expect(result.args).toBe('some args'); |
| expect(result.canonicalPath).toEqual(['parent', 'takes']); |
| }); |
|
|
| it('should NOT backtrack if parent has NO action', () => { |
| const noActionCommands: readonly SlashCommand[] = [ |
| { |
| name: 'parent', |
| description: 'Parent without action', |
| kind: CommandKind.BUILT_IN, |
| subCommands: [ |
| { |
| name: 'notakes', |
| description: 'Subcommand without args', |
| kind: CommandKind.BUILT_IN, |
| takesArgs: false, |
| action: async () => {}, |
| }, |
| ], |
| }, |
| ]; |
| const result = parseSlashCommand( |
| '/parent notakes some args', |
| noActionCommands, |
| ); |
| |
| expect(result.commandToExecute?.name).toBe('notakes'); |
| expect(result.args).toBe('some args'); |
| expect(result.canonicalPath).toEqual(['parent', 'notakes']); |
| }); |
|
|
| it('should NOT backtrack if subcommand is NOT marked with takesArgs: false', () => { |
| const result = parseSlashCommand( |
| '/parent takes some args', |
| backtrackingCommands, |
| ); |
| expect(result.commandToExecute?.name).toBe('takes'); |
| expect(result.args).toBe('some args'); |
| expect(result.canonicalPath).toEqual(['parent', 'takes']); |
| }); |
|
|
| it('should backtrack if subcommand has takesArgs: false and args are provided (like /plan copy foo)', () => { |
| const result = parseSlashCommand( |
| '/parent notakes some prompt', |
| backtrackingCommands, |
| ); |
| expect(result.commandToExecute?.name).toBe('parent'); |
| expect(result.args).toBe('notakes some prompt'); |
| expect(result.canonicalPath).toEqual(['parent']); |
| }); |
| }); |
| }); |
|
|