feat(telegram): /help + command menu — startup populate, single source, enriched (DPLAN-0220)

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEQZXCtgnF3NQtcttTErpq
This commit is contained in:
AIOSAI
2026-06-24 16:59:07 -07:00
co-authored by Claude Opus 4.8
parent 9af4c8ac05
commit bf301bc231
5 changed files with 155 additions and 25 deletions
+9
View File
@@ -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
@@ -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 <branch>",
"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",
},
}
@@ -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)
@@ -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,
]
@@ -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