import asyncio import inspect from types import SimpleNamespace import pytest from app.services.ai_pmo.draft_confirm_handler import DraftConfirmHandler from app.services.integrations.feishu_signal_pipeline.mixin_gate_project import ( FeishuPipelineGateProjectMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_command_commit_project import ( FeishuPipelineCommandCommitProjectMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_command_commit_task import ( FeishuPipelineCommandCommitTaskMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_context_prefill_b import ( FeishuPipelineContextPrefillBMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_extraction import ( FeishuPipelineExtractionMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_item_processing import ( FeishuPipelineItemProcessingMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_followup_a import ( FeishuPipelineFollowupAMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_followup_b import FeishuPipelineFollowupBMixin from app.services.integrations.feishu_signal_pipeline.mixin_gate_dispatch import ( FeishuPipelineGateDispatchMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_item_finalize_routing import ( FeishuPipelineItemFinalizeRoutingMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_message_utils_b import ( FeishuPipelineMessageUtilsBMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_message_utils_a import ( FeishuPipelineMessageUtilsAMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_process_message_helpers_b import ( FeishuPipelineProcessMessageHelpersBMixin, ) from app.services.integrations.feishu_signal_pipeline.mixin_queue_confirm_undo import ( FeishuPipelineQueueConfirmUndoMixin, ) from app.services.integrations.feishu_signal_pipeline.service import FeishuSignalPipelineService from app.services.integrations.feishu_signal_pipeline import mixin_process_message_core from app.services.signal.signal_capture_service import mixin_capture_entry from app.services.signal.signal_capture_service.service import ProactiveSignalCaptureService from app.services.signal.signal_capture_service.mixin_task_orchestration_main import ( SignalCaptureTaskOrchestrationMainMixin, ) from app.services.signal.signal_capture_service.mixin_task_orchestration_skill import ( SignalCaptureTaskOrchestrationSkillMixin, ) class _Item: def __init__(self, *, item_id: int, level: str, operation_code: str, raw_message: str = "") -> None: self.id = item_id self.level = level self.card_type = operation_code.replace(".", "_") self.raw_message = raw_message self.result_payload = {"operation_code": operation_code} self.feishu_message_id = f"msg-{item_id}" def test_followup_status_target_requires_unique_match() -> None: items = [ _Item(item_id=1, level="command", operation_code="project.create"), _Item(item_id=2, level="command", operation_code="project.create"), ] target = FeishuPipelineFollowupBMixin._select_followup_status_target( message="刚才创建项目现在怎么样了", recent_items=items, ) assert target is None def test_followup_status_target_rejects_single_item_without_anchor() -> None: items = [ _Item(item_id=1, level="project", operation_code="project.create"), ] target = FeishuPipelineFollowupBMixin._select_followup_status_target( message="它现在怎么样?", recent_items=items, ) assert target is None def test_followup_status_target_does_not_use_lexical_project_hints_without_anchor() -> None: items = [ _Item(item_id=1, level="project", operation_code="project.create"), ] target = FeishuPipelineFollowupBMixin._select_followup_status_target( message="刚才创建项目现在怎么样了", recent_items=items, ) assert target is None def test_followup_status_target_prefers_thread_anchor() -> None: items = [ _Item(item_id=1, level="project", operation_code="project.create"), _Item(item_id=2, level="project", operation_code="project.create"), ] items[0].feishu_message_id = "root-1" items[1].feishu_message_id = "root-2" target = FeishuPipelineFollowupBMixin._select_followup_status_target( message="他是什么时候创建的?", recent_items=items, thread_context={"root_message_id": "root-1"}, ) assert target is items[0] def test_followup_status_target_rejects_unmatched_thread_anchor() -> None: items = [ _Item(item_id=1, level="project", operation_code="project.create"), _Item(item_id=2, level="project", operation_code="project.create"), ] items[0].feishu_message_id = "root-1" items[1].feishu_message_id = "root-2" target = FeishuPipelineFollowupBMixin._select_followup_status_target( message="他是什么时候创建的?", recent_items=items, thread_context={"reply_to_message_id": "reply-miss"}, ) assert target is None @pytest.mark.asyncio async def test_followup_status_reply_reports_project_created_at(monkeypatch) -> None: from datetime import datetime class _FakeDBResult: def __init__(self, value) -> None: self._value = value def scalar_one_or_none(self): return self._value class _FakeDB: async def execute(self, query): _ = query return _FakeDBResult(type("ProjectRow", (), {"id": 7, "tenant_id": 1, "title": "自动开票系统", "name": "自动开票系统", "created_at": datetime(2026, 3, 1, 9, 30)})()) class _FakeFollowup(FeishuPipelineFollowupBMixin): def __init__(self) -> None: self.db = _FakeDB() self.tenant_id = 1 async def _get_project_suggestion(self, item_id: int): _ = item_id return type("Suggestion", (), {"created_project_id": 7})() @staticmethod def _normalize_route_status(value): return str(value) item = _Item(item_id=1, level="project", operation_code="project.create") item.route_status = "auto_done" item.matched_project_id = 7 reply = await _FakeFollowup()._build_followup_status_reply(item, message="他是什么时候创建的?") assert "创建于 2026-03-01 09:30" in reply @pytest.mark.asyncio async def test_followup_queue_handler_ignores_unmatched_thread_anchor() -> None: class _FakeItem: def __init__(self) -> None: self.id = 1 self.route_status = "queued" self.level = "project" self.feishu_message_id = "queue-root-1" self.blocking_reason = "请确认" class _FakeFollowup(FeishuPipelineFollowupAMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 @staticmethod def _looks_like_continue_followup(message: str) -> bool: _ = message return False async def _load_recent_chat_items(self, *, source_chat_id, source_sender_open_id, sender, limit=5): _ = (source_chat_id, source_sender_open_id, sender, limit) return [_FakeItem()] result = await _FakeFollowup()._maybe_handle_queue_followup( message="他是什么时候创建的?", sender="测试用户", source_chat_id="chat-1", source_sender_open_id="ou-1", feishu_message_id="msg-1", process_started_at=0.0, thread_context={"reply_to_message_id": "reply-miss"}, ) assert result is None def test_process_message_no_longer_calls_legacy_queue_followup() -> None: source = inspect.getsource(mixin_process_message_core.FeishuPipelineProcessMessageCoreMixin.process_message) assert "_maybe_handle_queue_followup" not in source def test_feishu_signal_pipeline_service_runtime_excludes_legacy_followup_mixins() -> None: source = inspect.getsource(FeishuSignalPipelineService) assert "FeishuPipelineFollowupAMixin" not in source assert "FeishuPipelineFollowupBMixin" not in source def test_proactive_signal_capture_service_runtime_excludes_legacy_capture_mixin() -> None: source = inspect.getsource(ProactiveSignalCaptureService) assert "SignalCaptureLegacyMixin" not in source def test_decorate_capture_result_adds_queue_action_card_metadata() -> None: result = FeishuPipelineProcessMessageHelpersBMixin._decorate_capture_result( { "status": "captured", "results": [{"item_id": 301, "route_status": "queued_for_decision"}], "direct_reply_text": "已转待确认。", }, chat_type="group", normalized_text="请创建项目", route="group.intent_gate.project_create", db_mutations=[], ) assert result["reply_template_key"] == "queue_pending_confirm" assert result["queue_item_id"] == 301 def test_rag_conversation_key_uses_stable_scope_without_reply_anchor() -> None: key = FeishuPipelineMessageUtilsAMixin._build_rag_conversation_key( source_chat_id="chat-1", source_sender_open_id="ou-1", sender="测试用户", source_message_id="msg-123", thread_context=None, ) assert key == "feishu:chat-1:ou-1" def test_rag_conversation_key_prefers_thread_anchor_when_reply_present() -> None: key = FeishuPipelineMessageUtilsAMixin._build_rag_conversation_key( source_chat_id="chat-1", source_sender_open_id="ou-1", sender="测试用户", source_message_id="msg-123", thread_context={"reply_to_message_id": "om-bot-reply-1"}, ) assert key == "feishu:chat-1:om-bot-reply-1" @pytest.mark.asyncio async def test_build_direct_qa_capture_exposes_reason_code_and_anchor_kind() -> None: class _FakeHelpers(FeishuPipelineProcessMessageHelpersBMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 async def _write_processing_log(self, **kwargs): _ = kwargs def _build_rag_reply_text(self, *, answer: str, sources: list[dict[str, object]]) -> str: _ = sources return answer helpers = _FakeHelpers() capture = await helpers._build_direct_qa_capture( message="这个是哪个模型回复的?", rag_result={ "answer": "这条回复命中的逻辑层是 low。", "clarification_code": None, "reason_code": "meta_followup_answered", "anchor_kind": "meta_recent_reply", "meta_followup_kind": "reply_model_identity", "sources": [], }, inferred_project_id=None, feishu_message_id="msg-1", process_started_at=0.0, route_reason="direct qa", intent_label="question", ) assert capture["reason_code"] == "meta_followup_answered" assert capture["anchor_kind"] == "meta_recent_reply" assert capture["meta_followup_kind"] == "reply_model_identity" @pytest.mark.asyncio async def test_intent_gate_query_does_not_inherit_project_context(monkeypatch) -> None: captured: dict[str, object] = {} class _FakeAdaptiveRAGService: def __init__(self, db, tenant_id: int): captured["tenant_id"] = tenant_id _ = db async def classify_intent(self, text: str): captured["classified_text"] = text return {"intent": "question", "question_part": text} async def answer_question(self, **kwargs): captured["answer_kwargs"] = kwargs return {"answer": "AI 行业动态正常", "sources": []} class _FakeGate(FeishuPipelineGateDispatchMixin, FeishuPipelineProcessMessageHelpersBMixin): def __init__(self) -> None: class _FakeDB: async def commit(self) -> None: return None self.db = _FakeDB() self.tenant_id = 1 async def _write_processing_log(self, **kwargs): captured["write_log"] = kwargs def _build_rag_conversation_key(self, **kwargs): _ = kwargs return "feishu:chat-1" def _build_rag_reply_text(self, *, answer: str, sources: list[dict[str, object]]) -> str: _ = sources return answer monkeypatch.setattr( "app.services.integrations.feishu_signal_pipeline.mixin_gate_dispatch.AdaptiveRAGService", _FakeAdaptiveRAGService, ) result = await _FakeGate()._maybe_handle_intent_gate_capture( intent_gate={"intent": "query"}, message="最近AI行业有什么动态", sender="测试用户", source_chat_id="chat-1", source_sender_open_id="ou-1", feishu_message_id="msg-1", captured_at=None, inferred_project_id=6, projects=[], process_started_at=0.0, normalized_chat_type="group", thread_context={"reply_to_message_id": "msg-root-1"}, ) assert isinstance(result, dict) assert result["linked_project_ids"] == [] assert captured["answer_kwargs"]["project_id"] is None assert captured["answer_kwargs"]["source_sender_open_id"] == "ou-1" assert captured["answer_kwargs"]["thread_context"] == {"reply_to_message_id": "msg-root-1"} @pytest.mark.asyncio async def test_feishu_question_like_messages_use_sync_high_accuracy_path(monkeypatch) -> None: calls = {"sync": 0, "session": 0, "cache": 0} class _FakeBundle: def __init__(self) -> None: self.primary_workflow = "query_flow" self.workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] self.diagnostics = {} def to_dict(self) -> dict[str, object]: return { "bundle_id": "semantic-query", "semantic_version": "2026-03-27.workflow-v1", "source_system": "feishu", "primary_workflow": self.primary_workflow, "workflows": [ { "workflow_type": "query_flow", "workflow_action": "answer", "target_scope": "global", "operation_codes": ["query.answer"], "requires_confirmation": False, "confidence": 0.9, "slots": [], "clarification_needed": False, "metadata": {}, } ], "message_gate": "auto_execute", "reply_contract": { "mode": "aggregate_card", "template_key": "workflow_bundle", "summary": "query_flow", }, "diagnostics": {}, } class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-1" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def _get_or_create_session(self, **kwargs): calls["session"] += 1 _ = kwargs raise AssertionError("question-like Feishu messages should not create an async queue session") def _is_short_confirmation_message(self, **kwargs): _ = kwargs return False async def process_feishu_message(self, **kwargs): calls["sync"] += 1 _ = kwargs return { "handled": True, "status": "answered", "classification": "question", "linked_project_ids": [], "signal_id": None, "created_project_id": None, "auto_tasks": [], "ai_trace_id": "trace-1", "direct_reply_required": True, "direct_reply_text": "垂直行业建议先看目标客户与交付边界。", } async def _noop_sync_recent_cache(self, **kwargs): calls["cache"] += 1 _ = kwargs monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache) monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) service = ProactiveSignalCaptureService(object()) result = await service.capture_from_feishu( group_chat_id="chat-1", message_content="我感觉第一个垂直应用前景比较好,但是垂直哪个行业呢?", sender="测试用户", tenant_id=1, mentioned_bot_names=["PMFlow"], feishu_message_id="msg-1", chat_type="group", ) assert result["status"] == "answered" assert "已收到,正在处理。" not in str(result.get("direct_reply_text") or "") assert calls["sync"] == 1 assert calls["session"] == 0 assert calls["cache"] == 1 @pytest.mark.asyncio async def test_feishu_question_like_messages_require_explicit_group_trigger(monkeypatch) -> None: calls = {"sync": 0, "session": 0} class _FakeBundle: def __init__(self) -> None: self.primary_workflow = "query_flow" self.workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] self.diagnostics = {} class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-query-no-trigger" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def _get_or_create_session(self, **kwargs): calls["session"] += 1 _ = kwargs raise AssertionError("group query without explicit trigger should not create an async queue session") async def process_feishu_message(self, **kwargs): calls["sync"] += 1 _ = kwargs raise AssertionError("group query without explicit trigger should not run sync QA") monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) service = ProactiveSignalCaptureService(object()) result = await service.capture_from_feishu( group_chat_id="chat-query-no-trigger", message_content="最近AI行业有什么动态", sender="测试用户", tenant_id=1, mentioned_bot_names=[], feishu_message_id="msg-query-no-trigger", chat_type="group", thread_context=None, ) assert result["status"] == "ignored" assert result["reason"] == "group_explicit_trigger_required" assert result["direct_reply_required"] is False assert calls["sync"] == 0 assert calls["session"] == 0 @pytest.mark.asyncio async def test_feishu_business_review_query_forwards_semantic_bundle_into_kernel(monkeypatch) -> None: captured: dict[str, object] = {} class _FakeBundle: def __init__(self) -> None: self.primary_workflow = "query_flow" self.workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] self.diagnostics = { "message_intent": "business_review", "consultant_mode_preference": "clarify", } def to_dict(self) -> dict[str, object]: return { "primary_workflow": "query_flow", "workflows": [ { "workflow_type": "query_flow", "workflow_action": "answer", } ], "diagnostics": dict(self.diagnostics), } class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-business-review" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def _get_or_create_session(self, **kwargs): _ = kwargs raise AssertionError("business review sync path should not create async queue session") def _is_short_confirmation_message(self, **kwargs): _ = kwargs return False async def process_feishu_message(self, **kwargs): captured["kwargs"] = kwargs return { "handled": True, "status": "answered", "classification": "question", "linked_project_ids": [], "signal_id": None, "created_project_id": None, "auto_tasks": [], "ai_trace_id": "trace-br", "direct_reply_required": True, "direct_reply_text": "五大口径评审中。", } async def _noop_sync_recent_cache(self, **kwargs): _ = (self, kwargs) monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache) monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) service = ProactiveSignalCaptureService(object()) result = await service.capture_from_feishu( group_chat_id="chat-br-1", message_content="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,", sender="测试用户", tenant_id=1, mentioned_bot_names=["PMFlow"], feishu_message_id="msg-business-review", chat_type="group", ) assert result["status"] == "answered" kwargs = captured["kwargs"] assert isinstance(kwargs, dict) semantic_bundle = kwargs["semantic_bundle"] assert semantic_bundle["primary_workflow"] == "query_flow" assert semantic_bundle["diagnostics"]["message_intent"] == "business_review" @pytest.mark.asyncio async def test_force_sync_feishu_query_flow_still_forwards_semantic_bundle_into_kernel(monkeypatch) -> None: captured: dict[str, object] = {} class _FakeBundle: def __init__(self) -> None: self.primary_workflow = "query_flow" self.workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] self.diagnostics = { "message_intent": "business_review", "consultant_mode_preference": "clarify", } def to_dict(self) -> dict[str, object]: return { "primary_workflow": "query_flow", "workflows": [ { "workflow_type": "query_flow", "workflow_action": "answer", } ], "diagnostics": dict(self.diagnostics), } class _FakeDB: async def commit(self) -> None: return None async def rollback(self) -> None: return None class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-force-sync-query" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def _get_or_create_session(self, **kwargs): _ = kwargs raise AssertionError("force_sync query path should not create async queue session") def _is_short_confirmation_message(self, **kwargs): _ = kwargs return False async def process_feishu_message(self, **kwargs): captured["kwargs"] = kwargs return { "handled": True, "status": "answered", "classification": "question", "linked_project_ids": [], "signal_id": None, "created_project_id": None, "auto_tasks": [], "ai_trace_id": "trace-force-sync-query", "direct_reply_required": True, "direct_reply_text": "先做行业调研和需求收敛。", } async def _noop_sync_recent_cache(self, **kwargs): _ = (self, kwargs) monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache) monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) service = ProactiveSignalCaptureService(_FakeDB()) result = await service.capture_from_feishu( group_chat_id="chat-force-sync-query", message_content="想了解下当前电动车的行业,以此做一个mvp需求", sender="测试用户", tenant_id=1, feishu_message_id="msg-force-sync-query", chat_type="group", force_sync=True, ) assert result["status"] == "answered" kwargs = captured["kwargs"] assert isinstance(kwargs, dict) semantic_bundle = kwargs["semantic_bundle"] assert semantic_bundle["primary_workflow"] == "query_flow" assert semantic_bundle["diagnostics"]["message_intent"] == "business_review" @pytest.mark.asyncio async def test_process_feishu_with_kernel_sync_rolls_back_current_session_and_surfaces_real_error() -> None: class _FakeDB: def __init__(self) -> None: self.rollback_calls = 0 async def rollback(self) -> None: self.rollback_calls += 1 class _FakeKernel: async def process_feishu_message(self, **kwargs): _ = kwargs return { "handled": False, "reason": "kernel_exception", "error": "No row was found when one was required", } service = ProactiveSignalCaptureService(_FakeDB()) result = await service._process_feishu_with_kernel_sync( trace_id="trace-kernel-error", tenant_id=1, kernel=_FakeKernel(), group_chat_id="chat-rollback", cleaned_message="先不要直接创建,我还在补充需求。", proposer_name="测试用户", proposer_open_id="ou-rollback", captured_dt=mixin_capture_entry.now_cn_aware(), feishu_message_id="msg-rollback", chat_type="group", ) assert service.db.rollback_calls == 1 assert result["status"] == "error" assert result["reason"] == "kernel_exception" assert "No row was found when one was required" in result["direct_reply_text"] @pytest.mark.asyncio async def test_process_feishu_with_kernel_sync_commits_successful_runtime_finalize() -> None: class _FakeDB: def __init__(self) -> None: self.commit_calls = 0 self.rollback_calls = 0 async def commit(self) -> None: self.commit_calls += 1 async def rollback(self) -> None: self.rollback_calls += 1 class _FakeKernel: async def process_feishu_message(self, **kwargs): _ = kwargs return { "handled": True, "status": "captured", "classification": "command", "work_item_id": 55, "direct_reply_required": True, "direct_reply_text": "已进入 mounted runtime。", } class _FakeService(ProactiveSignalCaptureService): async def _apply_pipeline_post_actions(self, **kwargs): return kwargs["pipeline_result"] async def _sync_feishu_recent_cache(self, **kwargs): _ = kwargs return None service = _FakeService(_FakeDB()) result = await service._process_feishu_with_kernel_sync( trace_id="trace-sync-commit", tenant_id=1, kernel=_FakeKernel(), group_chat_id="chat-sync-commit", cleaned_message="先不要创建,我还在补范围。", proposer_name="测试用户", proposer_open_id="ou-sync-commit", captured_dt=mixin_capture_entry.now_cn_aware(), feishu_message_id="msg-sync-commit", chat_type="group", ) assert result["status"] == "captured" assert service.db.commit_calls == 1 assert service.db.rollback_calls == 0 @pytest.mark.asyncio async def test_capture_from_feishu_outer_exception_rolls_back_current_session_and_surfaces_error(monkeypatch) -> None: class _FakeDB: def __init__(self) -> None: self.rollback_calls = 0 async def rollback(self) -> None: self.rollback_calls += 1 class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-outer-exception" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def process_feishu_message(self, **kwargs): _ = kwargs raise RuntimeError("boom") class _FakeSemanticBundle: primary_workflow = "query_flow" diagnostics: dict[str, object] = {} workflows: list[object] = [] def to_dict(self) -> dict[str, object]: return { "primary_workflow": self.primary_workflow, "diagnostics": {}, "workflows": [], } async def _fake_aplan_message(**kwargs): _ = kwargs return _FakeSemanticBundle() monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", _fake_aplan_message) service = ProactiveSignalCaptureService(_FakeDB()) result = await service.capture_from_feishu( group_chat_id="chat-outer-exception", message_content="帮我看看这个需求", sender="测试用户", tenant_id=1, feishu_message_id="msg-outer-exception", chat_type="group", force_sync=True, ) assert service.db.rollback_calls == 1 assert result["status"] == "error" assert result["reason"] == "kernel_failed" assert "boom" in result["direct_reply_text"] @pytest.mark.asyncio async def test_capture_from_feishu_skips_when_source_activation_whitelist_misses(monkeypatch) -> None: class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-gated" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def _get_or_create_session(self, **kwargs): raise AssertionError("source activation miss should stop before queue session creation") async def process_feishu_message(self, **kwargs): raise AssertionError("source activation miss should not reach kernel processing") class _FakeMountedRuntimeWriteService: def __init__(self, db): _ = db async def resolve_source_activation_scope( self, *, tenant_id: int, source_system: str, channel_ref: str | None, thread_context: dict | None, ): _ = (tenant_id, source_system, channel_ref, thread_context) return None, True monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService) service = ProactiveSignalCaptureService(object()) result = await service.capture_from_feishu( group_chat_id="chat-not-whitelisted", message_content="这个需求帮我继续推进一下", sender="测试用户", tenant_id=1, feishu_message_id="msg-gated", chat_type="group", ) assert result["status"] == "ignored" assert result["reason"] == "mounted_runtime_scope_not_activated" assert result["direct_reply_required"] is False def test_capture_from_feishu_no_longer_references_legacy_fallback() -> None: source = inspect.getsource(mixin_capture_entry.SignalCaptureEntryMixin.capture_from_feishu) assert "_capture_from_feishu_legacy_flow" not in source @pytest.mark.asyncio async def test_should_process_feishu_high_accuracy_treats_requirement_plan_as_sync(monkeypatch) -> None: class _FakeBundle: primary_workflow = "query_flow" workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] diagnostics = {"message_intent": "requirement_plan"} monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) should_sync, reason = await mixin_capture_entry.SignalCaptureEntryMixin._should_process_feishu_high_accuracy( message_text="@PMFlow 想跟你聊聊需求,做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。", ) assert should_sync is True assert reason == "semantic:requirement_plan" @pytest.mark.asyncio async def test_should_process_feishu_high_accuracy_treats_requirement_plan_slot_answer_as_sync(monkeypatch) -> None: class _FakeBundle: primary_workflow = "query_flow" workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] diagnostics = {"message_intent": "requirement_plan"} monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) should_sync, reason = await mixin_capture_entry.SignalCaptureEntryMixin._should_process_feishu_high_accuracy( message_text="@PMFlow 先按技术指标来,主要给投顾看。", ) assert should_sync is True assert reason == "semantic:requirement_plan" @pytest.mark.asyncio async def test_should_process_feishu_high_accuracy_no_longer_uses_question_suffix_fallback(monkeypatch) -> None: class _FakeBundle: primary_workflow = "project_signal_flow" workflows = [SimpleNamespace(workflow_type="project_signal_flow", workflow_action="capture")] diagnostics = {"message_intent": "casual_chat"} monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) should_sync, reason = await mixin_capture_entry.SignalCaptureEntryMixin._should_process_feishu_high_accuracy( message_text="@PMFlow 现在几点?", ) assert should_sync is False assert reason == "async_queue" @pytest.mark.asyncio async def test_capture_from_feishu_requirement_plan_skips_async_ack_and_forwards_semantic_bundle(monkeypatch) -> None: captured: dict[str, object] = {} class _FakeBundle: primary_workflow = "query_flow" workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] diagnostics = {"message_intent": "requirement_plan"} def to_dict(self) -> dict[str, object]: return { "primary_workflow": "query_flow", "workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}], "diagnostics": dict(self.diagnostics), } class _FakeDB: async def commit(self) -> None: return None async def rollback(self) -> None: return None class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-plan-sync" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def _get_or_create_session(self, **kwargs): _ = kwargs raise AssertionError("requirement plan sync path should not create async queue session") def _is_short_confirmation_message(self, **kwargs): _ = kwargs return False async def process_feishu_message(self, **kwargs): captured["kwargs"] = kwargs return { "handled": True, "status": "answered", "classification": "question", "linked_project_ids": [], "signal_id": None, "created_project_id": None, "auto_tasks": [], "ai_trace_id": "trace-plan-sync", "direct_reply_required": True, "direct_reply_text": "可以,我们先只聊需求,不创建。先定一个最关键的问题:你这个分数主要想根据什么来打,比如技术指标、情绪数据,或者你已经有一套规则想自动化?", } class _FakeMountedRuntimeWriteService: def __init__(self, db): _ = db async def resolve_source_activation_scope( self, *, tenant_id: int, source_system: str, channel_ref: str | None, thread_context: dict | None, ): _ = (tenant_id, source_system, channel_ref, thread_context) return SimpleNamespace(id=1, source_system="feishu"), True async def _noop_sync_recent_cache(self, **kwargs): _ = (self, kwargs) monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService) monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache) monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) service = ProactiveSignalCaptureService(_FakeDB()) result = await service.capture_from_feishu( group_chat_id="chat-plan-sync", message_content="@PMFlow 想跟你聊聊需求,做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。", sender="测试用户", tenant_id=1, feishu_message_id="msg-plan-sync", chat_type="group", ) assert result["status"] == "answered" assert result["direct_reply_text"].startswith("可以,我们先只聊需求") kwargs = captured["kwargs"] assert isinstance(kwargs, dict) semantic_bundle = kwargs["semantic_bundle"] assert semantic_bundle["diagnostics"]["message_intent"] == "requirement_plan" assert semantic_bundle["primary_workflow"] == "query_flow" @pytest.mark.asyncio async def test_capture_from_feishu_requirement_plan_slot_answer_stays_sync_without_async_ack(monkeypatch) -> None: captured: dict[str, object] = {} class _FakeBundle: primary_workflow = "query_flow" workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] diagnostics = {"message_intent": "requirement_plan"} def to_dict(self) -> dict[str, object]: return { "primary_workflow": "query_flow", "workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}], "diagnostics": dict(self.diagnostics), } class _FakeDB: async def commit(self) -> None: return None async def rollback(self) -> None: return None class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-plan-followup-sync" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def _get_or_create_session(self, **kwargs): _ = kwargs raise AssertionError("requirement plan slot answers should not create async queue session") def _is_short_confirmation_message(self, **kwargs): _ = kwargs return False async def process_feishu_message(self, **kwargs): captured["kwargs"] = kwargs return { "handled": True, "status": "answered", "classification": "question", "linked_project_ids": [], "signal_id": None, "created_project_id": None, "auto_tasks": [], "ai_trace_id": "trace-plan-followup-sync", "direct_reply_required": True, "direct_reply_text": "好,我先记下:第一版按技术指标打分,主要给投顾看。下一个关键问题:你更想让系统怎么给你这个结果,是飞书日报、页面看板,还是只推送一个分数加一句解释?", } class _FakeMountedRuntimeWriteService: def __init__(self, db): _ = db async def resolve_source_activation_scope( self, *, tenant_id: int, source_system: str, channel_ref: str | None, thread_context: dict | None, ): _ = (tenant_id, source_system, channel_ref, thread_context) return SimpleNamespace(id=1, source_system="feishu"), True async def _noop_sync_recent_cache(self, **kwargs): _ = (self, kwargs) monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService) monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache) monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) service = ProactiveSignalCaptureService(_FakeDB()) result = await service.capture_from_feishu( group_chat_id="chat-plan-followup-sync", message_content="@PMFlow 先按技术指标来,主要给投顾看。", sender="测试用户", tenant_id=1, feishu_message_id="msg-plan-followup-sync", chat_type="group", ) assert result["status"] == "answered" assert "技术指标" in result["direct_reply_text"] semantic_bundle = captured["kwargs"]["semantic_bundle"] assert semantic_bundle["diagnostics"]["message_intent"] == "requirement_plan" @pytest.mark.asyncio async def test_capture_from_feishu_allows_group_choice_protocol_without_repeat_mention_when_requirement_plan_active( monkeypatch, ) -> None: captured: dict[str, object] = {} class _FakeBundle: primary_workflow = "query_flow" workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] diagnostics = {} def to_dict(self) -> dict[str, object]: return { "primary_workflow": "query_flow", "workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}], "diagnostics": {}, } class _FakeDB: async def commit(self) -> None: return None async def rollback(self) -> None: return None class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-plan-choice-followup" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def _bind_active_context_target(self, **kwargs): captured["bind_kwargs"] = dict(kwargs) return SimpleNamespace(id=185), SimpleNamespace(id=31, state="collecting"), "requirement_plan" async def _get_or_create_session(self, **kwargs): _ = kwargs raise AssertionError("active requirement choice followup should stay sync") async def process_feishu_message(self, **kwargs): captured["kwargs"] = kwargs return { "handled": True, "status": "answered", "classification": "question", "linked_project_ids": [], "signal_id": None, "created_project_id": None, "auto_tasks": [], "ai_trace_id": "trace-plan-choice-followup", "direct_reply_required": True, "direct_reply_text": "已确认选项 A,继续下一轮。", } class _FakeMountedRuntimeWriteService: def __init__(self, db): _ = db async def resolve_source_activation_scope( self, *, tenant_id: int, source_system: str, channel_ref: str | None, thread_context: dict | None, ): _ = (tenant_id, source_system, channel_ref, thread_context) return SimpleNamespace(id=1, source_system="feishu"), True async def _noop_sync_recent_cache(self, **kwargs): _ = (self, kwargs) monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService) monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache) monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) service = ProactiveSignalCaptureService(_FakeDB()) result = await service.capture_from_feishu( group_chat_id="chat-plan-choice-followup", message_content="A", sender="测试用户", tenant_id=1, proposer_open_id="ou-plan-choice-followup", mentioned_bot_names=[], feishu_message_id="msg-plan-choice-followup", chat_type="group", thread_context=None, ) assert result["status"] == "answered" assert result["direct_reply_text"] == "已确认选项 A,继续下一轮。" assert isinstance(captured.get("bind_kwargs"), dict) assert str(captured["bind_kwargs"]["message_text"]) == "A" assert isinstance(captured.get("kwargs"), dict) @pytest.mark.asyncio async def test_capture_from_feishu_allows_group_confirm_without_repeat_mention_when_draft_pending_confirm_active( monkeypatch, ) -> None: captured: dict[str, object] = {} class _FakeBundle: primary_workflow = "query_flow" workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] diagnostics = {} def to_dict(self) -> dict[str, object]: return { "primary_workflow": "query_flow", "workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}], "diagnostics": {}, } class _FakeDB: async def commit(self) -> None: return None async def rollback(self) -> None: return None class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-draft-confirm-followup" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def _bind_active_context_target(self, **kwargs): captured["bind_kwargs"] = dict(kwargs) return SimpleNamespace(id=185), SimpleNamespace(id=31, state="draft_pending_confirm"), "draft_pending_confirm" async def _get_or_create_session(self, **kwargs): _ = kwargs raise AssertionError("active draft confirm followup should stay sync") async def process_feishu_message(self, **kwargs): captured["kwargs"] = kwargs return { "handled": True, "status": "answered", "classification": "question", "linked_project_ids": [119], "signal_id": 408, "created_project_id": 119, "auto_tasks": [], "ai_trace_id": "trace-draft-confirm-followup", "direct_reply_required": True, "direct_reply_text": "✅ 已入库", } class _FakeMountedRuntimeWriteService: def __init__(self, db): _ = db async def resolve_source_activation_scope( self, *, tenant_id: int, source_system: str, channel_ref: str | None, thread_context: dict | None, ): _ = (tenant_id, source_system, channel_ref, thread_context) return SimpleNamespace(id=1, source_system="feishu"), True async def _noop_sync_recent_cache(self, **kwargs): _ = (self, kwargs) monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService) monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache) monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) service = ProactiveSignalCaptureService(_FakeDB()) result = await service.capture_from_feishu( group_chat_id="chat-draft-confirm-followup", message_content="确认", sender="测试用户", tenant_id=1, proposer_open_id="ou-draft-confirm-followup", mentioned_bot_names=[], feishu_message_id="msg-draft-confirm-followup", chat_type="group", thread_context=None, ) assert result["status"] == "answered" assert result["direct_reply_text"] == "✅ 已入库" assert isinstance(captured.get("bind_kwargs"), dict) assert str(captured["bind_kwargs"]["message_text"]) == "确认" assert isinstance(captured.get("kwargs"), dict) @pytest.mark.asyncio async def test_draft_confirm_handler_resolves_active_mounted_draft_when_topic_mode_is_draft_ready( monkeypatch, ) -> None: class _FakeResult: def __init__(self, rows) -> None: self._rows = list(rows) def all(self): return list(self._rows) draft_row = SimpleNamespace( id=31, topic_workspace_id=185, active_work_item_id=901, state="draft_pending_confirm", latest_reply_message_id=None, latest_source_message_id="msg-draft-root", ) topic_row = SimpleNamespace( id=185, tenant_id=1, source_system="feishu", mode="draft_ready", status="active", channel_ref="chat-draft-confirm-followup", conversation_scope_key="feishu:chat-draft-confirm-followup:ou-draft-confirm-followup", plan_status="draft_pending_confirm", root_message_id="msg-draft-root", thread_key="", last_message_id="msg-draft-root", updated_at=None, ) class _FakeDB: async def execute(self, query): sql = str( query.compile(compile_kwargs={"literal_binds": True}) ).lower() if "mounted_topic_workspaces.mode = 'plan'" in sql: return _FakeResult([]) if "'draft_ready'" in sql and "'plan'" in sql: return _FakeResult([(draft_row, topic_row)]) return _FakeResult([]) async def _fake_load_work_item_target_by_id(self, *, work_item_id: int): return SimpleNamespace(id=work_item_id, state="draft_pending_confirm"), None async def _fake_release_mounted_draft_binding(self, **kwargs): raise AssertionError(f"active mounted draft should not be released: {kwargs}") monkeypatch.setattr( DraftConfirmHandler, "_load_work_item_target_by_id", _fake_load_work_item_target_by_id, ) monkeypatch.setattr( DraftConfirmHandler, "_release_mounted_draft_binding", _fake_release_mounted_draft_binding, ) handler = DraftConfirmHandler(SimpleNamespace(db=_FakeDB(), tenant_id=1)) result = await handler._resolve_active_mounted_draft( sender="测试用户", source_system="feishu", channel_ref="chat-draft-confirm-followup", source_sender_open_id="ou-draft-confirm-followup", thread_context={"reply_to_message_id": "msg-draft-root"}, ) assert result == (draft_row, topic_row) @pytest.mark.asyncio async def test_capture_from_feishu_group_choice_protocol_prefers_active_requirement_over_older_pending_draft( monkeypatch, ) -> None: captured: dict[str, object] = {} class _FakeBundle: primary_workflow = "query_flow" workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] diagnostics = {} def to_dict(self) -> dict[str, object]: return { "primary_workflow": "query_flow", "workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}], "diagnostics": {}, } class _FakeDB: async def commit(self) -> None: return None async def rollback(self) -> None: return None class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-plan-choice-priority" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) async def _find_active_requirement_plan_workspace(self, **kwargs): captured["finder_kwargs"] = dict(kwargs) return SimpleNamespace(id=185), SimpleNamespace(id=31, state="collecting") @staticmethod def _derive_active_context_kind(*, topic, draft): _ = (topic, draft) return "requirement_plan" async def _bind_active_context_target(self, **kwargs): captured["bind_kwargs"] = dict(kwargs) return SimpleNamespace(id=178), SimpleNamespace(id=30, state="draft_pending_confirm"), "draft_pending_confirm" async def _get_or_create_session(self, **kwargs): _ = kwargs raise AssertionError("choice protocol should stay sync on active requirement") async def process_feishu_message(self, **kwargs): captured["kwargs"] = kwargs return { "handled": True, "status": "answered", "classification": "question", "linked_project_ids": [], "signal_id": None, "created_project_id": None, "auto_tasks": [], "ai_trace_id": "trace-plan-choice-priority", "direct_reply_required": True, "direct_reply_text": "已确认选项 A,继续下一轮。", } class _FakeMountedRuntimeWriteService: def __init__(self, db): _ = db async def resolve_source_activation_scope( self, *, tenant_id: int, source_system: str, channel_ref: str | None, thread_context: dict | None, ): _ = (tenant_id, source_system, channel_ref, thread_context) return SimpleNamespace(id=1, source_system="feishu"), True async def _noop_sync_recent_cache(self, **kwargs): _ = (self, kwargs) monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService) monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache) monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) service = ProactiveSignalCaptureService(_FakeDB()) result = await service.capture_from_feishu( group_chat_id="chat-plan-choice-priority", message_content="A", sender="测试用户", tenant_id=1, proposer_open_id="ou-plan-choice-priority", mentioned_bot_names=[], feishu_message_id="msg-plan-choice-priority", chat_type="group", thread_context=None, ) assert result["status"] == "answered" assert result["direct_reply_text"] == "已确认选项 A,继续下一轮。" assert isinstance(captured.get("finder_kwargs"), dict) assert isinstance(captured.get("kwargs"), dict) @pytest.mark.asyncio async def test_capture_from_feishu_group_choice_protocol_uses_executor_requirement_finder_when_kernel_lacks_direct_finder( monkeypatch, ) -> None: captured: dict[str, object] = {} class _FakeBundle: primary_workflow = "query_flow" workflows = [SimpleNamespace(workflow_type="query_flow", workflow_action="answer")] diagnostics = {} def to_dict(self) -> dict[str, object]: return { "primary_workflow": "query_flow", "workflows": [{"workflow_type": "query_flow", "workflow_action": "answer"}], "diagnostics": {}, } class _FakeDB: async def commit(self) -> None: return None async def rollback(self) -> None: return None class _FakeExecutor: async def _find_active_requirement_plan_workspace(self, **kwargs): captured["finder_kwargs"] = dict(kwargs) return SimpleNamespace(id=185, root_message_id="om-active-plan-root"), SimpleNamespace(id=31, state="collecting") class _FakeKernel: @staticmethod def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str): _ = (source_system, source_message_id, channel_ref, sender, message_text) return "msg-plan-choice-executor-finder" def __init__(self, db, tenant_id: int): _ = (db, tenant_id) self.executor = _FakeExecutor() @staticmethod def _derive_active_context_kind(*, topic, draft): _ = (topic, draft) return "requirement_plan" async def _bind_active_context_target(self, **kwargs): captured["bind_kwargs"] = dict(kwargs) return None async def _get_or_create_session(self, **kwargs): _ = kwargs raise AssertionError("executor-based requirement finder should keep choice followup sync") async def process_feishu_message(self, **kwargs): captured["kwargs"] = kwargs return { "handled": True, "status": "answered", "classification": "question", "linked_project_ids": [], "signal_id": None, "created_project_id": None, "auto_tasks": [], "ai_trace_id": "trace-plan-choice-executor-finder", "direct_reply_required": True, "direct_reply_text": "已确认选项 A,继续下一轮。", } class _FakeMountedRuntimeWriteService: def __init__(self, db): _ = db async def resolve_source_activation_scope( self, *, tenant_id: int, source_system: str, channel_ref: str | None, thread_context: dict | None, ): _ = (tenant_id, source_system, channel_ref, thread_context) return SimpleNamespace(id=1, source_system="feishu"), True async def _noop_sync_recent_cache(self, **kwargs): _ = (self, kwargs) monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel) monkeypatch.setattr(mixin_capture_entry, "MountedAIRuntimeWriteService", _FakeMountedRuntimeWriteService) monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache) monkeypatch.setattr( mixin_capture_entry.WorkflowSemanticPlanner, "aplan_message", classmethod(lambda cls, **kwargs: asyncio.sleep(0, result=_FakeBundle())), ) service = ProactiveSignalCaptureService(_FakeDB()) result = await service.capture_from_feishu( group_chat_id="chat-plan-choice-executor-finder", message_content="A", sender="测试用户", tenant_id=1, proposer_open_id="ou-plan-choice-executor-finder", mentioned_bot_names=[], feishu_message_id="msg-plan-choice-executor-finder", chat_type="group", thread_context=None, ) assert result["status"] == "answered" assert result["direct_reply_text"] == "已确认选项 A,继续下一轮。" assert isinstance(captured.get("finder_kwargs"), dict) assert isinstance(captured.get("kwargs"), dict) thread_context = captured["kwargs"]["thread_context"] assert isinstance(thread_context, dict) assert thread_context["root_message_id"] == "om-active-plan-root" assert thread_context["reply_to_message_id"] == "om-active-plan-root" @pytest.mark.asyncio async def test_project_context_prefill_skips_chatwide_reply_fallback(monkeypatch) -> None: called = {"fallback": False} class _FakePrefill(FeishuPipelineContextPrefillBMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 @staticmethod def _is_contextual_project_request(message: str) -> bool: return True async def _load_recent_chat_turns(self, **kwargs): _ = kwargs return [] async def _load_recent_chat_reply_fallback_text(self, *, source_chat_id: str, limit: int = 5) -> str: _ = (source_chat_id, limit) called["fallback"] = True raise AssertionError("reply fallback should not be used when no recent turns are available") fields = { "title": "", "description": "", "project_scope": "", "missing_fields": ["title", "description", "project_scope"], "ready": False, } result = await _FakePrefill()._maybe_prefill_project_fields_from_context( message="根据这个创建一个项目", source_chat_id="chat-1", source_sender_open_id="ou-1", sender="测试用户", fields=fields, source_message_id="msg-1", thread_context={"reply_to_message_id": "msg-root-1"}, ) assert result == fields assert called["fallback"] is False @pytest.mark.asyncio async def test_project_prefill_fallback_leaves_owner_and_timeline_empty() -> None: class _FakeLLM: async def generate(self, *args, **kwargs): _ = args, kwargs raise RuntimeError("boom") class _FakeExtraction(FeishuPipelineExtractionMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 self.llm = _FakeLLM() result = await _FakeExtraction()._extract_project_prefill( "我们想做一个内部知识库升级项目,先把项目名称和范围整理出来。" ) assert result["suggested_owner"] is None assert result["suggested_timeline"] is None assert float(result["confidence"]) <= 0.35 assert len(result["reasoning"]) >= 1 @pytest.mark.asyncio async def test_create_project_suggestion_preserves_uncertain_owner_and_timeline() -> None: class _FakeDB: def __init__(self) -> None: self.added = [] def add(self, obj): self.added.append(obj) async def flush(self): return None class _FakeProcessing(FeishuPipelineItemProcessingMixin): def __init__(self) -> None: self.db = _FakeDB() self.tenant_id = 1 async def _extract_project_prefill(self, message: str): _ = message return { "suggested_name": "内部知识库升级", "suggested_description": "围绕内部知识库问答能力升级", "suggested_owner": None, "suggested_timeline": None, "confidence": 0.31, "reasoning": ["需求里没有明确负责人", "需求里没有明确时间线"], } project_info, suggestion = await _FakeProcessing()._create_project_suggestion( item_id=12, content="我们想做一个内部知识库升级项目,先把项目名称和范围整理出来。", sender="测试用户", fallback_confidence=0.31, ) assert project_info["suggested_owner"] is None assert project_info["suggested_timeline"] is None assert suggestion.suggested_owner is None assert suggestion.suggested_timeline is None def test_project_create_idempotency_key_prefers_message_key() -> None: key_one = FeishuPipelineCommandCommitProjectMixin._build_project_create_idempotency_key( idempotency_key="feishu:sm-123", trace_id="trace-1", ) key_two = FeishuPipelineCommandCommitProjectMixin._build_project_create_idempotency_key( idempotency_key="feishu:sm-123", trace_id="trace-2", ) fallback_key = FeishuPipelineCommandCommitProjectMixin._build_project_create_idempotency_key( idempotency_key=None, trace_id="trace-3", ) assert key_one == "feishu:sm-123" assert key_two == "feishu:sm-123" assert fallback_key == "trace-3" def test_task_orchestration_charter_context_flattens_modules() -> None: charter = SimpleNamespace( id=55, title="内部知识库升级宪章", version=2, status="aligning", content={ "modules": { "module_1": { "problem_statement": "减少重复检索", "goal_statement": "提升命中率 / 降低重复提问", "kpis": "首响 < 3s", }, "module_2": { "in_scope": "问答检索,知识补充", "out_of_scope": "权限模型重构", }, "module_4": { "milestones": "需求冻结\n开发完成", }, "module_5": { "risk_register": "需求膨胀; 依赖不清", }, "module_6": { "governance_rule": "附件仅作证据引用", }, } }, ) context = SignalCaptureTaskOrchestrationMainMixin._build_task_orchestration_charter_context(charter) assert context["title"] == "内部知识库升级宪章" assert context["summary"] == "减少重复检索" assert context["objectives"] == ["提升命中率", "降低重复提问"] assert context["in_scope"] == ["问答检索", "知识补充"] assert context["out_of_scope"] == ["权限模型重构"] assert context["milestones"] == ["需求冻结", "开发完成"] assert "附件仅作证据引用" in context["constraints"][0] def test_task_orchestration_prompt_embeds_charter_context() -> None: prompt = SignalCaptureTaskOrchestrationMainMixin._build_task_orchestration_prompt( project_payload={ "id": 6, "title": "内部知识库升级", "description": "围绕内部知识库问答能力升级", "assigned_role": "backend", "status": "active", }, charter_payload={ "id": 55, "title": "内部知识库升级宪章", "version": 2, "status": "aligning", "summary": "减少重复检索", "objectives": ["提升命中率", "降低重复提问"], "scope": "知识库检索与问答", "in_scope": ["问答检索", "知识补充"], "out_of_scope": ["权限模型重构"], "success_criteria": ["首响 < 3s"], "constraints": ["附件仅作证据引用"], "milestones": ["需求冻结", "开发完成"], "risks": ["需求膨胀"], }, signal_payload={ "signal_id": 8, "message": "支持导出知识库内容", "classification": {"classification": "new_project"}, "proposer": "测试用户", "captured_at": "2026-03-25T10:00:00+08:00", }, team_members=[ {"user_id": 1, "name": "Alice", "workload_ratio": 42.0, "skill_tag_ids": [11, 12]}, ], ) assert "charter_json" in prompt assert "内部知识库升级宪章" in prompt assert "success_criteria" in prompt assert "附件仅作证据引用" in prompt def test_task_orchestration_fallback_uses_charter_context() -> None: class _FakeSkill(SignalCaptureTaskOrchestrationSkillMixin): @staticmethod def _normalize_single_line_text(text: str, max_len: int = 64) -> str: return " ".join(str(text or "").split())[:max_len] tasks = _FakeSkill()._build_task_orchestration_fallback( cleaned_message="新增内部知识库导出需求", classification={"summary": "补充内部知识库导出能力"}, charter_context={ "title": "内部知识库升级宪章", "summary": "围绕内部知识库问答体验升级", }, ) assert len(tasks) == 2 assert all("内部知识库升级宪章" in task["description"] for task in tasks) def test_queue_confirm_undo_prefers_feishu_message_id_for_idempotency() -> None: item = _Item(item_id=42, level="command", operation_code="project.create") key = FeishuPipelineQueueConfirmUndoMixin._build_operation_idempotency_key(item=item) assert key == "feishu:msg-42" def test_task_create_idempotency_key_prefers_message_key() -> None: key_one = FeishuPipelineCommandCommitTaskMixin._build_task_create_idempotency_key( idempotency_key="feishu:sm-456", trace_id="trace-1", ) key_two = FeishuPipelineCommandCommitTaskMixin._build_task_create_idempotency_key( idempotency_key="feishu:sm-456", trace_id="trace-2", ) fallback_key = FeishuPipelineCommandCommitTaskMixin._build_task_create_idempotency_key( idempotency_key=None, trace_id="trace-3", ) assert key_one == "feishu:sm-456" assert key_two == "feishu:sm-456" assert fallback_key == "trace-3" @pytest.mark.asyncio async def test_task_create_replays_existing_ledger_without_persisting_task(monkeypatch) -> None: from app.services.integrations.feishu_signal_pipeline import mixin_command_commit_task as task_commit_module class _FakeResult: def __init__(self, value) -> None: self._value = value def scalar_one_or_none(self): return self._value def scalars(self): return self def first(self): return self._value class _FakeDB: def __init__(self) -> None: self.calls = 0 self.committed = False async def execute(self, query): self.calls += 1 if self.calls == 1: return _FakeResult(type("ProjectRow", (), {"id": 11, "tenant_id": 1})()) if self.calls == 2: return _FakeResult(type("TruthNodeRow", (), {"id": 22, "tenant_id": 1})()) raise AssertionError("unexpected execute") async def commit(self): self.committed = True class _FakeLedger: def __init__(self) -> None: self.status = "applied" self.result_payload = { "status": "applied", "message": "task created #777", "entity_type": "task", "entity_id": "777", "result": { "task_id": 777, "title": "补充埋点", "project_id": 11, }, } class _FakeCommit(FeishuPipelineCommandCommitTaskMixin): def __init__(self) -> None: self.db = _FakeDB() self.tenant_id = 1 self.sync_called = False async def _load_task_create_ledger(self, *, idempotency_key): self.loaded_key = idempotency_key return _FakeLedger() async def _claim_task_create_ledger(self, *, idempotency_key, trace_id, payload): raise AssertionError("ledger claim should not be reached when replaying an applied result") def _sync_task_rag(self, *, task): self.sync_called = True async def _fail_persist_task(*args, **kwargs): raise AssertionError("persist_task should not be called when replaying an existing ledger") monkeypatch.setattr(task_commit_module, "persist_task", _fail_persist_task) kernel = _FakeCommit() result = await kernel._commit_task_create( operator_user=type("User", (), {"id": 9})(), payload={"project_id": 11, "title": "补充埋点", "description": "补充行为埋点"}, trace_id="trace-task-1", idempotency_key="feishu:msg-777", ) assert result["status"] == "applied" assert result["result"]["task_id"] == 777 assert kernel.loaded_key == "feishu:msg-777" assert kernel.db.committed is False assert kernel.sync_called is False @pytest.mark.asyncio async def test_project_gate_always_queues_ready_create(monkeypatch) -> None: called: dict[str, object] = {} class _FakeGate(FeishuPipelineGateProjectMixin): def __init__(self) -> None: self.db = object() self.tenant_id = 1 @staticmethod def _build_project_clarification_message(missing_fields): return "请补充项目信息。" @staticmethod def _stable_positive_int(*parts): return 42 async def _resolve_command_operator_user(self, *, sender): called["operator_sender"] = sender return object() def _can_execute_high_value_action(self, operator_user): called["can_execute"] = operator_user return True async def _queue_project_intent_item(self, **kwargs): called["queue_kwargs"] = kwargs return { "item_id": 42, "level": "project", "project_id": None, "signal_id": None, "route_status": "queued", "clarification_message": "建项目已进入待确认队列,请点击继续执行。", } async def _write_processing_log(self, **kwargs): called["write_processing_log"] = kwargs async def _execute_controlled_operation(self, **kwargs): raise AssertionError("project.create should not execute before confirmation") kernel = _FakeGate() result = await kernel._handle_create_project_gate( message="请创建一个新项目《AI PMO闭环重构实战-0314》:目标是完成工作流路由重构并上线验收。", sender="林宇航", source_chat_id="chat-1", source_sender_open_id="ou-1", feishu_message_id="msg-1", captured_at=None, intent_gate={ "intent": "project.create", "fields": { "ready": True, "title": "AI PMO闭环重构实战-0314", "description": "目标是完成工作流路由重构并上线验收。", "project_scope": "workflow routing", }, }, process_started_at=0.0, ) assert result["direct_reply_required"] is True assert result["created_project_id"] is None assert result["results"][0]["item_id"] == 42 assert result["reply_template_key"] == "queue_pending_confirm" assert result["queue_item_id"] == 42 assert called["queue_kwargs"]["message"].startswith("请创建一个新项目") @pytest.mark.asyncio async def test_signal_route_item_ignores_low_confidence_threshold_when_project_is_matched() -> None: class _FakeDBResult: def scalar_one_or_none(self): return None class _FakeDB: def __init__(self) -> None: self.added = [] async def execute(self, query): _ = query return _FakeDBResult() def add(self, obj): self.added.append(obj) async def flush(self): return None class _FakeFinalize(FeishuPipelineItemFinalizeRoutingMixin, FeishuPipelineMessageUtilsBMixin): def __init__(self) -> None: self.db = _FakeDB() self.tenant_id = 1 self.log_kwargs: dict[str, object] = {} self.notified: tuple[int, int] | None = None async def _ensure_signal_link(self, *, signal_id: int, project_id: int) -> None: _ = (signal_id, project_id) async def _write_processing_log(self, **kwargs): self.log_kwargs = kwargs async def _notify_signal_auto_entered(self, *, project_id: int, signal) -> None: self.notified = (project_id, int(signal.id)) item = SimpleNamespace(id=1) signal = SimpleNamespace(id=88, title="直播礼物量分析") kernel = _FakeFinalize() result = await kernel._route_signal_item( item=item, signal=signal, matched_project_id=62, confidence=0.03, cold_start_enabled=False, signal_auto_enter_threshold=0.80, ) assert result == 62 assert item.safe_to_auto_execute is True assert item.auto_execute_state == "done" assert item.blocking_reason is None assert item.route_status == "auto_done" assert kernel.notified == (62, 88)