From 8f4a718f957d5d8fdb6264552ef46bb1c2ce4047 Mon Sep 17 00:00:00 2001 From: xxxigm Date: Sun, 14 Jun 2026 17:02:21 +0700 Subject: [PATCH] test(discord): guard slash-command registration against the 100 cap Registers 200 plugin commands on top of the native + COMMAND_REGISTRY set and asserts the tree never exceeds Discord's 100-command limit, that native high-priority commands survive the cap, and that overflow is actually dropped. Regression guard for the recurring error 30032 ("Maximum number of application commands reached") sync failures. --- tests/gateway/test_discord_slash_commands.py | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/gateway/test_discord_slash_commands.py b/tests/gateway/test_discord_slash_commands.py index 8d44f77302e..5ef6812f537 100644 --- a/tests/gateway/test_discord_slash_commands.py +++ b/tests/gateway/test_discord_slash_commands.py @@ -292,6 +292,58 @@ async def test_plugin_command_name_conflict_skipped(adapter): ) +# ------------------------------------------------------------------ +# 100-command cap (Discord error 30032 guard) +# ------------------------------------------------------------------ + + +@pytest.mark.asyncio +async def test_slash_command_registration_stays_under_discord_limit(adapter): + """Registering far more commands than Discord allows must NOT push the + tree over the 100-command hard cap. + + Discord rejects the ENTIRE command sync with error 30032 once the + desired set exceeds 100 global application commands, silently breaking + every slash command. The adapter must bound the desired set instead. + Regression guard for samuraiheart's recurring + "Maximum number of application commands reached (100)" sync failures. + """ + from plugins.platforms.discord.adapter import _DISCORD_MAX_APP_COMMANDS + + adapter._run_simple_slash = AsyncMock() + + # 200 plugin commands — way past Discord's limit on their own. + many_plugins = { + f"plug{i:03d}": { + "handler": lambda _a: "ok", + "description": f"Plugin command {i}", + "args_hint": "", + "plugin": "stress-plugin", + } + for i in range(200) + } + + with patch("hermes_cli.plugins.get_plugin_commands", return_value=many_plugins): + adapter._register_slash_commands() + + tree_names = set(adapter._client.tree.commands.keys()) + + # Contract: never exceed Discord's hard cap. + assert len(tree_names) <= _DISCORD_MAX_APP_COMMANDS, ( + f"registered {len(tree_names)} commands — exceeds Discord's " + f"{_DISCORD_MAX_APP_COMMANDS} limit and would fail sync with 30032" + ) + + # Native, high-priority commands are registered first and must survive + # the cap — they are the core UX, not droppable overflow. + for native in ("status", "stop", "new", "model", "help"): + assert native in tree_names, f"/{native} (native) was dropped by the cap" + + # The cap must actually have dropped overflow — not every plugin fit. + registered_plugins = [n for n in tree_names if n.startswith("plug")] + assert len(registered_plugins) < 200, "cap did not drop any overflow commands" + + # ------------------------------------------------------------------ # _handle_thread_create_slash — success, session dispatch, failure # ------------------------------------------------------------------