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:
co-authored by
Claude Opus 4.8
parent
9af4c8ac05
commit
bf301bc231
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user