import inspect from types import SimpleNamespace import pytest 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_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" @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 _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) service = ProactiveSignalCaptureService(object()) result = await service.capture_from_feishu( group_chat_id="chat-1", message_content="我感觉第一个垂直应用前景比较好,但是垂直哪个行业呢?", sender="测试用户", tenant_id=1, 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_business_review_query_forwards_semantic_bundle_into_kernel(monkeypatch) -> None: captured: dict[str, object] = {} 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) service = ProactiveSignalCaptureService(object()) result = await service.capture_from_feishu( group_chat_id="chat-br-1", message_content="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,", sender="测试用户", tenant_id=1, 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" 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_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 _looks_like_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)