from types import SimpleNamespace import pytest from app.config.ai_prompts import ( PROMPT_EXTERNAL_TREND_QA, PROMPT_FEISHU_DIRECT_QA, PROMPT_FEISHU_MIXED_QA, PROMPT_PROJECT_STATUS_QA, ) from app.services.ai_pmo.ai_pmo_kernel_common_core.constants import STATE_SUCCEEDED from app.services.ai_pmo.draft_confirm_handler import DraftConfirmHandler from app.services.ai_pmo.ai_pmo_kernel_ingress_flow import mixin_auto from app.services.adaptive_rag_service.mixin_deep_guard_answer import AdaptiveRAGDeepGuardAnswerMixin from app.services.integrations.feishu_integration_service.mixin_reply_llm_core import ( FeishuReplyLLMCoreMixin, ) def test_parse_action_confirm() -> None: action, note = DraftConfirmHandler.parse_action_from_text(message_text="确认") assert action == "confirm" assert note == "" def test_parse_action_revise_with_note() -> None: action, note = DraftConfirmHandler.parse_action_from_text(message_text="修改 改成每周跟进") assert action == "revise" assert note == "改成每周跟进" def test_parse_action_cancel() -> None: action, note = DraftConfirmHandler.parse_action_from_text(message_text="取消") assert action == "cancel" assert note == "" def test_parse_action_yes_as_confirm() -> None: action, note = DraftConfirmHandler.parse_action_from_text(message_text="yes") assert action == "confirm" assert note == "" def test_draft_confirm_handler_thread_anchor_match() -> None: ingest = type( "Ingest", (), { "payload": { "thread_id": "th_1", "root_message_id": "root_1", "reply_to_message_id": "reply_1", }, "source_message_id": "src_1", }, )() work_item = type("WorkItem", (), {"source_message_id": "wi_1"})() assert DraftConfirmHandler._matches_thread_anchor( work_item=work_item, ingest=ingest, anchor_context={"reply_to_message_id": "reply_1"}, ) is True assert DraftConfirmHandler._matches_thread_anchor( work_item=work_item, ingest=ingest, anchor_context={"reply_to_message_id": "missing"}, ) is False assert DraftConfirmHandler._normalize_thread_context( {"thread_id": "th_1", "root_message_id": "root_1", "reply_to_message_id": "reply_1"} ) == { "thread_id": "th_1", "root_message_id": "root_1", "reply_to_message_id": "reply_1", } def test_draft_confirm_handler_thread_anchor_matches_draft_reply_message_id() -> None: ingest = type( "Ingest", (), { "payload": { "thread_id": "th_1", "root_message_id": "draft_msg_1", "reply_to_message_id": "draft_msg_1", }, "source_message_id": "original_msg_1", }, )() work_item = type("WorkItem", (), {"source_message_id": "original_msg_1"})() assert DraftConfirmHandler._matches_thread_anchor( work_item=work_item, ingest=ingest, anchor_context={"root_message_id": "draft_msg_1"}, draft_reply_message_id="draft_msg_1", ) is True def test_reply_outbox_extract_sent_message_id() -> None: from app.services.integrations.feishu_integration_service.mixin_reply_outbox import FeishuReplyOutboxMixin assert FeishuReplyOutboxMixin._extract_sent_message_id({"data": {"message_id": "msg_123"}}) == "msg_123" assert FeishuReplyOutboxMixin._extract_sent_message_id({"message_id": "msg_456"}) == "msg_456" @pytest.mark.asyncio async def test_draft_confirm_handler_repeated_confirm_returns_inbox_success_copy() -> None: class _FakeResult: def __init__(self, locked): self._locked = locked def scalar_one_or_none(self): return self._locked class _FakeDB: def __init__(self, locked): self._locked = locked async def execute(self, query): # noqa: ARG002 return _FakeResult(self._locked) locked = SimpleNamespace( id=42, state=STATE_SUCCEEDED, intent="draft", project_id=7, signal_id=9, draft_version=1, draft_expires_at=None, ) handler = DraftConfirmHandler(type("Kernel", (), {"db": _FakeDB(locked), "tenant_id": 1})()) result = await handler.confirm_draft( target_work_item=SimpleNamespace(id=42, state=STATE_SUCCEEDED, intent="draft", project_id=7, signal_id=9), target_ingest=SimpleNamespace(), sender="测试用户", source_system="feishu", source_message_id="msg-1", message_text="确认", expected_version=None, ) assert result["handled"] is True assert result["status"] == "succeeded" assert result["followup_reply"] == "已入库成功,无需重复确认。" @pytest.mark.asyncio async def test_draft_confirm_handler_ignores_non_draft_reply_without_target(monkeypatch) -> None: kernel = type("Kernel", (), {"db": object(), "tenant_id": 1})() handler = DraftConfirmHandler(kernel) async def _fake_resolve_pending_draft(**kwargs): _ = kwargs return None monkeypatch.setattr(handler, "_resolve_pending_draft", _fake_resolve_pending_draft) result = await handler.handle_text_reply( message_text="创建时间是多少?", sender="林宇航", source_system="feishu", source_message_id="msg-1", channel_ref="chat-1", source_sender_open_id="ou_xxx", thread_context={"reply_to_message_id": "reply-1"}, ) assert result["handled"] is False assert result["status"] == "ignored" assert result["followup_reply"] == "" def test_kernel_ingress_mixin_auto_safe_int_regression() -> None: assert callable(mixin_auto._safe_int) assert mixin_auto._safe_int("123") == 123 assert mixin_auto._safe_int("") is None assert mixin_auto._safe_int(None) is None def test_adaptive_rag_process_entry_and_mixed_format() -> None: mixin = AdaptiveRAGDeepGuardAnswerMixin() assert mixin._is_ai_pmo_process_entry("pmflow_manual") is True assert mixin._is_ai_pmo_process_entry("feishu") is False mixed = mixin._format_mixed_two_block_output( answer_text="[回答] 接口契约已对齐\n[已收到需求] 新增任务创建确认流", question_part="现在进展怎么样", signal_part="补齐任务确认", ) assert mixed.startswith("[回答] ") assert "\n[已收到需求] " in mixed assert mixed.endswith(",已入库处理") def test_adaptive_rag_select_scene_prompt_prefers_mixed() -> None: mixin = AdaptiveRAGDeepGuardAnswerMixin() scene_key, prompt = mixin._select_scene_prompt( question="现在进展怎么样?", project_id=7, asked_via="pmflow_manual", message_intent="mixed", mixed_signal_part="新增任务确认流", conversation_text="无", context_blocks_text="无", system_snapshot={}, user_snapshot={}, system_snapshot_text="{}", user_snapshot_text="{}", relevance_max=0.0, ) assert scene_key == "mixed" assert "现在进展怎么样" in prompt assert "新增任务确认流" in prompt def test_adaptive_rag_select_scene_prompt_ai_pmo_manual_entry() -> None: mixin = AdaptiveRAGDeepGuardAnswerMixin() scene_key, prompt = mixin._select_scene_prompt( question="帮我总结这个项目", project_id=9, asked_via="pmflow_manual", message_intent=None, mixed_signal_part=None, conversation_text="无", context_blocks_text="无", system_snapshot={}, user_snapshot={}, system_snapshot_text="{}", user_snapshot_text="{}", relevance_max=0.0, ) assert scene_key == "ai_pmo" assert "帮我总结这个项目" in prompt def test_adaptive_rag_select_scene_prompt_prefers_decision_choice() -> None: mixin = AdaptiveRAGDeepGuardAnswerMixin() scene_key, prompt = mixin._select_scene_prompt( question="如果 AI 助手第一次上线只允许一种提醒,你会把它做成桌面通知还是飞书消息?为什么?", project_id=None, asked_via="feishu", message_intent=None, mixed_signal_part=None, conversation_text="无", context_blocks_text="无", system_snapshot={}, user_snapshot={}, system_snapshot_text="{}", user_snapshot_text="{}", relevance_max=0.0, ) assert scene_key == "decision_choice" assert "明确选择" in prompt assert "飞书消息" in prompt def test_adaptive_rag_select_scene_prompt_prefers_industry_trend_without_project_context() -> None: mixin = AdaptiveRAGDeepGuardAnswerMixin() scene_key, prompt = mixin._select_scene_prompt( question="帮我看看宠物行业最新动态", project_id=None, asked_via="feishu", message_intent=None, mixed_signal_part=None, conversation_text="无", context_blocks_text="无", system_snapshot={"assistant_profile": {"name": "PMFlow"}}, user_snapshot={"projects": [{"id": 1}]}, system_snapshot_text="{}", user_snapshot_text="{}", relevance_max=0.0, ) assert scene_key == "industry_trend" assert "当前未接入实时数据" in prompt assert "project_context" not in prompt def test_adaptive_rag_select_scene_prompt_filters_member_context_noise() -> None: mixin = AdaptiveRAGDeepGuardAnswerMixin() scene_key, prompt = mixin._select_scene_prompt( question="杨宇现在负载情况如何?", project_id=None, asked_via="feishu", message_intent=None, mixed_signal_part=None, conversation_text="无", context_blocks_text=( "[1] 来源: project\n相关度: 0.991\n内容: Enterprise Knowledge Base 项目列表\n" "[2] 来源: workload\n相关度: 0.884\n内容: 杨宇 当前负载 265.56% 超载\n" ), system_snapshot={"team_members": []}, user_snapshot={"projects": [{"id": 1}]}, system_snapshot_text="{\"team_members\": []}", user_snapshot_text="{\"projects\": [{\"id\": 1}]}", relevance_max=0.0, ) assert scene_key == "member_workload" assert "Enterprise Knowledge Base 项目列表" not in prompt assert "杨宇 当前负载 265.56% 超载" in prompt assert '"projects"' not in prompt def test_adaptive_rag_select_scene_prompt_ignores_negated_project_context_for_trend() -> None: mixin = AdaptiveRAGDeepGuardAnswerMixin() scene_key, prompt = mixin._select_scene_prompt( question="帮我看看宠物行业最新动态,重点只说消费侧和产品侧的变化,不要提项目。", project_id=7, asked_via="feishu", message_intent=None, mixed_signal_part=None, conversation_text="无", context_blocks_text="无", system_snapshot={"assistant_profile": {"name": "PMFlow"}}, user_snapshot={"projects": [{"id": 1}]}, system_snapshot_text="{}", user_snapshot_text="{}", relevance_max=0.0, ) assert scene_key == "industry_trend" assert "当前未接入实时数据" in prompt assert "project_context" not in prompt def test_adaptive_rag_select_scene_prompt_uses_project_trend_when_project_context_explicit() -> None: mixin = AdaptiveRAGDeepGuardAnswerMixin() scene_key, prompt = mixin._select_scene_prompt( question="这个项目所在行业最新动态怎么样?", project_id=7, asked_via="feishu", message_intent=None, mixed_signal_part=None, conversation_text="无", context_blocks_text="无", system_snapshot={"assistant_profile": {"name": "PMFlow"}}, user_snapshot={"projects": [{"id": 1}]}, system_snapshot_text="{}", user_snapshot_text="{}", relevance_max=0.0, ) assert scene_key == "external_trend" assert "项目上下文" in prompt assert "外部搜索结果" in prompt def test_adaptive_rag_select_scene_prompt_prefers_project_status_for_progress_questions() -> None: mixin = AdaptiveRAGDeepGuardAnswerMixin() scene_key, prompt = mixin._select_scene_prompt( question="这个项目最新进展怎么样?", project_id=7, asked_via="feishu", message_intent=None, mixed_signal_part=None, conversation_text="无", context_blocks_text="无", system_snapshot={"assistant_profile": {"name": "PMFlow"}}, user_snapshot={"projects": [{"id": 1}]}, system_snapshot_text="{}", user_snapshot_text="{}", relevance_max=0.0, query_type="status", ) assert scene_key == "project_status" assert "项目状态分析助手" in prompt assert "完成率" in prompt assert "当前阶段" in prompt def test_feishu_qa_prompts_prefer_professional_answer_style() -> None: assert "先给结论" in PROMPT_FEISHU_DIRECT_QA assert "信息不足,缺少" in PROMPT_FEISHU_DIRECT_QA assert "适合飞书消息阅读" in PROMPT_FEISHU_DIRECT_QA assert "先回答问题部分" in PROMPT_FEISHU_MIXED_QA assert "[已收到需求]" in PROMPT_FEISHU_MIXED_QA assert "专业" in PROMPT_FEISHU_MIXED_QA assert "XXXX年" not in PROMPT_EXTERNAL_TREND_QA assert "通用行业认知" in PROMPT_EXTERNAL_TREND_QA assert "项目状态分析助手" in PROMPT_PROJECT_STATUS_QA def test_feishu_reply_llm_fallback_is_failure_oriented() -> None: reply_text = FeishuReplyLLMCoreMixin._fallback_reply_text( { "status": "captured", "classification": "project", } ) assert "当前回复" in reply_text assert "已收到需求并完成初步识别" not in reply_text assert "已入库处理" not in reply_text