fix(skills): SchedulerBot.run() crash-looped on real ExecStart — wrap super().run() (TDPLAN-0008)
- run() called a non-existent self._poll_loop() AND duplicated the parent lifecycle incompletely (missing signal handlers, offset load/save) — bot exited 1 in <1s under systemd. Now wraps super().run() with digest start/stop only. - Fixed broken 'from .json import json_handler' (relative path did not exist) → absolute import; log queue_requested op so json_structure standard is met by use, not noqa - Added missing docstrings: handle_message / handle_file / get_custom_commands - ROOT CAUSE: 26 unit tests never executed run() (it blocks on polling) → green tests, failing ExecStart (key-learning #77: test the real ExecStart, not the drone/mocked form). Verified live: bot now active+polling via systemd, 26 tests + seedgo 30 still green.
This commit is contained in:
@@ -19,6 +19,7 @@ import threading
|
||||
from typing import Optional
|
||||
|
||||
from aipass.prax import logger
|
||||
from aipass.skills.apps.handlers.json import json_handler
|
||||
|
||||
from .base_bot import BaseBot
|
||||
|
||||
@@ -37,12 +38,14 @@ class SchedulerBot(BaseBot):
|
||||
self._scheduler_chat_id: Optional[int] = None
|
||||
|
||||
def handle_message(self, chat_id: int, text: str, message: dict) -> None:
|
||||
"""Reject free-text — this bot only serves commands (no tmux/Claude spawn)."""
|
||||
self.send_message(
|
||||
chat_id,
|
||||
"I'm the scheduler bot — I only handle commands.\nTry /queue or /help",
|
||||
)
|
||||
|
||||
def handle_file(self, chat_id: int, message: dict) -> None:
|
||||
"""Reject files — this bot only serves commands."""
|
||||
self.send_message(chat_id, "I don't process files. Try /queue or /help")
|
||||
|
||||
def _dispatch_command(self, chat_id: int, parsed: tuple) -> bool:
|
||||
@@ -53,6 +56,7 @@ class SchedulerBot(BaseBot):
|
||||
return super()._dispatch_command(chat_id, parsed)
|
||||
|
||||
def get_custom_commands(self) -> dict:
|
||||
"""Add /queue to the slash-menu alongside the inherited commands."""
|
||||
cmds = super().get_custom_commands()
|
||||
cmds["queue"] = {
|
||||
"description": "Show all scheduled daemon jobs — next fire, status, type",
|
||||
@@ -62,6 +66,7 @@ class SchedulerBot(BaseBot):
|
||||
|
||||
def _handle_queue_command(self, chat_id: int) -> None:
|
||||
"""Fetch queue from daemon and send formatted message."""
|
||||
json_handler.log_operation("queue_requested", {"chat_id": chat_id})
|
||||
queue_data = self._fetch_queue()
|
||||
if queue_data is None:
|
||||
self.send_message(chat_id, "Failed to fetch queue from daemon.")
|
||||
@@ -184,44 +189,16 @@ class SchedulerBot(BaseBot):
|
||||
# =============================================
|
||||
|
||||
def run(self) -> int:
|
||||
"""Start the bot with digest thread."""
|
||||
logger.info("=" * 60)
|
||||
logger.info("%s starting (bot_id=%s)", self.bot_name, self.bot_id)
|
||||
|
||||
if not self.verify_connection():
|
||||
logger.error("Startup health check FAILED — cannot reach Telegram API")
|
||||
return 1
|
||||
|
||||
logger.info("Connected to Telegram API")
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
self._health["started_at"] = datetime.now().isoformat()
|
||||
|
||||
from .json import json_handler
|
||||
|
||||
json_handler.log_operation("bot_started", {"bot_id": self.bot_id})
|
||||
|
||||
self._set_command_menu()
|
||||
|
||||
self._boot_monitor()
|
||||
|
||||
if self._check_lock():
|
||||
logger.error("Another instance of bot-%s is already running", self.bot_id)
|
||||
return 1
|
||||
"""Run the bot: start the hourly digest, then delegate to the parent poll loop.
|
||||
|
||||
BaseBot.run() owns the full lifecycle (verify, command menu, lock, signal
|
||||
handlers, offset tracking, poll loop, cleanup). We only wrap it to start the
|
||||
digest thread before the blocking loop and stop it on exit.
|
||||
"""
|
||||
chat_id = getattr(self, "_config_chat_id", None)
|
||||
if chat_id:
|
||||
self.start_digest(int(chat_id))
|
||||
|
||||
self._create_lock()
|
||||
|
||||
try:
|
||||
self._poll_loop()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Interrupted")
|
||||
return super().run()
|
||||
finally:
|
||||
self.stop_digest()
|
||||
self._cleanup()
|
||||
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user