Files
ewoooc/tests/test_openclaw_bot_routes_webhook.py
OoO c021945047
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
fix: route telegram vision through ollama first
2026-05-18 14:07:49 +08:00

919 lines
31 KiB
Python
Raw Permalink 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.main_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_webhook_clears_stale_user_context_at_request_start():
from routes import openclaw_bot_routes as bot
bot._CURRENT_USER_ID_CTX.set(999)
app = _build_request_app()
with app.test_request_context("/bot/telegram/webhook", method="POST", json=None):
bot.telegram_webhook()
assert bot._CURRENT_USER_ID_CTX.get() is None
def test_menu_command_returns_full_main_menu_keyboard(monkeypatch):
from routes import openclaw_bot_routes as bot
from services.openclaw_bot import menu_keyboards
captured = {}
def fake_send_message(chat_id, text, reply_to=None, keyboard=None, parse_mode="Markdown", **_kwargs):
captured["chat_id"] = chat_id
captured["text"] = text
captured["keyboard"] = keyboard
captured["parse_mode"] = parse_mode
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
monkeypatch.setattr(bot, "send_message", fake_send_message)
monkeypatch.setattr(bot, "BOT_USERNAME", "@KnownBot")
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
app = _build_request_app()
payload = {
"update_id": 10002,
"message": {
"message_id": 56,
"chat": {"id": -200, "type": "supergroup"},
"from": {"id": 777},
"text": "/menu",
},
}
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
bot.telegram_webhook()
assert captured["chat_id"] == -200
assert captured["keyboard"] == menu_keyboards.main_menu_keyboard()
assert captured["parse_mode"] == "Markdown"
assert "OpenClaw小O" in captured["text"]
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_photo_message_uses_ollama_vision_before_gemini(monkeypatch):
from routes import openclaw_bot_routes as bot
sent = []
handled = []
class FakeResponse:
def __init__(self, json_data=None, content=b"fake-image"):
self._json_data = json_data or {}
self.content = content
def json(self):
return self._json_data
def fake_get(url, **_kwargs):
if "getFile" in url:
return FakeResponse({"result": {"file_path": "photos/product.jpg"}})
return FakeResponse(content=b"fake-image")
monkeypatch.setattr(bot.requests, "get", fake_get)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
monkeypatch.setattr(
bot,
"send_message",
lambda *args, **kwargs: sent.append((args, kwargs)),
)
monkeypatch.setattr(
bot,
"handle_cmd",
lambda cmd, arg, chat_id, reply_to: handled.append((cmd, arg, chat_id, reply_to)),
)
monkeypatch.setattr(
bot,
"_identify_product_name_with_ollama_vision",
lambda img_b64, request_id: "理膚寶水 B5 修復霜",
)
monkeypatch.setattr(
bot,
"_identify_product_name_with_gemini_vision",
lambda img_b64, request_id: (_ for _ in ()).throw(AssertionError("Gemini should not run first")),
)
app = _build_request_app()
payload = {
"update_id": 10030,
"message": {
"message_id": 80,
"chat": {"id": 777, "type": "private"},
"from": {"id": 777777},
"photo": [{"file_id": "small"}, {"file_id": "large"}],
},
}
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
bot.telegram_webhook()
assert handled == [("competitor", "理膚寶水 B5 修復霜", 777, 80)]
assert "理膚寶水 B5 修復霜" in sent[0][0][1]
def test_photo_message_falls_back_to_gemini_when_ollama_empty(monkeypatch):
from routes import openclaw_bot_routes as bot
handled = []
calls = []
class FakeResponse:
def __init__(self, json_data=None, content=b"fake-image"):
self._json_data = json_data or {}
self.content = content
def json(self):
return self._json_data
def fake_get(url, **_kwargs):
if "getFile" in url:
return FakeResponse({"result": {"file_path": "photos/product.jpg"}})
return FakeResponse(content=b"fake-image")
def fake_ollama(_img_b64, _request_id):
calls.append("ollama")
return ""
def fake_gemini(_img_b64, _request_id):
calls.append("gemini")
return "飛利浦 Sonicare"
monkeypatch.setattr(bot.requests, "get", fake_get)
monkeypatch.setattr(bot, "_is_authorized", lambda _chat_type, _chat_id, _uid: True)
monkeypatch.setattr(bot, "send_typing", lambda _chat_id: None)
monkeypatch.setattr(bot, "send_message", lambda *args, **kwargs: None)
monkeypatch.setattr(
bot,
"handle_cmd",
lambda cmd, arg, chat_id, reply_to: handled.append((cmd, arg, chat_id, reply_to)),
)
monkeypatch.setattr(bot, "_identify_product_name_with_ollama_vision", fake_ollama)
monkeypatch.setattr(bot, "_identify_product_name_with_gemini_vision", fake_gemini)
app = _build_request_app()
payload = {
"update_id": 10031,
"message": {
"message_id": 81,
"chat": {"id": 777, "type": "private"},
"from": {"id": 777777},
"photo": [{"file_id": "small"}, {"file_id": "large"}],
},
}
with app.test_request_context("/bot/telegram/webhook", method="POST", json=payload):
bot.telegram_webhook()
assert calls == ["ollama", "gemini"]
assert handled == [("competitor", "飛利浦 Sonicare", 777, 81)]
def test_obs_heal_audit_uses_current_callback_user(monkeypatch):
from types import SimpleNamespace
from routes import openclaw_bot_routes as bot
import services.ollama_service as ollama_service
import services.auto_heal_service as auto_heal_module
captured = {}
class FakeAutoHeal:
def handle_exception(self, error_type, context):
captured["error_type"] = error_type
captured["context"] = context
return SimpleNamespace(success=True, action="ALERT_ONLY", message="ok")
sent = []
monkeypatch.setattr(bot, "send_message", lambda *args, **kwargs: sent.append((args, kwargs)))
monkeypatch.setattr(ollama_service, "_is_unhealthy", lambda _host: True)
monkeypatch.setattr(auto_heal_module, "auto_heal_service", FakeAutoHeal())
token = bot._CURRENT_USER_ID_CTX.set(777001)
try:
bot.handle_cmd("obs_heal", "GCP-A", -200, 55)
finally:
bot._CURRENT_USER_ID_CTX.reset(token)
assert captured["error_type"] == "ollama_unhealthy"
assert captured["context"]["triggered_by"] == "telegram_user_777001"
assert sent
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_event_ignore_callback_edits_and_audits(monkeypatch):
from routes import openclaw_bot_routes as bot
edited = []
sent = []
audits = []
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_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}
def fake_audit(event_id, user_label, ts_label):
audits.append((event_id, user_label, ts_label))
monkeypatch.setattr(bot, "edit_message_text", fake_edit_message_text)
monkeypatch.setattr(bot, "send_message", fake_send_message)
monkeypatch.setattr(bot, "_write_event_ignore_audit", fake_audit)
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": 10103,
"callback_query": {
"id": "cb-eig-1",
"from": {"id": 777, "username": "alice"},
"message": {
"message_id": 79,
"chat": {"id": -200, "type": "supergroup"},
"text": "<b>待處理事件</b>",
},
"data": "momo:eig:event-123",
},
}
with app.test_request_context(
"/bot/telegram/webhook", method="POST", json=payload
):
bot.telegram_webhook()
assert audits and audits[0][0:2] == ("event-123", "alice")
assert len(edited) == 1
_, _, text, keyboard, parse_mode = edited[0]
assert "&lt;b&gt;待處理事件&lt;/b&gt;" in text
assert "已忽略" in text
assert "alice" in text
assert keyboard is None
assert parse_mode == "HTML"
assert sent == []
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)]