205 lines
7.6 KiB
Python
205 lines
7.6 KiB
Python
import pytest
|
|
|
|
from app.services.integrations.feishu_integration_service.mixin_card_action import FeishuCardActionMixin
|
|
from app.services.integrations.feishu_integration_service.service import FeishuIntegrationService
|
|
|
|
|
|
def test_extract_card_action_payload_parses_dict_value() -> None:
|
|
payload = FeishuCardActionMixin._extract_card_action_payload(
|
|
{
|
|
"open_message_id": "om_123",
|
|
"open_chat_id": "oc_123",
|
|
"operator": {"open_id": "ou_abc", "name": "测试用户"},
|
|
"action": {
|
|
"value": {
|
|
"action": "confirm_draft",
|
|
"work_item_id": 88,
|
|
"draft_version": 3,
|
|
"revision_note": "保持现有范围",
|
|
}
|
|
},
|
|
}
|
|
)
|
|
assert payload["action"] == "confirm"
|
|
assert payload["work_item_id"] == 88
|
|
assert payload["draft_version"] == 3
|
|
assert payload["source_message_id"] == "om_123"
|
|
assert payload["chat_id"] == "oc_123"
|
|
assert payload["sender_open_id"] == "ou_abc"
|
|
|
|
|
|
def test_extract_card_action_payload_parses_json_string_value_and_aliases() -> None:
|
|
payload = FeishuCardActionMixin._extract_card_action_payload(
|
|
{
|
|
"message_id": "msg_1",
|
|
"chat_id": "chat_9",
|
|
"operator": {"user_id": "ou_user", "display_name": "张三"},
|
|
"action": {"value": '{"draft_action":"修改","workItemId":"100","version":"4","note":"改成下周上线"}'},
|
|
}
|
|
)
|
|
assert payload["action"] == "revise"
|
|
assert payload["work_item_id"] == 100
|
|
assert payload["draft_version"] == 4
|
|
assert payload["revision_note"] == "改成下周上线"
|
|
assert payload["source_message_id"] == "msg_1"
|
|
assert payload["chat_id"] == "chat_9"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_message_event_dispatches_card_action(monkeypatch) -> None:
|
|
service = FeishuIntegrationService(db=object())
|
|
called: dict[str, object] = {}
|
|
|
|
async def _fake_card_handler(event):
|
|
called["event_type"] = event.get("type")
|
|
return {"code": 0, "msg": "success", "data": {"handled": True}}
|
|
|
|
monkeypatch.setattr(service, "_handle_card_action_trigger", _fake_card_handler)
|
|
|
|
result = await service.handle_message_event({"type": "card.action.trigger", "event": {}})
|
|
assert result["code"] == 0
|
|
assert called["event_type"] == "card.action.trigger"
|
|
|
|
|
|
def test_build_action_card_for_draft_pending_confirm() -> None:
|
|
card = FeishuCardActionMixin._build_action_card_from_capture_result(
|
|
capture_result={
|
|
"reply_template_key": "draft_pending_confirm",
|
|
"work_item_id": 321,
|
|
"draft_version": 5,
|
|
"direct_reply_text": "请确认草稿内容。",
|
|
},
|
|
mention_open_id="ou_xxx",
|
|
mention_name="测试同学",
|
|
)
|
|
assert isinstance(card, dict)
|
|
assert card["header"]["title"]["content"] == "AI PMO 草稿待确认"
|
|
action_row = next((item for item in card["elements"] if item.get("tag") == "action"), {})
|
|
actions = action_row.get("actions") if isinstance(action_row.get("actions"), list) else []
|
|
action_names = {((btn.get("value") or {}).get("action")) for btn in actions if isinstance(btn, dict)}
|
|
assert {"confirm", "revise", "cancel"} <= action_names
|
|
|
|
|
|
def test_build_action_card_for_pending_manual_continue() -> None:
|
|
card = FeishuCardActionMixin._build_action_card_from_capture_result(
|
|
capture_result={
|
|
"reply_template_key": "pending_manual",
|
|
"results": [{"work_item_id": 99}],
|
|
"direct_reply_text": "需要继续执行。",
|
|
},
|
|
mention_open_id=None,
|
|
mention_name=None,
|
|
)
|
|
assert isinstance(card, dict)
|
|
assert card["header"]["title"]["content"] == "AI PMO 待继续执行"
|
|
action_row = next((item for item in card["elements"] if item.get("tag") == "action"), {})
|
|
actions = action_row.get("actions") if isinstance(action_row.get("actions"), list) else []
|
|
assert len(actions) == 1
|
|
assert (actions[0].get("value") or {}).get("action") == "continue"
|
|
assert (actions[0].get("value") or {}).get("work_item_id") == 99
|
|
|
|
|
|
def test_build_action_card_for_queue_pending_confirm_returns_none() -> None:
|
|
card = FeishuCardActionMixin._build_action_card_from_capture_result(
|
|
capture_result={
|
|
"reply_template_key": "queue_pending_confirm",
|
|
"results": [{"item_id": 201, "route_status": "queued_for_decision"}],
|
|
"direct_reply_text": "需要你确认后继续执行。",
|
|
},
|
|
mention_open_id=None,
|
|
mention_name=None,
|
|
)
|
|
assert card is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_card_action_trigger_continue_dispatches_resume(monkeypatch) -> None:
|
|
called: dict[str, object] = {}
|
|
|
|
class DummyDB:
|
|
async def commit(self) -> None:
|
|
called["committed"] = True
|
|
|
|
async def rollback(self) -> None:
|
|
called["rolled_back"] = True
|
|
|
|
service = FeishuIntegrationService(db=DummyDB())
|
|
service.tenant_id = 7
|
|
|
|
async def _fake_reply_text(**kwargs):
|
|
called["reply_text"] = kwargs.get("text")
|
|
called["reply_chat"] = kwargs.get("chat_id")
|
|
|
|
monkeypatch.setattr(service, "_safe_reply_text", _fake_reply_text)
|
|
|
|
class _FakeKernel:
|
|
def __init__(self, db, tenant_id):
|
|
called["kernel_tenant_id"] = tenant_id
|
|
|
|
async def process_ingress(self, **kwargs):
|
|
called["ingress_kwargs"] = kwargs
|
|
return {
|
|
"handled": True,
|
|
"status": "captured",
|
|
"direct_reply_text": "已继续执行。",
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
"app.services.ai_pmo.ai_pmo_kernel.ai_pmo_kernel_runtime.AIPMOKernelService",
|
|
_FakeKernel,
|
|
)
|
|
|
|
result = await service._handle_card_action_trigger(
|
|
{
|
|
"open_message_id": "om_continue_1",
|
|
"open_chat_id": "oc_continue_1",
|
|
"operator": {"open_id": "ou_continue", "name": "继续用户"},
|
|
"action": {"value": {"action": "continue", "work_item_id": 55}},
|
|
}
|
|
)
|
|
|
|
assert result["code"] == 0
|
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
assert data.get("handled") is True
|
|
assert data.get("target_work_item_id") == 55
|
|
ingress_kwargs = called["ingress_kwargs"]
|
|
assert ingress_kwargs["message_text"] == "继续处理 work_item #55"
|
|
assert ingress_kwargs["source_system"] == "ai_pmo_ui"
|
|
assert called["reply_text"] == "已继续执行。"
|
|
assert called["reply_chat"] == "oc_continue_1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_card_action_trigger_queue_continue_is_ignored(monkeypatch) -> None:
|
|
called: dict[str, object] = {}
|
|
|
|
class DummyDB:
|
|
async def commit(self) -> None:
|
|
called["committed"] = True
|
|
|
|
async def rollback(self) -> None:
|
|
called["rolled_back"] = True
|
|
|
|
service = FeishuIntegrationService(db=DummyDB())
|
|
service.tenant_id = 7
|
|
|
|
async def _fake_reply_text(**kwargs):
|
|
called["reply_text"] = kwargs.get("text")
|
|
|
|
monkeypatch.setattr(service, "_safe_reply_text", _fake_reply_text)
|
|
|
|
result = await service._handle_card_action_trigger(
|
|
{
|
|
"open_message_id": "om_queue_1",
|
|
"open_chat_id": "oc_queue_1",
|
|
"operator": {"open_id": "ou_queue", "name": "队列用户"},
|
|
"action": {"value": {"action": "continue", "queue_item_id": 201}},
|
|
}
|
|
)
|
|
|
|
assert result["code"] == 0
|
|
assert result["msg"] == "card_action_ignored"
|
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
assert data.get("reason") == "missing_work_item_id"
|
|
assert "reply_text" not in called
|