From bf301bc231349903f309f1da7f27930ae71ccbbc Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Wed, 24 Jun 2026 16:59:07 -0700 Subject: [PATCH] =?UTF-8?q?feat(telegram):=20/help=20+=20command=20menu=20?= =?UTF-8?q?=E2=80=94=20startup=20populate,=20single=20source,=20enriched?= =?UTF-8?q?=20(DPLAN-0220)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the build_botfather_commands design call (Patrick: KEEP, not delete). POPULATE: base_bot sets its Telegram command menu on startup (setMyCommands) after verify_connection, so every bot — base or minted — gets a populated slash-menu, not just create_bot'd ones (the live @aipass was hand-launched and had none). SYNC: build_botfather_commands (telegram_standards) is now the single source feeding base_bot-startup AND create_bot; DEFAULT_BOT_COMMANDS retired. The Telegram menu and /help list the same commands incl. /create + /cancel. ENRICH: friendlier command descriptions + /help intro/footer. Wiring the builder (vs deleting it as 'dead') lifted Unused_Function 92->93%. 6 new tests (menu==help sync, enriched copy, startup-menu, custom cmds); telegram 460/460, skills 252/252. Running bots need a restart to pick up the startup menu. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QEQZXCtgnF3NQtcttTErpq --- CHANGELOG.md | 9 ++ .../lib/telegram/apps/handlers/base_bot.py | 24 +++- .../lib/telegram/apps/handlers/bot_factory.py | 12 +- .../apps/handlers/telegram_standards.py | 18 +-- .../telegram/tests/test_multibot_config.py | 117 +++++++++++++++++- 5 files changed, 155 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccfb536b..3efa9500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,15 @@ PyPI version — not the changelog header. `set_secret('telegram', bot_id, config, as_json=True)` (fail-loud) so the create→@api→load round-trip works and the mother-bot can mint startable bots. New round-trip + fail-loud tests; telegram suite 454/454. + (6) **/help + Telegram command menu** — `setMyCommands` only ran inside + `create_bot`, so hand-launched bots (like the live `@aipass`) had no slash-menu, + and the menu list had drifted from `/help`; `base_bot` now sets its menu on + startup from a single source (`build_botfather_commands`, also used by + `create_bot` — `DEFAULT_BOT_COMMANDS` retired), so the Telegram menu and `/help` + list the same enriched commands incl. `/create`/`/cancel`. Wiring the builder + (rather than deleting it as "dead") also lifted Unused_Function 92→93%. 6 new + tests, telegram suite 460/460. (A running bot needs a restart to pick up the + startup menu.) (DPLAN-0220) - **seedgo CLI help checkers green-lit non-compliant `--help` output** — the diff --git a/src/aipass/skills/lib/telegram/apps/handlers/base_bot.py b/src/aipass/skills/lib/telegram/apps/handlers/base_bot.py index cf475aec..f97f3d9a 100644 --- a/src/aipass/skills/lib/telegram/apps/handlers/base_bot.py +++ b/src/aipass/skills/lib/telegram/apps/handlers/base_bot.py @@ -84,9 +84,11 @@ from .file_handler import ( ) from .bot_factory import ( create_bot, + set_bot_commands, validate_branch, validate_token, ) +from .telegram_standards import build_botfather_commands from .bot_registry import ( list_bots as registry_list_bots, get_bot_by_branch, @@ -247,6 +249,9 @@ class BaseBot: self._health["started_at"] = datetime.now().isoformat() json_handler.log_operation("bot_started", {"bot_id": self.bot_id}) + # Set Telegram command menu (idempotent, runs once per startup) + self._set_command_menu() + # Check for existing lock if self._check_lock(): logger.error("Another instance of bot-%s is already running", self.bot_id) @@ -1582,6 +1587,17 @@ class BaseBot: """ return + def _set_command_menu(self) -> None: + """Set the Telegram command menu via setMyCommands on startup.""" + merged_commands = {**self.custom_commands, **self.get_custom_commands()} + commands = build_botfather_commands(custom_commands=merged_commands or None) + if self.bot_token: + ok = set_bot_commands(self.bot_token, commands) + if ok: + logger.info("Command menu set (%d commands)", len(commands)) + else: + logger.warning("Failed to set command menu") + def get_custom_commands(self) -> dict: """ Hook: return additional bot-specific commands. @@ -1594,12 +1610,12 @@ class BaseBot: """ return { "create": { - "description": "Create a new branch bot: /create chat ", - "menu_text": "Create branch bot", + "description": "Create a Telegram bot for a branch — e.g. /create chat devpulse", + "menu_text": "New branch bot", }, "cancel": { - "description": "Cancel active /create flow", - "menu_text": "Cancel", + "description": "Cancel an in-progress /create", + "menu_text": "Cancel create", }, } diff --git a/src/aipass/skills/lib/telegram/apps/handlers/bot_factory.py b/src/aipass/skills/lib/telegram/apps/handlers/bot_factory.py index 0d8292f9..c00cf61a 100644 --- a/src/aipass/skills/lib/telegram/apps/handlers/bot_factory.py +++ b/src/aipass/skills/lib/telegram/apps/handlers/bot_factory.py @@ -49,6 +49,8 @@ from aipass.skills.apps.handlers.json import json_handler # noqa: F401 # Internal imports from aipass.api.apps.modules.secrets import set_secret as _api_set_secret +from .telegram_standards import build_botfather_commands + from .bot_registry import ( deregister_bot, ensure_registry, @@ -65,13 +67,7 @@ TELEGRAM_API = "https://api.telegram.org/bot{token}" SYSTEMD_DIR = Path.home() / ".config" / "systemd" / "user" _BOT_CONFIG_DIR = Path.home() / ".aipass" / "telegram_bots" -# Default commands set on every new bot via BotFather -DEFAULT_BOT_COMMANDS = [ - {"command": "start", "description": "Start the bot"}, - {"command": "help", "description": "Show available commands"}, - {"command": "status", "description": "Show session status"}, - {"command": "new", "description": "Start a fresh session"}, -] +# Command menu built from telegram_standards (single source of truth) # ============================================= # TELEGRAM API HELPERS @@ -493,7 +489,7 @@ def create_bot( return None # Step 6: Set BotFather commands - set_bot_commands(bot_token, DEFAULT_BOT_COMMANDS) + set_bot_commands(bot_token, build_botfather_commands()) # Step 7: Enable systemd service enable_service(bot_id) diff --git a/src/aipass/skills/lib/telegram/apps/handlers/telegram_standards.py b/src/aipass/skills/lib/telegram/apps/handlers/telegram_standards.py index 76727af1..2bb93be6 100644 --- a/src/aipass/skills/lib/telegram/apps/handlers/telegram_standards.py +++ b/src/aipass/skills/lib/telegram/apps/handlers/telegram_standards.py @@ -8,19 +8,19 @@ from typing import Optional STANDARD_COMMANDS: dict[str, dict[str, str]] = { "start": { - "description": "Welcome message and command list", - "menu_text": "Start / welcome message", + "description": "Welcome — what this bot is and how to use it", + "menu_text": "What this bot does", }, "help": { - "description": "Show available commands", - "menu_text": "Show help", + "description": "Show every command and what it does", + "menu_text": "List commands", }, "new": { - "description": "Kill current session and start fresh (clean Claude context)", + "description": "Start a fresh conversation (clears Claude's current context)", "menu_text": "Fresh session", }, "status": { - "description": "Show session info (branch, uptime, session state)", + "description": "Show the branch, uptime, and whether a session is active", "menu_text": "Session status", }, } @@ -34,7 +34,7 @@ PROCESSING_MSG = "Processing..." ERROR_TEMPLATE = "Something went wrong: {error}" -HELP_FOOTER = "\nSend any message to chat with Claude." +HELP_FOOTER = "\nJust send any message to talk to me — or use a command above." # Internal templates (used by builder functions) _WELCOME_HEADER = "Hello! I'm {bot_name}." @@ -91,7 +91,7 @@ def build_help_text( standard_commands = STANDARD_COMMANDS parts: list[str] = [ - "Commands:", + "Available commands:", _format_command_list(standard_commands, custom_commands), HELP_FOOTER, ] @@ -123,7 +123,7 @@ def build_welcome_text( _WELCOME_HEADER.format(bot_name=bot_name), _WELCOME_BRANCH.format(branch_name=branch_name), "", - "Commands:", + "Available commands:", _format_command_list(standard_commands, custom_commands), HELP_FOOTER, ] diff --git a/src/aipass/skills/lib/telegram/tests/test_multibot_config.py b/src/aipass/skills/lib/telegram/tests/test_multibot_config.py index f0356ad3..46ddfe74 100644 --- a/src/aipass/skills/lib/telegram/tests/test_multibot_config.py +++ b/src/aipass/skills/lib/telegram/tests/test_multibot_config.py @@ -336,7 +336,7 @@ class TestHandleStandardCommand: bot_name="AIPass Assistant Bot", ) assert isinstance(result, str) - assert "Commands:" in result + assert "Available commands:" in result def test_new_returns_tuple(self) -> None: """The 'new' command returns tuple of ('new', response_text).""" @@ -411,7 +411,7 @@ class TestBuildHelpText: def test_includes_all_standard_commands(self) -> None: """Help text includes all standard commands.""" result = build_help_text() - assert "Commands:" in result + assert "Available commands:" in result for cmd in STANDARD_COMMANDS: assert f"/{cmd}" in result @@ -430,7 +430,7 @@ class TestBuildHelpText: def test_includes_footer(self) -> None: """Help text includes the help footer.""" result = build_help_text() - assert "Send any message to chat with Claude" in result + assert "Just send any message to talk to me" in result # ============================================= @@ -463,7 +463,7 @@ class TestBuildWelcomeText: bot_name="TestBot", branch_name="test", ) - assert "Commands:" in result + assert "Available commands:" in result for cmd in STANDARD_COMMANDS: assert f"/{cmd}" in result @@ -882,3 +882,112 @@ class TestCreateBotRoundTrip: bot_token="222:BBB-test-token", ) assert result is None + + +# ============================================= +# COMMAND MENU SYNC + POPULATE TESTS +# ============================================= + + +class TestCommandMenuSync: + """Verify /help text and Telegram menu use the same single-source command list.""" + + def test_menu_and_help_have_same_commands(self): + """The command names in build_botfather_commands match those in build_help_text.""" + from apps.handlers.telegram_standards import ( + build_botfather_commands, + build_help_text, + ) + from apps.handlers.base_bot import BaseBot + + custom = BaseBot.get_custom_commands(None) + menu_commands = build_botfather_commands(custom_commands=custom) + menu_names = {c["command"] for c in menu_commands} + + help_text = build_help_text(custom_commands=custom) + help_names = set() + for line in help_text.split("\n"): + if line.startswith("/"): + cmd = line.split(" ")[0].lstrip("/").split("-")[0].strip() + help_names.add(cmd) + + assert menu_names == help_names + + def test_help_contains_enriched_descriptions(self): + """The /help text includes the enriched descriptions.""" + from apps.handlers.telegram_standards import build_help_text + from apps.handlers.base_bot import BaseBot + + custom = BaseBot.get_custom_commands(None) + help_text = build_help_text(custom_commands=custom) + + assert "what this bot is and how to use it" in help_text.lower() + assert "show every command" in help_text.lower() + assert "fresh conversation" in help_text.lower() + assert "branch, uptime" in help_text.lower() + assert "create a telegram bot" in help_text.lower() + assert "cancel an in-progress" in help_text.lower() + + def test_help_footer_updated(self): + """The /help footer uses the enriched text.""" + from apps.handlers.telegram_standards import build_help_text + + help_text = build_help_text() + assert "Just send any message to talk to me" in help_text + + def test_create_bot_uses_single_source(self): + """create_bot calls set_bot_commands with build_botfather_commands output.""" + from apps.handlers import bot_factory + from apps.handlers.telegram_standards import build_botfather_commands + + expected = build_botfather_commands() + with patch.object(bot_factory, "set_bot_commands") as mock_set: + with patch.object(bot_factory, "validate_token", return_value={"username": "t", "id": 1}): + with patch.object(bot_factory, "ensure_registry"): + with patch.object(bot_factory, "register_bot", return_value=True): + with patch.object(bot_factory, "_api_set_secret", return_value=None): + with patch.object(bot_factory, "enable_service"): + with patch.object(bot_factory, "start_bot_process", return_value=True): + bot_factory._BOT_CONFIG_DIR.mkdir(parents=True, exist_ok=True) + bot_factory.create_bot("sync_test", "999:ZZZ-token") + + mock_set.assert_called_once_with("999:ZZZ-token", expected) + + +class TestBaseBotStartupMenu: + """Verify base_bot sets command menu on startup.""" + + @patch("apps.handlers.base_bot.set_bot_commands", return_value=True) + def test_set_command_menu_called_on_startup(self, mock_set_commands): + """_set_command_menu calls set_bot_commands with merged commands.""" + from apps.handlers.base_bot import BaseBot + from apps.handlers.telegram_standards import build_botfather_commands + + bot = BaseBot.__new__(BaseBot) + bot.bot_token = "123:ABC" + bot.custom_commands = {} + + bot._set_command_menu() + + mock_set_commands.assert_called_once() + actual_commands = mock_set_commands.call_args[0][1] + expected = build_botfather_commands(custom_commands=bot.get_custom_commands()) + assert actual_commands == expected + + @patch("apps.handlers.base_bot.set_bot_commands", return_value=True) + def test_menu_includes_custom_commands(self, mock_set_commands): + """Menu includes /create and /cancel from get_custom_commands.""" + from apps.handlers.base_bot import BaseBot + + bot = BaseBot.__new__(BaseBot) + bot.bot_token = "123:ABC" + bot.custom_commands = {} + + bot._set_command_menu() + + actual_commands = mock_set_commands.call_args[0][1] + cmd_names = {c["command"] for c in actual_commands} + assert "create" in cmd_names + assert "cancel" in cmd_names + assert "start" in cmd_names + assert "help" in cmd_names