PMFlow/backend/tests/test_feishu_card_action_con...

235 lines
9.0 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() -> 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 isinstance(card, dict)
assert card["header"]["title"]["content"] == "待确认动作"
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 {"continue", "cancel"} <= action_names
queue_ids = {((btn.get("value") or {}).get("queue_item_id")) for btn in actions if isinstance(btn, dict)}
assert queue_ids == {201}
@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_dispatches_pipeline(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 _FakeOperator:
id = 77
class _FakePipeline:
def __init__(self, db, tenant_id):
called["pipeline_tenant_id"] = tenant_id
async def _resolve_command_operator_user(self, *, sender):
called["operator_sender"] = sender
return _FakeOperator()
async def confirm_item(self, *, item_id, user_id):
called["confirm_item"] = {"item_id": item_id, "user_id": user_id}
return {"project_id": "18", "source_registry_id": "501"}
monkeypatch.setattr(
"app.services.integrations.feishu_signal_pipeline.FeishuSignalPipelineService",
_FakePipeline,
)
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
data = result.get("data") if isinstance(result.get("data"), dict) else {}
assert data.get("handled") is True
assert data.get("queue_item_id") == 201
assert called["confirm_item"] == {"item_id": 201, "user_id": 77}
assert called["reply_text"] == "已继续处理:信号源注册#501 已接入项目#18。"
assert called["reply_chat"] == "oc_queue_1"