import type { Logger, ModuleRegistry } from '@n8n/backend-common'; import { CliParser } from '@n8n/backend-common'; import { CommandMetadata } from '@n8n/decorators'; import { Container } from '@n8n/di'; import { mock } from 'jest-mock-extended'; import { z } from 'zod'; import { CommandRegistry } from '../command-registry'; jest.mock('fast-glob'); import glob from 'fast-glob'; describe('CommandRegistry', () => { let commandRegistry: CommandRegistry; let commandMetadata: CommandMetadata; const moduleRegistry = mock(); const logger = mock(); let originalProcessArgv: string[]; let mockProcessExit: jest.SpyInstance; const cliParser = new CliParser(logger); class TestCommand { flags: any; init = jest.fn(); run = jest.fn(); catch = jest.fn(); finally = jest.fn(); } beforeEach(() => { jest.resetAllMocks(); originalProcessArgv = process.argv; mockProcessExit = jest.spyOn(process, 'exit').mockImplementation((() => {}) as any); (glob as unknown as jest.Mock).mockResolvedValue([]); commandMetadata = new CommandMetadata(); Container.set(CommandMetadata, commandMetadata); commandMetadata.register('test-command', { description: 'Test command description', class: TestCommand, examples: ['--example'], flagsSchema: z.object({ flag1: z.string().describe('Flag one description').optional(), flag2: z.boolean().describe('Flag two description').optional(), shortFlag: z.number().alias('s').describe('Short flag with alias').optional(), }), }); Container.set(TestCommand, new TestCommand()); }); afterEach(() => { process.argv = originalProcessArgv; mockProcessExit.mockRestore(); jest.resetAllMocks(); }); it('should execute the specified command', async () => { process.argv = ['node', 'n8n', 'test-command']; commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger, cliParser); await commandRegistry.execute(); expect(moduleRegistry.loadModules).toHaveBeenCalled(); const commandInstance = Container.get(commandMetadata.get('test-command')!.class); expect(commandInstance.init).toHaveBeenCalled(); expect(commandInstance.run).toHaveBeenCalled(); expect(commandInstance.finally).toHaveBeenCalledWith(undefined); }); it('should handle command errors', async () => { process.argv = ['node', 'n8n', 'test-command']; const error = new Error('Test error'); const commandClass = commandMetadata.get('test-command')!.class; const commandInstance = Container.get(commandClass); commandInstance.run = jest.fn().mockRejectedValue(error); commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger, cliParser); await commandRegistry.execute(); expect(commandInstance.catch).toHaveBeenCalledWith(error); expect(commandInstance.finally).toHaveBeenCalledWith(error); }); it('should parse and apply command flags', async () => { process.argv = ['node', 'n8n', 'test-command', '--flag1', 'value1', '--flag2', '-s', '123']; commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger, cliParser); await commandRegistry.execute(); const commandInstance = Container.get(commandMetadata.get('test-command')!.class); expect(commandInstance.flags).toEqual({ flag1: 'value1', flag2: true, shortFlag: 123, }); }); it('should handle alias flags', async () => { process.argv = ['node', 'n8n', 'test-command', '--flag1', 'value1', '-s', '123']; commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger, cliParser); await commandRegistry.execute(); const commandInstance = Container.get(commandMetadata.get('test-command')!.class); expect(commandInstance.flags).toEqual({ flag1: 'value1', shortFlag: 123, }); }); it('should exit with error when command not found', async () => { process.argv = ['node', 'n8n', 'non-existent-command']; commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger, cliParser); await commandRegistry.execute(); expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('not found')); expect(mockProcessExit).toHaveBeenCalledWith(1); }); it('should display help when --help flag is used', async () => { process.argv = ['node', 'n8n', 'test-command', '--help']; commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger, cliParser); await commandRegistry.execute(); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('USAGE')); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('FLAGS')); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('DESCRIPTION')); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('EXAMPLES')); expect(mockProcessExit).toHaveBeenCalledWith(0); }); it('should list all commands when global help is requested', async () => { process.argv = ['node', 'n8n', '--help']; commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger, cliParser); await commandRegistry.execute(); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Available commands')); expect(mockProcessExit).toHaveBeenCalledWith(0); }); it('should display proper command usage with printCommandUsage', () => { process.argv = ['node', 'n8n', 'test-command']; commandRegistry = new CommandRegistry(commandMetadata, moduleRegistry, logger, cliParser); const commandEntry = commandMetadata.get('test-command')!; commandRegistry.printCommandUsage(commandEntry); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('USAGE')); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('test-command')); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('--flag1 ')); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('-s, --shortFlag ')); expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Flag one description')); }); });