wynn/ts/src/discord/botevent/command_parser.spec.ts

428 lines
13 KiB
TypeScript
Raw Normal View History

2025-06-14 21:17:42 +00:00
import { describe, it, expect, vi } from 'vitest'
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, CreateApplicationCommand } from '@discordeno/types'
import { createCommandHandler, ExtractCommands } from './command_parser'
import type { InteractionData } from '..'
// 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<typeof TEST_COMMANDS>({
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<typeof TEST_COMMANDS>({
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<typeof TEST_COMMANDS>({
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<typeof TEST_COMMANDS>({
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<typeof TEST_COMMANDS>({
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<typeof TEST_COMMANDS>({
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<typeof TEST_COMMANDS>({
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<typeof TEST_COMMANDS>({
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<typeof TEST_COMMANDS>
// 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<typeof TEST_COMMANDS>({
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<typeof EMPTY_COMMANDS>
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<typeof TYPE_TEST_COMMANDS>
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()
})
})