fix(gateway): resolve Telegram's underscored /commands to skill/plugin keys

Telegram's Bot API disallows hyphens in command names, so
_build_telegram_menu registers /claude-code as /claude_code. When the
user taps it from autocomplete, the gateway dispatch did a direct
lookup against skill_cmds (keyed on the hyphenated form) and missed,
silently falling through to the LLM as plain text. The model would
then typically call delegate_task, spawning a Hermes subagent instead
of invoking the intended skill.

Normalize underscores to hyphens in skill and plugin command lookup,
matching the existing pattern in _check_unavailable_skill.
This commit is contained in:
analista
2026-04-05 09:35:26 +00:00
committed by Teknium
parent afccbf253c
commit 4a75aec433
3 changed files with 82 additions and 5 deletions

View File

@@ -10,6 +10,7 @@ from agent.skill_commands import (
build_plan_path,
build_preloaded_skills_prompt,
build_skill_invocation_message,
resolve_skill_command_key,
scan_skill_commands,
)
@@ -101,6 +102,53 @@ class TestScanSkillCommands:
assert "/disabled-skill" not in result
class TestResolveSkillCommandKey:
"""Telegram bot-command names disallow hyphens, so the menu registers
skills with hyphens swapped for underscores. When Telegram autocomplete
sends the underscored form back, we need to find the hyphenated key.
"""
def test_hyphenated_form_matches_directly(self, tmp_path):
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
_make_skill(tmp_path, "claude-code")
scan_skill_commands()
assert resolve_skill_command_key("claude-code") == "/claude-code"
def test_underscore_form_resolves_to_hyphenated_skill(self, tmp_path):
"""/claude_code from Telegram autocomplete must resolve to /claude-code."""
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
_make_skill(tmp_path, "claude-code")
scan_skill_commands()
assert resolve_skill_command_key("claude_code") == "/claude-code"
def test_single_word_command_resolves(self, tmp_path):
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
_make_skill(tmp_path, "investigate")
scan_skill_commands()
assert resolve_skill_command_key("investigate") == "/investigate"
def test_unknown_command_returns_none(self, tmp_path):
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
_make_skill(tmp_path, "claude-code")
scan_skill_commands()
assert resolve_skill_command_key("does_not_exist") is None
assert resolve_skill_command_key("does-not-exist") is None
def test_empty_command_returns_none(self, tmp_path):
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
scan_skill_commands()
assert resolve_skill_command_key("") is None
def test_hyphenated_command_is_not_mangled(self, tmp_path):
"""A user-typed /foo-bar (hyphen) must not trigger the underscore fallback."""
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
_make_skill(tmp_path, "foo-bar")
scan_skill_commands()
assert resolve_skill_command_key("foo-bar") == "/foo-bar"
# Underscore form also works (Telegram round-trip)
assert resolve_skill_command_key("foo_bar") == "/foo-bar"
class TestBuildPreloadedSkillsPrompt:
def test_builds_prompt_for_multiple_named_skills(self, tmp_path):
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):