Files
FernandoJVideiraandClaude Sonnet 5.5 132a2cd986 Commands for everything a dashboard can do
- /music skip, pause, resume, stop, repeat, remove, move (DJ role)
- /config view, clear, toggletwitch, streamers, beta-features (server owner)
- /slvl status, remove-reward; /moderation bans, timeouts; /security history
- /beta is now the bot owner's DM-only command: set, off, servers, remove-server, status
- betasettings service and repo methods, with integration tests

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-23 02:10:05 +01:00

107 lines
3.3 KiB
Go

package commands
import (
"regexp"
"testing"
"unicode/utf8"
"github.com/bwmarrin/discordgo"
)
// Discord refuses a command that is over its limits, and then the whole
// command silently doesn't exist. These checks catch that in a test instead
// of after a deploy. See
// https://discord.com/developers/docs/interactions/application-commands
var commandName = regexp.MustCompile(`^[-_\p{L}\p{N}]{1,32}$`)
func checkOptions(t *testing.T, path string, options []*discordgo.ApplicationCommandOption, size *int) {
t.Helper()
if len(options) > 25 {
t.Errorf("%s: %d options, at most 25 allowed", path, len(options))
}
seen := map[string]bool{}
sawOptional := false
for _, o := range options {
where := path + " " + o.Name
if !commandName.MatchString(o.Name) || o.Name != lower(o.Name) {
t.Errorf("%s: option names are 1 to 32 lowercase letters, digits, - or _", where)
}
if seen[o.Name] {
t.Errorf("%s: duplicate option name", where)
}
seen[o.Name] = true
if n := utf8.RuneCountInString(o.Description); n < 1 || n > 100 {
t.Errorf("%s: description is %d characters, must be 1 to 100", where, n)
}
*size += utf8.RuneCountInString(o.Name) + utf8.RuneCountInString(o.Description)
if len(o.Choices) > 25 {
t.Errorf("%s: %d choices, at most 25", where, len(o.Choices))
}
for _, c := range o.Choices {
*size += utf8.RuneCountInString(c.Name)
if s, ok := c.Value.(string); ok {
*size += utf8.RuneCountInString(s)
if utf8.RuneCountInString(s) > 100 || utf8.RuneCountInString(c.Name) > 100 {
t.Errorf("%s: a choice is over 100 characters", where)
}
}
}
switch o.Type {
case discordgo.ApplicationCommandOptionSubCommand, discordgo.ApplicationCommandOptionSubCommandGroup:
if len(o.Options) == 0 && o.Type == discordgo.ApplicationCommandOptionSubCommandGroup {
t.Errorf("%s: a group needs subcommands", where)
}
checkOptions(t, where, o.Options, size)
default:
// Required options come before optional ones.
if o.Required && sawOptional {
t.Errorf("%s: a required option after an optional one", where)
}
if !o.Required {
sawOptional = true
}
}
}
}
func lower(s string) string {
out := []rune(s)
for i, r := range out {
if r >= 'A' && r <= 'Z' {
out[i] = r + 32
}
}
return string(out)
}
func TestCommandsStayWithinDiscordLimits(t *testing.T) {
all := append(append(append([]*discordgo.ApplicationCommand{}, AllCommands...), BetaCommands...), OwnerCommands...)
names := map[string]bool{}
for _, c := range all {
if names[c.Name] {
t.Errorf("/%s is defined twice", c.Name)
}
names[c.Name] = true
if !commandName.MatchString(c.Name) || c.Name != lower(c.Name) {
t.Errorf("/%s: names are 1 to 32 lowercase letters, digits, - or _", c.Name)
}
if n := utf8.RuneCountInString(c.Description); n < 1 || n > 100 {
t.Errorf("/%s: description is %d characters, must be 1 to 100", c.Name, n)
}
size := utf8.RuneCountInString(c.Name) + utf8.RuneCountInString(c.Description)
checkOptions(t, "/"+c.Name, c.Options, &size)
if size > 8000 {
t.Errorf("/%s: %d characters in total, Discord allows 8000", c.Name, size)
}
if _, ok := handlers[c.Name]; !ok {
t.Errorf("/%s has no handler", c.Name)
}
}
for name := range handlers {
if !names[name] {
t.Errorf("a handler for /%s exists but the command is never registered", name)
}
}
}