from __future__ import annotations import pytest from app.schemas.pmflow_ai_turn_runtime import ( PMFlowAIGoalFrame, PMFlowAITargetCollection, PMFlowAITargetScope, PMFlowAITurnActionState, PMFlowAITurnState, ) from app.schemas.rag import RAGAskRequest from app.services.pmflow_ai_evidence_tool_registry import PMFlowAIEvidenceToolRegistry from app.services.pmflow_ai_turn_runtime_service import PMFlowAITurnRuntimeService from app.services.pmflow_ai_turn_state_reviser import PMFlowAITurnStateReviser def _project_collection_state() -> PMFlowAITurnState: scope = PMFlowAITargetScope(scope_type="current_space", binding_strength="surface") frame = PMFlowAIGoalFrame( frame_id="current-space-projects", operation_kind="list_collection", target_scope=scope, target_collection=PMFlowAITargetCollection(object_type="project", required=True, scope=scope), target_object_requirement="none", answer_shape="fact_list", ) return PMFlowAITurnState(selected_frame=frame, candidate_frames=[frame]) @pytest.mark.asyncio async def test_packet_c_surface_tool_emits_runtime_observation_for_collection() -> None: state = _project_collection_state() payload = await PMFlowAIEvidenceToolRegistry(object(), tenant_id=1).run( action_type="inspect_surface_context", request=RAGAskRequest( question="现在有哪些项目", surface_context={ "space_runtime_context": { "workbench": { "projects": [ {"id": 141, "title": "童析", "status": "进行中"}, {"id": 142, "title": "星河", "status": "待确认"}, ] } } }, ), current_user=object(), turn_state=state, ) assert payload is not None observation = payload["runtime_observation"] assert observation["observation_type"] == "collection_snapshot" assert observation["object_type"] == "project" assert observation["can_answer_operations"] == ["list_collection"] assert observation["count"] >= 2 assert any("童析" in item["title"] for item in observation["items"]) parsed = PMFlowAITurnStateReviser().observe_payload( turn_state=state, action_type="inspect_surface_context", payload=payload, ) PMFlowAITurnStateReviser().revise_from_observation(turn_state=state, observation=parsed) assert state.sufficiency_state.can_answer_now is True assert state.customer_packet_draft["operation_kind"] == "list_collection" @pytest.mark.asyncio async def test_packet_c_surface_tool_reads_capacity_packet_as_evidence_only() -> None: state = _project_collection_state() payload = await PMFlowAIEvidenceToolRegistry(object(), tenant_id=1).run( action_type="inspect_surface_context", request=RAGAskRequest( question="我的工作负载怎么样,有什么任务安排吗", surface_context={ "space_operating_packet": { "capacity_packet": { "packet_version": "space_capacity_packet.v1", "project_capacity": [ { "project_id": 141, "project_title": "童析", "team": {"effective_minutes": 480, "planned_minutes": 90}, "personal": { "status": "normal", "effective_minutes": 480, "planned_minutes": 90, "remaining_minutes": 318, }, "ai_reduction_potential_minutes": 30, "recommendations": ["AI 预处理预计可节省 30 分钟人工处理。"], } ], } } }, ), current_user=object(), turn_state=state, ) assert payload is not None result = payload["runtime_evidence_tool_result"] facts = [item["fact"] for item in result["findings"]] assert payload["answer_mode"] == "turn_runtime_evidence_tool" assert "answer_canvas" not in payload assert "quick_actions" not in payload assert any("已计划 90 分钟 / 有效 480 分钟" in fact for fact in facts) assert any("AI 可减少约 30 分钟人工处理" in fact for fact in facts) @pytest.mark.asyncio async def test_packet_c_empty_tool_observation_does_not_mark_state_answerable() -> None: state = _project_collection_state() payload = await PMFlowAIEvidenceToolRegistry(object(), tenant_id=1).run( action_type="inspect_surface_context", request=RAGAskRequest(question="现在有哪些项目", surface_context={"space_runtime_context": {"workbench": {}}}), current_user=object(), turn_state=state, ) assert payload is not None observation = payload["runtime_observation"] assert observation["can_answer_operations"] == [] assert observation["remaining_gaps"][0]["gap_type"] == "system_evidence_gap" parsed = PMFlowAITurnStateReviser().observe_payload( turn_state=state, action_type="inspect_surface_context", payload=payload, ) PMFlowAITurnStateReviser().revise_from_observation(turn_state=state, observation=parsed) assert state.sufficiency_state.can_answer_now is False assert state.operation_gaps assert all(gap.user_actionable is False for gap in state.operation_gaps) def test_packet_c_sufficiency_honors_observation_answerability_before_remaining_candidates() -> None: state = _project_collection_state() state.sufficiency_state.can_answer_now = True state.action_state = PMFlowAITurnActionState() decision = PMFlowAITurnRuntimeService(object(), tenant_id=1).assess_sufficiency(state) assert decision.can_answer_now is True assert decision.needs_action is False