feat: Implement new bot architecture including handlers, services, analysis modules, and comprehensive tests.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.bot.handlers.basic import BasicCommandHandler
|
||||
from src.bot.runtime_coordinator import RuntimeStatus
|
||||
|
||||
|
||||
class DummyBot:
|
||||
def __init__(self):
|
||||
self.replies = []
|
||||
|
||||
def reply_to(self, message, text, parse_mode=None):
|
||||
self.replies.append({"text": text, "parse_mode": parse_mode, "chat_id": message.chat.id})
|
||||
|
||||
def send_message(self, chat_id, text, parse_mode=None): # pragma: no cover
|
||||
pass
|
||||
|
||||
def message_handler(self, *args, **kwargs): # pragma: no cover - decorator stub
|
||||
def _decorator(func):
|
||||
return func
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
def _message(text: str):
|
||||
return SimpleNamespace(
|
||||
text=text,
|
||||
from_user=SimpleNamespace(id=1, username="u", first_name="U"),
|
||||
chat=SimpleNamespace(id=100),
|
||||
)
|
||||
|
||||
|
||||
def test_basic_handler_diag_returns_html():
|
||||
runtime = RuntimeStatus(
|
||||
started_at="2026-03-12 00:00:00 UTC",
|
||||
loops=[],
|
||||
entitlement_enabled=False,
|
||||
protected_commands=["/city", "/deb"],
|
||||
)
|
||||
bot = DummyBot()
|
||||
io_layer = SimpleNamespace(
|
||||
build_welcome_text=lambda: "WELCOME",
|
||||
build_points_rank_text=lambda _user: "TOP",
|
||||
)
|
||||
handler = BasicCommandHandler(
|
||||
bot=bot,
|
||||
io_layer=io_layer,
|
||||
runtime_status_provider=lambda: runtime,
|
||||
)
|
||||
|
||||
handler.handle_diag(_message("/diag"))
|
||||
|
||||
assert len(bot.replies) == 1
|
||||
assert bot.replies[0]["parse_mode"] == "HTML"
|
||||
assert "Bot 启动诊断" in bot.replies[0]["text"]
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
from src.bot.command_guard import CommandGuard
|
||||
from src.bot.services.entitlement_service import BotEntitlementService
|
||||
|
||||
|
||||
class _FakeDB:
|
||||
def __init__(self, user):
|
||||
self._user = user
|
||||
|
||||
def get_user(self, _user_id):
|
||||
return self._user
|
||||
|
||||
|
||||
def _message():
|
||||
return SimpleNamespace(
|
||||
from_user=SimpleNamespace(id=123, username="tester", first_name="Tester"),
|
||||
chat=SimpleNamespace(id=999),
|
||||
)
|
||||
|
||||
|
||||
def test_guard_blocks_non_premium_when_entitlement_enabled():
|
||||
fake_bot = SimpleNamespace(reply_to=Mock())
|
||||
io_layer = SimpleNamespace(bot=fake_bot, ensure_query_points=Mock(return_value=True))
|
||||
entitlement = BotEntitlementService(db=_FakeDB(user=None), enabled=True)
|
||||
guard = CommandGuard(io_layer=io_layer, entitlement_service=entitlement)
|
||||
|
||||
ok = guard.ensure_access_and_points(_message(), 1, "/city")
|
||||
|
||||
assert ok is False
|
||||
assert io_layer.ensure_query_points.call_count == 0
|
||||
assert fake_bot.reply_to.call_count == 1
|
||||
|
||||
|
||||
def test_guard_allows_premium_then_charges_points():
|
||||
fake_bot = SimpleNamespace(reply_to=Mock())
|
||||
io_layer = SimpleNamespace(bot=fake_bot, ensure_query_points=Mock(return_value=True))
|
||||
entitlement = BotEntitlementService(
|
||||
db=_FakeDB(user={"is_web_premium": 1, "is_group_premium": 0}),
|
||||
enabled=True,
|
||||
)
|
||||
guard = CommandGuard(io_layer=io_layer, entitlement_service=entitlement)
|
||||
|
||||
ok = guard.ensure_access_and_points(_message(), 1, "/city")
|
||||
|
||||
assert ok is True
|
||||
assert io_layer.ensure_query_points.call_count == 1
|
||||
assert fake_bot.reply_to.call_count == 0
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
from src.bot.handlers.city import CityCommandHandler
|
||||
from src.bot.handlers.deb import DebCommandHandler
|
||||
from src.bot.services.city_command_service import CityReportResult, CityResolveResult
|
||||
from src.bot.services.deb_command_service import DebReportResult
|
||||
|
||||
|
||||
class DummyBot:
|
||||
def __init__(self):
|
||||
self.replies = []
|
||||
self.sent = []
|
||||
|
||||
def reply_to(self, message, text, parse_mode=None):
|
||||
self.replies.append(
|
||||
{
|
||||
"chat_id": message.chat.id,
|
||||
"text": text,
|
||||
"parse_mode": parse_mode,
|
||||
}
|
||||
)
|
||||
|
||||
def send_message(self, chat_id, text, parse_mode=None):
|
||||
self.sent.append(
|
||||
{
|
||||
"chat_id": chat_id,
|
||||
"text": text,
|
||||
"parse_mode": parse_mode,
|
||||
}
|
||||
)
|
||||
|
||||
def message_handler(self, *args, **kwargs): # pragma: no cover - decorator stub
|
||||
def _decorator(func):
|
||||
return func
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
def _message(text: str):
|
||||
return SimpleNamespace(
|
||||
text=text,
|
||||
from_user=SimpleNamespace(id=123, username="tester", first_name="Tester"),
|
||||
chat=SimpleNamespace(id=999),
|
||||
)
|
||||
|
||||
|
||||
def test_city_handler_missing_city_shows_usage():
|
||||
bot = DummyBot()
|
||||
guard = SimpleNamespace(ensure_access_and_points=Mock(return_value=True))
|
||||
city_service = SimpleNamespace(
|
||||
resolve_city=Mock(),
|
||||
build_report=Mock(),
|
||||
)
|
||||
handler = CityCommandHandler(bot=bot, guard=guard, city_service=city_service)
|
||||
|
||||
handler.handle(_message("/city"))
|
||||
|
||||
assert len(bot.replies) == 1
|
||||
assert "请输入城市名称" in bot.replies[0]["text"]
|
||||
assert guard.ensure_access_and_points.call_count == 0
|
||||
|
||||
|
||||
def test_city_handler_happy_path_pushes_progress_and_report():
|
||||
bot = DummyBot()
|
||||
guard = SimpleNamespace(ensure_access_and_points=Mock(return_value=True))
|
||||
city_service = SimpleNamespace(
|
||||
resolve_city=Mock(
|
||||
return_value=CityResolveResult(ok=True, city_name="london", supported_cities=["london"])
|
||||
),
|
||||
build_report=Mock(return_value=CityReportResult(ok=True, report="CITY_REPORT")),
|
||||
)
|
||||
handler = CityCommandHandler(bot=bot, guard=guard, city_service=city_service)
|
||||
|
||||
handler.handle(_message("/city london"))
|
||||
|
||||
assert len(bot.sent) == 2
|
||||
assert "正在查询 London 的天气数据" in bot.sent[0]["text"]
|
||||
assert bot.sent[1]["text"] == "CITY_REPORT"
|
||||
assert bot.sent[1]["parse_mode"] == "HTML"
|
||||
|
||||
|
||||
def test_deb_handler_history_missing_returns_hint():
|
||||
bot = DummyBot()
|
||||
guard = SimpleNamespace(ensure_access_and_points=Mock(return_value=True))
|
||||
deb_service = SimpleNamespace(
|
||||
resolve_city=Mock(return_value="ankara"),
|
||||
has_history=Mock(return_value=False),
|
||||
build_report=Mock(return_value=DebReportResult(ok=True, report="DEB_REPORT")),
|
||||
)
|
||||
handler = DebCommandHandler(bot=bot, guard=guard, deb_service=deb_service)
|
||||
|
||||
handler.handle(_message("/deb ankara"))
|
||||
|
||||
assert len(bot.replies) == 1
|
||||
assert "暂无 ankara 的历史数据" in bot.replies[0]["text"]
|
||||
assert guard.ensure_access_and_points.call_count == 0
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from src.bot.runtime_coordinator import RuntimeStatus, StartupCoordinator, render_runtime_status_html
|
||||
|
||||
|
||||
class DummyBot:
|
||||
pass
|
||||
|
||||
|
||||
def test_startup_coordinator_respects_disable_flags(monkeypatch):
|
||||
monkeypatch.setenv("TELEGRAM_ALERT_PUSH_ENABLED", "false")
|
||||
monkeypatch.setenv("POLYGON_WALLET_WATCH_ENABLED", "false")
|
||||
monkeypatch.setenv("POLYMARKET_WALLET_ACTIVITY_ENABLED", "false")
|
||||
monkeypatch.delenv("TELEGRAM_CHAT_ID", raising=False)
|
||||
|
||||
coordinator = StartupCoordinator(
|
||||
bot=DummyBot(),
|
||||
config={},
|
||||
entitlement_enabled=False,
|
||||
protected_commands=["/city", "/deb"],
|
||||
)
|
||||
runtime = coordinator.start_all()
|
||||
loop_map = runtime.loop_map()
|
||||
|
||||
assert loop_map["trade_alert_push"].configured_enabled is False
|
||||
assert loop_map["trade_alert_push"].started is False
|
||||
assert loop_map["trade_alert_push"].reason == "disabled_by_env"
|
||||
assert loop_map["polygon_wallet_watch"].reason == "disabled_by_env"
|
||||
assert loop_map["polymarket_wallet_activity"].reason == "disabled_by_env"
|
||||
|
||||
|
||||
def test_render_runtime_status_html_contains_key_fields():
|
||||
runtime = RuntimeStatus(
|
||||
started_at="2026-03-12 00:00:00 UTC",
|
||||
entitlement_enabled=True,
|
||||
protected_commands=["/city", "/deb"],
|
||||
loops=[],
|
||||
)
|
||||
html = render_runtime_status_html(runtime)
|
||||
|
||||
assert "Bot 启动诊断" in html
|
||||
assert "Entitlement" in html
|
||||
assert "/city, /deb" in html
|
||||
|
||||
Reference in New Issue
Block a user