Skip to content

Answer every wrong command invocation - #16

Merged
Isma-L154 merged 1 commit into
mainfrom
fix/command-argument-errors
Aug 21, 2026
Merged

Answer every wrong command invocation#16
Isma-L154 merged 1 commit into
mainfrom
fix/command-argument-errors

Conversation

@Isma-L154

Copy link
Copy Markdown
Owner

Closes #7

The bug

Several commands produced no response at all when called with missing or badly-typed arguments. The user types something, the bot says nothing, and there is no way to tell whether the input was wrong or the bot is down.

Input Before After
!volume (silence) Missing the vol argument. Usage: !volume <vol>
!remove (silence) Missing the index argument. Usage: !remove <index>
!move 1 (silence) Missing the to_pos argument. Usage: !move <from_pos> <to_pos>
!queue abc (silence) One of those arguments isn't the right type. Usage: !queue [page=1]
!remove abc (silence) One of those arguments isn't the right type. Usage: !remove <index>

Root cause

on_command_error bailed out on MissingRequiredArgument:

if isinstance(error, commands.MissingRequiredArgument):
    return                # per-command handlers deal with these

The comment was wrong — only play had such a handler. Every other command with a required argument fell through the gap. BadArgument was not special-cased at all, so it hit the generic branch, got logged server-side, and the user still saw nothing.

The fix

Handling moves to utils/errors.py and keys on UserInputError, the common base of MissingRequiredArgument, BadArgument, TooManyArguments and the rest — so no subclass can slip through unanswered as new commands are added.

The usage hint is generated from the Command object's own signature, not hardcoded. <name> for required parameters, [name=default] for optional ones, so it cannot drift when a parameter is renamed.

Cooldowns and MaxConcurrencyReached move there too: they apply to any command, not just play.

What still stays quiet

Deliberately, two cases:

  • CommandNotFound — people type a prefix by accident; the bot should not nag.
  • CheckFailure — the voice-state guards already send their own explanation.

Everything else that is not user input gets logged with a traceback (exc_info, which the old handler did not capture) and stays out of the channel, since an internal failure is the operator's problem.

Removed: the per-command handlers

@play.error and @volume.error are deleted — they now duplicate the central behaviour. This matters beyond tidiness: a private handler suppresses the central one, so leaving one in place is exactly how the silent-failure bug comes back for that command. test_no_music_command_keeps_a_private_error_handler guards against that (verified non-vacuous: it scans all 15 Music commands).

Why a new module rather than more code in main.py

Importing main.py runs config.validate(), which calls sys.exit(1) when DISCORD_TOKEN is unset — so nothing in it can be unit-tested. utils/errors.py imports cleanly, and main.py's event handler is now a one-liner delegating to it.

Verification

143 passed, 22 of them new.

Beyond the mocked cases, test_usage_matches_the_real_command_definitions reads signatures off the actual Command objects and asserts the rendered hint, so these strings are checked against the real commands:

!play <query>              !move <from_pos> <to_pos>
!volume <vol>              !queue [page=1]
!remove <index>            !loop [mode=track]
!skip

Also covered: the original exception being unwrapped from CommandInvokeError, a Forbidden on send not escalating out of the error handler, and ctx.command being None.

Startup was verified by booting main.py with a dummy token — all cogs and handlers load.

Several commands produced no response at all when called with missing or
badly-typed arguments. `!volume`, `!remove`, `!move`, `!move 1`, `!queue abc`
and `!remove abc` were all silent, which is indistinguishable from the bot
being offline.

on_command_error returned early for MissingRequiredArgument on the grounds that
"per-command handlers deal with these", but only `play` had one. BadArgument
was not special-cased at all, so it was logged server-side and the user saw
nothing.

Move the handling into utils.errors, keyed on UserInputError so every subclass
is covered, and reply with the command's own signature - `!move <from_pos>
<to_pos>` is generated from the Command object, so it cannot drift when a
parameter is renamed. Cooldowns and concurrency limits move there too, since
they apply to any command rather than just `play`.

The per-command @play.error and @volume.error handlers are removed. They now
duplicate the central behaviour, and a private handler suppresses the central
one, which is exactly how the silent-failure bug would come back. A test asserts
no Music command keeps one.

Splitting this out of main.py also makes it testable: importing main.py calls
config.validate(), which exits the process when DISCORD_TOKEN is unset.

Closes #7
@Isma-L154
Isma-L154 merged commit 49faff3 into main Aug 21, 2026
3 checks passed
@Isma-L154
Isma-L154 deleted the fix/command-argument-errors branch August 21, 2026 01:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Commands silently ignore missing or invalid arguments

1 participant