import { ApplicationCommandOptionTypes, ApplicationCommandTypes, type CreateApplicationCommand } from '@discordeno/types' import { describe, expect, it, vi } from 'vitest' import type { InteractionData } from '..' import { type ExtractCommands, createCommandHandler } from './command_parser' // Test command definitions const TEST_COMMANDS = [ { name: 'simple', description: 'A simple command', type: ApplicationCommandTypes.ChatInput, options: [ { name: 'message', description: 'A message to echo', type: ApplicationCommandOptionTypes.String, required: true, }, { name: 'count', description: 'Number of times', type: ApplicationCommandOptionTypes.Integer, required: false, }, ], }, { name: 'complex', description: 'A command with subcommands', type: ApplicationCommandTypes.ChatInput, options: [ { name: 'list', description: 'List items', type: ApplicationCommandOptionTypes.SubCommand, options: [ { name: 'filter', description: 'Filter string', type: ApplicationCommandOptionTypes.String, required: false, }, ], }, { name: 'create', description: 'Create item', type: ApplicationCommandOptionTypes.SubCommand, options: [ { name: 'name', description: 'Item name', type: ApplicationCommandOptionTypes.String, required: true, }, { name: 'enabled', description: 'Is enabled', type: ApplicationCommandOptionTypes.Boolean, required: false, }, ], }, ], }, ] as const satisfies CreateApplicationCommand[] describe('createCommandHandler', () => { it('should handle a simple command with required string argument', async () => { const simpleHandler = vi.fn() const notFoundHandler = vi.fn() const handler = createCommandHandler({ handler: { simple: simpleHandler, complex: { list: vi.fn(), create: vi.fn(), }, }, notFoundHandler, }) const interactionData: InteractionData = { name: 'simple', options: [{ name: 'message', type: ApplicationCommandOptionTypes.String, value: 'Hello world' }], } await handler(interactionData) expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello world' }) expect(notFoundHandler).not.toHaveBeenCalled() }) it('should handle a simple command with optional arguments', async () => { const simpleHandler = vi.fn() const notFoundHandler = vi.fn() const handler = createCommandHandler({ handler: { simple: simpleHandler, complex: { list: vi.fn(), create: vi.fn(), }, }, notFoundHandler, }) const interactionData: InteractionData = { name: 'simple', options: [ { name: 'message', type: ApplicationCommandOptionTypes.String, value: 'Hello' }, { name: 'count', type: ApplicationCommandOptionTypes.Integer, value: 5 }, ], } await handler(interactionData) expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello', count: 5 }) }) it('should handle subcommands correctly', async () => { const listHandler = vi.fn() const createHandler = vi.fn() const notFoundHandler = vi.fn() const handler = createCommandHandler({ handler: { simple: vi.fn(), complex: { list: listHandler, create: createHandler, }, }, notFoundHandler, }) const interactionData: InteractionData = { name: 'complex', options: [ { name: 'create', type: ApplicationCommandOptionTypes.SubCommand, options: [ { name: 'name', type: ApplicationCommandOptionTypes.String, value: 'Test Item' }, { name: 'enabled', type: ApplicationCommandOptionTypes.Boolean, value: true }, ], }, ], } await handler(interactionData) expect(createHandler).toHaveBeenCalledWith({ name: 'Test Item', enabled: true }) expect(listHandler).not.toHaveBeenCalled() expect(notFoundHandler).not.toHaveBeenCalled() }) it('should call notFoundHandler for unknown commands', async () => { const notFoundHandler = vi.fn() const handler = createCommandHandler({ handler: { simple: vi.fn(), complex: { list: vi.fn(), create: vi.fn(), }, }, notFoundHandler, }) const interactionData: InteractionData = { name: 'unknown', options: [], } await handler(interactionData) expect(notFoundHandler).toHaveBeenCalledWith({}) }) it('should call notFoundHandler for unknown subcommands', async () => { const notFoundHandler = vi.fn() const handler = createCommandHandler({ handler: { simple: vi.fn(), complex: { list: vi.fn(), create: vi.fn(), }, }, notFoundHandler, }) const interactionData: InteractionData = { name: 'complex', options: [ { name: 'unknown', type: ApplicationCommandOptionTypes.SubCommand, options: [], }, ], } await handler(interactionData) expect(notFoundHandler).toHaveBeenCalledWith({}) }) it('should handle missing interaction data', async () => { const notFoundHandler = vi.fn() const handler = createCommandHandler({ handler: { simple: vi.fn(), complex: { list: vi.fn(), create: vi.fn(), }, }, notFoundHandler, }) await handler(null as any) expect(notFoundHandler).toHaveBeenCalledWith({}) await handler({} as any) expect(notFoundHandler).toHaveBeenCalledTimes(2) }) it('should handle commands without options', async () => { const simpleHandler = vi.fn() const handler = createCommandHandler({ handler: { simple: simpleHandler, complex: { list: vi.fn(), create: vi.fn(), }, }, notFoundHandler: vi.fn(), }) const interactionData: InteractionData = { name: 'simple', // No options provided } await handler(interactionData) expect(simpleHandler).toHaveBeenCalledWith({}) }) it('should ignore subcommand options when parsing top-level args', async () => { const simpleHandler = vi.fn() const handler = createCommandHandler({ handler: { simple: simpleHandler, complex: { list: vi.fn(), create: vi.fn(), }, }, notFoundHandler: vi.fn(), }) const interactionData: InteractionData = { name: 'simple', options: [ { name: 'message', type: ApplicationCommandOptionTypes.String, value: 'Hello' }, // This should be ignored in parseOptions { name: 'subcommand', type: ApplicationCommandOptionTypes.SubCommand, options: [] }, ], } await handler(interactionData) expect(simpleHandler).toHaveBeenCalledWith({ message: 'Hello' }) }) }) describe('ExtractCommands type utility', () => { it('should correctly extract command types', () => { // Type tests - these compile-time checks ensure the types work correctly type TestHandlers = ExtractCommands // Test that the type structure is correct const handlers: TestHandlers = { simple: async (args) => { // TypeScript should know that args has message as required string and count as optional number const message: string = args.message const count: number | undefined = args.count expect(message).toBeDefined() expect(count).toBeDefined() }, complex: { list: async (args) => { // TypeScript should know that args has filter as optional string const filter: string | undefined = args.filter expect(filter).toBeDefined() }, create: async (args) => { // TypeScript should know that args has name as required string and enabled as optional boolean const name: string = args.name const enabled: boolean | undefined = args.enabled expect(name).toBeDefined() expect(enabled).toBeDefined() }, }, } // This is just to satisfy TypeScript - the real test is that the above compiles expect(handlers).toBeDefined() }) it('should infer types correctly without manual typing', () => { // This test verifies that args are inferred correctly without manual type annotations const handler = createCommandHandler({ notFoundHandler: async () => {}, handler: { simple: async (args) => { // args should be automatically typed // This would error if args.message wasn't a string const upper = args.message.toUpperCase() // This would error if args.count wasn't number | undefined const doubled = args.count ? args.count * 2 : 0 expect(upper).toBeDefined() expect(doubled).toBeDefined() }, complex: { list: async (args) => { // This would error if args.filter wasn't string | undefined const filtered = args.filter?.toLowerCase() expect(filtered !== undefined).toBeDefined() }, create: async (args) => { // This would error if types weren't correct const nameLength = args.name.length const isEnabled = args.enabled === true expect(nameLength).toBeDefined() expect(isEnabled).toBeDefined() }, }, }, }) expect(handler).toBeDefined() }) it('should handle commands with no options', () => { const EMPTY_COMMANDS = [ { name: 'ping', description: 'Ping command', type: ApplicationCommandTypes.ChatInput, }, ] as const satisfies CreateApplicationCommand[] type EmptyHandlers = ExtractCommands const handlers: EmptyHandlers = { ping: async (args) => { // args should be an empty object expect(args).toEqual({}) }, } expect(handlers).toBeDefined() }) it('should handle different option types', () => { const TYPE_TEST_COMMANDS = [ { name: 'types', description: 'Test different types', type: ApplicationCommandTypes.ChatInput, options: [ { name: 'str', description: 'String param', type: ApplicationCommandOptionTypes.String, required: true }, { name: 'int', description: 'Integer param', type: ApplicationCommandOptionTypes.Integer, required: true }, { name: 'bool', description: 'Boolean param', type: ApplicationCommandOptionTypes.Boolean, required: true }, { name: 'user', description: 'User param', type: ApplicationCommandOptionTypes.User, required: true }, { name: 'channel', description: 'Channel param', type: ApplicationCommandOptionTypes.Channel, required: true }, { name: 'role', description: 'Role param', type: ApplicationCommandOptionTypes.Role, required: true }, { name: 'num', description: 'Number param', type: ApplicationCommandOptionTypes.Number, required: true }, { name: 'mention', description: 'Mentionable param', type: ApplicationCommandOptionTypes.Mentionable, required: true }, { name: 'attach', description: 'Attachment param', type: ApplicationCommandOptionTypes.Attachment, required: true }, ], }, ] as const satisfies CreateApplicationCommand[] type TypeHandlers = ExtractCommands const handlers: TypeHandlers = { types: async (args) => { // Test that all types are correctly mapped const str: string = args.str const int: number = args.int const bool: boolean = args.bool const user: string = args.user const channel: string = args.channel const role: string = args.role const num: number = args.num const mention: string = args.mention const attach: string = args.attach expect(str).toBeDefined() expect(int).toBeDefined() expect(bool).toBeDefined() expect(user).toBeDefined() expect(channel).toBeDefined() expect(role).toBeDefined() expect(num).toBeDefined() expect(mention).toBeDefined() expect(attach).toBeDefined() }, } expect(handlers).toBeDefined() }) })