Files
ewoooc/tests/test_openclaw_bot_routes_webhook.py
OoO 7b6423fa67
All checks were successful
CD Pipeline / deploy (push) Successful in 2m55s
fix(openclaw): route wakeup phrases back to menu
2026-05-02 16:03:49 +08:00

666 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from flask import Flask
def _build_request_app():
return Flask(__name__)
def test_webhook_menu_command_handles_bot_suffix(monkeypatch):
from routes import openclaw_bot_routes as bot
calls = []
def fake_handle_cmd(cmd, arg, chat_id, reply_to):
calls.append((cmd, arg, chat_id, reply_to))
monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "BOT_USERNAME", "@KnownBot")
app = _build_request_app()
payload = {
"update_id": 10001,
"message": {
"message_id": 55,
"chat": {"id": -200, "type": "supergroup"},
"from": {"id": 777},
"text": "/menu@OtherBot",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert calls == [("menu", "", -200, 55)]
def test_openclaw_answer_wakeup_query_returns_menu():
from routes import openclaw_bot_routes as bot
text, kb = bot.openclaw_answer("小龍蝦")
assert "OpenClaw小O" in text
assert kb == bot.quick_menu_keyboard()
def test_openclaw_answer_variants_are_menu_wakeup():
from routes import openclaw_bot_routes as bot
assert bot._looks_like_wakeup_prompt("小O_小龍蝦") is True
assert bot._looks_like_wakeup_prompt("Hello, 小O") is True
assert bot._looks_like_wakeup_prompt("今天業績") is False
assert bot._looks_like_wakeup_prompt("你好") is True
def test_private_menu_command_is_allowed_when_no_whitelist_and_fallback_enabled(monkeypatch):
from routes import openclaw_bot_routes as bot
calls = []
def fake_handle_cmd(cmd, arg, chat_id, reply_to):
calls.append((cmd, arg, chat_id, reply_to))
monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
monkeypatch.setattr(bot, "BOT_USERNAME", "@KnownBot")
monkeypatch.setattr(bot, "_ALLOW_PRIVATE_WITHOUT_WHITELIST", True)
monkeypatch.setattr(bot, "ALLOWED_USERS", set())
app = _build_request_app()
payload = {
"update_id": 10020,
"message": {
"message_id": 55,
"chat": {"id": 777, "type": "private"},
"from": {"id": 777777},
"text": "/menu",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert calls == [("menu", "", 777, 55)]
def test_is_authorized_private_mode_switch(monkeypatch):
from routes import openclaw_bot_routes as bot
monkeypatch.setattr(bot, "ALLOWED_USERS", set())
monkeypatch.setattr(bot, "_ALLOW_PRIVATE_WITHOUT_WHITELIST", True)
assert bot._is_authorized("private", 777, 42) is True
monkeypatch.setattr(bot, "_ALLOW_PRIVATE_WITHOUT_WHITELIST", False)
assert bot._is_authorized("private", 777, 42) is False
def test_webhook_menu_callback_edits_existing_message(monkeypatch):
from routes import openclaw_bot_routes as bot
edited = []
def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
edited.append((chat_id, message_id, text, keyboard, parse_mode))
return {"ok": True}
monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload = {
"update_id": 10002,
"callback_query": {
"id": "cb1",
"from": {"id": 777},
"message": {
"message_id": 66,
"chat": {"id": -200, "type": "supergroup"},
},
"data": "menu:main",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert len(edited) == 1
chat_id, message_id, text, keyboard, parse_mode = edited[0]
assert chat_id == -200
assert message_id == 66
assert text == "👋 *OpenClaw* — 請選擇功能類別"
assert isinstance(keyboard, list)
assert parse_mode == "Markdown"
def test_webhook_legacy_menu_callback_normalizes_prefix(monkeypatch):
from routes import openclaw_bot_routes as bot
edited = []
def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
edited.append((chat_id, message_id, text, keyboard, parse_mode))
return {"ok": True}
monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload = {
"update_id": 10005,
"callback_query": {
"id": "cb-legacy",
"from": {"id": 777},
"message": {"message_id": 123, "chat": {"id": -200, "type": "supergroup"}},
"data": "menu_main",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert len(edited) == 1
assert edited[0][0] == -200
assert edited[0][1] == 123
assert edited[0][2] == "👋 *OpenClaw* — 請選擇功能類別"
def test_webhook_await_callback_edits_existing_message(monkeypatch):
from routes import openclaw_bot_routes as bot
edited = []
def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
edited.append((chat_id, message_id, text, keyboard, parse_mode))
return {"ok": True}
monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload = {
"update_id": 10003,
"callback_query": {
"id": "cb2",
"from": {"id": 777},
"message": {
"message_id": 77,
"chat": {"id": -200, "type": "supergroup"},
},
"data": "await:date_range_sales",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert len(edited) == 1
_, _, text, keyboard, _ = edited[0]
assert "輸入 `/取消` 可退出_" in text
assert keyboard == [[{"text": "✖ 取消", "callback_data": "menu:main"}]]
def test_webhook_cmd_callback_updates_with_message_edit(monkeypatch):
from routes import openclaw_bot_routes as bot
edited = []
sent = []
def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
edited.append((chat_id, message_id, text, keyboard, parse_mode))
return {"ok": True}
def fake_handle_cmd(cmd, arg, chat_id, reply_to):
bot.send_message(chat_id, f"{cmd}:{arg}", reply_to=reply_to)
def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown"):
sent.append((chat_id, text, reply_to, keyboard, parse_mode))
return {"ok": True}
monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
monkeypatch.setattr(bot, "send_message", fake_send_message)
monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload = {
"update_id": 10004,
"callback_query": {
"id": "cb3",
"from": {"id": 777},
"message": {"message_id": 88, "chat": {"id": -200, "type": "supergroup"}},
"data": "cmd:sales:2026/04/30",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert len(edited) == 1
assert edited[0][0] == -200
assert edited[0][1] == 88
assert edited[0][2] == "sales:2026/04/30"
assert edited[0][4] == "Markdown"
assert sent == []
def test_webhook_text_command_uses_agent_dispatch_for_sales(monkeypatch):
from routes import openclaw_bot_routes as bot
dispatched = []
sent = []
queried = []
def fake_openclaw_answer(question, chat_id=None):
dispatched.append((question, chat_id))
return ("NL response", None)
def fake_query_sales(date):
queried.append(date)
return {'found': True, 'date': date}
monkeypatch.setattr(bot, "openclaw_answer", fake_openclaw_answer)
monkeypatch.setattr(bot, "_OPENCLAW_AGENT_DISPATCH_ENABLED", True)
monkeypatch.setattr(bot, "_AGENT_DISPATCH_CMDS", {"sales"})
monkeypatch.setattr(bot, "send_message", lambda *_args, **_kwargs: sent.append(_args))
monkeypatch.setattr(bot, "query_sales", fake_query_sales)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
monkeypatch.setattr(bot, "BOT_USERNAME", "@KnownBot")
app = _build_request_app()
payload = {
"update_id": 10011,
"message": {
"message_id": 123,
"chat": {"id": -200, "type": "supergroup"},
"from": {"id": 777},
"text": "/sales 2026/04/30",
},
}
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
bot.telegram_webhook()
assert dispatched == [("請查 2026/04/30 的業績數字(包含營收、訂單數、毛利率)", -200)]
assert queried == []
assert sent
assert sent[0][1] == "NL response"
def test_webhook_cmd_callback_stays_structured_when_dispatch_enabled(monkeypatch):
from routes import openclaw_bot_routes as bot
dispatched = []
queried_sales = []
edited = []
def fake_dispatch(cmd, arg, chat_id, reply_to):
dispatched.append((cmd, arg, chat_id, reply_to))
return True
def fake_query_sales(date):
queried_sales.append(date)
return {'found': False, 'date': date}
def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
edited.append((chat_id, message_id, text))
return {"ok": True}
def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown", **_kwargs):
return {"ok": True}
monkeypatch.setattr(bot, "_agent_dispatch_cmd", fake_dispatch)
monkeypatch.setattr(bot, "_OPENCLAW_AGENT_DISPATCH_ENABLED", True)
monkeypatch.setattr(bot, "_AGENT_DISPATCH_CMDS", {"sales"})
monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
monkeypatch.setattr(bot, "send_message", fake_send_message)
monkeypatch.setattr(bot, "query_sales", fake_query_sales)
monkeypatch.setattr(bot, "query_top_products", lambda *_args, **_kwargs: [])
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload = {
"update_id": 10012,
"callback_query": {
"id": "cb-menu-sales",
"from": {"id": 777},
"message": {"message_id": 444, "chat": {"id": -200, "type": "supergroup"}},
"data": "cmd:sales:2026/04/30",
},
}
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
bot.telegram_webhook()
assert dispatched == []
assert queried_sales == ["2026/04/30"]
assert len(edited) == 1
assert edited[0][0] == -200
assert edited[0][1] == 444
assert edited[0][2].startswith("⚠️ *查無資料*")
def test_webhook_duplicate_update_id_is_skipped(monkeypatch):
from routes import openclaw_bot_routes as bot
calls = []
answered = []
def fake_handle_cmd(cmd, arg, chat_id, reply_to):
calls.append((cmd, arg, chat_id, reply_to))
monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": answered.append(_cq_id))
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
monkeypatch.setattr(bot, "send_message", lambda *_args, **_kwargs: {"ok": True})
app = _build_request_app()
payload = {
"update_id": 20001,
"callback_query": {
"id": "dup-cb",
"from": {"id": 777},
"message": {"message_id": 99, "chat": {"id": -200, "type": "supergroup"}},
"data": "menu:main",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert calls == []
assert answered == ["dup-cb", "dup-cb"]
def test_webhook_cmd_callback_ignores_not_modified(monkeypatch):
from routes import openclaw_bot_routes as bot
edited = []
sent = []
def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
edited.append((chat_id, message_id, text, keyboard, parse_mode))
return {"ok": False, "description": "Bad Request: message is not modified"}
def fake_handle_cmd(cmd, arg, chat_id, reply_to):
bot.send_message(chat_id, f"{cmd}:{arg}", reply_to=reply_to)
def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown", **_kwargs):
sent.append((chat_id, text, reply_to, keyboard, parse_mode))
return {"ok": True}
monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
monkeypatch.setattr(bot, "send_message", fake_send_message)
monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload = {
"update_id": 20002,
"callback_query": {
"id": "cb4",
"from": {"id": 777},
"message": {"message_id": 101, "chat": {"id": -200, "type": "supergroup"}},
"data": "cmd:sales:2026/04/30",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert edited
assert sent == []
def test_webhook_menu_callback_does_not_duplicate_on_message_not_found(monkeypatch):
from routes import openclaw_bot_routes as bot
sent = []
def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
return {
"ok": False,
"error_code": 404,
"description": "Bad Request: message to edit not found",
}
def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown", **_kwargs):
sent.append((chat_id, text, reply_to, keyboard, parse_mode))
return {"ok": True}
monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
monkeypatch.setattr(bot, "send_message", fake_send_message)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload = {
"update_id": 10006,
"callback_query": {
"id": "cb5",
"from": {"id": 777},
"message": {"message_id": 222, "chat": {"id": -200, "type": "supergroup"}},
"data": "menu:main",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert sent == []
def test_webhook_cmd_callback_does_not_duplicate_on_message_not_found(monkeypatch):
from routes import openclaw_bot_routes as bot
sent = []
def fake_edit_message_text(chat_id, message_id, text, keyboard=None, parse_mode="Markdown"):
return {
"ok": False,
"error_code": 404,
"description": "Bad Request: MESSAGE_ID_INVALID",
}
def fake_handle_cmd(cmd, arg, chat_id, reply_to):
bot.send_message(chat_id, f"{cmd}:{arg}", reply_to=reply_to)
def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown", **_kwargs):
sent.append((chat_id, text, reply_to, keyboard, parse_mode))
return {"ok": True}
monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
monkeypatch.setattr(bot, "send_message", fake_send_message)
monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload = {
"update_id": 10007,
"callback_query": {
"id": "cb6",
"from": {"id": 777},
"message": {"message_id": 333, "chat": {"id": -200, "type": "supergroup"}},
"data": "cmd:sales:2026/04/30",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert sent == []
def test_webhook_callback_dedup_key_without_update_id(monkeypatch):
from routes import openclaw_bot_routes as bot
calls = []
# 當 callback 沒有 update_id 時,第二次同樣 payload 要直接被 dedupe
# 但第一次仍應可正常執行一次。
from services import telegram_update_guard as guard
guard._seen_update_ids.clear()
guard._seen_update_id_set.clear()
def fake_handle_cmd(cmd, arg, chat_id, reply_to):
calls.append((cmd, arg, chat_id, reply_to))
monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload = {
"callback_query": {
"id": "cb-no-id",
"from": {"id": 777},
"message": {"message_id": 123, "chat": {"id": -200, "type": "supergroup"}},
"data": "cmd:sales:2026/04/30",
},
}
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
bot.telegram_webhook()
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
bot.telegram_webhook()
assert calls == [('sales', '2026/04/30', -200, 123)]
def test_webhook_callback_dedup_key_varies_by_message_id(monkeypatch):
from routes import openclaw_bot_routes as bot
calls = []
from services import telegram_update_guard as guard
guard._seen_update_ids.clear()
guard._seen_update_id_set.clear()
def fake_handle_cmd(cmd, arg, chat_id, reply_to):
calls.append((cmd, arg, chat_id, reply_to))
monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload_1 = {
"callback_query": {
"id": "cb-no-id",
"from": {"id": 777},
"message": {"message_id": 201, "chat": {"id": -200, "type": "supergroup"}},
"data": "cmd:sales:2026/04/30",
},
}
payload_2 = {
"callback_query": {
"id": "cb-no-id",
"from": {"id": 777},
"message": {"message_id": 202, "chat": {"id": -200, "type": "supergroup"}},
"data": "cmd:sales:2026/04/30",
},
}
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload_1):
bot.telegram_webhook()
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload_2):
bot.telegram_webhook()
assert calls == [
('sales', '2026/04/30', -200, 201),
('sales', '2026/04/30', -200, 202),
]
def test_webhook_callback_dedup_with_same_callback_query_id_different_update_id(monkeypatch):
from routes import openclaw_bot_routes as bot
calls = []
from services import telegram_update_guard as guard
guard._seen_update_ids.clear()
guard._seen_update_id_set.clear()
def fake_handle_cmd(cmd, arg, chat_id, reply_to):
calls.append((cmd, arg, chat_id, reply_to))
monkeypatch.setattr(bot, "handle_cmd", fake_handle_cmd)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "answer_callback", lambda _cq_id, text="": None)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
app = _build_request_app()
payload_1 = {
"update_id": 30001,
"callback_query": {
"id": "cb-repeat",
"from": {"id": 777},
"message": {"message_id": 301, "chat": {"id": -200, "type": "supergroup"}},
"data": "cmd:sales:2026/04/30",
},
}
payload_2 = {
"update_id": 30002,
"callback_query": {
"id": "cb-repeat",
"from": {"id": 777},
"message": {"message_id": 301, "chat": {"id": -200, "type": "supergroup"}},
"data": "cmd:sales:2026/04/30",
},
}
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload_1):
bot.telegram_webhook()
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload_2):
bot.telegram_webhook()
assert calls == [('sales', '2026/04/30', -200, 301)]