from __future__ import annotations import asyncio from typing import Any import pytest from app.services.ai_pmo.semantic_context.decision_schema import SemanticDecision from app.services.ai_pmo.workflow_semantic_planner import WorkflowSemanticPlanner def _decision(*, workflows: list[dict[str, Any]], scene_metadata: dict[str, Any] | None = None) -> SemanticDecision: return SemanticDecision( topic_relation="new_topic", mode_decision="answer_only", workflow_plans=workflows, scene_metadata=scene_metadata or {}, requested_actions=["workflow.bundle.compose"], confidence=0.91, ) def test_workflow_semantic_planner_plan_message_uses_model_decision(monkeypatch: pytest.MonkeyPatch) -> None: async def _fake_decide(cls, *, message_text: str, source_system: str, metadata: dict[str, Any] | None = None): assert message_text == "帮我创建一个项目,并把这个需求接入信号源" assert source_system == "feishu" return _decision( workflows=[ { "workflow_type": "new_project_flow", "workflow_action": "create", "target_scope": "global", "operation_codes": ["project.create"], "confidence": 0.93, }, { "workflow_type": "signal_source_flow", "workflow_action": "onboard", "target_scope": "global", "operation_codes": ["signal_source.onboard"], "confidence": 0.92, }, ] ) monkeypatch.setattr(WorkflowSemanticPlanner, "_decide_cold_start_semantics", classmethod(_fake_decide)) bundle = WorkflowSemanticPlanner.plan_message( message_text="帮我创建一个项目,并把这个需求接入信号源", source_system="feishu", ) assert bundle.primary_workflow == "new_project_flow" assert [item.workflow_type for item in bundle.workflows] == ["new_project_flow", "signal_source_flow"] assert bundle.reply_contract.mode == "aggregate_card" @pytest.mark.asyncio async def test_workflow_semantic_planner_aplan_message_preserves_scene_metadata(monkeypatch: pytest.MonkeyPatch) -> None: async def _fake_decide(cls, *, message_text: str, source_system: str, metadata: dict[str, Any] | None = None): assert message_text == "想跟你聊个新需求" return _decision( workflows=[ { "workflow_type": "query_flow", "workflow_action": "answer", "target_scope": "global", "operation_codes": ["query.answer"], "confidence": 0.88, "metadata": {"message_intent": "requirement_plan"}, } ], scene_metadata={ "message_intent": "requirement_plan", "consultant_mode_preference": "clarify", "topic_switch": "new", }, ) monkeypatch.setattr(WorkflowSemanticPlanner, "_decide_cold_start_semantics", classmethod(_fake_decide)) bundle = await WorkflowSemanticPlanner.aplan_message( message_text="想跟你聊个新需求", source_system="feishu", ) assert bundle.primary_workflow == "query_flow" assert bundle.workflows[0].metadata["message_intent"] == "requirement_plan" assert bundle.diagnostics["topic_switch"] == "new" def test_workflow_semantic_planner_sync_wrapper_fails_inside_running_loop() -> None: async def _runner() -> None: with pytest.raises(RuntimeError): WorkflowSemanticPlanner.plan_message(message_text="test", source_system="feishu") asyncio.run(_runner()) def test_workflow_semantic_planner_no_longer_exposes_legacy_semantic_helpers() -> None: legacy_names = ( "plan_from_intent", "classify_scene", "is_action_request_capture", "parse_followup_action", "extract_project_lookup_keyword", "wants_followup_continue", "wants_followup_status", "looks_like_project_create_request", "looks_like_task_create_request", "looks_like_signal_source_request", "looks_like_external_connect_request", "looks_like_contextual_write_confirmation", "is_query_like_question", "is_obvious_question", ) for name in legacy_names: assert not hasattr(WorkflowSemanticPlanner, name), name